* feat: add Nordic nRF54L15-DK variant (Zephyr + BLE + LoRa)
Adds a community hardware variant for the Nordic nRF54L15-DK (PCA10156)
with an external EBYTE E22-900M30S (SX1262) LoRa module. First Meshtastic
port running on the Zephyr RTOS; all other Nordic targets use the nRF5
SoftDevice stack.
Scope
-----
- New Zephyr-based platform layer under src/platform/nrf54l15/ providing
Arduino-compatible shims (Arduino.h, SPI, Wire, Print, Stream) over the
Zephyr APIs plus a LittleFS-backed InternalFileSystem on SPIM20.
- Bluetooth LE peripheral (NRF54L15Bluetooth.*) built on the Zephyr BT
host stack, exposing the Meshtastic GATT service with legacy
connectable advertising, just-works pairing, dynamic MTU exchange
(up to 247 bytes), and iOS connection-parameter tweaks.
- Variant directory variants/nrf54l15/nrf54l15dk/ with pin map for the
E22 module on connector J1, PlatformIO env (nrf54l15dk), Zephyr
DT overlay and a wiring README.
- Zephyr project config (zephyr/prj.conf + board overlay) tuned for
BT + LoRa: 16 KB main stack, 4 KB BT RX thread, RTT logging in
immediate mode, newlib-nano heap sized to leave room for the GATT
pools while still allowing ATT MTU=247.
- extra_scripts/nrf54l15_linker.py works around a PlatformIO + old Ninja
issue where Zephyr's two-pass linker script generation does not run
automatically; the post-script parses build.ninja and invokes the
gcc -E step directly before the final link.
- boards/nrf54l15dk.json board definition (PlatformIO needs it for the
DK; the Seeed platform only ships the XIAO variants).
- variants/rp2350/rp2350.ini excludes platform/nrf54l15/ from RP2350
build_src_filter so the shared platform tree does not leak between
targets.
- .gitignore: add nRF J-Link / RTT debug artifacts (flash.jlink,
rtt_*.txt).
Shared source changes
---------------------
- src/main.{cpp,h}, src/RedirectablePrint.cpp, src/FSCommon.{cpp,h},
src/mesh/{Channels,NodeDB,RadioLibInterface,MeshService,PhoneAPI}.cpp,
src/mesh/RadioLibInterface.h, src/modules/AdminModule.cpp: add small
guards / helpers so the Zephyr build compiles alongside the Arduino
targets. Behavior on existing boards is unchanged.
Hardware model
--------------
HW_VENDOR maps to meshtastic_HardwareModel_PRIVATE_HW until a dedicated
protobuf enum value is assigned upstream. The variant declares
custom_meshtastic_hw_model = 132 so the maintainers can wire the new
enum value through the protobufs repo after merge.
Hardware note
-------------
The E22-900M30S does not connect its DIO2 pin to TXEN internally — a
wire/solder bridge between DIO2 and TXEN on the module is required for
TX to work. Details and full pin map are in the variant README.
Validation
----------
Built clean against develop. On real hardware (April 2026) the port
passes end-to-end: iOS companion app pairs and connects, configuration
round-trip works, LoRa TX/RX reaches a canonical tbeam on the same mesh
channel, NodeDB updates propagate both ways, and traceroute completes.
* fix(nrf54l15): use atomic fs_rename instead of copy fallback
Zephyr LittleFS on nrf54l15 supports fs_rename natively, so route it
through the same atomic path as ESP32. The previous copyFile+remove
fallback truncated the destination before copying, leaving 0-byte files
if interrupted mid-write.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(nrf54l15): expand storage_partition from 36KB to 700KB
LittleFS on the default 9-block (36KB) storage_partition ran out of
space during copy-on-write of config.proto, causing fs_write to return
ENOSPC and pb_encode to surface "io error" when saving configuration
via the mobile app.
Reclaim slot1_partition (the MCUboot secondary slot — unused since we
flash directly via J-Link) and grow storage_partition to span
0xb6000..0x165000 (~175 blocks).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(nrf54l15): drop USERPREFS_LORACONFIG_* so LoRa config stays mutable
NodeDB rewrites LoRa config from USERPREFS_LORACONFIG_* on every boot,
which prevented reconfiguration via the BLE/serial app. Drop the
variant-level defaults; users configure region and modem preset through
the app like every other variant.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(nrf54l15): enforce MITM passkey pairing on GATT service
- Add MESH_PERM_READ/MESH_PERM_WRITE macros (READ_AUTHEN/WRITE_AUTHEN)
on all mesh service characteristics so clients must complete passkey
exchange before accessing fromNum/fromRadio/toRadio/logRadio.
- Wire FIXED_PIN mode to bt_passkey_set() so the device advertises a
known PIN (config.bluetooth.fixed_pin); RANDOM_PIN keeps default
per-pairing random passkey.
- Reduce BleDeferredThread HARD_WATCHDOG_MS from 3min to 1min.
- prj.conf: CONFIG_BT_SMP_ENFORCE_MITM=y, CONFIG_BT_FIXED_PASSKEY=y,
CONFIG_BT_SMP_SC_PAIR_ONLY=n (legacy fallback for clients that abort
SC pairing with reason 0x01 within 150ms).
* fix(nrf54l15): resolve develop-merge conflict + cppcheck warnings
The `Merge branch 'develop'` left two ~RadioLibInterface() declarations
in src/mesh/RadioLibInterface.h: the inline version added upstream by
PR #10254 (which independently applied the same UAF guard this PR was
carrying) and the out-of-line version this PR introduced. GCC rejects
the duplicate, breaking every platform build. Drop the out-of-line
declaration + definition; keep upstream's inline form.
Also silence the 13 cppcheck low warnings introduced by the new
nrf54l15 Arduino shim — Arduino's `String`/`SPISettings` API contract
relies on implicit single-arg constructors used pervasively by
existing Meshtastic code, so suppress `noExplicitConstructor` inline
with a comment instead of breaking the API. The few mechanical wins
(`const tmp[2]`, `const uint32_t *sp`) are applied directly.
* fmt: fix Trunk Check lint issues on nrf54l15-port
- extra_scripts/nrf54l15_linker.py: move regular imports above
Import("env") to silence E402, add trunk-ignore-all(F821) for the
PIO/SCons SConstruct injection (matches esp32_pre.py / nrf52_extra.py
convention)
- src/platform/nrf54l15/NRF54L15Bluetooth.cpp: clang-format 16.0.3
- boards/nrf54l15dk.json + variants/nrf54l15/nrf54l15dk/README.md:
prettier 3.8.3 (also resolves markdownlint MD060 on README tables)
No behavior change.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(nrf54l15): address Copilot review comments + correct clang-format style
Six review threads from the 2026-04-30 Copilot review:
- src/platform/nrf54l15/nrf54l15_main.cpp: validate PSP against the nRF54L15
SRAM range (0x20000000..0x20040000) and 4-byte alignment before walking the
faulting thread's stack, and clamp the walk so it never reads past the end
of RAM. Prevents a second fault inside the fatal handler when PSP is
corrupted (common in real faults).
- src/platform/nrf54l15/nrf54l15_arduino.cpp: gate the bring-up printk traces
in digitalWrite/digitalRead (CS/NRESET toggle log, BUSY-before-NRESET
snapshot, BUSY periodic timeline) behind a new -DNRF54L15_GPIO_DEBUG flag
that is off by default. The "dev NOT READY" message stays unconditional —
it indicates a genuine hardware/DTS misconfig.
- src/modules/AdminModule.cpp: don't mutate config.device.output_gpio_enabled
from handleGetConfig(). Reflect the live pin state in the response payload
only — a getter must not write back to disk-persisted state.
- src/platform/nrf54l15/InternalFileSystem.h: derive totalBytes() from
FIXED_PARTITION_SIZE(storage_partition) at compile time so it tracks the
DK overlay's ~700 KB partition instead of the stale 36 KB hard-coded value.
Updated the file header comment accordingly.
- extra_scripts/nrf54l15_linker.py: make _extract_gcc_command() handle the
POSIX Ninja COMMAND format (no `cmd.exe /C "..."` wrapper) in addition to
the Windows form, so the script doesn't hard-fail on Linux/macOS hosts.
- src/platform/nrf54l15/NRF54L15Bluetooth.cpp: clamp NO_PIN to RANDOM_PIN
with a one-shot LOG_WARN. The mesh GATT permissions are declared with
BT_GATT_PERM_*_AUTHEN and prj.conf sets CONFIG_BT_SMP_ENFORCE_MITM=y, so
NO_PIN with no auth callbacks would leave every characteristic returning
BT_ATT_ERR_AUTHENTICATION. Falling back to RANDOM_PIN keeps the link
usable instead of silently broken. Also re-formatted this file with the
project's .trunk/configs/.clang-format (Linux braces, 4-space indent,
130-col) — the previous lint-fix commit a2aca3234 accidentally used the
default LLVM style here, which CI's clang-format would have rejected.
Build verified: pio run -e nrf54l15dk passes, RAM 47.4%, Flash 28.6%.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(nrf54l15): address remaining Copilot review threads
Round 2/3 review fixes — bugs first, then docs/portability:
BLE concurrency (NRF54L15Bluetooth.cpp):
- onNowHasData / sendLog / BleDeferredThread / shutdown: acquire
active_conn under ble_mutex via new acquire_active_conn() helper so
disconnected_cb can't free the conn between the null check and
bt_conn_ref/bt_gatt_notify (use-after-unref).
- write_toradio: reject writes that exceed MAX_TO_FROM_RADIO_SIZE with
ATT_ERR_INVALID_ATTRIBUTE_LEN instead of returning success and silently
dropping the payload (would hide failed config writes from the phone).
- start_advertising: truncate the device name to fit the 31-byte legacy
scan-response limit and switch to BT_DATA_NAME_SHORTENED so
bt_le_adv_start() doesn't reject the payload when the name approaches
CONFIG_BT_DEVICE_NAME_MAX=32.
Linker / portability:
- main.h: drop the rp2040Loop() forward declaration that had no
definition and no callers — would surface as a link error if any RP2040
build added a call to the symbol.
- nrf54l15_arduino.cpp: transfer16() now uses static __aligned(4) DMA
buffers (matching transfer()), removing the EasyDMA-reachability hazard
of caller-stack buffers on this part.
Filesystem (InternalFileSystem.h):
- usedBytes(): return real usage from fs_statvfs() instead of 0 so OTA
/ range-test free-space guards work.
- rewindDirectory(): close the dir before reopening — Zephyr fs_dir_t has
no rewind, and re-fs_opendir on an open handle leaks LittleFS state.
Crash handler (nrf54l15_main.cpp):
- After the stack walk, busy-wait 50 ms to flush RTT/printk and call
sys_reboot(SYS_REBOOT_COLD) directly so the saved_crash record is
actually reported on the next boot. Default Zephyr config has
RESET_ON_FATAL_ERROR=n, so the previous k_fatal_halt() spun forever.
Generalization / config:
- PhoneAPI.cpp: replace the NRF54L15_DK ifdef with a
MESHTASTIC_EXCLUDE_FILES_MANIFEST capability flag (defined in the
nrf54l15dk env) so future variants can opt in/out without touching
shared code.
- variants/nrf54l15/nrf54l15.ini: parameterize libdeps include paths via
${PIOENV} so additional nRF54L15 envs sharing nrf54l15_base don't break.
- prj.conf: drop the stale "36 KB storage_partition" comments — the DK
overlay reclaims slot1 to ~700 KB and runtime size comes from
FIXED_PARTITION_SIZE.
- nrf54l15dk overlay: remove the zephyr,console / zephyr,shell-uart
chosen entries that conflicted with CONFIG_UART_CONSOLE=n + RTT
console; keep uart30 enabled so swapping the console is one Kconfig
flip away.
Build: nrf54l15dk SUCCESS (flash 28.6%, RAM 47.4%); wiznet_5500_evb_pico2
SUCCESS (verifies the shared main.h change).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(nrf54l15dk): use PRIVATE_HW (255) for custom_meshtastic_hw_model
Per @thebentern's review: the nRF54L15-DK is a development kit, not a
canonical Meshtastic SKU, so it falls under HardwareModel::PRIVATE_HW
(255) — the same enum value already used at runtime via HW_VENDOR. The
placeholder 132 is removed; no dedicated enum number will be assigned
for DK boards. Slug stays NRF54L15_DK as a human-readable identifier.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(nrf54l15): unstarve bt_long_wq so SC pairing completes
bt_pub_key_gen() runs the ECC P256 key generation on bt_long_wq. At default
prio=10 (preemptible) and stack=1400 it gets starved by Meshtastic app
threads at boot — sc_public_key stays NULL for minutes, smp_public_key()
defers with SMP_FLAG_PKEY_SEND, and every SC pairing attempt stalls right
after the public-key exchange. iOS shows "Connecting…" forever with no
PIN prompt; bleak/CLI fails the first CCC notify write with
"Protocol Error 0x05: Insufficient Authentication".
Set CONFIG_BT_LONG_WQ_PRIO=0 (highest preemptible, ties with main) and
CONFIG_BT_LONG_WQ_STACK_SIZE=4096 (margin for the P256M driver frames).
Validated E2E with iOS Meshtastic app: bt_smp_pkey_ready fires within ~40 s
of boot, 20 SC Passkey Entry rounds complete with matching pcnf/cfm,
encrypt 0x01 / sec_level 0x04 (Authenticated MITM), bonded=1.
* feat(nrf54l15): hardware I2C bus via TWIM30 + sensor telemetry
Adds the Arduino TwoWire layer for the nRF54L15-DK so Meshtastic's
sensor drivers can talk to external I2C devices over the hardware
TWIM30 peripheral.
Bus binding:
- &uart30 disabled in the board overlay (peripheral instance 30 is
shared between UARTE30 / TWIM30 / SPIM30 — pick one). Console stays
on RTT via CONFIG_RTT_CONSOLE.
- New i2c30_default / i2c30_sleep pinctrl with SDA=P0.03 / SCL=P0.04.
External 4.7 kOhm pull-ups required on both lines.
- &i2c30 enabled at I2C_BITRATE_FAST (400 kHz).
- button_3 (SW3, P0.04) deleted from DTS so the pad can be claimed by
i2c30 pinctrl; SW3 is still wired to the pad on the DK, do not press
it during I2C use or it will short SCL to GND.
Arduino layer:
- src/platform/nrf54l15/Wire.cpp resolves the DT node at compile time
via DEVICE_DT_GET(DT_NODELABEL(i2c30)) and dispatches Arduino's
beginTransmission / write / endTransmission / requestFrom to
i2c_write / i2c_write_read / i2c_read. Buffer is sized to 256 bytes
for forward compatibility with the SE050 secure element on the
custom PCB.
- Wire.h drops the prior compile-only stubs and exposes the real
TwoWire surface.
- Arduino.h: BitOrder becomes an enum (not #define) so Adafruit_BusIO's
`typedef BitOrder BusIOBitOrder;` compiles.
Variant + build flags:
- nrf54l15.ini flips HAS_WIRE / HAS_SENSOR / HAS_TELEMETRY from 0 to 1
and cherry-picks the sensor libs Meshtastic needs (BusIO, Sensor,
BMP280, BME280, INA219/226/260/3221, SHT4X). The full
environmental_base group is avoided because it pulls
Adafruit_SSD1306 / Adafruit_GFX which rely on Arduino pin macros the
Zephyr shim does not implement.
- nrf54l15dk variant.h defines PIN_WIRE_SDA / PIN_WIRE_SCL for parity
with the Arduino convention used by other variants. The actual bus
wiring is fixed by the overlay pinctrl above.
Validated 2026-05-14/15 on the DK with BMP280 @ 0x76 (temperature +
barometric pressure) and INA3221 @ 0x42 (rail voltage / current);
EnvironmentTelemetry / PowerTelemetry packets transmit successfully
over LoRa.
Footprint cost on nrf54l15dk: +45 KB flash, +1.7 KB RAM.
* feat(nodedb): honor USERPREFS for environment telemetry on first boot
installDefaultConfig() now respects two new compile-time prefs:
USERPREFS_CONFIG_ENV_TELEM_UPDATE_INTERVAL
USERPREFS_CONFIG_ENVIRONMENT_MEASUREMENT_ENABLED
The mobile apps enforce a 30 min floor on environment_update_interval
in the settings UI, which makes short-interval bring-up testing of new
sensor hardware painful — you have to wait half an hour for the first
LoRa packet to confirm wiring + driver. With these prefs baked into
the variant, the firmware can ship a freshly-flashed device that
broadcasts on a shorter cadence (e.g. 900 s) the moment storage_partition
is empty.
Both prefs are gated on #ifdef so the behavior is unchanged for any
variant that does not opt in. Documented in userPrefs.jsonc with the
existing telemetry-interval pref block.
* fix(nrf54l15): allow multiple bonded BLE peers
CONFIG_BT_MAX_PAIRED defaults to 1, so once the first peer (e.g. an
iOS phone) has paired and bonded, every subsequent pairing attempt
from a different MAC fails inside bt_keys_get_addr() with no free
key slot — the host returns BT_SECURITY_ERR_KEY_DOES_NOT_EXIST and
the second peer never gets past SMP.
Raise the slot count to 4 so the device can simultaneously hold an
iOS phone, a Windows host, a Linux host, and one spare bond. Add
BT_KEYS_OVERWRITE_OLDEST so that once the table fills, the LRU peer
is evicted on the next pairing rather than rejecting the new peer.
This matches the behavior other Meshtastic ports already provide
(nRF52 uses CONFIG_BT_PERIPHERAL_PRIO_CONN with similar semantics).
Discovered while bringing up the Python CLI on Windows alongside
the existing iOS bond.
* fix(nrf54l15): zero-initialize TwoWire buffers + clang-format Wire
cppcheck on every CI target (esp32s3, rp2040, rp2350, nrf52840, ...) was
failing the build with two `uninitMemberVar` warnings on TwoWire's
constructor: `txBuf` and `rxBuf` (256-byte arrays) were not initialized.
Even though the buffers are only read after txLen/rxLen is set, leaving
them uninitialized is a footgun if any future caller bypasses the
len-set step. Use C++11 value-initialization in the member initializer
list — costs ~512 B of memset at boot, gains a clean cppcheck pass and
defensive-against-future-changes semantics.
Also reformat Wire.{cpp,h} with the project's `.trunk/configs/.clang-format`
config so the Trunk Check Runner passes — clang-format moved the
`<errno.h>` include before the Zephyr-namespaced ones in Wire.cpp and
collapsed two inline overloads to single lines in Wire.h.
* fix(AdminModule): remove dead OUTPUT_GPIO_PIN/GpioOutputModule references
OUTPUT_GPIO_PIN is never defined and modules/GpioOutputModule.h doesn't
exist in the codebase; all #ifdef OUTPUT_GPIO_PIN branches were dead code
introduced by the nRF54L15-DK variant commit. Strips the include, the
output_gpio_enabled OFF→ON/ON→OFF transition logic in handleSetConfig(),
and the digitalRead() reflection in handleGetConfig().
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
* 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>
* 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>
* 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
* dependency swap - INA3221Sensor
update INA3221 initialization and measurement methods for compatibility with Rob Tillaart's library
Co-authored-by: Copilot <copilot@github.com>
* Addresses copilot review
Co-authored-by: Copilot <copilot@github.com>
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* Refine comments on USB detection and INA3221
Updated comments regarding USB detection and INA3221 usage.
* Fix static_assert conditions for INA3221 channel definitions
Co-authored-by: Copilot <copilot@github.com>
* moved macro defines earlier to allow better use.
Co-authored-by: Copilot <copilot@github.com>
* Add raw register read methods for bus voltage and shunt current in INA3221Sensor
Co-authored-by: Copilot <copilot@github.com>
---------
Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* 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>
* PositionModule::sendLostAndFoundText: use stack buffer, eliminate heap alloc
The lost-and-found message was built with an unnecessary heap allocation:
char *message = new char[60];
sprintf(message, "..."...);
...
delete[] message;
Two problems:
1. **Buffer too small.** The format string expands with two %f (IEEE 754
doubles), which `sprintf` prints with full precision — easily 15+
digits each plus separators — so the actual rendered string can run
40-50 characters before even considering the emoji (4 UTF-8 bytes)
and the embedded BEL. A pathological lat/lon can overflow 60 bytes
and corrupt heap metadata. Unbounded `sprintf` with no size check.
2. **Heap churn in a GPS callback.** This function is called from the
position-update path which is already heap-sensitive. An infrequent
60-byte transient alloc isn't catastrophic, but stack is trivially
available here and removes the failure mode entirely.
Fix: replace with a 128-byte stack buffer and `snprintf` bounded by
`sizeof(message)`. Drop the matching `delete[]` since there's nothing
to delete.
Behavior is identical on the happy path; the overflow case now
truncates safely instead of scribbling over heap.
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* PositionModule.cpp: add trailing newline for clang-format
* Address Copilot review: cleaner snprintf size handling
Review feedback from @Copilot on PR #10251: the ternary-plus-static-cast
form mixed signed/unsigned types (int written vs. pb_size_t payload.size
vs. size_t sizeof(message)) and was harder to read than necessary.
Cleaner form:
const size_t msg_len = std::min(static_cast<size_t>(written), sizeof(message) - 1);
p->decoded.payload.size = msg_len;
Same behaviour (clamp to buffer-minus-NUL) with one explicit cast and
a size_t variable that names the meaning. Handles the encoding-error
path (written < 0) separately so no bad values leak into payload.size.
* Trunk
---------
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
`memcpy(... p.payload.bytes, meshtastic_Constants_DATA_PAYLOAD_LEN)`
reads past the actual payload when the incoming packet's payload is
shorter than `DATA_PAYLOAD_LEN` (237 bytes). The code just above
already records the correct size:
this->packetHistory[...].payload_size = p.payload.size;
but then the memcpy ignores that and copies the full buffer capacity,
pulling uninitialized / adjacent memory bytes into the history entry.
Those extra bytes are later rebroadcast whenever the Store & Forward
module replays the packet.
Fix: memcpy using `p.payload.size` (the actual payload length) instead
of the constant buffer capacity.
Classification: bounded out-of-bounds READ into the protobuf scratch
buffer. Not directly exploitable for RCE (the destination buffer is
also DATA_PAYLOAD_LEN), but leaks adjacent memory into replayed
packets and is a latent correctness bug.
* Infinite calibration loop fix
* Save calibration
* Screen refresh
* reduce repeated code
* reduce repeated code to reduce flash
* fix Waypoint compass size and no fix no heading labels
* Don't show compass unless we have a heading and location
* If no calculated heading from moving, we should have no heading
* Slow walking calculated heading and auto stale heading when not moving
* Triming flash space
* cleanup
* show "?" when no location or heading for distance and heading screen
* cleanup
* Stale heading logic
* final trim
* Compass Calibration screen redesign
* Trunk Fix
* Compile fix
* patch
* Update src/motion/MotionSensor.cpp
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* Update WaypointModule.cpp
---------
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* Implementation of Status Message
* Change drawNodeInfo to drawFavoriteNode
* Truncate overflow on Favorite frame
* Set MAX_RECENT_STATUSMESSAGES to 5 to meet memory usage targets
* Consolidate SHTs into one class
* Remove separate SHT imports
* Create one single SHTXX sensor type
* Let the SHTXXSensor class handle variant detection
* Minor logging improvements
* Add functions to set accuracy on SHT3X and SHT4X
* Fix variable init in constructor
* Add bus to SHT sensor init
* Update src/modules/Telemetry/Sensor/SHTXXSensor.cpp
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* Update src/modules/Telemetry/Sensor/SHTXXSensor.cpp
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* Fix accuracy conditions on SHTXX
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* Merge upstream
* Add SHT2X detection on 0x40
* Read second part of SHT2X serial number
---------
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
SerialModule's weather station parser divides by velCount and dirCount
to compute wind speed/direction averages. Both counters are only
incremented when their respective sensor readings arrive, but the
division runs whenever gotwind is true (set by EITHER reading) and
the averaging interval has elapsed.
If only WindDir arrives without WindSpeed (or vice versa), or if the
timer fires before any readings accumulate, the division produces
undefined behavior (floating-point divide by zero on embedded = NaN
or hardware fault depending on platform).
Fix: add velCount > 0 && dirCount > 0 guard to the averaging block.
Co-authored-by: Patrickschell609 <patrickschell609@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
* fix(routing): prevent licensed users from rebroadcasting packets from unlicensed or unknown users
* fix(routing): prevent licensed users from rebroadcasting packets to or from unlicensed users
---------
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
* Fix TransmitHistory to improve epoch handling
* Enable epoch handling in unit tests
* Improve comments and test handling for epoch persistence in TransmitHistory
* Add boot-relative timestamp handling and unit tests for TransmitHistory
* loadFromDisk should handle legacy entries and clean up old v1 files after migration
* Revert "loadFromDisk should handle legacy entries and clean up old v1 files after migration"
This reverts commit eb7e5c7acf.
* Add NodeInfoModule integration for RTC quality changes and trigger immediate checks
* Update test conditions for RTC quality checks
* fix(routing): prevent licensed users from rebroadcasting packets from unlicensed or unknown users
* fix(routing): prevent licensed users from rebroadcasting packets to or from unlicensed users
---------
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
* fix: apply all LoRa config changes live without rebooting
All LoRa radio settings (SF, BW, CR, frequency, power, preset,
sx126x_rx_boosted_gain) now apply immediately via reconfigure()
without requiring a node reboot.
- AdminModule: requiresReboot = false for all LoRa config changes;
LoRa changes were already handled by the configChanged observer
calling reconfigure() but the reboot flag was set unnecessarily
- AdminModule: validate LORA_24 region against radio hardware at
config time; reject with BAD_REQUEST if hardware lacks 2.4 GHz
capability (wideLora() returns false or no radio instance)
- SX126xInterface/LR11x0Interface: apply sx126x_rx_boosted_gain in
reconfigure(); register 0x08AC is writable in STDBY mode (SX1261/2
datasheet §9.6); retention registers written so setting survives
warm-sleep cycles; log warning on setter failure
- DebugRenderer: show BW/SF/CR on debug screen when custom modem is
active instead of the preset name
- DisplayFormatters: clarify comment on getModemPresetDisplayName
* fix: remove redundant reboot after LoRa config changes in on-device menus
Region, frequency slot, and radio preset pickers in MenuHandler all
called reloadConfig() then immediately set rebootAtMsec. reloadConfig()
already fires the configChanged observer which calls reconfigure(), so
the forced reboot was unnecessary — same rationale as the parent commit.
* fix: guard LORA_24 region selection against hardware capability in on-device menu
Without a reboot, reconfigure() now applies region changes directly.
Previously getRadio() caught the LORA_24-on-sub-GHz mismatch post-reboot
and reverted to UNSET — that safety net is gone. Add an explicit wideLora()
check in LoraRegionPicker so sub-GHz-only hardware silently ignores LORA_24
selection instead of attempting a live reconfigure with an invalid frequency.
---------
Co-authored-by: elwimen <elwimen@users.noreply.github.com>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
* Enhance LoRa configuration with modem presets and validation logic
* Rename bootstrapLoRaConfigFromPreset tests to validateModemConfig for clarity and consistency
* additional tidy-ups to the validateModemConfig - still fundamentally broken at this point
* Enhance region validation by adding numPresets to RegionInfo and implementing validateRegionConfig in RadioInterface
* Add validation for modem configuration in applyModemConfig
* Fix region unset handling and improve modem config validation in handleSetConfig
* Refactor LoRa configuration validation methods and introduce clamping method for invalid settings
* Update handleSetConfig to use fromOthers parameter to either correct or reject invalid settings
* Fix some of the copilot review comments for LoRa configuration validation and clamping methods; add tests for region and preset handling
* Redid the slot default checking and calculation. Should resolve the outstanding comments.
* Add bandwidth calculation for LoRa modem preset fallback in clampConfigLora
* Remove unused preset name variable in validateConfigLora and fix default frequency slot check in applyModemConfig
* update tests for region handling
* Got the synthetic colleague to add mock service for testing
* Flash savings... hopefully
* Refactor modem preset handling to use sentinel values and improve default preset retrieval
* Refactor region handling to use profile structures for modem presets and channel calculations
* added comments for clarity on parameters
* Add shadow table tests and validateConfigLora enhancements for region presets
* Add isFromUs tests for preset validation in AdminModule
* Respond to copilot github review
* address copilot comments
* address null poointers
* fix build errors
* Fix the fix, undo the silly suggestions from synthetic reviewer.
* we all float here
* Fix include path for AdminModule in test_main.cpp
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* More suggestion fixes
* admin module merge conflicts
* admin module fixes from merge hell
* fix: initialize default frequency slot and custom channel name; update LNA mode handling
* save some bytes...
* fix: simplify error logging for bandwidth checks in LoRa configuration
* Update src/mesh/MeshRadio.h
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* Fix NodeInfo suppression logic to ensure suppression only applies to external requests
* Ensure NodeInfo reply suppression logic to only apply for external requests which are actually nodeinfo packets
* Add ESP32 Power Management lessons learned document
Documents our experimentation with ESP-IDF DFS and why it doesn't
work well for Meshtastic (RTOS locks, BLE locks, USB issues).
Proposes simpler alternative: manual setCpuFrequencyMhz() control
with explicit triggers for when to go fast vs slow.
* Addition of traffic management module
* Fixing compile issues, but may still need to update protobufs.
* Fixing log2Floor in cuckoo hash function
* Adding support for traffic management in PhoneAPI.
* Making router_preserve_hops work without checking if the previous hop was a router. Also works for CLIENT_BASE.
* Adding station-g2 and portduino varients to be able to use this module.
* Spoofing from address for nodeinfo cache
* Changing name and behavior for zero_hop_telemetry / zero_hop_position
* Name change for exhausting telemetry packets and setting hop_limit to 1 so it will be 0 when sent.
* Updated hop logic, including exhaustRequested flag to bypass some checks later in the code.
* Reducing memory on nrf52 nodes further to 12 bytes per entry, 12KB total using 8 bit hashes with 0.4% collision. Probably ok. Adding portduino to the platforms that don't need to worry about memory as much.
* Fixing hopsAway for nodeinfo responses.
* traffic_management.nodeinfo_direct_response_min_hops -> traffic_management.nodeinfo_direct_response_max_hops
* Removing dry run mode
* Updates to UnifiedCacheEntry to use a common cache, created defaults for some values, reduced a couple bytes per entry by using a resolution-scale time selection based on configuration value.
* Enhance traffic management logging and configuration. Updated log messages in NextHopRouter and Router to include more context. Adjusted traffic management configuration checks in AdminModule and improved cache handling in TrafficManagementModule. Ensured consistent enabling of traffic management across various variants.
* Implement destructor for TrafficManagementModule and improve cache allocation handling. The destructor ensures proper deallocation of cache memory based on its allocation source (PSRAM or heap). Additionally, updated cache allocation logic to log warnings only when PSRAM allocation fails.
* Update TrafficManagementModule with enhanced comments for clarity and improve cache handling logic. Update protobuf submodule to latest commit.
* Creating consistent log messages
* Remove docs/ESP32_Power_Management.md from traffic_module
* Add unit tests for Traffic Management Module functionality
* Fixing compile issues, but may still need to update protobufs.
* Adding support for traffic management in PhoneAPI.
* Making router_preserve_hops work without checking if the previous hop was a router. Also works for CLIENT_BASE.
* Enhance traffic management logging and configuration. Updated log messages in NextHopRouter and Router to include more context. Adjusted traffic management configuration checks in AdminModule and improved cache handling in TrafficManagementModule. Ensured consistent enabling of traffic management across various variants.
* Implement destructor for TrafficManagementModule and improve cache allocation handling. The destructor ensures proper deallocation of cache memory based on its allocation source (PSRAM or heap). Additionally, updated cache allocation logic to log warnings only when PSRAM allocation fails.
* Update TrafficManagementModule with enhanced comments for clarity and improve cache handling logic. Update protobuf submodule to latest commit.
* Add mock classes and unit tests for Traffic Management Module functionality.
* Refactor setup and loop functions in test_main.cpp to include extern "C" linkage
* Update comment to include reduced memory requirements
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* Re-arranging comments for programmers with the attention span of less than 5 lines of code.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* Update comments in TrafficManagementModule to reflect changes in timestamp epoch handling and memory optimization details.
* bug: Use node-wide config_ok_to_mqtt setting for cached NodeInfo replies.
* Better way to handle clearing the ok_to_mqtt bit
* Add bucketing to cuckoo hashing, allowing for 95% occupied rate before major eviction problems.
* Extend nodeinfo cache for psram devices.
* Refactor traffic management to make hop exhaustion packet-scoped. Nice catch.
* Implement better position precision sanitization in TrafficManagementModule.
* Added logic in TrafficManagementModule to invalidate stale traffic state. Also, added some tests to avoid future me from creating a regression here.
* Fixing tests for native
* Enhance TrafficManagementModule to improve NodeInfo response handling and position deduplication logic. Added tests to ensure local packets bypass transit filters and that NodeInfo requests correctly update the requester information in the cache. Updated deduplication checks to prevent dropping valid position packets under certain conditions.
---------
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>