mirror of
https://github.com/meshtastic/firmware.git
synced 2026-09-16 08:30:04 -04:00
https-heap-headroom
12698
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
56ce743f75 | Show waypoints sent with no expiry and stop expiring on an unset clock (#11600) | ||
|
|
9c0a331309 |
fix(test): stop the survivor scan reporting a hit as a miss (#11603)
* test-state: match the sandbox HOME in-shell so a survivor hit cannot report as a miss * Trim the survivor-scan comment to two lines * test-state: read the environ with a NUL-delimited read loop, not mapfile -d * test-state-check: report the survivor's actual HOME and the wrapper's stderr * test-state-check: re-run the scan when it reports a miss, to separate a race from a mismatch * test-state-check: let the survivor fixture finish exec before the suite returns * test-state-check: fail the survivor fixture instead of staging a pid it never saw exec |
||
|
|
7b0004806a |
fix(graphics): drive GPIO backlights from the stored brightness level (#11588)
* fix(graphics): drive GPIO backlights from the stored brightness level Screen::handleSetOn restored PIN_EINK_EN only when screen_brightness was exactly 1. The field is 0..255 and defaults to 153, so the frontlight stayed off after a screen timeout until the next reboot. InputBroker read screen_brightness as "currently lit" for the touch backlight, so a stored level made touch-to-light a no-op. The HAPTIC_FEEDBACK_PIN block then reassigned touchConfig.onPress and onRelease, dropping those handlers on any variant defining both. MINI_EPAPER_S3 names its panel power rail PIN_EINK_EN. It was switched off with the screen and never restored. graphics::Backlight gains a GPIO backend covering PIN_EINK_EN and PCA_PIN_EINK_EN, so Screen, MenuHandler and InputBroker call backlightOn, backlightOff, backlightToggle and backlightIsLit instead of touching pins. backlightIsLit reports the driven state, separate from the configured level. Power-up state is declared per variant with GPIO_BACKLIGHT_DEFAULT_ON rather than hardcoded in the e-ink driver. The backend stores only 0 or 255, so any other stored level falls back to the variant default and no board changes its existing behaviour. MINI_EPAPER_S3 is excluded and keeps its rail powered. Touch handlers are merged so backlight and haptic feedback compose. Verified on ThinkNode M1: lit at boot, off on timeout, lit on wake, and an explicit off surviving both wake and reboot. * chore(thinknode_m1): correct the LED pin comments P0.13 drives the blue indicator, not a green one. P1.06 is a second drive for the same red LED as LED_POWER, which is why it stays disabled. * fix(graphics): clamp GPIO backlight levels at the setter backlightSet stored whatever level it was given, so a caller passing an intermediate value left backlightGet and the persisted config holding a level the rail cannot drive. Clamp to off or on in the setter, which keeps the invariant at the single write point instead of only at init. |
||
|
|
c45b66352b | Idle the Wio Tracker L1 buzzer pin at boot (#11601) | ||
|
|
4528018b12 |
fix(xmodem): close the file when a transmit is aborted (#11598)
The `else if (isTransmitting)` branch in handlePacket() cancels the transfer and clears isTransmitting without closing the open file. It is the only terminal path that does not close: EOT, CAN, and the ACK/EOT completion paths all do. The next transmit then reassigns `file` in the STX handler, orphaning the previous handle. Any client that can speak the XModem ToRadio path can drive this in a loop (STX seq=0 to start a transmit, then any non-seq-0 frame to hit the abort branch), and XModem is not subject to the PhoneAPI packet throttle, so the loop runs as fast as the link allows. Measured on real hardware with a DEBUG_HEAP build, 60 iterations: Heltec Mesh Node T096 (nRF52840) 44,908 -> 32,896 B free (-200 B/iter) Heltec Wireless Tracker V2 (S3) 57,360 -> 51,472 B free ( -98 B/iter) The heap is not reclaimed afterwards. On the T096 that exhausts ~45 KB of free heap in roughly 225 iterations. With this change, 200 iterations on a T096 leave free heap unchanged (44,968 B before and after). |
||
|
|
48357538ba |
Meshnology W10: enable the AXP2101 power key as a second button (#11593)
* Meshnology W10: enable the AXP2101 power key as a second button SW3 is wired to the AXP2101 PWRON pin (via R44 510R, schematic W10-MB-V1.1 pg3), but the key did nothing in firmware. Power::runOnce() already polls the PMU IRQ status registers over I2C and maps a PEK short press to INPUT_BROKER_CANCEL when PMU_POWER_BUTTON_IS_CANCEL is set. However the matching PMU->enableIRQ() lives inside #ifdef PMU_IRQ, while PMU init runs disableIRQ(ALL) first. Without PMU_IRQ the PKEY_SHORT status bit is never armed, so the polled read is always false and the define alone is inert. AXP_IRQ on this board reaches only expander EXIO5 and is not routed to any ESP32 GPIO, so define PMU_IRQ as the MCP23017 virtual pin, mirroring how LORA_DIO1 is handled on this variant. The attachInterrupt() and gpio_wakeup_enable() uses of PMU_IRQ are inert on a non-GPIO value (both are unchecked calls, so an invalid pin is ignored rather than fatal); what the define buys is the enableIRQ() they gate. Tested on Meshnology W10 hardware: short presses of SW3 now log "[Power] Input: Corona Button Click", the existing GPIO0 user button continues to work independently, and the board boots normally. Events surface on the 20s Power::runOnce() cadence, since with no real interrupt the ISR's setIntervalFromNow(0) never runs to force an immediate poll. * style: apply clang-format to meshnology-w10 variant.h Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com> |
||
|
|
e257e2a487 |
chore(deps): update meshtastic/device-ui digest to 27443d0 (#11571)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> |
||
|
|
15e16839d3 |
chore(deps): update meshtastic-esp8266-oled-ssd1306 digest to bb93fd4 (#11594)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> |
||
|
|
e8d4573af7 |
fix(t-watch-ultra): wrap esp_flash_read so NVS survives, keeping BLE bonds (#11583)
* fix(t-watch-ultra): wrap esp_flash_read so NVS survives, keeping BLE bonds The IDF 5.5 manual-read regression on this board's flash is already worked around for esp_partition_read, but nvs_flash does not use that API: it reads the NVS partition through the lower-level esp_flash_read, which still returns 0x00. NVS therefore initialised empty on every boot -- zero entries, zero namespaces -- even though the data was intact on flash. Everything stored through NVS was lost each boot, including NimBLE's bond table. A phone that had already paired was not recognised on reconnect, so the device ran a fresh pairing and displayed a new passkey every time. The PIN worked, but the bond never persisted. Wrap esp_flash_read the same way, using the raw (non-partition) spi_flash_mmap so it serves callers that never go through the esp_partition_t API. Reads for any chip other than the default fall back to the real implementation, as do mmap failures. Gated on T_WATCH_ULTRA; no other board is affected. * fix(t-watch-ultra): keep the raw-read contract when flash encryption is on esp_flash_read is specified to return raw, still-encrypted bytes; the flash cache is what decrypts transparently. Reading through spi_flash_mmap therefore hands back plaintext where the caller asked for ciphertext. No target here enables CONFIG_SECURE_FLASH_ENC_ENABLED, so nothing is affected today, but --wrap is a global interposition and encryption can be burned into efuse independently of the build config. Check at runtime and leave encrypted flash to the real implementation. --------- Co-authored-by: Ben Meadors <benmmeadors@gmail.com> |
||
|
|
8a15d9258f |
fix(test): unbreak test_radio under ASan (#11589)
* test_radio: prove the rejected packet was released via pool accounting, not pointer identity * test-state.sh: silence the shell's own open failure when scanning /proc for survivors * Trim the comments added with the test_radio and test-state fixes |
||
|
|
d8a95a76b4 |
fix(TFTDisplay): give the CO5300 its post-sleep-out settle time (#11587)
LovyanGFX's Panel_CO5300 init table issues Sleep Out and Display On back-to-back with zero delay. The controller needs up to 120 ms after Sleep Out before it accepts Display On; when it is slow to wake, Display On is swallowed and the panel stays dark until the next re-init (screen wake), while the firmware runs normally. Override the init table with the datasheet delays. Fix belongs in LovyanGFX ultimately; this carries it until the pin updates. |
||
|
|
ee48094ea8 |
Fix backwards GPS_RX_PIN/GPS_TX_PIN direction comments (#11585)
GPS.cpp passes GPS_RX_PIN as the MCU's RX pin and GPS_TX_PIN as its TX pin. Nine variants documented the opposite, which reads as if the pins were swapped on working hardware (see #11584). Comment-only change; no pin assignment is touched. |
||
|
|
0271be9369 |
fix(SafeFile): remove a stale .tmp before opening it for write (#11428)
* fix(SafeFile): remove a stale .tmp before opening it for write SafeFile writes to <filename>.tmp, verifies it by readback, then renames it over the real file. openFile() never removed a pre-existing .tmp - an unfinished FIXME - and FILE_O_WRITE appends rather than truncates on Adafruit_LittleFS (nRF52) and STM32 LittleFS. So a .tmp left behind by a reset in the window between close() and renameFile() is appended to on the next save. The readback hash covers only the bytes just written, so it mismatches, close() returns false, and the tmp is left behind again - the failure latches and every subsequent save of that file fails. Today saveProto() discards close()'s result, so this is silent and permanent. Guard the remove with exists(): a bare remove() of a missing file logs on Portduino. The same guarded pattern is already used for this exact append trap in xmodem.cpp. Note the FIXME's commented-out body named the wrong path - it removed 'filename', the real file, not 'filenameTmp' - so it would have destroyed the good copy had it ever been enabled. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(SafeFile): cover the stale-.tmp path, and trim the fix comment Adds test_safefile, the first coverage of SafeFile's write-tmp / verify-by-readback / rename-over path that every saveProto() caller goes through. Five cases pin the contract the fix restores: whatever the backend does on open, a completed save leaves the real file holding exactly the bytes written and nothing else, on both the fullAtomic and the !fullAtomic construction, with no .tmp left behind. These tests cannot go red on the native host, and that gap cannot be closed here. FILE_O_WRITE is an append-then-seek-to-end open only on Adafruit_LittleFS (nRF52) and on the in-repo STM32 port (STM32_LittleFS_File.cpp: LFS_O_RDWR | LFS_O_CREAT followed by lfs_file_seek to LFS_SEEK_END). On Portduino FILE_O_WRITE is the string "w" (FSCommon.h:13), which reaches fopen() and truncates. Reverting the source fix and re-running leaves all five green, verified rather than assumed. test_write_open_truncates _on_this_host asserts that premise out loud, so if the host ever gains the append behaviour the suite starts discriminating instead of quietly agreeing. Why the original FIXME stayed commented out, since that is the real history here. It read "if (fullAtomic) FSCom.remove(filename)" and named the real file, not the tmp. Running it would delete the last good copy before the replacement had been written and verified, which is precisely the guarantee fullAtomic exists to provide. Disabling it was correct. The fix under test removes filenameTmp instead, which is the file that actually carries the stale bytes, and is safe to drop at any point because nothing has been promised about it yet. Scoping the remove to fullAtomic would be wrong for the same reason. Both paths open the same filenameTmp with the same FILE_O_WRITE; fullAtomic only decides whether the real file is nuked up front to free space. The !fullAtomic path is the space-constrained one, so it is if anything the more likely to be interrupted mid-write and inherit a stale tmp. Test 2 pins that. On the cost of the added exists(). Every saveProto() already ends in SafeFile::close(), which calls testReadback(): it reopens the tmp and reads the whole proto back one byte at a time through f2.read() to XOR a verification hash, then renames. So the per-save cost is already an open, a full write, a close, a full byte-wise reread, and a rename. One exists() is a single path lookup with no erase, no program and no data read, and on the common path there is no remove() at all. Next to the readback loop it is noise. Happy to put a number on it if wanted. Scoping it to fullAtomic would also not do what it looks like it does. SafeFile's constructor defaults fullAtomic to false (SafeFile.h:28), and of the saveProto call sites only saveDeviceStateToDisk passes true. Config, moduleconfig, channels, nodedatabase and backup all take the default, so scoping would leave the stale tmp live on almost every save path, including the space-constrained one most likely to be interrupted mid-write. Also trims the fix's comment to two lines per AGENTS.md, and drops the stale-tmp removal log from LOG_WARN to LOG_DEBUG: an interrupted write is recoverable and self-healing, so it does not warrant a warning on every boot after one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(SafeFile): fail the write when a stale tmp cannot be removed openFile() ignored the result of FSCom.remove(). If the removal failed on an append-on-write backend, the open that follows appended to the stale bytes, and the readback hash is an 8 bit XOR over the whole tmp, so polluted content has a real chance of verifying and being renamed over the good file. It now logs and returns an invalid File. SafeFile::write() already no-ops on !f and close() already returns false, so the caller sees the save fail rather than silently getting a corrupt one. This is the only checked FSCom.remove() in the tree; the other call sites are all best-effort cleanups where failure does not compromise anything. Also gates test_write_open_truncates_on_this_host to ARCH_PORTDUINO. It asserts that this host truncates on FILE_O_WRITE, which is false by design on the Adafruit_LittleFS and STM32 backends the fix exists for, so running the suite there would fail on a premise that is only meant to describe the test host. Both reported by CodeRabbit on #11428. --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
bfd1e1a231 |
Add Heltec RC32, RC52 and RCC6 boards, and LC760CA GNSS support (#11572)
* refactor(graphics): select Arduino_GFX panels with a capability flag TFTDisplay tested `defined(HACKADAY_COMMUNICATOR)` in a dozen places to mean "this panel is driven by Arduino_GFX rather than LovyanGFX". Every new Arduino_GFX board had to be appended to all of them. Move the decision into the variant as USE_ARDUINO_GFX so the display code stops naming individual boards. No behaviour change: the Hackaday Communicator is still the only board that sets it. * feat(boards): add Heltec RC32, RC52 and RCC6 Three boards around the same 128x220 NV3001B panel: RC32 (ESP32-S3), RCC6 (ESP32-C6) and RC52 (nRF52840). They differ only in how the panel bus is wired, so they share one branch in TFTDisplay behind TFT_NV3001B. RC32 and RC52 also carry a rotary encoder on a TCA6408 I2C expander. That lands as its own input source rather than as board conditionals inside i2cButton, which is the M5Stack UnitC6L button driver and stays untouched. On RC52 and RCC6 the panel is an add-on module, so probe it before reporting a screen. The probe reuses the bit-banged SPI helper that already backs the T114 ST7789 check. Arduino_GFX is pinned to the upstream commit that added the NV3001B driver; it has not shipped in a tagged release yet. Co-Authored-By: Quency-D <55523105+Quency-D@users.noreply.github.com> * feat(gps): detect and configure the LC760CA GNSS module The LC760CA is another Unicore part, so it joins the $PDTINFO probe family and reuses the CM121 message-rate setup. It answers with CC1161W. GNSS_MODEL_LC760CA goes immediately before GNSS_MODEL_GENERIC_NMEA: the sentinel has to stay last because isValidGnssModel() uses it as the exclusive upper bound on values the probe cache may hold. Placing the new model after it would leave LC760CA permanently uncacheable. Co-Authored-By: Quency-D <55523105+Quency-D@users.noreply.github.com> * fix(graphics): re-init the NV3001B after the panel rail comes back DISPLAYOFF de-asserts VTFT_CTRL, which cuts power to the panel, so the controller loses MADCTL, COLMOD and gamma. displayOn() only sends sleep-out and cannot restore them, leaving the panel dark or in the wrong format after wake. Re-run begin() once the rail has settled, and repaint in full since the re-init leaves display RAM undefined. Also stop the TCA6408 rotary polling from two threads at once. Registering as an InputPollable meant InputBroker's pollSoon task could call pollOnce() while runOnce() was mid-transfer on the main thread, with nothing serialising Wire or the decoder state. Drop InputPollable and have the interrupt wake the thread instead, the way ButtonThread does, so the bus and the decode stay on one thread. * fix(graphics): skip the NV3001B wake when re-init fails begin() reports whether the bus came up. Ignoring it meant a failed re-init still lit the backlight and drove a full-screen repaint at a panel that was never initialised. * chore(boards): ship the Heltec RC boards at release level release is the normal level for a variant; the matrix generator still builds each of these in this PR because they add a new platformio.ini. --------- Co-authored-by: Quency-D <55523105+Quency-D@users.noreply.github.com> |
||
|
|
ac330e6a6b |
fix(radio): MeshBeacon heap leak and runtime packet payload size check (#11573)
* Fix for MeshBeacon packet leakage * fix: add runtime payload size check against radiobuffer * review fix for PR#11573: clear target radio settings before MeshBeacon packet release * add unit test for radio buffer capacity check, removing related assert for the test * review fix for PR#11573: add explicit verifaction against rejected packets |
||
|
|
05f6474108 |
meshnology-w10: define HAS_SPI_TFT so the TFT screen initializes again (#11042)
#10803 refactored main.cpp to key SPI-TFT Screen creation on HAS_SPI_TFT instead of the per-controller define list. The W10 variant (#10911) was written before that refactor and crossed it mid-air, so it never defines HAS_SPI_TFT and develop builds fall through to the I2C-OLED autodetect branch: no Screen is ever constructed and the display stays dark, while everything else (radio, GPS, BLE) works. Verified on a real W10: with the define, the boot log shows TFTDisplay creation, backlight power-on and the boot screen, and the ST7789 panel renders the UI again. Co-authored-by: Ben Meadors <benmmeadors@gmail.com> |
||
|
|
73f7b35bea |
Report the right hardware model on four boards (#11570)
Four variants declare a custom_meshtastic_hw_model that the build never reaches, so the device announces something else in NodeInfo and the apps cannot match it for OTA. Mini ePaper S3 (125) and Heltec V4 R8 (132) had no arm in the esp32 HW_VENDOR chain at all, so both fell through to #else and reported PRIVATE_HW. Heltec Mesh Node T096 (127) had none in the nrf52 chain and reported NRF52_UNKNOWN. WisMesh Tap V2 defines both RAK3312 and RAK_WISMESH_TAP_V2, and the generic RAK3312 arm sat first, so the board reported RAK3312 (106) instead of WISMESH_TAP_V2 (116). Order the specific arm ahead of the generic one, the same way the nrf52 chain already keeps custom RAK4630 boards ahead of the generic RAK4630. Verified by preprocessing each platform's HW_VENDOR chain with the env's full define set - build flags resolved through extends, the board JSON's build.extra_flags, and the bare #defines in the variant's own variant.h. All four now match their manifest, and rak3312, heltec-v4, heltec-v4-tft and the ThinkNode M9 arm added in #11567 are unchanged. |
||
|
|
f6f116a39d |
Fill in device registry metadata for recently added hardware (#11567)
Audit of the custom_meshtastic_* manifest on the variants backing the newest boards, against the protobuf HardwareModel enum, the compiled HW_VENDOR, the board flash size and the artwork actually published by the web flasher. No support flag changes here - actively_supported is left exactly as each variant already had it. ThinkNode M9 had no HW_VENDOR arm, so every M9 has been reporting PRIVATE_HW while its manifest advertised 131; add the mapping and rename the slug to the enum name (THINKNODE_M9) it is meant to mirror. Seeed SenseCAP Mesh-Tracker X1 moves from the PR matrix to release, and its images entry now points at seeed_mesh_tracker_x1.svg, which is what the flasher actually ships - the hyphenated name resolved to nothing. T-Beam BPF, T-Beam 1W and Heltec Wireless Tracker V2 declared the architecture as "esp32s3"; the value is copied verbatim into the manifest, and the flash flow matches on the normalized "esp32-s3". T-Beam BPF and M5Stack Unit C6L both build default_16MB.csv on 16 MB flash but declared no partition scheme, which leaves the flasher on the 4 MB fallback offsets for a legacy clean install. Meshnology W10 and W12 gain the artwork and vendor tag that already exist for them. |
||
|
|
4de20187f5 |
Actions: Update to trunk-io/trunk-action v2 -- remove annotations (#11563)
trunk-action v2 removed support for PR annotations (they have been broken for a while anyways) |
||
|
|
f22ce82f5a |
fix t-deck-pro: disable BHI260AP support until SensorLib replacement is available
Missed in the previous commit |
||
|
|
5f7077c44e | fix t-deck-pro-v1.1: disable BHI260AP support until SensorLib replacement is available (#11562) | ||
|
|
4d9d0f8a16 | chore(deps): Correct library dependencies for T-Deck Pro and T-Watch Ultra (#11561) | ||
|
|
4c640270f2 |
chore(deps): update lovyangfx to v1.2.27 (#11533)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> |
||
|
|
bc035bb812 |
feat(lora): state a pinned userPrefs preset as the unset region's intent (#11507)
* feat(lora): state a pinned userPrefs preset as the unset region's intent A vendor build can pin USERPREFS_LORACONFIG_MODEM_PRESET while leaving the region unset, so a fresh flash comes up as region UNSET plus a deliberate preset. Stock installs come up as region UNSET plus the LONG_FAST placeholder, and nothing in FromRadio told the two apart - so clients treat every unset-region node as factory-fresh and replace its preset with the region default as soon as the user picks a region. A mesh pinned to SHORT_TURBO loses every new node to LONG_FAST or LONG_TURBO, silently. getRegionPresetMap() now emits an UNSET entry when, and only when, the build pins a preset, stating that preset as both the group's sole entry and its default. Stock builds are unchanged on the wire: no UNSET entry, which clients already read as unconstrained. This is intent, not enforcement. supportsPreset() still accepts any known preset while the region is unset (#11496) and the radio is held silent either way, so the device continues to honour whatever the user or an admin sets. Costs one group slot and one region slot on pinned builds only (6->7 of 8, 34->35 of 38); exhaustion is logged and degrades to the existing unconstrained behaviour. * Trim comments to the project's one-to-two-line limit |
||
|
|
1afcdabbe9 |
chore(deps): update esp8266audio digest to 3430246 (#11557)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> |
||
|
|
68bfe015e6 |
ci: build newly added variants in the PR matrix (#11549)
* ci: build newly added variants in the PR matrix A new board declares board_level = release, so it gets no CI build until after merge. Build the first env of each platformio.ini added by a PR, regardless of board_level. Only added files qualify; adding an env to an existing config does not. * ci: also detect added variants in merge_group runs merge_group uses the same --level pr subset as pull_request, so a newly added variant was skipped there. Derive the diff base from github.event.merge_group.base_sha for those runs. * ci: fail the matrix step when the variant diff errors Process substitution hides the exit status, so a failed diff silently yielded an empty list and dropped the new board from the matrix. Capture into a variable so 'set -e' aborts the step instead. |
||
|
|
0b906b4d15 |
T-Watch Ultra support (#8171)
* feat: T-Watch Ultra support * fix init touch controller * add framebuffer * update to device-ui * trunk fmt * update amoled driver reference * PMU cosmetics * power off lora * fix NodeDB defaults * trySetRTC when fixedPosition * haptic touch (only BaseUI) * init lora RF switch * update LovyanGFX 1.2.19 * earlyInitVariant() adaptations acc. #9438 * update device-ui / touch handling * Set NFC_CS disabled on boot * Get t-watch-ultra working better on BaseUI * Fix compilation * Fix flash reads on t-watch-ultra * Get baseui drawing to the screen correctly again on t-watch and add touch IRQ handling * Add PMU IRQ handling * Add IMU support * Change define to avoid collision * BaseUI changes to support t-watch-s3 rounded screen (#10786) * BaseUI changes to support t-watch-s3 rounded screen * Extend margin work to CannedMessages * Finish merge * Get audio working on watch-ultra * trunk fmt * added custom_meshtastic boilerplate * T-Echo-Plus: disable BHI260AP while assumingly not implemented * Drop the duplicate origBold declaration from the merge * Inset incoming message bubbles on rounded screens * Fix RTTTL tempo, WiFi screen margins, PMU guard and a duplicate define * fix compile errror (the 2nd time) * fix SDcard * fix/workaround CO5300 pixel flush to SPI * trunk fmt --------- Co-authored-by: Jonathan Bennett <jbennett@incomsystems.biz> Co-authored-by: Thomas Göttgens <tgoettgens@gmail.com> |
||
|
|
389559bddb |
fix(NodeDB): re-derive my_node_num when ensurePkiKeys() mints the identity keypair (#11426)
* fix(pki): re-derive NodeNum when setting a region mints the identity key
A node's mesh address is derived from its identity key:
my_node_num == crc32Buffer(config.security.public_key.bytes, 32)
NodeDB::createNewIdentity() is what establishes that, and NodeDB::
generateCryptoKeyPair() is the only thing that called it.
CryptoEngine::ensurePkiKeys() generates or re-derives the keypair and writes
security.public_key, security.private_key and user.public_key - but never
re-derives my_node_num. Boot-time keygen is suppressed while the LoRa region is
UNSET (generateCryptoKeyPair()'s regionBlocksKeygen guard), so on a fresh device
my_node_num is still the MAC-derived value from pickNewNodeNum(). The user then
sets the region - the stock onboarding flow - ensurePkiKeys() mints a key, and
the invariant is broken.
The node then signs its broadcasts (Router.cpp signs when !pki_encrypted &&
(owner.is_licensed || isBroadcast(p->to))). Every receiver runs
verifyFirstContactNodeInfo, fails crc32Buffer(user.public_key) != p->from, and
drops the NodeInfo. The node's identity beacons are invisible to the mesh.
Nothing reboots to repair it: AdminModule sets requiresReboot = false for LoRa
changes ("All LoRa radio changes apply live via configChanged observer") and
MenuHandler ends at service->reloadConfig(changes).
Four call sites reached ensurePkiKeys():
1. AdminModule set_config LORA, region first set (phone app - the common path)
2. MenuHandler applyLoraRegion (on-device region picker)
3. InkHUD MenuApplet applyLoRaRegion (schedules a reboot, so it
self-healed at next boot)
4. portduino wasm wasm_set_region
The reference implementation was already in the tree: the *licensed* branch of
call site 1, thirteen lines below the broken unlicensed one, calls
nodeDB->generateCryptoKeyPair() (which reaches createNewIdentity()) and widens
the persisted mask with SEGMENT_DEVICESTATE | SEGMENT_NODEDATABASE.
Rather than repeat that at four call sites, the key-mint is routed through one
chokepoint that owns both halves of the identity: NodeDB::ensurePkiIdentity()
calls crypto->ensurePkiKeys() and then createNewIdentity(). It lives in NodeDB
because createNewIdentity() operates on the devicestate/node-DB globals, which
CryptoEngine deliberately does not touch - ensurePkiKeys() takes the security
config and user by reference precisely so it stays free of that dependency, and
it is unit-tested against a standalone CryptoEngine.
ensurePkiIdentity() returns true only when my_node_num actually moved
(createNewIdentity() early-returns when the key is unchanged, so a repeat region
change does not disturb the self entry or force a needless flash write). Callers
use that to widen their save mask; my_node_num lives in devicestate and the self
row moves in the node DB, so both segments must be persisted or the fix would
revert at the next boot. SEGMENT_CONFIG, which carries the key itself, is
already unconditional on all four paths.
The InkHUD reboot is left as-is. It is now redundant for this invariant, but it
covers the rest of that menu's behaviour and a redundant reboot is not a bug.
Adds test_handleSetConfig_persistsUnlicensedFirstRegionIdentity, the unlicensed
twin of the existing licensed test, asserting both the segment mask and
my_node_num == crc32(public_key).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* style(NodeDB): trim identity-recovery comments and guard the WASM nodeDB deref
Two review asks, no behaviour change on any built target.
Copilot flagged the unguarded nodeDB deref in the WASM region setter; it is the
only ensurePkiIdentity() call site that did not check the pointer first.
The rest is comment length. AGENTS.md:83 caps code comments at two lines, and the
identity-recovery comments across the four call sites plus the NodeDB.h doc block
ran to four and six lines. The rationale they carried is in the commit messages
and the PR body, which is where AGENTS.md says it belongs.
The PR's own fix in AdminModule.cpp is deliberately untouched.
* fix(NodeDB): keep the identity move authoritative when the self record cannot be created
createNewIdentity() removes the old node entry and assigns myNodeInfo.my_node_num
before it tries to create the row for the new number. If getOrCreateMeshNode()
came back null it returned false, so the first-region callers left
SEGMENT_DEVICESTATE and SEGMENT_NODEDATABASE out of the save mask.
The number had already moved in RAM at that point, and the freshly minted key
goes to flash under SEGMENT_CONFIG regardless. The next boot therefore reloads
the old number alongside the new key, which is exactly the
crc32(public_key) != my_node_num break this path exists to prevent, reached
through the error branch instead of the happy one.
Rolling the number back is not an option either, since the key has already been
replaced by the time this runs. So the move is now reported as the fact it is and
the missing self record is logged separately; getOrCreateMeshNode() will recreate
that row on the next contact. Reachable when the self record is absent and the
table is full of protected nodes.
Reported by CodeRabbit on #11426.
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
90a6dec3f3 |
fix(NodeDB): require a full 32-byte key when demoting to the warm tier (#11431)
* fix(NodeDB): require a full 32-byte key when demoting to the warm tier meshtastic_User.public_key is a wire `bytes` field with max_size 32, so any size in 0..32 decodes off the air, and nothing validates it on ingress: NodeInfoModule hands the decoded User straight to NodeDB::updateUser, whose PKI gates are all `== 32` and so fall through for a partial key, and TypeConversions::CopyUserToNodeInfoLite then stores it with the short size. demoteOldestHotNodesToWarm() admitted that partial key into the warm tier on a `size > 0` gate. WarmNodeEntry has no length field - it distinguishes "has a key" from "no key" purely by all-zero - so N real bytes plus 32-N zeros become indistinguishable from a genuine key. copyPublicKeyAuthoritative() then hands that fabricated key back with size = 32 and reports it AUTHORITATIVE, and re-admission writes size = 32 into the hot store. From then on updateUser's key pin permanently rejects the node's real NodeInfo, and DMs to it are encrypted to a key nobody holds. Require a full 32-byte key, so a partial one is absorbed as "no key" (nullptr) rather than as a truncated one. WarmNodeStore::place() already treats a null key as keyless and clears the slot's stale key when repurposing it. This aligns the site with its two siblings, which both already gate on `size == 32` (the purge path in cleanupMeshDB and the runtime eviction in getOrCreateMeshNode). The ingress gap - updateUser accepting a 1..31-byte key at all - is a separate, larger change and is left for its own review. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(NodeDB): shorten warm-demotion comment to two lines Repo guideline (AGENTS.md): keep code comments to one or two lines. Retains the non-obvious invariant - warm entries have no key length field - and drops the restated detail. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(NodeDB): cover short-key demotion into the warm tier A warm record stores 32 raw key bytes with no length field, so a partial hot-store key is indistinguishable from a real one once demoted. The public_key.size == 32 gate in demoteOldestHotNodesToWarm() is what keeps a truncated key from being laundered into a full-looking warm key, but nothing exercised it. test_migration_dropsShortKeyOnDemotion overflows the hot store with one node carrying a 31-byte key and asserts it lands as a keyless placeholder while a genuine 32-byte key still survives. push() grows a keySize parameter to seed the partial key, and clearWarm() gives the test an empty warm tier, which it needs because the warm store outlives setUp() and a prior run's warm.dat. Verified to discriminate: with the size gate reverted to size > 0 the new test fails on "a 31-byte key must not be demoted as if it were a full key", and passes again once restored. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(NodeDB): assert the keyless placeholder carries last_heard The test only proved a warm metadata row survived the demotion, not that the placeholder does the job the nullptr is there for, which is preserving last_heard when the key is dropped. Asserting the value needed the seeds fixing first. Warm entries pack role, protected category and the xeddsa flag into the low 7 bits of last_heard (WARM_TIME_MASK is 0xFFFFFF80), so warm time has 128 second granularity and the old seeds of 1, 2, 3 all quantised to 0. They are now multiples of 128, which keeps the demotion ordering identical and makes the values survive the round trip. Real last_heard is epoch seconds, so this is closer to production than the old counter was. Reads the entry through WarmNodeStore::take() rather than getOrCreateMeshNode(), which does not restore last_heard from the warm tier and would have been asserting a path that does not exist. Reported by CodeRabbit on #11431. --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Ben Meadors <benmmeadors@gmail.com> |
||
|
|
abd3348790 |
chore(deps): update meshtastic/device-ui digest to 44b86e1 (#11552)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> |
||
|
|
80f8611e65 |
feat(variants): add Seeed Wio Tracker L1 Pro 1W (#11542)
* fix(sx126x): allow boards to opt out of the PA optimization table Boards driving an external PA can define SX126X_NO_POWER_OPTIMIZATION_TABLE to use the fixed PA config instead of RadioLib's table, which is tuned for a bare SX126x. Default behaviour is unchanged. init() applies the fixed config after begin(), which programs power through the table. * feat(variants): add Seeed Wio Tracker L1 Pro 1W nRF52840 + SX1262 with a 1 W external PA, L76K GNSS, SH1106 OLED. Uses hw_model 144 (meshtastic/protobufs#1038), opts into SX126X_NO_POWER_OPTIMIZATION_TABLE and declares SX126X_MAX_POWER explicitly. The PA gain table is indexed by SX1262 output power in dBm. Requires protobufs#1038 and a protobuf regen before it builds. * chore(deps): bump RadioLib to 510e00cf Carries the current LR11x0 and LR2021 fixes. * fix(variants): correct L1 Pro 1W QSPI pins and clean up comments PIN_QSPI_* are logical pin indices. The QSPI flash sits at D19-D24 in variant.cpp, but the defines carried D21-D26 from seeed_solar_node, where that block does start at D21. D25 and D26 are trackball pins. Also replaces mis-encoded characters in the pin comments and drops the migration note, which referenced a private repo path and a stale PINS_COUNT. * fix(variants): move L1 Pro 1W out of the per-PR build matrix board_level = pr is the high-attention tier that builds on every PR. This board belongs with the mainline set, which uses board_level = release. |
||
|
|
9c027a24ea |
Toggle GPS and buzzer together on the ThinkNode M8 function button double click (#11551)
* Toggle GPS and buzzer together on the ThinkNode M8 function button double click * Shorten the comments added with the ThinkNode M8 double click toggle * Only sync the buzzer when the GPS mode actually toggles, and unmute before the tone plays |
||
|
|
bb6a81f1e9 |
Pass framebuffer rotation through DisplayDriverConfig (#11534)
* tftSetup: pass framebuffer rotation via DisplayDriverConfig Replaces the MESHTASTIC_FB_ROTATION environment variable with DisplayDriverConfig::rotation(), which device-ui reads in FBDriver::create(const DisplayDriverConfig &). * tftSetup: carry framebuffer rotation in the panel config Use the DisplayDriverConfig builder with panel_config_t::offset_rotation instead of a dedicated rotation setter. Width and height fall back to the device-ui defaults when the yaml does not set them. * tftSetup: pass the framebuffer panel config unfiltered Take Display.Width, Display.Height and Display.OffsetRotate straight from the portduino config, like the CUSTOM_TFT branch does. |
||
|
|
93d15a5368 |
Add AS3935 lightning sensor support (#10931)
* Add AS3935 lightning sensor support Implements meshtastic/firmware#10774: an AS3935Sensor (TelemetrySensor subclass) that reports lightning_strike_count_1h and lightning_distance_km on the normal environment telemetry interval, like a rain gauge - strikes are counted over a fixed rolling ~1h window and read non-destructively, so replying to a peer's telemetry request in between broadcasts can't silently drop counted strikes. The AS3935's IRQ pin (opt-in per board via AS3935_IRQ) is polled with a plain digitalRead() in runOnce(), deliberately not attachInterrupt(): the IRQ line is a level that stays asserted until its interrupt register is read, so polling can't miss an event regardless of timing, matching the SparkFun library's own reference examples. An interrupt would also buy nothing here even setting that aside - classification requires an I2C read (readInterruptReg(), which itself calls delay(2) per the datasheet's settle-time requirement), and blocking I2C/delay() calls aren't safe from ISR context on any of this codebase's target platforms, so the ISR could only ever set a flag for later draining - no less work than just polling the pin directly on the next tick. A genuine lightning classification also requests an immediate out-of-cycle send via a new EnvironmentTelemetryModule:: requestImmediateSend() hook. There's no fixed debounce on the request itself - EnvironmentTelemetryModule's existing airtime/duty-cycle gate already paces every send, so it sends as often as airtime allows rather than an arbitrary fixed rate. The request does expire after 5 minutes unfulfilled, so it can't fire an arbitrarily stale broadcast if airtime was blocked for a long stretch. The AS3935's I2C addresses (0x01-0x03) fall inside the range this codebase's I2C scanner otherwise skips as reserved, so detection is a small dedicated probe gated behind AS3935_IRQ and respecting the caller's address filter, rather than a change to the general scan loop. Presence is confirmed via a register write/readback round-trip rather than a fixed expected value, since the AS3935 has no WHOAMI register and a power-on-reset-only check can't survive a warm reboot that doesn't power-cycle the sensor (initDevice() permanently rewrites that register on first configuration). Generated files under src/mesh/generated/ are intentionally excluded from this commit - they're regenerated from the protobufs submodule by update_protobufs.yml, and hand edits get overwritten and conflict once the companion protobufs PR merges and the submodule pointer updates. Assisted-by: Claude Sonnet 5 <noreply@anthropic.com> Signed-off-by: Andrew Yong <me@ndoo.sg> * fix(as3935): calibration and telemetry logging initDevice() never called the library's calibrateOsc(). The AS3935's internal oscillators are calibrated against the antenna's resonance, which the AFE/watchdog/spike-rejection thresholds depend on; without it, only a directly-driven IRQ pin (bypassing detection entirely) reacted during testing. The sensor could already have a historical detection event latching the IRQ pin high before our initialization. Added an explicit drain read after the IRQ pin is configured, so the sensor doesn't start out stuck asserting IRQ. EnvironmentTelemetryModule::sendTelemetry() logs every other environment metric category on send but was missing lightning; added a matching log line. Assisted-by: Claude Sonnet 5 <noreply@anthropic.com> Signed-off-by: Andrew Yong <me@ndoo.sg> * Support AS3935 without an IRQ line, make the antenna trim configurable Detection no longer requires AS3935_IRQ. The probe is gated like the other environmental sensors, so an I2C-only breakout is found on any board. Where AS3935_IRQ is defined the pin still gates the I2C read, otherwise runOnce() polls the interrupt register, which latches until read. Antenna tuning capacitance moves to AdminMessage.sensor_config.as3935_config, persisted to /prefs/as3935.dat and defaulting to 96pF. The chip does not retain it across power loss. Disturbers are masked in the chip, since runOnce() now polls every second. The lightning telemetry log is guarded so nodes without the sensor no longer log it on every send. Requires meshtastic/protobufs#981. * Revert protobufs pointer to the develop baseline The submodule bump conflicts on merge and the generated headers come from an out of band CI job, so the pointer moves with that job rather than in this branch. * Report lightning strikes over a true rolling hour strikeCountWindow was zeroed on a fixed interval, so lightning_strike_count_1h reported strikes since the last reset rather than over the preceding hour. RollingCounter is a fixed memory sliding window: one counter per bucket, nothing stored per event, so a storm cannot grow it. The ring holds one bucket more than the window needs so none is recycled while part of it is still inside, and the oldest bucket contributes only the fraction still in range. Both are needed to hold the span at exactly the window length rather than letting it drift by a bucket either way. Expiry is exact to one bucket rather than to the event, which is below the 5 minute floor on mesh telemetry sends. The distance expires with the last strike in the window instead of on the interval reset. Covered by test/test_rolling_counter. * Widen the RollingCounter edge weighting to 64 bit counts * inWindow is a 32 bit product, so a bucket holding more than 2^32 / BucketMs events wraps. At a 5 minute width that is about 14k: a bucket of 50000 reported 11367 instead of 40000 once it reached the window edge. Below the threshold nothing changes, so lightning was unaffected, but the helper is meant to be reused by counters with far higher rates. test_large_burst_at_window_edge covers it. The existing burst test sampled only inside the window, where the bucket is whole and never weighted. * Trim RollingCounter comments to the house limit --------- Signed-off-by: Andrew Yong <me@ndoo.sg> Co-authored-by: Thomas Göttgens <tgoettgens@gmail.com> |
||
|
|
74119c088b |
fix(mesh): don't reference the position module on MESHTASTIC_EXCLUDE_GPS builds
The event-channel position-request reply added in #11545 calls positionModule-> replyOnPositionChannel() guarded only by USERPREFS_BLOCK_POSITION_ON_EVENT_CHANNEL. Targets that set MESHTASTIC_EXCLUDE_GPS (repeaters such as rak_wismesh_repeater_mini_hp) never construct PositionModule in Modules.cpp, so an event build for one of those fails to link: undefined reference to `PositionModule::replyOnPositionChannel(...)' undefined reference to `positionModule' Guard the call, the include and the isEventChannelPositionRequestForUs() helper with !MESHTASTIC_EXCLUDE_GPS, matching how AdminModule guards its positionModule use. A node with no position module has nothing to answer a position request with, so skipping the reply is the correct behavior there. Not reachable on develop, where the userpref defaults off and the whole block compiles out - it only breaks builds that enable it, which is why #11545 was green. Verified by building rak_wismesh_repeater_mini_hp with the pref enabled. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
9fcb289643 |
fix(thinknode_m9): define SPI_FREQUENCY for the non-MUI build (#11546)
The M9's variant.h defines ST7789_CS, so TFTDisplay.cpp compiles its ST7789 LGFX branch, which reads SPI_FREQUENCY for the panel write clock (SPI_READ_FREQUENCY, its pair, is already in variant.h). The flag was only set in the -tft env, so `build (thinknode_m9, esp32s3)` has failed on develop since the board landed in #10908: src/graphics/TFTDisplay.cpp:504:30: error: 'SPI_FREQUENCY' was not declared in this scope; did you mean 'SD_SPI_FREQUENCY'? Move the flag up into thinknode_m9_base, keeping the 75 MHz the -tft env already used for the same panel and matching the SD card's 75 MHz on the bus they share. The -tft env inherits the base flags, so device-ui's LGFX_GENERIC.h - which falls back to 20 MHz when the macro is absent - still sees the identical value. |
||
|
|
a5fc95f774 |
fix(mesh): coerce coordinate traffic to the position channel on event builds (#11545)
Under USERPREFS_BLOCK_POSITION_ON_EVENT_CHANNEL every coordinate packet a client aimed at the event channel was rejected with the "Location sharing is disabled on this channel" notification - including the phone's own location feed. Both apps hand a GPS-less node its fix as a POSITION_APP packet addressed to the node itself on channel 0; that packet never leaves the device (Router::sendLocal delivers it locally) but resolved to the event channel and was dropped before PositionModule saw it. Result: the toast on every location tick, and nodes without a GPS never learned a position to share on their private channel. Position traffic now converges on the position channel - findPositionChannel(), the first channel with non-zero on-wire precision, which is never the event channel: - From-us-to-us coordinate packets are exempt from the event block. - Local coordinate sends aimed at the event channel (phone share-location, request-position, waypoints, any module/UI originator) are moved onto the position channel in Router::sendLocal and PhoneAPI instead of rejected. The client notification is only sent when no channel carries positions at all. - A position request DM'd to us on the event channel is answered on the position channel at that channel's precision (request_id preserved, same reply throttle); the requester's coordinates are still not stored, forwarded, relayed or published. want_response from the bitfield is merged before the event-channel decode short-circuit so such requests are seen. - PositionModule::sendOurPosition, positionUnchangedSinceLastSend and MeshService::trySendPosition use the shared helper instead of three copies of the same walk. Non-event builds are unaffected: the coercion compiles out and the helper matches the previous walk. Tests: coverage-event-policy (test_event_channel_phone_api, test_event_channel_router, test_position_precision, test_mqtt, test_nexthop_routing) and the same suites with the policy off. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
8fe246e250 |
fix(mesh): relay foreign packets whose channel hash collides with a local channel (#11544)
* fix(mesh): relay foreign packets whose channel hash collides with a local channel
The channel hash is one byte, so a foreign channel's name/PSK can fold to the
same hash as a local channel (~1/256 per local channel held). Since
|
||
|
|
48699a7a48 |
fix(http): keep reaping open TLS connections under low heap so the heap can recover (#11539)
* fix(http): keep reaping open TLS connections under low heap so the heap can recover Once free heap dropped below MIN_HEAP_FOR_SSL (40 KB) with HTTPS connections open, the node's heap never came back and every later HTTPS or TCP-API connection failed until a reset - node alive, on WiFi, unusable. handleWebResponse() skipped secureServer->loop() entirely under low heap so no new TLS handshake would be attempted on a heap that can't hold its context. But HTTPServer::loop() is the only place already-accepted connections are serviced and reaped: its first pass calls ->loop() on each open one (where the 20 s idle timeout and the SSL close-notify state machine run) and deletes the closed ones. Skipping the whole loop froze the up-to-MAX_HTTPS_CONNECTIONS TLS sessions already open. Never looped, they never timed out, their mbedTLS contexts and pbufs were never freed, so free heap never climbed back over 40 KB, so the loop was skipped forever. The guard's own precondition was what kept it from clearing. Split the two halves. Under low heap keep driving and reaping the connections we already hold, and only skip the accept. HTTPServer keeps its connection table protected, so a thin MeshHTTPSServer subclass exposes serviceExistingConnections(), the first half of HTTPServer::loop() verbatim. Log line reworded to say what now happens: not accepting, not skipping. Verified on a Heltec V3 (Endor AP) against a control build with #11537 (so the node survives the squeeze instead of aborting first): - Recipe: held sockets on 80/4403 + pending TLS, 100 s of HTTPS pokes, repeat. Control: Low heap pins at 6-17 KB, HTTPS dead, and 3 min after all pressure is released heap is still ~12 KB with Low heap firing every 30 s - permanent until reset. Fix: never dips under 40 KB, both pressure rounds 3/3, 65 KB after. - Branch driven deliberately (verify-only heap hog pinning free heap at ~28 KB with a real idle TLS session held open): under the guard the fix logs open=1 -> reaped=1 at the 20 s idle timeout, and heap goes 26 -> 65 KB before the hog is even released. On the control logic that session stays frozen for the whole window. Fixes #11538. * fix(http): trim the low-heap comments to the two-line guideline The mechanism is in the commit message and PR; the source keeps the one-line why. No code change. (CodeRabbit) |
||
|
|
692adc8131 |
Update protobufs (#11543)
Co-authored-by: caveman99 <25002+caveman99@users.noreply.github.com> |
||
|
|
fe15786dc1 |
fix(api): stop rebooting ESP32 nodes when a client connects to a fragmented heap (#11537)
* fix(api): stop rebooting ESP32 nodes when a client connects to a fragmented heap
Connecting a client to an ESP32 node over WiFi/TCP rebooted the node. Two
allocations on the accept + config path use operator new, and on ESP32 that is
fatal when it fails: the framework builds with CONFIG_COMPILER_CXX_EXCEPTIONS=n
(esp32-common.ini), and ESP-IDF's cxx component then --wraps __cxa_throw and
every unwinder entry point straight to abort(). libstdc++'s operator new throws
std::bad_alloc on a NULL from malloc, so any new that cannot get its block is a
reboot with no chance to recover. Both hit on a Meshnology W12 running develop
|
||
|
|
ee401242aa |
Update protobufs (#11536)
Co-authored-by: vidplace7 <1779290+vidplace7@users.noreply.github.com> |
||
|
|
83fd62b756 |
test(native): add 14 suites for routing, persistence, parsing and identity gaps (#11515)
* test(native): add 14 suites for routing, persistence, parsing and identity gaps Coverage audit of the native test tree; adds the highest-value untested logic as 11 new suites and extends 3 existing ones (200 test functions). New: test_stream_framing, test_nodedb_boot_recovery, test_nodedb_legacy_migration, test_nodedb_v25_roundtrip, test_nodedb_identity_hygiene, test_channel_keys, test_reliable_ack_matrix, test_hop_start_policy, test_routing_response_hops, test_phone_api_config_dump, test_observer. Extended: test_rtc, test_mqtt, test_xmodem. Two source changes the audit produced: - StreamAPI::handleRecStream copied stream->read()'s `cInt < 0` EOF check into the buffer-fed path, where there is no EOF sentinel; with signed char any byte >= 0x80 (START1 is 0x94) aborted the parse. Read the byte as uint8_t directly. Latent on develop (no callers), pinned by test_stream_framing. - Extract the post-decode pre-hop predicate from Router::handleReceived into shouldSkipHandleForPostDecodeHop() (NodeDB.h) so test_hop_start_policy drives the exact expression the router calls. No behavior change. test/state-manifest.tsv declares the suites that construct a NodeDB. Full 68-suite Docker coverage run matches the pre-change baseline. * test(native): address review - harden observer dispatch, trim comments Review follow-ups on the coverage-audit suites: - Observable::notifyObservers() erased list nodes while holding an iterator into them, so an observer that unobserves itself from onNotify corrupted the dispatch. Today the only self-detacher (PhoneAPI::onNotify -> checkConnectionTimeout -> close -> unobserve) survives solely because it returns -1 and aborts the chain before the increment; that unwritten contract is now gone. Removal during a dispatch nulls the entry and the outermost notify sweeps afterwards, which keeps self-detach, next-detach and destruction-during-notify all safe without an allocation. Hoisting the next iterator instead would have inverted the hazard and broken the existing next-detach case. Two regression tests added. - Correct the documented caller of shouldSkipHandleForPostDecodeHop: the call is in Router::dispatchReceived, not handleReceived. - Cast hop fields to unsigned at the %u call site in test_hop_start_policy. - Trim the new suites' file headers to the one-or-two-line rule in AGENTS.md. - Rename eight test functions whose names were exactly `test_` + 35 chars: that is the shape of a Lob API key, so trufflehog flagged them as secrets and failed the Trunk CI check. Full 68-suite Docker coverage run matches the pre-change baseline. * test(native): revert the observer dispatch change, keep the contract test Backs out the notifyObservers() deferred-removal hardening from the previous commit. It was reviewer-driven scope creep: nothing in the coverage audit needed it, no test required it, and it changes dispatch semantics in a header with ~76 observe() call sites on native verification alone. The hazard it addressed is not reachable today. The only observer that unobserves itself from onNotify is PhoneAPI (onNotify -> checkConnectionTimeout -> close -> unobserve), and it returns -1, which aborts the chain before the iterator is advanced past the erased node. test_self_detach_with_abort_during_notify stays: it passes against the unmodified dispatch and pins that the -1 is load-bearing, so a later cleanup that "simplifies" it away goes red. The unsafe variant (self-detach returning 0) is documented in a comment rather than tested, since asserting it would be asserting UB. * fix(serial): recover the frame behind a stray framing marker A byte that failed the START2 check was discarded rather than re-tested as a possible START1, so 0x94 0x94 0xc3 ... lost the real frame: one corrupted byte on a noisy UART silently dropped the frame behind it. Re-test the byte in place instead. Applied to both copies of the receive state machine. readStream() is the one that matters in the field - it is the serial path every phone client uses - while handleRecStream() still has no callers on develop. Strictly widens what the parser accepts; no frame that parsed before parses differently. test_stream_framing covers it on both receive paths, plus a run of stray markers and a START1-then-unrelated-byte resync. This was originally documented as a known gap in the framing suite. Fixing it instead was NomDeTom's call on review: a passing test asserting the bad behavior is what makes it hard to change later, and it is the same defect shape as the signedness fix three functions away. Also: use Throttle::deadlinePassed() in test_reliable_ack_matrix rather than a bare millis() compare, matching the house deadline rule. * test(native): cover the stray-marker resync on the buffer path too The stray-marker fix went into both copies of the receive state machine, but only test_stray_start1_before_frame_still_delivers drove both. The repeated- marker and unrelated-byte cases drove readStream() alone, so a regression in handleRecStream() would have gone unnoticed by two of the three. Verified load-bearing: reverting only the handleRecStream() half of the fix turns test_repeated_stray_start1_before_frame_still_delivers red on the new assertion. test_start1_then_unrelated_byte_resyncs stays green under that mutation by design - its failing byte is 0x00, where both branches reset to 0 - and covers the other half of the ternary. Also drops the stale header on test_stray_start1_before_frame_still_delivers, which still described the gap as pinned-as-is after the fix landed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(native): make the hop-start truth table assert the rows it prints test_truth_table_summary was six TEST_MESSAGE lines and no assertion, so it reported as a case that could not fail - the anti-pattern #11517 names in its unfinished assertion-presence lint, and the one exception to NomDeTom's "no RUN_TEST without an assertion" pass over this PR. The printed row and the checked expectation now come from one struct, so the summary cannot narrate a table the predicates no longer implement. It also covers the consequence columns the per-row tests do not assert together: classifyHopStart, shouldDropPacketForPreHop and shouldSkipHandleForPostDecodeHop for the same packet, with the expectations gated on MESHTASTIC_PREHOP_DROP. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
e1ea653a45 |
fix(graphics): crash and leak fixes across display drivers (#11455)
* fix(graphics): crash and leak fixes across display drivers - TFTDisplay (portduino): _touch_instance was an uninitialized member, and the touch-config block only assigns it for xpt2046/stmpe610/ ft5x06 while the guard accepts any configured module. A gt911 entry in config.yaml (supported by the color-UI path) reached _touch_instance->config() through an indeterminate pointer. Initialize to nullptr and guard the config block. - Screen: the destructor freed normalFrames but leaked the owned dispdev (driver + framebuffer) and ui objects. Screen is genuinely destroyed on the portduino reboot path (screen = nullptr in Power.cpp). - EInkDisplay2: GxEPD2_BW's constructor takes the low-level driver by value and stores a copy, so the 'new EINK_DISPLAY_MODEL' at nine sites was orphaned the moment connect() returned. Pass temporaries, as GxEPD2Multi already does. - EInkParallelDisplay: the async full-refresh task cleared asyncFullRunning before nulling asyncTaskHandle, so the destructor could observe running==false with a stale handle and vTaskDelete a freed TCB. Null the handle first (same ordering fix the eink/Drivers/EInkParallel.cpp sibling already carries). - Panel_sdl: initFrameBuffer only null-checked the first of its three allocations and returned true regardless, leaving the line array full of null+offset garbage on failure; later redraws would write through those. Check all three, release partial allocations, and return false. * fix(graphics): propagate Panel_sdl framebuffer allocation failure from init() Per review: initFrameBuffer() can now fail cleanly, so init() must not register the monitor and report success when it does. |
||
|
|
c308d0aca4 |
feat: Support Elecrow ThinkNode M9 (#10908)
* thinknode-m9 variant * move lora to SPI1 device * enable SDcard * use HSPI * BaseUI tft -> HSPI * buzzer, webdav lib * fix build issues * M9 default to MUI, no BT, short ringtone * add keyboard long-press config * update variant * add ThingNode-M9 GPS string * GPS 115200 baud * Basic BaseUI support * Fixup power detection * Compass and KB fixes for M9 * add timed Lock::lock() * add SD card * point device-ui to thinknode m9 draft branch * trunk fmt * fix FusionCompass * Fix t-deck-tft linker arg list overflow in CI * SDcard/lora fix: SPI1 must not be declared twice in arduino 3.x -> reuse SPI1 defined in FSCommon.cpp * update battery parameters * reinit SD card when updating; fix PSRAM size * update lib versions * fix wakeup on key press (KB_INT) * fix default nag_timeout for TFT/MUI devices with buzzer * increase PSRAM and SD freq * trunk fmt * update lovyanGFX 1.2.26 * update device-ui commit reference * fix screen definition * remove DONE; maybe a keyword or other used identifier * fixed CI error nag_timeout * fix prepareSleep initialization * trunk fmt * reduce SD SPI frequency * update device-ui * fix SDcard issue * stage * fix device-ui commit reference * fix device-ui commit * update device-ui commit (fixed keyboard lag) * fix QMI8658 * trunk fmt * update .ini meta information, align SD freq * fix device-ui reference to target (ready to merge) * device-ui for all other targets * make the rabbit happy * trunk fmt * fixed lock screen * fix compile error * SPI lock timeout * apply device-ui fix * revert bad RadioLib commit hash in platformio.ini Co-authored-by: mverch67 <71137295+mverch67@users.noreply.github.com> * fix wrong commit hash change * fix fix commit fix * I love changing random numbers in random files --------- Co-authored-by: Jonathan Bennett <jbennett@incomsystems.biz> Co-authored-by: Ben Meadors <benmmeadors@gmail.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> |
||
|
|
ef2be877a5 |
Stop breaking TestUtil.cpp on Windows, dangit! (#11529)
* Stop breaking TestUtil.cpp on Windows, dangit! * Update test/TestUtil.cpp Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> |
||
|
|
5dffd22584 |
Update protobufs and classes (#11531)
Co-authored-by: caveman99 <25002+caveman99@users.noreply.github.com> |
||
|
|
3a7c499722 |
fix(mesh): dedup opaque relays to prevent an undecryptable-frame broadcast storm (#11522)
* NextHopRouter: dedup opaque relays to prevent a broadcast storm
Undecryptable ("opaque") frames are relayed by relayOpaquePacket(), which by
design never enters PacketHistory - so unauthenticated frames can't poison
next-hop learning or ACK matching (packet-authenticity policy,
|
||
|
|
c773049b1d |
I2C reclock guard - avoid gazillion calls to reclock on SENXX sensors (#11412)
* Add SEN6X * Adds new SENXX class for SEN5X and SEN6X * Adds CO2 sensor calibration class to be shared among othre CO2 sensors * Make existing CO2 sensor draw from CO2Sensor class * Minor coment for CO2 sensor class * Move away from getRTC in SENXX class to keep track of time changes. * Change all sensors to millis for tracking time, instead of using getRTC * Add comments regarding VOC state * Avoid storing non-valid RTC Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Avoid CO2 sensor warm-up time to be below PM measured started Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Fix limits in CO2 sensor calibration Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Add pragma once on headers * Avoid non-working ASC commands Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Fix data poll * Move pm measure started before warmup check * Make cleaning non-blocking * Restore previous state if cleaning fails. Fix data ready condition. * Fix CO2 sensor checks for calibration * Add ReClockI2C guard to simplify calls to Reclock. Make SENXX calls to reclock outside of readBuffer, to avoid bizillion calls * Add new reClockGuard to all sensor classes that require it * Make clock guard store values on each construction and restore them directly * Reduce log messages * Update ADS1X15 to new guard --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> |
||
|
|
bca7c0b480 |
Tom fiddles with the test suite - again (#11517)
* test: make every suite run its own binary, and fail the run when it does not PlatformIO links every native test program to the one $BUILD_DIR/$PROGNAME path and attributes Unity output by text alone, never checking that the source file a case came from belongs to the suite it thinks it ran. Both harnesses had been split into a build pass (--without-testing) and a run pass (--without-building), and for a non-embedded platform the run pass never relinks - so all 57 suites executed whichever suite was linked last, each reporting PASSED under its own name. Introduced for CI in |