Commit Graph
12223 Commits
Author SHA1 Message Date
Thomas GöttgensandGitHub fb922c2413 fix(tracker-x1): silence the buzzer during init (#11007) 2026-07-14 10:44:31 +02:00
Jonathan BennettandGitHub eb4e24b2f6 Avoid setting HeaderBackground for transparent backgrounds (#11002) 2026-07-14 09:22:57 +02:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
1af7048c18 Update meshtastic-esp8266-oled-ssd1306 digest to a6adfe3 (#11003)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-14 09:19:25 +02:00
dd509a8ecf Compute GeoCoord's UTM/MGRS/OSGR/OLC lazily instead of eagerly (#10996)
GeoCoord's constructor unconditionally computed all five coordinate
representations (DMS, UTM, MGRS, OSGR, OLC) via setCoords(), even
though most callers only ever read one. The UTM conversion alone pulls
in a chain of libm trig functions (atan, __kernel_tan, __ieee754_acos,
__ieee754_pow, __kernel_rem_pio2/__ieee754_rem_pio2) that then have to
be linked in regardless of whether anything is ever displayed.

The only screen consumer (UIRenderer.cpp's GPS coordinate display)
already dispatches on a single configured format and reads exactly one
representation per call - never more than one. Compute DMS eagerly
(cheap, most commonly needed) and defer UTM/MGRS/OSGR/OLC to first
access via their own getters, tracked with per-representation mutable
dirty flags, so a caller that never touches a given representation
never pulls in its conversion code or the trig functions it needs.

On stm32wl, GeoCoord's heavy constructor is reached only through
NMEAWPL.cpp's NMEA/CalTopo serial export (GPS-gated, so wio-e5 only),
which only ever reads the DMS getters - so this recovers 8,288 bytes
flash there (of the 10,172-byte ceiling if the whole feature were cut)
with the feature fully intact and zero behavior change on any
platform.

Updated test_geocoord_extreme_coords_no_oob (the existing regression
test for a historical out-of-bounds crash in the UTM/MGRS conversion
on extreme lat/lon) to explicitly call each representation's getter,
since it previously relied on the constructor eagerly triggering all
four conversions - which this change intentionally defers. Verified:
full native test suite passes (586/586 test cases, including this one
under ASan), and rak4631 (nRF52, screen-equipped, where the
UTM/MGRS/OSGR/OLC getters are actually reachable) builds successfully.


Assisted-by: Claude Sonnet 5 <noreply@anthropic.com>

Signed-off-by: Andrew Yong <me@ndoo.sg>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2026-07-13 20:04:24 -05:00
269b974e96 feat: populate MyNodeInfo.device_id on all platforms (#10995)
* feat: populate MyNodeInfo.device_id on all platforms

RP2040/RP2350 use the 64-bit pico unique board id, STM32WL the 96-bit
silicon UID, ESP32-S2 joins the existing OPTIONAL_UNIQUE_ID efuse branch,
and everything else (classic ESP32 in particular) falls back to a
deterministic factory-MAC-derived id, resolving the long-standing FIXME.
Portduino keeps the config-supplied id preferred and now uses the MAC
fallback when the config omits one.

No proto or persistence changes; the id is re-read from silicon each
boot and PhoneAPI still zeroes it for unauthenticated clients.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: declare zero_mac const to satisfy cppcheck

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: address review feedback on device_id derivation

Clear any disk-loaded device_id before the silicon derivation so a failed
read leaves it unset rather than stale (Copilot), and size the portduino
config copy with sizeof instead of a literal 16 (CodeRabbit).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor: extract device_id generation into per-arch getDeviceId()

Per review feedback on #10995: move the platform-specific
MyNodeInfo.device_id derivation out of the #if/#elif ladder in
NodeDB.cpp into a getDeviceId() interface (target_specific.h)
implemented per-architecture alongside each platform's getMacAddr():

  - esp32:     efuse OPTIONAL_UNIQUE_ID (C3/S2/S3/C6); classic ESP32 -> MAC
  - nrf52:     FICR DEVICEID + DEVICEADDR
  - nrf54l15:  FICR->INFO.DEVICEID + DEVICEADDR (NRF_FICR-guarded, MAC fallback)
  - rp2xx0:    pico_get_unique_board_id()
  - stm32wl:   HAL_GetUIDw0/1/2()
  - portduino: config-supplied id preferred, else MAC

The shared MAC-derived fallback moves to meshUtils as
getMacAddrDeviceId(). NodeDB.cpp now zero-inits the field and makes a
single getDeviceId() call, dropping ~65 lines of platform boilerplate
plus the esp_efuse/pico/stm32 includes that came with it. No behavior
change: device_id is still re-read from silicon each boot and never
persisted.

Builds green: native-macos, tbeam, heltec-v3, rak4631, rak11310, rak3172.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix: address review of device_id refactor

- getMacAddrDeviceId(): zero-init the mac[6] buffer. getMacAddr() can return
  without writing (e.g. Portduino with no MAC source), so the old uninitialized
  buffer let stack garbage pass the all-zeros guard and become device_id. The
  pre-refactor code relied on the zero-initialized static ourMacAddr; restore
  that guarantee.
- nrf54l15 getDeviceId(): drop the `#if defined(NRF_FICR)` guard and read FICR
  unconditionally (as the pre-refactor NodeDB code did). The guarded #else fell
  back to getMacAddr()'s hard-coded placeholder MAC, which would give every unit
  an identical device_id; a missing NRF_FICR should be a loud compile error.
- NodeDB.cpp: `#include "target_specific.h"` instead of hand-copied externs for
  getMacAddr/getDeviceId; retire the stale FIXME. Same for meshUtils.cpp (whose
  extern comment wrongly claimed the TU was Arduino-free).
- Delete the orphaned commented-out device_id hex-dump block in NodeDB.cpp.
- Trim/de-duplicate the getDeviceId contract comments (single-sourced in
  target_specific.h).

Builds green: native-macos, tbeam.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: trim device_id comments to the repo's 1-2 line guideline

Addresses CodeRabbit review nitpick on #10995: shorten the getDeviceId()
(target_specific.h), getMacAddrDeviceId() (meshUtils.h), device-id refresh
(NodeDB.cpp), and nrf54l15 getDeviceId comments to two lines each, per the
"one or two lines maximum" coding guideline. Comment-only; no behavior change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 16:28:25 -05:00
fe288a70e4 stm32wl: exclude PKT_HISTORY_HASH, RANGETEST, WAYPOINT, POWER_TELEMETRY (#10992)
Adds four MESHTASTIC_EXCLUDE_* flags to stm32_base, applying them to
every stm32wl variant (rak3172, wio-e5, russell, milesight_gs301,
CDEBYTE_E77-MBL):

- PKT_HISTORY_HASH: mirrors the accepted nRF52 precedent
  (variants/nrf52840/nrf52.ini) - drops PacketHistory's hash index;
  the O(n) fallback is negligible given stm32wl's small node table.
  No feature loss.
- POWER_TELEMETRY: only affects external power-monitor ICs
  (INA219/INA260 etc.) via PowerTelemetryModule; internal battery ADC
  reading (src/Power.cpp) is a separate, ungated code path and is
  unaffected. No current stm32wl variant wires up external
  power-monitor hardware.
- RANGETEST: real feature loss on wio-e5 specifically, the only
  stm32wl variant with GPS enabled (RangeTest requires GPS to run at
  all, so it was already dead on every other variant).
- WAYPOINT: real feature loss on rak3172/wio-e5 (russell already
  excluded this individually; that duplicate is removed here since
  stm32_base now covers it).

Measured on wio-e5 against upstream/develop @ 7a25ffd0a: 2,768 bytes
flash saved (240,648 -> 237,880), 260 bytes RAM saved. Sanity-built
russell and rak3172 to confirm no regressions from the russell
dedup.


Assisted-by: Claude Sonnet 5 <noreply@anthropic.com>

Signed-off-by: Andrew Yong <me@ndoo.sg>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2026-07-13 07:08:20 -05:00
IrisandGitHub 5e99139a43 Guard LR1121/LR2021 radio configs with #ifdef (#10998)
Add conditional compilation guards for LR1121 and LR2021 radio chip configurations in rfswitch.h to prevent compilation errors when these radios are not in use. Move USE_LR2021 definition to the radio section in variant.h for consistency with other radio chip definitions.
2026-07-13 05:50:56 -05:00
kevin fd2e3e0e1f up md 2026-07-13 18:32:30 +08:00
Jonathan BennettandGitHub 77292c8415 Fid double free in virtual keyboard (#10999)
Found via portduino. As there are two separate references to this pointer, they both could call free
2026-07-12 16:00:24 -05:00
5513c36757 Add helpful checks of channel names and PSK (#10792)
* added warnings from firmware for simple channel setting mistakes

* more and better checks

* one ping only

* Drop literal 'AQ==' from blank-PSK channel warnings

The base64 encoding of the default channel key isn't something a user
should type in by hand; state the condition instead of a raw key value.

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-07-12 07:43:28 -05:00
Thomas GöttgensandGitHub 7a25ffd0a2 fix: evaluate pre-hop hop_start bitfield guard post-decode instead of before decryption (#10758) 2026-07-11 12:10:25 -05:00
Ben MeadorsandGitHub c5355641d3 Add MEDIUM_TURBO modem preset (#10988)
* Protobufs

* Wire up MEDIUM_TURBO modem preset

MEDIUM_TURBO (500 kHz, SF9, CR 4/5) already existed in the protobuf enum but
was never wired into firmware, so selecting it silently fell through to the
LONG_FAST default and rendered an "Invalid" display name.

Add its bw/sf/cr mapping (modemPresetToParams), display name (MediumTurbo/MedT),
PRESETS_STD membership (standard regions only — 500 kHz does not fit EU868's
250 kHz band, so it stays out of PRESETS_EU_868 and is rejected/clamped there),
and the MEDIUM SNR-grading bucket. Includes positive coverage in test_radio,
EU868-reject + US-accept coverage in test_admin_radio and test_mesh_beacon,
the STD preset count 9->10, an extended fuzz range, and the client-spec doc.

* Address review feedback on MEDIUM_TURBO tests

- test_mesh_beacon: assert has_mesh_beacon before checking the invalid preset was
  cleared, so the EU868-cleared test can't pass on a dropped message (matches the
  existing SHORT_TURBO test).
- test_fuzz_packets: draw modem presets from _ModemPreset_ARRAYSIZE instead of a
  hard-coded 17 so the fuzz range tracks future enum additions automatically.
2026-07-11 08:24:35 -05:00
p0nsandGitHub ca833d944c Fix serial protobuf corruption on short USB CDC writes (#10976)
* Fix serial protobuf corruption on short writes

SerialConsole shared raw debug output and framed protobuf traffic on one
HWCDC stream. Raw text could interleave inside an active frame when API
logging was disabled. Separately, HWCDC deliberately returns a short write
after bounded backpressure; StreamAPI abandoned that frame after PhoneAPI
had already advanced, so the next 0x94c3 header landed inside the previous
declared payload.

Suppress all unframed output after protobuf mode starts and honor the
existing config-replay log pause. For HWCDC, retain short frame tails in the
persistent tx buffers and finish them on later loop passes before dequeuing
another FromRadio packet. Logs remain best-effort and only start when their
complete frame fits; main packets are deferred rather than dropped if a
synchronous log is pending. Do not call HWCDC flush for framed serial output,
since its no-progress path can discard queued bytes. TCP and non-HWCDC
transports keep their existing behavior.

Validated on Cardputer ADV (200-node DB) and Heltec Tracker V2: 400 initial
full-DB sessions plus 140 final sessions across both API-log settings, zero
malformed frames/timeouts/incomplete DBs; 2-20s forced reader stalls resume
with complete DBs; abrupt stalled-client replacement 5/5 per board; 150s
post-close reboot counts stable. Builds pass for Cardputer, Heltec, tbeam,
and rak4631.

* Add serial frame continuation regression tests

Extract the HWCDC pending/deferred frame state machine into a small
transport-independent helper so native tests exercise the same production
logic used by SerialConsole. Keep framing, persistent buffer ownership and
locking in SerialConsole.

Cover short-tail continuation, deferred main-frame ordering, best-effort log
admission, bounded zero-progress calls, generic StreamAPI failure semantics,
PhoneAPI advancement gating, framed-log gating and raw output suppression.
The coverage suite passes 31/31 suites and 582/582 tests.

* Address review: guard flush, assert deferred invariant, dedup framing

- Make SerialConsole::flush() a no-op in protobuf mode: HWCDC::flush()'s
  no-progress path discards queued TX bytes, which would tear a framed
  stream when the sleep path flushes with a stalled host.
- Assert the single required-frame producer invariant in
  StreamFrameWriter::writeFrame() instead of silently dropping a second
  required frame.
- Hoist 0x94C3 header construction into StreamAPI::buildFrameHeader() so
  SerialConsole no longer re-hardcodes the framing constants.

* Test retained serial tail across client replacement

Model a replacement client arriving while an older required frame has an
unwritten tail. Require the old frame to complete before the new frame starts,
so a new 0x94C3 header can never land inside the old declared payload.

* Document serial frame APIs and regression tests

Add concise Doxygen comments for the frame continuation hooks, production
helper, native test doubles, and regression scenarios. Document why retained
frame tails intentionally survive client disconnects: HWCDC may still hold the
accepted prefix, so dropping metadata could insert a new frame header inside
the old declared payload.
2026-07-11 06:27:50 -05:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
8e27a8c715 Update actions/stale action to v10.4.0 (#10985)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-11 05:32:02 -05:00
ManuelandGitHub f52e2ea8ef fix SPI1 instances (#10983) 2026-07-10 15:00:32 -05:00
Ben MeadorsandGitHub 8abae90838 Give the 4MB T3-S3 boards an app partition that fits (#10978)
* fix(device-install): flash littlefs at the offset from firmware metadata

device-install.sh read the spiffs offset out of the .mt.json metadata into
SPIFFS_OFFSET, but flashed the littlefs image at $OFFSET -- a separate
variable still holding the hardcoded 0x300000 default. The metadata value
was never used, so any board with a non-default partition table had its
filesystem written to the wrong place.

Unify on SPIFFS_OFFSET, and guard both metadata overrides so a table that
omits ota_1 or spiffs falls back to the built-in default instead of handing
esptool an empty offset. Quote both offsets at the call site: the spiffs one
is newly consumed from jq, and a multi-line result would otherwise word-split
into esptool's argv and silently mis-pair address with file.

device-install.bat already does all of this correctly; this brings the shell
script to parity.

* fix(tlora-t3s3): give the 4MB T3-S3 boards an app partition that fits

tlora-t3s3-v1 and tlora-t3s3-epaper have overflowed the shared 4MB
partition-table.csv app slot (ota_0 = 0x250000 = 2,424,832 bytes) on every
develop build since 22072c5f4 (2026-06-20). The last green build,
ca7d82629, cleared it by 881 bytes; develop is now ~38.7KB over. These envs
have no board_level, so they only build in the full matrix on push to
develop -- which is why no PR ever caught it.

Add partition-table-t3s3.csv, dedicated to the 4MB ESP32-S3FH4R2 T3-S3
boards, and point all three t3s3 envs at it:

  app      ota_0  0x010000  0x290000   (was 0x250000, +256KB)
  flashApp ota_1  0x2A0000  0x0A0000   (unchanged size)
  spiffs          0x340000  0x0C0000   (was 0x100000, -256KB)

The headroom comes out of spiffs, not flashApp: the unified BLE OTA image is
636,544 bytes against a 655,360 byte slot, so ota_1 has nothing to give. The
table is contiguous, ends exactly on the 4MB boundary, keeps both app
partitions 64KB-aligned, and leaves the littlefs image these boards ship well
inside the remaining 768KB.

Verified with a Docker build of all three envs:

  tlora-t3s3-v1             2,463,504  91.7%
  tlora-t3s3-epaper         2,458,487  91.5%
  tlora-t3s3-epaper-inkhud  2,400,851  89.4%

Trimming was considered and rejected. The entire emoji set is 6,304 bytes
(measured A/B build), and cutting traffic management plus the paxcounter,
storeforward, rangetest and atak modules recovers 46,636 -- enough to land at
99.7% full, i.e. weeks of headroom at develop's observed growth rate, in
exchange for shipping a feature-reduced board. The slot was simply mis-sized
for a board this full.

Note this changes flash layout: existing T3-S3 units must be erased and
factory-flashed once to pick up the new offsets. They already cannot run a
current develop build, so no working configuration regresses.
2026-07-10 10:18:09 -05:00
Thomas GöttgensandGitHub 838323cc07 Quote esp-idf include paths in esp32.ini for Windows builds (#10980)
${platformio.packages_dir} resolves to a backslash path on Windows.
PlatformIO parses build_flags with POSIX shell rules, which strip the
backslashes and turn the NimBLE include paths into broken relative
paths (d:platformiopackages/...), failing the build with
'host/ble_gap.h: No such file or directory'. Quoting the flags
preserves backslashes through parsing.
2026-07-10 10:17:47 -05:00
HarukiToredaandGitHub f4efbd9149 tlora-t3s3-epaper: e-ink startup and build cleanup (#10973)
* T3s3 Eink

* Update SerialConsole.cpp

* Update

* Update SerialConsole.h

* Update EInkDisplay2.cpp
2026-07-10 05:50:30 -05:00
Jonathan BennettandGitHub 9060ab4418 add linuxJoystick input module (#10970)
* add linuxJoystick input module

* close epollfd to avoid leaks
2026-07-10 00:29:23 -05:00
IrisandGitHub 91043faced add configs for FrameTastic and PiTastic (#10959) 2026-07-09 15:07:59 -05:00
Ben MeadorsandGitHub b3ddeca9d0 ci: nightly develop build published to github.io firmware-nightly/ (#10957)
Adds a scheduled (09:00 UTC) run of the CI build off develop that does
everything a workflow_dispatch build does except create a GitHub release.
Instead it refreshes a single, stable firmware-nightly/ folder on
meshtastic.github.io with the current develop build, leaving that folder's
hand-maintained release_notes.md untouched so it can be edited manually.

On a nightly run (the schedule, or a manual nightly=true dispatch) only the
firmware build + gather-artifacts + the new publish-nightly job run; the
tests/docker/wasm/macOS/debian-src/size jobs and the three release jobs are
skipped, so no release or tag is created.

publish-nightly downloads the per-board artifacts, generates the
firmware-<version>.json release manifest and an index.json version pointer
(read by the web-flasher), then publishes to firmware-nightly/ via the same
peaceiris/actions-gh-pages action used for releases. keep_files:false is scoped
to destination_dir, so it clears stale nightly binaries while leaving sibling
release folders untouched; the hand-maintained release_notes.md is fetched and
carried forward first (fail-closed) so it is never clobbered.
2026-07-09 11:52:30 -05:00
144b07986b Fix ESP32-S3 USB CDC: post-disconnect task-WDT reboot from blocking console log writes (#10956)
* Fix ESP32-S3 USB CDC: post-disconnect task-WDT reboot from blocking console log writes

When a serial API client disconnects (USB cable still attached), the host
stops draining the USB-Serial/JTAG CDC buffer. The next raw-text debug log
write can then block the main loop task indefinitely (measured: a single
write blocked 52.4 s). The loop task stops feeding the app task watchdog
(APP_WATCHDOG_SECS = 90 s, trigger_panic), so the device reboots with
esp_reset_reason = ESP_RST_TASK_WDT ~97 s after every serial disconnect.

On 2.7.x (arduino-esp32 2.x / IDF 4.4) the reboot also re-enumerated USB;
since the Arduino 3.x migration the reboot is silent (USB stays enumerated),
making it look like a random reboot ~90 s after using the CLI.

Fix: on USB CDC targets, keep console TX in non-blocking mode (txTimeout 0,
drop-oldest) whenever no API client is provably alive, and restore the
normal bounded timeout while a client is connected so protobuf API frames
are never truncated. Toggle points: boot, handleToRadio (host sent bytes),
and onConnectionChanged (set non-blocking before disconnect handling emits
more log lines to a dead port).

Repro/validation on HELTEC_WIRELESS_TRACKER_V2 (macOS + Linux hosts):
open+close any meshtastic-python session, wait 150 s: reboot_count +1 every
time on unpatched builds; flat with this fix. Max observed log write stall
drops from 52405 ms to 30 ms.

* Condense comments per review feedback

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2026-07-09 11:42:15 -05:00
6768bd08df fix(modules): null-check packetPool/clientNotificationPool/mqttClientProxyMessagePool allocations (#10951)
* fix(modules): null-check packetPool/clientNotificationPool/mqttClientProxyMessagePool allocations

Follow-up to fix/meshpacket-alloc-null-checks (PR meshtastic/firmware#10948),
which fixed the core routing path (Router/NextHopRouter/ReliableRouter/
MeshService/RadioLibInterface) after reproducing a HardFault on STM32WL
hardware under real mesh traffic. This covers the same allocCopy()/
allocZeroed() unchecked-return pattern in lower-frequency paths that were
out of scope for that PR: SimRadio.cpp (portduino sim RX/TX), NodeInfoModule,
the Telemetry modules' power-saving-sleep notifications, MQTT (map report,
client-proxy messages, config-validation notifications), PositionModule,
SerialModule, and KeyVerificationModule.

Sites where the allocation result was already read back through an
existing null check downstream (e.g. Telemetry's lastMeasurementPacket,
SimRadio's receivingPacket, AdminModule::sendWarning) were left as-is -
verified safe, not touched.

Router::allocForSending() (and everything that funnels through it, e.g.
allocDataProtobuf()) remains unguarded and out of scope here too - it's
called from 30+ sites across the codebase and needs its own audit.

Assisted-by: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Andrew Yong <me@ndoo.sg>

* fix(KeyVerificationModule): bound cn->message writes with snprintf

CodeRabbit flagged unbounded sprintf() writes into the fixed-size
ClientNotification.message buffer as a static-analysis nitpick while
reviewing this PR's allocation null-checks. Align these four sites with
the snprintf(dest, sizeof(dest), ...) pattern already used for the same
field elsewhere in this PR (SerialModule.cpp, MQTT.cpp).

Assisted-by: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Andrew Yong <me@ndoo.sg>

---------

Signed-off-by: Andrew Yong <me@ndoo.sg>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2026-07-09 10:34:58 -05:00
Ben Meadors 515fe8b94f Clamp bw for malformed use_preset false with empty bandwidth 2026-07-09 10:30:11 -05:00
Ben MeadorsandGitHub 168f669e61 esp32: fix NimBLE use-after-free crash when disabling bluetooth while connected (#10950)
* esp32: fix NimBLE use-after-free crash when disabling bluetooth while connected

Saving certain configs over BLE (e.g. MQTT module config) calls
disableBluetooth() -> NimbleBluetooth::deinit() while the phone is still
connected. deinit() called BLEDevice::deinit(true) straight away, which
deletes the BLEServer before nimble_port_stop(). Stopping the NimBLE
port with a live connection makes the host synthesize unsubscribe events
via ble_gatts_connection_broken() and dispatch them into the freed
BLEServer -> LoadProhibited panic in BLEServer::handleGATTServerEvent
(iterating server->m_notifyChrVec on freed memory).

Backtrace tail:
  BLEServer::handleGATTServerEvent (SUBSCRIBE)
  <- ble_gatts_subscribe_event <- ble_gatts_connection_broken
  <- ble_gap_conn_broken <- ble_gap_rx_disconn_complete

It was reliably preceded by our onRead callback pinning the NimBLE host
task in its up-to-20s polling loop ('BLE onRead: timeout after 19920 ms,
4000 tries'), so the host task couldn't process the teardown until long
after the objects under it were freed.

Fix: drain before demolition in deinit(). Clear
onReadCallbackIsWaitingForData so an in-flight onRead returns
immediately; disconnect the peer and wait (bounded, ~2s) for the host
task to run onDisconnect (which clears nimbleBluetoothConnHandle) so the
unsubscribe/GATT teardown lands on the still-live server; only then
BLEDevice::deinit(true). isDeInit is raised after the drain (onDisconnect
early-returns when set and would otherwise never clear the handle), the
deferred advertising restart is dropped (the stack is going away), and
the now-dangling bleServer pointer is nulled.

deinit() runs on the main task, so waiting on the host task's callbacks
is safe; the waits are bounded regardless.

Upstream ordering bug (delete BLEServer before nimble_port_stop) to be
reported to espressif/arduino-esp32 separately.

* esp32: gate onRead during BLE teardown and use Throttle for drain wait

Addresses review feedback on the NimBLE deinit UAF fix:

- Set a bleDraining flag before disconnecting so onRead bails immediately
  instead of arming the up-to-20s wait. Without this, a read that re-arms
  after deinit() clears onReadCallbackIsWaitingForData could pin the NimBLE
  host task, block onDisconnect from clearing the conn handle, and make the
  2s drain expire. onRead now skips the wait when draining, and the wait
  loop also observes the flag so an in-flight read is released.
- Use Throttle::isWithinTimespanMs() for the bounded drain wait instead of
  a raw millis() subtraction, per coding guidelines.

* esp32: reset BLE teardown guards on re-init

deinit() latches bleDraining (new) and isDeInit (pre-existing) as teardown
guards, but setup() never cleared them. AdminModule's disable-bluetooth admin
command calls deinit() directly without a reboot, and PowerFSM can re-enable
via setBluetoothEnable(true) -> setup() on the same boot (isActive() is false
because deinit() nulls bleServer). Without resetting the flags, the re-initialized
stack has onRead permanently bailing on the drain path (empty reads) and
onDisconnect early-returning without clearing the connection handle.

Clear both flags at the top of setup() so a re-init recovers cleanly.
2026-07-09 09:42:35 -05:00
IxitxachitlandGitHub faf92f6720 Add new emojis: faces, gestures, weather, objects, holidays, and misc (#10930) 2026-07-09 05:03:57 -05:00
Andrew YongandGitHub 0ae44d7017 fix(router): null-check packetPool/clientNotificationPool allocations (#10948)
On platforms where these pools are heap-backed (MemoryDynamic - used
whenever there isn't enough static RAM for a fixed pool, e.g.
ARCH_STM32WL or BOARD_HAS_PSRAM), allocCopy()/allocZeroed() return
nullptr on allocation failure and already log a warning, but most
callers dereferenced the result unconditionally. Under real heap
pressure this reliably produced a HardFault - reproduced on STM32WL
hardware under sustained mesh traffic, including the RX entry point
(RadioLibInterface::handleReceiveInterrupt) where every received
packet is allocated.

Adds null checks at all call sites that were missing one, mirroring
the guard pattern already used correctly elsewhere in the same files
(e.g. RadioInterface.cpp's sendErrorNotification). On allocation
failure, callers now skip the send/retransmission/notification and
log nothing further (the allocator already did) rather than crash.

Assisted-by: Claude Sonnet 5 <noreply@anthropic.com>

Signed-off-by: Andrew Yong <me@ndoo.sg>
2026-07-09 04:36:36 -05:00
Ben MeadorsandGitHub b4b1ece8f1 nrf52: fix loop-task stack overflow causing reset on BLE pairing/first-sync (#10944)
The Arduino loop task runs Meshtastic's entire cooperative OSThread
scheduler on the Adafruit core's stock 4 KB (1024-word) stack. The 2.8
first-sync path - AdminModule::handleSetConfig -> saveChanges ->
configChanged -> RadioInterface::reloadConfig -> LR11x0 reconfigure ->
SPI Lock::lock, with vsnprintf/USB-CDC logging frames stacked on top -
overflows it.

SWD fault capture on a T1000-E (pyocd vector_catch=h) proved it: the
loop task's SP was driven 40 bytes BELOW its own pxStack base, the
stack paint was consumed to the floor, and the crash was a BusFault in
xQueueSemaphoreTake dereferencing a semaphore handle that adjacent-heap
corruption had overwritten with log text (0x3f3f207c = "| ??"). The
core's HardFault handler is a bare NVIC_SystemReset, so in the field
this presents as a silent reboot ~seconds after a phone pairs (or a 30 s
zombie hang -> supervision timeout 0x8 when the corruption lands on task
TCBs instead). Also explains the set_time_only -> immediate-NodeInfo
crash variant reported on empty-DB nodes: same task, different deep
chain. 2.7.26 is unaffected because the deep frames (XEdDSA, satellite
map conversion, replay engine) did not exist.

Fix: set -DLOOP_STACK_SZ=2048 (words = 8 KB) for all nrf52840 targets.
Requires the #ifndef guard from meshtastic/Adafruit_nRF52_Arduino#7;
until that merges the flag is a harmless redefinition warning. Validated
on hardware: pairing + full sync completes, stack paint shows healthy
margin under load.

Also fix the boot-time reset-reason log: the core's init() caches
RESETREAS and W1C-clears the register before setup() runs, so the raw
read here has always printed 0. Use readResetReason() instead.
(preFSBegin's RESETREAS==0 gate is intentionally left as-is - its
degenerate GPREGRET-only behavior is what makes lfs recovery work.)
2026-07-08 17:11:32 -05:00
Ben Meadors 9b5b2889e7 Remove triage workflows 2026-07-08 14:44:59 -05:00
Ben MeadorsandGitHub 8cff9e0ac3 Setup renovate for nrf52 arduino platform package (#10946) 2026-07-08 13:57:35 -05:00
Andrew YongandGitHub fcea931aeb fix(nrf52): enable USB/charge detection on Seeed Wio Tracker L1 (#10941)
isCharging()/getHasUSB() were hard-compiled to always return false on
this board: the nrfx_power_usbstatus_get() block in Power.cpp that
derives them is gated behind #ifdef NRF_APM, and this board's variant.h
never defined it - unlike tracker-t1000-e and wio-tracker-wm1110, which
do. Confirmed on hardware: before this change, `Battery: usbPower=0,
isCharging=0` regardless of actual USB/charge state; after, `usbPower=1,
isCharging=1` while genuinely on USB power with a battery attached.

Same underlying gap as #4367, fixed for tracker-t1000-e and
wio-tracker-wm1110 in #4376 (which also fixed a deeper bug where
PowerStatus notifications were gated behind a battery-level null check -
that part is already fixed generically in Power.cpp today, so this board
only needed the define). Looks like a simple oversight from when this
board was added, since it's newer than the two boards #4376 touched.

Assisted-by: Claude Sonnet 5 <noreply@anthropic.com>

Signed-off-by: Andrew Yong <me@ndoo.sg>
2026-07-08 11:10:23 -05:00
Ian McEwenandGitHub ade42830e0 Re-enable wantresponse and other fields in simradio when on the decoded path (#10934)
* Re-enable wantresponse and other fields in simradio when on the decoded path

* Make the comments less verbose
2026-07-08 08:59:07 -05:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
b97adf5547 Update meshtastic/device-ui digest to effbb92 (#10937)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-08 08:08:52 -05:00
ManuelandGitHub 20ad985f41 update sensecap arduino-esp32 fork to 3.3.9 (#10933) 2026-07-08 08:06:48 -05:00
Thomas GöttgensandGitHub 38074f584f Fix SENSOR power saving deep sleep truncating TX and skipping sleep on failed reads (#10939)
* Fix SENSOR power saving deep sleep behavior

Deep sleep could be entered while a telemetry packet was still queued
or on air, truncating the transmission. canSleep() gained a deepSleep
parameter and now vetoes in that case; light sleep is unchanged.
Telemetry modules defer a pending deep sleep (bounded to 30s) until the
radio is idle and no longer let the sensor polling interval override
the 5s pre-sleep grace period. A failed sensor read still arms deep
sleep instead of leaving the node awake for a full telemetry interval.

Fixes #10890
Fixes #10932

* Deduplicate telemetry deep sleep deferral logic

Move the radio-busy deferral and its counter into BaseTelemetryModule
and add an isPowerSavingSensor() helper. Removes the telemetry-specific
counter from OSThread. The sleep arming block stays per module because
it needs protected OSThread members not visible to the base class.
2026-07-08 07:30:46 -05:00
Thomas GöttgensandGitHub ba473bf529 size gate: emit flash_bytes from ELF for targets without a packaged .bin (#10940)
* size gate: emit flash_bytes from ELF for targets without a packaged .bin

nRF52 builds package hex/uf2/DFU zip but no raw .bin, so collect_sizes.py
dropped budgeted envs like rak4631 entirely and the size-budget-gate failed
closed. Emit ELF text+data as flash_bytes in the manifest and use it as the
fallback flash measurement.

* Factor shared size-tool invocation into run_size_tool helper
2026-07-08 07:22:26 -05:00
Ben MeadorsandGitHub 8d20606203 feat(config): make position & telemetry broadcast opt-in (#10929)
Position sharing was opt-out (the default primary channel shipped
position_precision=13) while device telemetry was already opt-in, and a
normal firmware upgrade preserves saved config, so existing nodes stayed
position-on. This makes both broadcasts opt-in, both on a fresh flash and via
a one-time migration for upgrading nodes.

- Fresh default: initDefaultChannel now sets position_precision=0.
- One-time migration in loadFromDisk, gated on a dedicated
  POSITION_TELEMETRY_OPTIN_VER (26) watermark kept separate from
  DEVICESTATE_CUR_VER (bumping that would re-run the NodeDatabase v24
  legacy decoder on already-migrated v25 DBs): disables position broadcast
  on PUBLIC/default-PSK channels and forces every device-telemetry
  mesh-broadcast flag plus the MQTT map-report location off.
- Private-PSK channels are left untouched: a channel with a custom key is a
  deliberate trusted-group setup, so its configured precision is preserved.
- Idempotent: ordinary saves never re-stamp .version, so a user who
  re-enables sharing is not re-disabled on the next boot.
- Added pure channelFileUsesPublicKey() (operates on the raw ChannelFile,
  since the channels singleton isn't initialized during loadFromDisk) and
  refactored Channels::usesPublicKey() to delegate to it.
- New test/test_optin_migration native suite (14 cases).
2026-07-07 14:58:58 -05:00
d16ae2b098 Rename power.h to Power.h for casing consistency with Power.cpp (#10919)
Every other .cpp/.h pair in src/ (350 of 351) uses identical
capitalization between the two files. Power.cpp/power.h was the sole
outlier; this aligns it with the rest of the codebase. No functional
change — all includes already resolved this file the same way on
case-sensitive filesystems.

Assisted-by: Claude Sonnet 5 <noreply@anthropic.com>

Signed-off-by: Andrew Yong <me@ndoo.sg>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2026-07-07 10:15:01 -05:00
Ben MeadorsandGitHub 97302f1845 Add Meshnology W10 AIOT Dev Kit (SX1262 via MCP23017 I2C expander) (#10911)
* Add software LoRa IRQ polling and an MCP23017 expander HAL for radios behind an I2C expander

Some boards route the SX126x control lines (NRESET/DIO1/BUSY) through an I2C
GPIO expander instead of native GPIOs, and don't wire the expander's /INT to
the MCU, so RadioLib cannot take a hardware DIO1 interrupt.

Two reusable pieces, both gated behind USE_MCP23017 / LORA_DIO1_SOFTWARE_POLL
so builds that don't define them are unaffected:

- ExtensionIOMCP23017 + MCP23017LockingArduinoHal: a RadioLib HAL that maps
  virtual pins (MCP23017_VPIN_BASE .. +15) onto an MCP23017's GPIO, so
  RESET/BUSY/DIO1 reads and writes become I2C transactions while the real SPI
  GPIOs (SCK/MOSI/MISO/CS) pass straight through. A failed I2C read skips the
  read-modify-write rather than clobbering the rest of the bank.
- LORA_DIO1_SOFTWARE_POLL: with no DIO1 interrupt available, the SX126x
  interface polls the radio IRQ status register from the radio thread and
  synthesizes the ISR_TX/ISR_RX events, filtering noisy preamble/header IRQs so
  they can't starve TX. A 1 ms ISR_POLL_TICK drives the poll; TX timers may
  overwrite the pending tick (pollMissedIrqs() is the backup that bounds the
  latency of the busy-Rx contention window). Behaviour is unchanged on all
  boards that keep the hardware interrupt.

* Add Meshnology W10 AIOT Dev Kit variant (SX1262 via MCP23017 expander)

ESP32-S3R8 + EBYTE E22-900MM22S (SX1262) + AXP2101 PMIC + Quectel L76KB GPS +
SPI TFT, with the radio's RESET/DIO1/BUSY and the LCD reset routed through an
MCP23017 I2C expander at 0x20. The expander's /INT is not wired to the ESP32,
so DIO1 uses the software poll. Pins come from the board schematic and the
vendor firmware; the expander is an MCP23017 despite the V1.1 schematic's stale
'TCA9555' label (the V1.2 placement diagram and all vendor code use the MCP23017
register map).

A local pins_arduino.h shadows the generic esp32s3 variant to drop RGB_BUILTIN,
which otherwise pulls the Arduino RMT HAL into the link and fails against this
build's trimmed FreeRTOS config.

Reports HardwareModel 140 (MESHNOLOGY_W10, already present in the protobufs).

Hardware-verified on a real W10 AIOT Dev Kit: AXP2101 PMU, MCP23017, SX1262
init + a full over-the-air TX/RX round trip through the expander, PCF85063 RTC,
SHT41 and QMI8658 auto-detected, and an L76KB GPS fix.

* meshnology-w10: enable ES8311 speaker for notification tones

Bring up the ES8311 codec (I2C 0x18) -> NS4150 amp -> speaker so the board can
play notification tones / ringtones over the I2S buzzer path (the
use_i2s_as_buzzer external-notification option). Codec2 voice stays out of
scope: it is SX1280-only and this is a sub-GHz board.

- variant.h: HAS_I2S + DAC_I2S pins (MCLK=1, BCK=2, WS=4, DOUT=5, DIN=3)
- platformio.ini: arduino-audio-driver + ESP8266Audio + ESP8266SAM
- extra_variants/meshnology_w10/variant.cpp: lateInitVariant() configures the
  ES8311, mirroring the other Meshtastic ES8311 boards. Kept codec-only (no
  main.h) so the audio driver's 'using namespace audio_driver' does not pull a
  conflicting GpioPin into scope alongside Meshtastic's class GpioPin.
- AudioThread.h: toggle the NS4150 amp (MCP23017 EXIO_PA_CTRL) around playback.

Verified on hardware: a test tune played audibly through the speaker.

* meshnology-w10: address review feedback on the MCP23017 driver

- ExtensionIOMCP23017: serialize register access with a mutex, since the radio
  HAL (BUSY/DIO1/RESET) and AudioThread (amp enable) now reach the expander from
  different threads and the read-modify-write paths are not atomic.
- digitalRead: return a fail-safe HIGH on a failed read instead of LOW, so a
  transient I2C error on the LoRa BUSY line can't look like 'ready' and let
  RadioLib start an SPI transaction early. (DIO1 is polled via the radio IRQ
  register, not this pin.)
- enablePinChangeInterrupt: use the checked readReg() overload and skip on a
  failed read, matching the other read-modify-write helpers.
- meshnology_w10 variant.cpp: log a warning if an ES8311 register write NACKs
  instead of silently leaving the codec half-configured.
- RadioInterface.cpp: guard the MCP23017 HAL branch with ARCH_ESP32 to match the
  include and driver-file guards.

* Replace board-model audio/sleep ifdefs with reusable variant capability macros

Two shared-code spots keyed off specific hardware models; move the board-specific
detail into opt-in macros the variants define, so the core code stays generic and
new boards can opt into the behavior class without touching shared files.

- AudioThread amp control: drop the T_LORA_PAGER / MESHNOLOGY_W10 ifdefs. A board
  with an I2S amp now defines AUDIO_AMP_ENABLE(on) in its variant.h (T-LoRa Pager
  and Meshnology W10 do), and AudioThread just calls it around playback. The
  expander includes are keyed on USE_XL9555 / USE_MCP23017 (capabilities) instead
  of the model name.

- Light-sleep / DIO1 wakeup: drop the SENSECAP_INDICATOR check. Boards whose
  LORA_DIO1 is an expander pin (not an ESP32 GPIO) define LORA_DIO1_EXTENDED_IO
  (SenseCAP Indicator and Meshnology W10), and sleep.cpp keys off that for both
  the light-sleep skip and the DIO1 GPIO-wakeup skip. This also fixes a latent
  issue on the SenseCAP: it previously reached the GPIO-wakeup path and called
  gpio_pulldown_en() on a virtual expander pin; it now skips that cleanly.

Builds verified on meshnology_w10, tlora-pager, and seeed-sensecap-indicator.

* meshnology-w10: silence cppcheck constParameterPointer on the ISR-callback helper

isIsrTxCallback() takes a function pointer it only compares; cppcheck's
constParameterPointer wants it 'pointer to const', which is meaningless for a
function pointer (the codebase already globally suppresses the sibling
constParameterCallback). Inline-suppress it to keep the check green.

* HopScaling: qualify the member assignment as this->count

cppcheck (CI's version) flags 'count = newCount;' at the end of trimIfNeeded()
as uselessAssignmentArg ('assignment of function parameter has no effect') - a
false positive, since count is a member that outlives the call. Write it as
this->count (matching the Step-1 assignment above) so the analyzer sees a member
write. This finding comes in via the develop merge, not this board; fixing it
here to unblock the PR's cppcheck check.
2026-07-07 09:58:54 -05:00
Ben Meadors f250cf3cdc Update protos 2026-07-07 06:34:22 -05:00
CopilotGitHubcopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
489cc7dd88 Fix develop CI regressions in HopScaling and TLora T3-S3 builds (#10916)
* fix: address develop CI regressions

* revert TLora T3-S3 partition table changes

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-07-07 06:08:58 -05:00
AustinandGitHub 392bac9d1c Initial coderabbit configuration (#10913)
- Ignore renovate
- Ignore drafts
- Ignore generated (protobufs)
- Target all branches (master, develop, event/*, etc)
2026-07-06 20:01:53 -05:00
8058cafe5b fix(power): correct charge/power detection preprocessor bugs (#10906)
* fix(power): check EXT_CHRG_DETECT with defined(), not truthiness

#elif EXT_CHRG_DETECT tests the macro's numeric value rather than
whether it's defined. Every existing board happens to assign it to a
nonzero pin, so this never misfired, but a board using pin 0 (e.g.
PA0 on STM32) would silently skip this branch even though the pin is
defined and configured.

Assisted-by: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Andrew Yong <me@ndoo.sg>

* fix(power): consider EXT_CHRG_DETECT in isVbusIn()

isVbusIn() used EXT_PWR_DETECT alone. On boards where that pin is a
charge-complete/standby signal rather than a continuous power-present
signal, isVbusIn() reports false for the entire duration of an active
charge. If EXT_CHRG_DETECT is also defined, OR it in: charging can
only happen when power is present, so this only adds correct true
results and is a no-op for boards where EXT_PWR_DETECT already means
"power present" continuously.

Assisted-by: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Andrew Yong <me@ndoo.sg>

---------

Signed-off-by: Andrew Yong <me@ndoo.sg>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2026-07-06 16:56:10 -05:00
Ben MeadorsandGitHub 24c1dccf00 CI: track RAM (.data+.bss) in size reports and gate on per-env budgets (#10899)
* CI: track RAM (.data+.bss) in size reports and gate on per-env budgets

The 2.8.0 nRF52840 heap regression (99% heap in field reports) shipped
invisibly because CI only tracked flash. On nRF52840 the heap arena is the
linker gap after .bss, so every byte of static .data+.bss growth shrinks the
usable heap 1:1 - RAM needs the same guardrails flash already has.

What's added:

- bin/platformio-custom.py emits ram_bytes (.data + .bss from the ELF, via
  the toolchain size tool) into each .mt.json manifest. Heap/stack
  placeholder sections are deliberately excluded.
- bin/collect_sizes.py records {flash_bytes, ram_bytes} per env;
  bin/size_report.py grows RAM and RAM-delta columns in the PR size report.
  Older artifacts without ram_bytes (and legacy int-schema baselines)
  degrade to "n/a" instead of crashing.
- bin/ram_budgets.json: per-env RAM/flash budgets, enforced only for envs
  listed there. Seeded with rak4631: ram 113,000 (current 110,948 + ~2 KB
  slack), flash 786,000 (current 765,192 + ~20 KB; the app region is
  0x27000..0xEA000 = 798,720 and the image must stay clear of the
  warm-store ring guard in extra_scripts/nrf52_warm_region.py).
- New size-budget-gate CI job runs size_report.py --enforce-budgets and
  fails the build on violation; the informational firmware-size-report job
  now also renders budget usage into the PR comment.
- src/main.cpp: opt-in boot heap watermark (-DMESHTASTIC_HEAP_WATERMARK_CHECK)
  logs LOG_ERROR when less than 20% of the heap is free at the end of
  setup(). Off by default; skipped on platforms without heap accounting.

How budgets are raised: deliberately, never automatically. If a change needs
more headroom, bump the env's limit in bin/ram_budgets.json in the same PR
and justify the increase in the PR description.

Verified: python3 bin/test_size_scripts.py (23/23 pass, including ram_bytes
parsing, n/a fallback, and over/under/missing-env budget-gate cases).

* Address review: fail the budget gate closed, fix RAM section matching

- size-budget-gate workflow: drop continue-on-error on the manifest
  download and the empty-dir fallback, so the job fails when the data it
  gates on cannot be fetched.
- size_report.py --enforce-budgets now fails closed on every
  missing-data path instead of trivially passing: empty collected sizes,
  a budgeted env that was not built, or a manifest without the budgeted
  metric. Report-only mode keeps rendering those as n/a.
- load_budgets() rejects zero/negative/non-integer budgets with a clear
  error (a typo'd budget could previously crash budget_markdown with
  ZeroDivisionError or silently skip the check); the percentage render
  keeps a defensive guard for direct callers.
- compute_ram_bytes(): count RISC-V small-data sections (.sdata/.sbss)
  and exclude ESP-IDF .rtc.* sections, which live outside the
  heap-competing SRAM.
- Trim the heap-watermark comment in main.cpp to two lines.

bin/test_size_scripts.py: 27/27 - the two fail-open assertions are
flipped to fail-closed, with new report-only counterparts plus cases for
empty sizes under enforcement and malformed budgets.
2026-07-06 14:52:22 -05:00
Ben MeadorsandGitHub 12b2d973a4 Add central memory-class ladder (MemClass.h) with fail-safe-small defaults (#10901)
* Right-size nRF52 heap tiers after 2.8.0 heap-exhaustion field reports

Field reports on 2.8.0 show nRF52840 devices at 99% heap (114/115 KB)
within minutes of boot; operator new asserts on OOM, so these devices
are one allocation from a reboot. The 2.8.0 cache sizing ladders gave
nRF52 the largest non-PSRAM tiers on the assumption that a BLE-only
part has a roomy heap - the arena is actually ~125 KB shared with the
FreeRTOS task stacks.

Per-target retiers (nRF52840 unless noted):
- Traffic Management cache 1000 -> 250 entries (10 KB -> 2.5 KB); the
  unclassified fallthrough drops 1000 -> 400 to match the classic-ESP32
  tier (also affects RP2040/RP2350)
- Warm node store 200 -> 100 entries (8 KB -> 4 KB); the non-XXAA
  fallthrough drops 320 -> 100 so an unclassified RAM-constrained part
  can't boot-allocate 12.8 KB
- MESSAGE_HISTORY_LIMIT 20 -> 10 (text pool 4.4 KB -> 2.2 KB), the tier
  classic ESP32 already ships
- MAX_RX_TOPHONE 32 -> 16, shrinking the static packet pool 70 -> 54
  slots (~6.6 KB of .bss returned to the heap arena)
- PacketHistory hash index off arch-wide (1 KB); O(n) over 240 records
  is negligible at LoRa packet rates
- OLEDDISPLAY_REDUCE_MEMORY arch-wide (~1 KB OLED back buffer); the five
  TFT variants -U it because TFTDisplay.cpp needs buffer_back for
  dirty-window diffing
- Drop the stale "for testing" 1024-entry TMM override on T1000-E

Measured on rak4631: heap arena grows 124,572 -> 131,180 B and boot
allocations drop ~15.7 KB, roughly +22 KB free heap on the field-report
device class.

Migration: the nRF52840 warm flash ring replays through place() (LRU),
so the newest 100 identities survive the shrink; the file backend
rejects oversized snapshots cleanly (new test covers this). Native
suites pass (536/536 Docker, 13/13 native-macos warm store); rak4631,
heltec-mesh-node-t114 (TFT) and tracker-t1000-e build green.

* Add central memory-class ladder (MemClass.h) with fail-safe-small defaults

The 2.8.0 nRF52840 heap exhaustion happened because each RAM-sized cache
picked its per-platform tier from its own chip #ifdef ladder, and every
ladder's fallthrough default was its largest non-PSRAM tier - nRF52 was
never named, so it silently got 1000-entry caches on a ~115 KB arena.

This introduces src/memory/MemClass.h: a single MESHTASTIC_MEM_CLASS
(TINY / SMALL / MEDIUM / LARGE) ranked by usable app heap after platform
overheads, with the deliberate property that an unclassified chip lands
in SMALL - a new target boots with small caches until someone opts it up
in one visible place.

The TMM cache, warm store, MAX_RX_TOPHONE and MAX_SATELLITE_NODES
ladders in mesh-pb-constants.h now key off the class; branches pinned by
something other than RAM stay explicit and say why (nRF52840's SoftDevice
arena, RP2040's warm.dat watchdog bound). MAX_NUM_NODES intentionally
stays separate - it is flash-shaped (nodes.proto vs LittleFS), not
heap-shaped.

A per-class MESHTASTIC_BOOT_CACHE_BUDGET static_assert now covers the
three big boot-allocated caches, so the next cache-adding PR that would
blow a small platform's budget fails to compile instead of exhausting
heap in the field.

No values change for any existing target: rak4631, tbeam, rak11310 and
wio-e5 build byte-identical before/after; all ladders remain
#ifndef-guarded so variant overrides keep working.

* Address review: fix RP2350 class-table doc, add PacketRecord static_assert

- MemClass.h's class table claimed RP2350 was MEDIUM while the mapping
  ladder classifies it SMALL (with RP2040) - the table now matches the
  ladder, with a note that RP2350 is a MEDIUM candidate whenever someone
  wants to tune it up (kept SMALL here so this header stays a behavioral
  no-op).
- The boot-cache budget comment referenced a static_assert pinning
  PacketHistory::PacketRecord at 20 B that did not exist (only a layout
  comment). Add the real static_assert so the budget math in
  mesh-pb-constants.h fails to compile if the record layout changes.

Also merges develop (the base #10898 landed there as a squash, which is
what made this stacked branch conflict); develop's mesh-pb-constants.h
is byte-identical to this branch's base, so the resolution keeps the
MemClass ladder unchanged. rak4631 and wio-e5 build green;
test_packet_history 47/47.

* Address review: share PACKETHISTORY_MAX, trim policy comments

- Hoist PACKETHISTORY_MAX from PacketHistory.cpp into
  mesh-pb-constants.h (next to the MAX_NUM_NODES it derives from) so the
  constructor clamp and the boot-cache budget static_assert use one
  definition instead of hand-mirrored arithmetic that could drift. The
  expression stays valid where MAX_NUM_NODES resolves at runtime
  (ESP32-S3, portduino); the pointless 2.0 double math becomes integer.
- Trim the MemClass.h header (36 -> 16 comment lines) and the budget /
  sizing-policy comments per the repo comment-length guideline, keeping
  the class table, the fail-safe-small rule, the override mechanism, and
  the include-order constraint.

rak4631 (compile-time MAX_NUM_NODES) and heltec-v3 (runtime) build
green; test_packet_history 47/47.
2026-07-06 14:51:41 -05:00
Ben MeadorsandGitHub 00685a4e61 nrf52840: right-size the SoftDevice RAM reservation (+8 KB heap arena) (#10903)
The app RAM ORIGIN in both nrf52840 linker scripts has been a hard-coded
0x20006000 (24 KB reserved for S140) since they were introduced, but
sd_ble_enable() with our fixed Bluefruit configuration (one peripheral
link, BANDWIDTH_MAX / ATT MTU 247, 0x1000 attribute table) requires
substantially less. The SoftDevice cannot use the gap and the app image
is linked above it, so every byte between the true requirement and the
ORIGIN is simply unusable RAM - on a part where 2.8.0 field reports
showed the heap arena at 99% use.

Lower the ORIGIN to 0x20004000 in both s140 v6 and v7 scripts. The heap
arena is the linker gap on this platform, so the change is worth exactly
+8,192 B of free heap on every nRF52840 board (verified: rak4631 .heap
section 124,572 -> 132,764 B; both v6- and v7-script boards link with
.data at 0x20004000).

Safety net: Bluefruit.begin()'s return value - previously discarded -
is now checked. If a future SoftDevice or Bluefruit config change raises
the requirement past the reservation, the node logs a critical error
instead of silently running without BLE, with instructions to re-measure
via CFG_DEBUG=1 ("SoftDevice's RAM requires: 0x...").
2026-07-06 14:13:18 -05:00
d846780a9b More fuzz tests and small fixes for the findings (#10864)
* first pass tests

* more tests

* Fix two crafted-admin-packet crashes found by the E5 fuzzer

Both are reachable from an authorized admin (local from==0, admin channel,
or PKC) - remote DoS:

1. SIGFPE in LoRa config validation. A set_config LoRaConfig with
   use_preset=false and bandwidth=0 makes freqSlotWidth 0, so numFreqSlots
   is 0 and `hash(name) % numFreqSlots` (RadioInterface.cpp) divides by
   zero. Guard the modulo; the existing channel_num check then rejects/
   clamps the config.

2. Stack overflow in Channels::getKey. A SECONDARY channel at the primary
   slot with an empty PSK recursed into getKey(primaryIndex) forever. Skip
   the primary-key borrow when chIndex == primaryIndex.

Re-enable the E5 admin fuzzer to hit both triggers again (use_preset both
ways incl. bandwidth 0, plus the set_channel tag) as regression guards.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Correct fuzz-test invariants after the crash fixes

- E5 admin fuzz: node eviction under a filling NodeDB is legitimate, so
  assert only the bounded-count invariant, not that a specific seed node
  survives 6000 mutating ops.
- TMM blitz: scope off the nodeinfo direct-response send path (it needs a
  fully-wired MeshService/phone queue the fixture doesn't provide; the
  deterministic directResponse tests cover it). The crafted-nodenum
  rate/unknown/position cache stress is unchanged.

clod helped too

* realistic tests

* test: dedup fuzz RNG into shared test/support/DeterministicRng.h

The four in-tree fuzz suites (test_fuzz_decode, test_fuzz_packets,
test_hop_scaling, test_traffic_management) each carried a byte-identical
copy of the seeded 64-bit LCG (rngSeed/rngNext/rngByte/rngRange). Hoist
it into one shared header so there is a single generator to reason about
and no risk of the copies drifting. static inline keeps per-suite state
per translation unit and avoids -Wunused-function for suites that don't
use every helper. Also corrects a stale comment in test_traffic_management
(the blitz's nodeinfo direct-response path is intentionally left off).

No behavioral change: same constants, same per-suite seeds.

clod helped too

* test: fuzz uncovered ProtobufModule handlers and the MQTT downlink ingress

Extend the in-tree fuzz coverage to packet sources that previously had
none:

- test_fuzz_packets E8/E9/E10: drive PositionModule, DeviceTelemetryModule
  and NeighborInfoModule at handleReceivedProtobuf directly (via using-shims,
  bypassing the ProtobufModule reply/send path so no router is needed). The
  fixture already stands up nodeDB/service/channels, and nodeStatus/powerStatus
  are auto-initialized in main.cpp, so no new globals are required. Adds a
  shared fuzzRxHeader() helper for crafting adversarial RX packet headers.
- test_fuzz_decode: add meshtastic_KeyVerification to the decode table. The
  KeyVerification and StoreForward handler paths are documented as decode-level
  only, with the concrete reason each is intrinsic (private-state gating /
  PSRAM + self-pointer wiring), not a fixture gap.
- test_mqtt: test_receiveFuzzServiceEnvelope blitzes the non-RF broker-push
  ingress (onReceiveProto) two ways - raw garbage bytes that must fail envelope
  decode cleanly, and a well-formed ServiceEnvelope wrapping a crafted inner
  MeshPacket over crafted channel_id/gateway_id - exercising the channel match,
  isFromUs, XEdDSA receive policy and perhapsDecode chain. Adds a deliverRaw()
  passthrough to MQTTUnitTest.

All under the coverage env (ASan/LSan). No firmware/src changes. Full sweep
GREEN 27/27, 544 cases.

clod helped too

* Harden LoRa/channel config against crafted admin messages; consolidate test helpers

Production (review findings on the hot-fuzz crash fixes):
- Clamp bandwidth at the source (clampBandwidthKHz) in checkOrClampConfigLora
  and applyModemConfig so numFreqSlots can never be 0 for any consumer; a
  bandwidth-0 set_config previously passed validation and re-armed the SIGFPE
  on the next applyModemConfig.
- Guard applyModemConfig's hash % numFreqSlots (the validator's sibling modulo
  was fixed earlier but this one was still unguarded).
- Enforce the primary-channel invariant in Channels::onConfigChanged: a config
  demoting every slot now re-promotes the stale SECONDARY slot (keeping its
  key) or restores the default channel if the slot is DISABLED, instead of
  leaving every getPrimaryIndex() reader on a non-primary slot. The getKey
  recursion guard stays as defense-in-depth.

Tests:
- New test/support/MockMeshService.h and AdminModuleTestShim.h replace four
  byte-identical mocks and three divergent admin shims (test_mqtt's capturing
  mock is genuinely different and stays).
- DeterministicRng.h: add rngFill() (replaces 14 hand-rolled fill loops) and
  rngEdgeNodeNum() (unifies the three NodeNum boundary pools).
- Extract fuzzChannelSettings() shared by the set_channel case and fuzzBeacon.
- fuzzBeacon: the un-terminated branch now fills the whole buffer with non-NUL
  bytes so the strnlen bound is actually stressed (~50% of iterations, not ~4%).
- E6 beacon fuzz: replace the TEST_ASSERT_TRUE(true) tautology with real
  invariants (handler never consumes; offers land in lastReceivedOffer keyed
  to the sender).
- Trim the seven over-long comment blocks flagged against the 1-2 line rule;
  the FINDINGS trailer moves to this commit message (see production notes).

Full native suite GREEN 27/27 under the coverage (ASan/LSan) env.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Clamp UTF-8 char length in the emote walkers; add test_fuzz_emotes

A TEXT_MESSAGE payload is opaque protobuf bytes, so PB_VALIDATE_UTF8 never
screens it - invalid UTF-8 and truncated multi-byte lead bytes reach the
emote/width render path verbatim. EmoteRenderer's walkers advanced by
utf8CharLen(lead) without clamping to the bytes actually remaining, so a
truncated lead (e.g. a lone 0xF0, which claims 4 bytes) near the end of the
buffer made getUtf8ChunkWidth's memcpy read past the string. ASan confirms a
heap-buffer-overflow READ from measureStringWithEmotes.

Add utf8CharLenClamped() and use it at every walk site (width measure,
truncation cut-loop, and the draw-path text-run/chunk builders); the one
already-guarded site (matchAtIgnoringModifiers) is unchanged.

New test/test_fuzz_emotes drives measureStringWithEmotes and truncateToWidth
over adversarial byte strings (biased to embed/end in truncated multi-byte
leads) in exact-sized heap buffers so any over-read is a hard ASan fault. Its
headless display uses a synthetic font (firstChar 0, fontData centered in a
large buffer) so the stock OLEDDisplay::getStringWidth - which indexes the
font jump table with a signed char and over-reads for any byte >= 0x80 - does
not mask the finding. native-suite-count bumped 27 -> 28.

Full native suite GREEN 28/28 under the coverage (ASan/LSan) env.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Keep emote width measurement in-bounds for non-ASCII bytes

OLEDDisplay::getStringWidth (the utf8=false path EmoteRenderer uses on default
builds) indexes the font jump table by (c - firstChar) with a signed char and
no bounds check, so any byte outside printable ASCII - high bytes from UTF-8
text, but also a stray control byte like 0x0A - reads outside the font array.
On-device this reads adjacent flash and returns a garbage width; under ASan the
test_fuzz_emotes fuzzer flags it as a global-buffer-overflow, and it made the
non-ASCII width measurement meaningless either way.

The OLED driver is a pinned upstream dependency, so guard it firmware-side in
EmoteRenderer's getStringWidth helper: measure a sanitized copy where any byte
outside [0x20, 0x7E] counts as a '?' placeholder. Printable ASCII is unchanged
and the UA/RU lookup path is untouched.

test_fuzz_emotes now drives a real ArialMT font instead of the synthetic
in-bounds font it needed before this fix, so the suite exercises the true
production width path (utf8CharLen clamp + this sanitizer) end to end. The same
fuzzer tripped the global-buffer-overflow before this change.

Full native suite GREEN 28/28 under the coverage (ASan/LSan) env.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 13:19:01 -05:00
Ben MeadorsandGitHub 6c7ee8afc7 Add MemAudit: per-subsystem heap accounting in the boot log (#10900)
* Add MemAudit: per-subsystem heap accounting in the boot log

The 2.8.0 nRF52840 heap-exhaustion field reports had to be diagnosed by
hand, reconstructing each subsystem's heap footprint from source and
build flags one report at a time. This makes every future report
self-diagnosing from the serial log: a tiny fixed-size registry
(src/memory/MemAudit.*) that big long-lived allocations report into,
printed as one line at the end of setup() and alongside the periodic
"Heap free:" log:

  MemAudit[boot]: tmm=2500 warm=4000 pkthist=5824 nodedb=13440
  msgstore=2200 pktpool(live)=3270 total=31234

Instrumented: NodeDB hot vector (nodedb) + satellite maps (satmaps,
rb-tree overhead estimated), WarmNodeStore (warm), PacketHistory records
and hash index (pkthist), TrafficManagement caches (tmm/tmm_ni),
MessageStore text pool (msgstore), TFT line/repaint buffers (display),
and live in-flight packets (pktpool(live)) via an optional audit tag on
the packet pool allocator - the one hot path, counted with a relaxed
32-bit atomic add (single instructions on Cortex-M, no locks).

Cost: 128 B RAM for the 16-tag table, well under 1 KB flash on rak4631.
MESHTASTIC_MEM_AUDIT=0 compiles it out to inline no-op stubs (call
sites need no ifdefs); STM32WL, the tightest flash target, defaults off.

New native suite test_mem_audit covers add/set/snapshot arithmetic, tag
reuse (pointer and cross-TU strcmp fallback), null/unknown tags, and
table-full behavior; test/native-suite-count bumped to 28.

* native-wasm: add src/memory/ to the curated source filter

The wasm env denies all sources and adds an explicit file list; MemAudit
callers (main, MeshService, NodeDB, PacketHistory) are in that list but
src/memory/MemAudit.cpp was not, so wasm-ld failed on undefined
memaudit:: symbols.
2026-07-06 13:18:49 -05:00
Ben MeadorsandGitHub ed03a69555 Right-size nRF52 heap tiers after 2.8.0 heap-exhaustion field reports (#10898)
* Right-size nRF52 heap tiers after 2.8.0 heap-exhaustion field reports

Field reports on 2.8.0 show nRF52840 devices at 99% heap (114/115 KB)
within minutes of boot; operator new asserts on OOM, so these devices
are one allocation from a reboot. The 2.8.0 cache sizing ladders gave
nRF52 the largest non-PSRAM tiers on the assumption that a BLE-only
part has a roomy heap - the arena is actually ~125 KB shared with the
FreeRTOS task stacks.

Per-target retiers (nRF52840 unless noted):
- Traffic Management cache 1000 -> 250 entries (10 KB -> 2.5 KB); the
  unclassified fallthrough drops 1000 -> 400 to match the classic-ESP32
  tier (also affects RP2040/RP2350)
- Warm node store 200 -> 100 entries (8 KB -> 4 KB); the non-XXAA
  fallthrough drops 320 -> 100 so an unclassified RAM-constrained part
  can't boot-allocate 12.8 KB
- MESSAGE_HISTORY_LIMIT 20 -> 10 (text pool 4.4 KB -> 2.2 KB), the tier
  classic ESP32 already ships
- MAX_RX_TOPHONE 32 -> 16, shrinking the static packet pool 70 -> 54
  slots (~6.6 KB of .bss returned to the heap arena)
- PacketHistory hash index off arch-wide (1 KB); O(n) over 240 records
  is negligible at LoRa packet rates
- OLEDDISPLAY_REDUCE_MEMORY arch-wide (~1 KB OLED back buffer); the five
  TFT variants -U it because TFTDisplay.cpp needs buffer_back for
  dirty-window diffing
- Drop the stale "for testing" 1024-entry TMM override on T1000-E

Measured on rak4631: heap arena grows 124,572 -> 131,180 B and boot
allocations drop ~15.7 KB, roughly +22 KB free heap on the field-report
device class.

Migration: the nRF52840 warm flash ring replays through place() (LRU),
so the newest 100 identities survive the shrink; the file backend
rejects oversized snapshots cleanly (new test covers this). Native
suites pass (536/536 Docker, 13/13 native-macos warm store); rak4631,
heltec-mesh-node-t114 (TFT) and tracker-t1000-e build green.

* Drop stale OLEDDISPLAY_REDUCE_MEMORY -U on t114 / mesh-solar-tft

Only USE_TFTDISPLAY variants (t1, t096, wismeshtap) compile
TFTDisplay.cpp and need the lib's buffer_back; t114 and mesh-solar-tft
render through the meshtastic-st7789 driver, which handles the
reduced-memory configuration fine - as #10894 (merged from develop)
already established by defining the flag there. Remove the -U guard and
the per-variant -D (redundant with the arch-wide define in nrf52_base
on this branch). Both variants verified building.
2026-07-06 13:18:25 -05:00
1e66ecff60 Fix Ethernet MQTT reconnect handling (#10896)
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2026-07-06 10:21:24 -05:00