* add PiMesh-1W v1 and v2 lora config presets
* Modify Lora Pimesh configuration settings
Updated configuration for Lora Pimesh module with new CS pin and added comments.
* Enhance header in lora-pimesh-1w-v1.yaml
Updated header to include module information and URL.
* Update comments in lora-pimesh-1w-v2.yaml
* Add metadata to lora-pimesh-1w-v1.yaml
Added metadata section with name, support, and compatibility information.
* Add metadata to lora-pimesh-1w-v2.yaml
Added metadata section with name, support, and compatibility.
* fix(esp32): skip RTC timer wake on user shutdown
Do not arm esp_sleep_enable_timer_wakeup when msecToWake is portMAX_DELAY (UI shutdown), matching nRF52 system_off semantics.
fix(rak_wismesh_tap_v2): Tag OCV curve and 16MB partition
Add OCV_ARRAY matching WisMesh Tag for accurate SOC. Use 16MB flash partition scheme for TAP V2 hardware.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Trunkt
* Add RAK WisMesh Repeater Mini V2 and HP variants
Introduce rak_wismesh_repeater_mini (RAK4631) and
rak_wismesh_repeater_mini_hp (RAK3401) with low-VDD System OFF.
Skip LPCOMP battery wake on user-initiated shutdown to avoid
immediate reboot near the LPCOMP threshold; keep LPCOMP for
brownout-driven shutdown via variant_enableBatteryLpcompWake.
* docs(variants): update RAK BOM numbers for Repeater Mini V2 & HP
* chore: run trunk fmt
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
* Allow key verification to work for unknown nodes.
* trunk
* More reliable admin key decryption
* Add admin key fallback tests
* Actually check haveRemoteKey
* Logging cleanup
* Address review feedback
- Persist the committed key + manually-verified flag in
commitVerifiedRemoteNode via saveToDisk(SEGMENT_NODEDATABASE),
replacing the "todo: initiate save"
- Guard the CryptoEngine pending-key slot with a dedicated internal
lock; the Router reads it while already holding the non-recursive
cryptLock, so the accessors cannot reuse that lock
- Draw the security number from the hardware RNG (CryptRNG fallback)
under cryptLock instead of random(); on nRF52 the entropy fill
toggles the same CC310 the BLE task's packet crypto uses
- Return true after fully handling the hash2 response (restores
develop behavior; consistent with the hash1 branch)
- Take uint32_t in the number-picker callbacks so 8-digit hex
nodenums can't truncate through int
- Trim over-long comments flagged by review
* feat(tests): add deterministic tests for admin session-key behavior
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
MeshService::injectAsReceived (gated by MESHTASTIC_ENABLE_FRAME_INJECTION, off by default) extends
the portduino SimRadio SIMULATOR_APP path to real hardware: a client-supplied frame, wrapped in a
Compressed envelope on the SIMULATOR_APP portnum, is delivered through the real receive pipeline
(router->enqueueReceivedMessage) as if it arrived off the LoRa chip. It therefore exercises from!=0
enforcement, channel/PKC decryption, remote-admin authorization, and hop/dedup/module dispatch -
paths the toRadio API cannot reach (it forces from=0). from==0 is dropped to match real RX.
This forges over-the-air traffic, so the flag must never ship enabled. Drive it with the
meshtastic-mcp inject_frame tool / cli/meshinject.py. Documents the technique in the agent-facing
copilot-instructions.md + AGENTS.md.
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>
* 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>
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>
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.
* 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>
* 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.
* 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.
* 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.
${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.
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.
* 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>
* 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>
* 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.
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>
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.)
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>
* 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#10890Fixes#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.
* 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
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).
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>
* 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.
* 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>