* Fix position precision for direct sends
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* Clarify zero position precision logging
* Use const channel reference for position precision
* Use C linkage for position precision test entrypoints
---------
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Move WiFi.onEvent(WiFiEvent) registration before createSSLCert() to
prevent a race where the ESP32 auto-reconnects during cert generation
and fires GOT_IP before the handler is attached, causing
onNetworkConnected() to never run and the TCP/HTTP API services to
never initialize when booting without USB serial.
Also call onNetworkConnected() from reconnectWiFi() on all platforms
(not just RP2040) as a safety net; it is already guarded by
APStartupComplete so it only runs once.
* Added NodeDB fixtures and refactored to use std maps for better efficiency
* Defer NodeDB save during xmodem transfer to prevent mid-transfer fsFormat
2026-05-12 17:23:29 -05:00
Ben MeadorsGitHubcopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
* ThinkNode G3, ETH support WIP
* ThinkNode G3, ETH support WIP
* ThinkNode G3, ETH support WIP
* ThinkNode G3, ETH support WIP
* ThinkNode G3, ETH support WIP
* ThinkNode G3, ETH support WIP
* ThinkNode G3, ETH support WIP
* rename variant and add guard macros
* older G3 operational. M7 next.
* Split out G3 and M7 to different variants. Completely new PCB design. The G3 stays on 'PRIVATE_HW'
* Define button behaviour and use all of the device flash
---------
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: caveman99 <25002+caveman99@users.noreply.github.com>
Co-authored-by: Jonathan Bennett <jbennett@incomsystems.biz>
* ThinkNode G3, ETH support WIP
* ThinkNode G3, ETH support WIP
* ThinkNode G3, ETH support WIP
* ThinkNode G3, ETH support WIP
* ThinkNode G3, ETH support WIP
* ThinkNode G3, ETH support WIP
* ThinkNode G3, ETH support WIP
* rename variant and add guard macros
* older G3 operational. M7 next.
* Split out G3 and M7 to different variants. Completely new PCB design. The G3 stays on 'PRIVATE_HW'
* Define button behaviour and use all of the device flash
---------
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: caveman99 <25002+caveman99@users.noreply.github.com>
Co-authored-by: Jonathan Bennett <jbennett@incomsystems.biz>
Two sites built ClientNotification messages with sprintf into a
fixed-size proto buffer with no length cap. The current format strings
fit comfortably, but a future caller editing either format string
without rechecking the buffer size would get a silent stack/heap
overrun. Switch to snprintf with sizeof so the bound is enforced at
the call site.
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
* LR2021 radio on NRF_Promicro
Co-authored-by: Copilot <copilot@github.com>
* Refactor LR2021 interface includes and conditional compilation for improved clarity
Co-authored-by: Copilot <copilot@github.com>
* Refactor LR20x0 interface: remove unused includes and update comments for clarity
* Fix LR2021 max power definitions and add radio type detection tests
* remove potato radio type detection tests
* Include placeholder for DCDC - currently requires godmode
* Added godmode features - not enabled by default
---------
Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
* Refactor memory management in Syslog and StoreForwardModule
* Implement destructor for Lock
* Refactor RotaryEncoder and PacketHistory to use smart pointers for better memory management
* CH341 should use unique_ptr for improved memory management
* Fix checks in PH
* Improve Syslog::vlogf to handle variable argument lists more safely
* Fix initOk method to use nullptr for null pointer check
* stm32wl: check HAL_FLASH_Unlock() return in _internal_flash_erase
_internal_flash_prog already checks HAL_FLASH_Unlock() and returns
LFS_ERR_IO on failure. _internal_flash_erase discarded the return
value, proceeding to erase even if the flash was not unlocked.
Apply the same check for consistency and safety.
Signed-off-by: Andrew Yong <me@ndoo.sg>
Assisted-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* stm32wl: fix _internal_flash_prog to abort on first write error
Previously the programming loop continued to the next doubleword after
HAL_FLASH_Program() failed, potentially writing to invalid addresses
and returning a misleading error code only at the end (last iteration).
HAL_FLASH_Lock() was also skipped on the mid-loop early return path.
- Move bounds check before the loop (validate full range at once)
- Break on first HAL error so subsequent doublewords are not written
- Move HAL_FLASH_Lock() after the loop so it always runs
Signed-off-by: Andrew Yong <me@ndoo.sg>
Assisted-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* stm32wl: clear stale flash SR error flags before erase and program
Stale error flags in FLASH->SR from a previous failed operation can
cause HAL_FLASH_Program() or HAL_FLASHEx_Erase() to return HAL_ERROR
immediately without attempting the operation.
Add __HAL_FLASH_CLEAR_FLAG(FLASH_FLAG_ALL_ERRORS) after each
HAL_FLASH_Unlock() in both _internal_flash_prog and
_internal_flash_erase to ensure a clean state before each operation.
Signed-off-by: Andrew Yong <me@ndoo.sg>
Assisted-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* stm32wl: reject flash prog writes not aligned to 8-byte doubleword
The STM32WL HAL minimum write unit is one 64-bit doubleword (8 bytes).
_internal_flash_prog silently truncated any trailing bytes when size % 8
!= 0 because dw_count = size / 8 drops the remainder. Return LFS_ERR_INVAL
early so LittleFS sees the error rather than a silent short write.
Signed-off-by: Andrew Yong <me@ndoo.sg>
Assisted-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(nrf52,fs): use atomic SafeFile rename instead of direct write
NRF52 was bypassing the .tmp/readback/rename path entirely — openFile()
deleted the target file and wrote directly to it, and close() returned
true without verifying the write or renaming anything.
Adafruit_LittleFS::rename() calls lfs_rename() directly (confirmed at
Adafruit_LittleFS.cpp:205). Remove both ARCH_NRF52 guards so NRF52
follows the same write-to-.tmp → readback-hash → rename path used by
all other platforms.
Signed-off-by: Andrew Yong <me@ndoo.sg>
Assisted-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(admin): skip uiconfig.proto save on devices without a screen
handleStoreDeviceUIConfig() was writing /prefs/uiconfig.proto
unconditionally. MenuHandler.cpp is already gated behind #if HAS_SCREEN,
so there is no path that populates UI config on screen-less platforms.
Guard the save with #if HAS_SCREEN to avoid wasting a flash block on
devices that will never use it.
The read path (handleGetDeviceUIConfig) does not touch the filesystem
and needs no change.
Signed-off-by: Andrew Yong <me@ndoo.sg>
Assisted-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fs: enable format-on-retry for all platforms in saveToDisk
The FSCom.format() call on save failure was guarded to ARCH_NRF52 with
a comment that other platforms were not ready (bug #4184). STM32WL was
added to the guard in a prior commit. All platforms now expose format
semantics and the retry logic is identical — remove the guard.
To keep NodeDB.cpp platform-agnostic and fix a CI failure on native-tft
(portduino's fs::FS has no format() method), introduce fsFormat() in
FSCommon as the single call-site for all callers:
- Embedded (ESP32, NRF52, STM32WL, RP2040): delegates to FSCom.format()
- Portduino: rmDir("/prefs") + FSBegin() (a no-op on portduino).
rmDir("/prefs") is already called unconditionally by factoryReset()
(NodeDB.cpp:504), so both primitives are proven on portduino.
Replace both direct FSCom.format() calls in NodeDB.cpp with fsFormat().
Note: we do not run portduino locally — portduino/native build testers
please verify the format-on-retry path.
Signed-off-by: Andrew Yong <me@ndoo.sg>
Assisted-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* DO NOT MERGE: nrf52(fs): add File() default constructor bound to InternalFS
Adds File() to the Adafruit LittleFS File class (in the Meshtastic
Adafruit_nRF52_Arduino fork), delegating to File(InternalFS). This
matches the default-constructible File API on all other platforms.
The constructor is implemented in Adafruit_LittleFS_File.cpp rather
than inline in the header to avoid a circular include between
Adafruit_LittleFS_File.h and InternalFileSystem.h.
FOLLOW-UP REQUIRED: nrf52.ini points to a commit SHA on the
mesh-malaysia/Adafruit_nRF52_Arduino fork instead of the upstream
meshtastic framework. Once meshtastic/Adafruit_nRF52_Arduino#5 is
merged, revert nrf52.ini to point back to the upstream meshtastic
framework URL.
Signed-off-by: Andrew Yong <me@ndoo.sg>
Assisted-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* stm32wl(fs): add File() default constructor and document LFS tunables
Adds File() to STM32_LittleFS_Namespace::File, delegating to
File(InternalFS). Implemented in the .cpp to avoid a circular include
between STM32_LittleFS_File.h (which cannot include LittleFS.h) and
the InternalFS extern declaration.
This matches the File API on ESP32/RP2040/Portduino and is a
prerequisite for removing the ARCH_STM32WL guard in xmodem.h.
No behavior change — the constructor leaves the file in the same
closed/unattached state as File(InternalFS) would.
Signed-off-by: Andrew Yong <me@ndoo.sg>
Assisted-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fs: remove arch-specific ifdefs from FSCommon, SafeFile, xmodem
Now that NRF52 and STM32WL have File() default constructors and NRF52
has working atomic SafeFile rename, the capability gaps are closed.
Remove all per-arch guards across the shared FS layer:
FSCommon.cpp — renameFile():
Use FSCom.rename() on all platforms. Adafruit_LittleFS::rename()
calls lfs_rename() directly (Adafruit_LittleFS.cpp:205). The
copy+delete fallback on NRF52/RP2040 was never necessary.
FSCommon.cpp — getFiles():
Replace four ARCH_ESP32 guards with a single filepath pointer at
the top of the loop (file.path() on ESP32, file.name() elsewhere).
Fix strcpy(fileInfo.file_name, filepath): bounded to
sizeof(fileInfo.file_name)-1 with explicit NUL termination to prevent
overflow of the 228-byte meshtastic_FileInfo::file_name array.
FSCommon.cpp — listDir():
Same filepath pointer approach. NRF52/STM32WL were in an else-branch
that only logged but never deleted — now all platforms follow the
unified del path. 12 guards → 2.
Fix three strncpy(buffer, ..., sizeof(buffer)) calls that did not
NUL-terminate when source length >= sizeof(buffer) (255 bytes).
Add explicit buffer[sizeof(buffer)-1] = '\0' after each.
FSCommon.cpp — rmDir():
Use listDir(del=true) everywhere. The ARCH_NRF52 rmdir_r() path and
the ARCH_ESP32|RP2040|PORTDUINO listDir() path collapse to one line.
SafeFile.cpp:
ARCH_NRF52 bypass removed (handled in preceding commit).
xmodem.h:
File file; now works on all platforms via default constructors
added in the two preceding commits.
Remaining #ifdef ARCH_ESP32 in FSCommon.cpp: exactly 4, all for the
file.path() vs file.name() API difference (ESP32 Arduino LittleFS
returns the full path; all others return only the name). That
difference lives in the framework and cannot be closed here.
Signed-off-by: Andrew Yong <me@ndoo.sg>
Assisted-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* stm32wl(fs): add write-behind page cache, reduce virtual block size and FS reservation (FORMAT BREAK)
Adds a write-behind (RMW) page cache to the STM32WL LittleFS driver,
modelled after the NRF52 Adafruit approach (flash_cache.c). This allows
LFS to use 256-byte virtual blocks backed by 2048-byte physical pages:
the erase/prog callbacks accumulate changes in a 2 KB RAM buffer; the
sync callback (and page eviction on page-change) flushes with a single
HAL physical-erase + doubleword-program pass.
LFS tunables changed (FORMAT BREAK — superblock parameters):
block_size: 2048 B → 256 B (8 virtual blocks per physical page)
read_size: 2048 B → 256 B (= block_size)
prog_size: 2048 B → 256 B (= block_size; hardware min is 8 B)
block_count: 112 → 80 (14 phys pages → 10 phys pages = 20 KiB)
Benefits:
- Internal fragmentation: max 2047 B/file → max 255 B/file
- Heap per open LFS file: ~4 KB → 512 B (prog + read buffers)
- Code flash headroom: 6.7 KB → ~14.1 KB (+7.4 KB)
- Block budget: 80 virtual blocks, worst-case peak ~20, ~60 free
Updates board_upload.maximum_size in wio-e5/platformio.ini from 233472
(256 KB − 28 KB) to 241664 (256 KB − 20 KB) to match the reduced FS
reservation.
Justification for the format break: the prior STM32WL firmware had
several flash write bugs fixed earlier in this series (missing error
flag clearing, no abort on first write failure, unaligned write
acceptance). These bugs very likely caused silent config corruption on
deployed devices. The format break should be treated as an enhancement:
it provides a clean, reliably-written starting point. Users will need
to reconfigure their device once after this update.
Correctness fixes applied to the cache implementation:
- alignas(8) on _page_cache: the buffer was uint8_t[] (alignment 1)
but _flash_cache_flush casts it to const uint64_t* — undefined
behaviour per C++ standard, potential Cortex-M hardfault. alignas(8)
guarantees the required alignment for the doubleword cast.
- HAL_FLASH_Lock() return value: was discarded. Now assigned to
lock_rc and propagated into rc if prior writes succeeded, so LFS
sees the error rather than a false success.
Signed-off-by: Andrew Yong <me@ndoo.sg>
Assisted-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* stm32wl(fs): reduce FS reservation from 10 pages to 7 pages (FORMAT BREAK)
Reduces LFS_FLASH_TOTAL_SIZE from 10 × 2 KiB pages (20 KiB) to
7 × 2 KiB pages (14 KiB), freeing 6 KiB for firmware.
board_upload.maximum_size updated accordingly across all STM32WL variants:
241664 (256 KiB - 20 KiB) → 247808 (256 KiB - 14 KiB)
This is a FORMAT BREAK: existing filesystems must be erased before use.
Assisted-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Andrew Yong <me@ndoo.sg>
* fix(fs): return false in renameFile() when FSCom is not defined
Avoids undefined behavior and -Wreturn-type warnings in configurations
that compile FSCommon.cpp without a filesystem backend.
Signed-off-by: Andrew Yong <me@ndoo.sg>
Assisted-by: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Signed-off-by: Andrew Yong <me@ndoo.sg>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
* Add clamping logic for milliseconds conversion and unit tests
* Simplify comments in secondsToMsClamped function
Removed detailed comments about seconds to milliseconds conversion.
Router::handleReceived stores its allocCopy of the encrypted packet in the
class member p_encrypted. callModules() invokes module replies that re-enter
the router via MeshService::sendToMesh -> Router::sendLocal, which on a
broadcast reply recursively calls handleReceived. The inner call overwrites
the outer's p_encrypted without releasing it; on the way out it nulls the
member, the outer release(p_encrypted) now releases nullptr, and the original
allocation is permanently leaked. ~381 B per recursion.
Promote p_encrypted to a function-local so each invocation owns its own copy
for its full lifetime. The MQTT-publish null check at the call site (added by
PR #9136 as a workaround for this bug) stays in place because allocCopy can
still legitimately return nullptr on packetPool exhaustion.
Copilot's review of PR #8999 (the original introduction) flagged this exact
pattern at merge time:
"Storing p_encrypted as a class member can cause issues with recursive or
concurrent calls to handleReceived() since each call would overwrite the
previous packet pointer."
The historical reason for the member (S&F needing to retain the encrypted
copy across calls) was satisfied differently by PR #9799 (ServerAPI converted
to std::unique_ptr + cleanup on connection close), so the member is no longer
load-bearing.
Reproduces issues #9632 / #10101 / #8729 (heap leak when MeshMonitor
connected; TCP drops on Station G2 / LILYGO ServerAPI dump abort).
Hardware A/B on Station G2 under sustained TCP-API retry storm (open :4403,
request config, disconnect mid-stream, repeat at ~0.6/s) - 9 min run:
| Build | heapFree drift | rebootCount delta |
| this patch | -1.5 KB (noise)| 0 |
| stock 2.7.13 | -73 KB (8.1KB/min) | +1 (OOM crash) |
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
Router::handleReceived stores its allocCopy of the encrypted packet in the
class member p_encrypted. callModules() invokes module replies that re-enter
the router via MeshService::sendToMesh -> Router::sendLocal, which on a
broadcast reply recursively calls handleReceived. The inner call overwrites
the outer's p_encrypted without releasing it; on the way out it nulls the
member, the outer release(p_encrypted) now releases nullptr, and the original
allocation is permanently leaked. ~381 B per recursion.
Promote p_encrypted to a function-local so each invocation owns its own copy
for its full lifetime. The MQTT-publish null check at the call site (added by
PR #9136 as a workaround for this bug) stays in place because allocCopy can
still legitimately return nullptr on packetPool exhaustion.
Copilot's review of PR #8999 (the original introduction) flagged this exact
pattern at merge time:
"Storing p_encrypted as a class member can cause issues with recursive or
concurrent calls to handleReceived() since each call would overwrite the
previous packet pointer."
The historical reason for the member (S&F needing to retain the encrypted
copy across calls) was satisfied differently by PR #9799 (ServerAPI converted
to std::unique_ptr + cleanup on connection close), so the member is no longer
load-bearing.
Reproduces issues #9632 / #10101 / #8729 (heap leak when MeshMonitor
connected; TCP drops on Station G2 / LILYGO ServerAPI dump abort).
Hardware A/B on Station G2 under sustained TCP-API retry storm (open :4403,
request config, disconnect mid-stream, repeat at ~0.6/s) - 9 min run:
| Build | heapFree drift | rebootCount delta |
| this patch | -1.5 KB (noise)| 0 |
| stock 2.7.13 | -73 KB (8.1KB/min) | +1 (OOM crash) |
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
* Use hash table for O(1) lookup of recently seen packets
* Eliminate a packet lookup during deduplication
* Infinite loop checks for find and remove
* Consolidate conditional compilation
* Exclude hash table from minimal build
* Additional comment on hash table capacity
* Unit tests for packet history changes
* Update incorrect comment about size clamp
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* Const
---------
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
* PhoneAPI: add missing tak_tag case + skip reserved gap in module-config iteration
The STATE_SEND_MODULECONFIG state machine iterates config_state through
ModuleConfigType enum values (1..MAX+1 = 16) and switches on
meshtastic_ModuleConfig_*_tag values. Two of the resulting tag values
land in the default / LOG_ERROR path:
1. `tak_tag` (16) — the meshtastic_ModuleConfig_TAKConfig struct exists
in the protobuf and has a `.tak` member in payload_variant, but no
PhoneAPI case ever sends it to the phone. As a result, Android
clients can't read the persisted TAK (Team Awareness Kit) module
config at all. Added case that sends moduleConfig.tak, matching the
pattern used for all other module-config tags (paxcounter,
traffic_management, etc.). NodeDB already persists the struct via
the moduleConfig protobuf save; this just wires the read path to
the phone.
2. Tag 14 — reserved gap in the oneof numbering. No payload_variant
member exists at tag 14. Without this patch, every phone reconnect
walks through config_state=14 and hits
`LOG_ERROR("Unknown module config type %d", config_state)`. On an
active deployment that's ~1,400 LOG_ERROR lines per day per node —
burying real errors. Added explicit `case 14: break;` so the gap
is silently skipped.
Also: lowered the `default:` log level from LOG_ERROR to LOG_DEBUG. A
truly-new unknown type number would indicate firmware lagging the
protobuf — annoying but not an error event worth LOG_ERROR, especially
since this path runs on every phone handshake. If a new ModuleConfig
tag appears, devs will notice via the phone UI missing it, not via log.
Observed on a Station G2 fleet: 1403 "Unknown module config type 16"
and 1427 "Unknown module config type 14" LOG_ERROR lines in 24 hours
from routine phone reconnects.
* Get rid of the placeholder
---------
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
The "Invalid protobufs (bad psk?)" and "Invalid portnum (bad psk?)"
messages fire every time a neighbor transmits on a channel whose
8-bit hash matches one of ours but the PSK differs. In RF environments
with multiple mesh groups nearby this is routine — a single device
can see dozens of these per minute from SAR/MeshCA/private networks
sharing a hash collision.
LOG_ERROR for a benign "not our PSK" event:
- spams the log when you have any neighboring mesh group
- makes a genuine PSK misconfiguration on YOUR own channel
indistinguishable from the constant cross-channel noise
- hides actual errors in the stream
LOG_DEBUG matches how similar decryption-failure paths are handled
elsewhere (eg. the PKC "decrypt attempted but failed" uses LOG_WARN).
Dropping the trailing "!" as well — these are expected events, not
exceptional ones.
The constructor sets `RadioLibInterface::instance = this` immediately,
before `init()` runs. `initLoRa()` in RadioInterface.cpp creates each
radio variant with `new SX1262Interface(...)` or similar, then calls
`init()`, and if init fails the `unique_ptr<RadioInterface>` is reset
to nullptr — destroying the object — while the static `instance`
pointer continues to point at the freed memory.
Main loop then checks `RadioLibInterface::instance != nullptr` and
calls `pollMissedIrqs()` or `resetAGC()` on the dangling pointer →
Guru Meditation (IllegalInstruction / LoadProhibited).
Reported in #9880 on an ESP32-S3 dev board without radio hardware
attached, where init always fails and the leftover pointer crashes
the device on the next `loop()` iteration.
Fix: add a virtual destructor to `RadioLibInterface` that clears the
static pointer iff it still references this object. A later successful
init() may have replaced `instance` with a different interface — the
`instance == this` guard preserves that case.
Fixes#9880
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>