tft_task_handler held spiLock for the entire LVGL cycle. Most of that
cycle is timer work and rendering into the draw buffer, which issues no
SPI at all - but on boards where the TFT, SD card and LoRa radio share
one bus (T-Deck), every radio operation on the main loop still waited it
out. That is tens to hundreds of milliseconds whenever the UI animates,
felt as mesh RX/TX latency.
device-ui now takes the lock around its own transfers instead
(meshtastic/device-ui#356), so the coarse hold here can go and the bus is
contended only during real traffic.
Lend it spiLock through a reentrant adapter. device-ui nests its guards -
SdFsCard::usedBytes() calls cardSize() and freeBytes(), each of which
takes the lock - while spiLock is a plain binary semaphore that would
self-deadlock on the second take, so track the owning task and only touch
the underlying lock on the outermost acquire.
Requires the device-ui pin bump included here.
Tested on T-Deck: flush, touch, panel init, SD detect, powersave sleep
and wake all exercised; LoRa RX decoding under a live UI, no deadlocks,
no watchdog resets. Also builds seeed-sensecap-indicator-tft.
Co-authored-by: Manuel <71137295+mverch67@users.noreply.github.com>
* Add Elecrow ThinkNode M8 variant scaffold (thinknode_m8)
nRF52840 + SX1262 + 2.4" e-paper + ATGM336H-5NR32 GPS.
All pins resolved from ThinkNode_M8_V0.3.sch; cross-checked
against meshtastic/firmware#9181 (Elecrow V0.1 reference).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Add Elecrow ThinkNode M8 board support (nRF52840/SX1262, 1.54in e-ink, ATGM336H GNSS, SC7A20, EC04 encoder)
* Address review: keep the stored backlight level out of blanking, match only the SC7A20 WHO_AM_I byte, and transfer detents atomically
* Use std::atomic for the press-and-turn detent counter so native builds compile
* Drop the ThinkNode M8 LED_BUILTIN redefinition that warned on every translation unit
---------
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* add LR 2021 to portduino, and allow Framebuffer devices to rotate the screen from config. Requires https://github.com/meshtastic/device-ui/pull/355 and supersedes https://github.com/meshtastic/firmware/pull/10567 and https://github.com/meshtastic/firmware/pull/11138
Many thanks to the original authors https://github.com/a-li3n and https://github.com/jessm33
* Build LR2021Interface.cpp in the wasm env
initLoRa() constructs LR2021Interface for Lora.Module: lr2021, so excluding
the file from the native-wasm source filter left the constructor undefined at
link time. LR20x0Interface.cpp stays excluded; it is template-only and comes in
via the InterfacesTemplates.cpp amalgamation.
Also replace the non-UTF-8 degree signs in the framebuffer rotation comment and
correct the rfswitch alias cleanup comment.
* Trim comments
* Report setenv failure for the framebuffer rotation
* Add T-Beam BPF (144-148 Mhz LoRa)
* minor correction to fix compiler warnings
* Add T-Beam BPF (144-148 Mhz LoRa)
* minor correction to fix compiler warnings
* Add ITU regions for this device and make GPS work.
* Switch pin after defining it as output
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* Lora CS is indeed 1, SD Card CS is 10
* Include the back option.
* Fix compilation with pioarduino (USB_MODE)
* Default ham to narrow_fast
* Default PROFILE_HAM to slot 17
This is an appropriate default in the USA but not the EU.
The slot override really should follow the region itself, not the regionprofile.
* Fix for ITU 2/3 split
* Add ITU region options to MenuAction enum
* Add HAS_HAM_2M definition to variant headers for 2M support
* Re-add PROFILE_HAM regionprofile
Accidentally removed in last merge
* Trunk fmt
* Initial default slots
* Switch to TinyFast
Still need to flesh out the default channels
* Adjust slotOverrides for TinyFast
* RadioLib doesn't accept 15.625 kHz
Use 15.6 instead
* Set RF95 pins for T-Beam Supreme
May cause regressions!!
* Remove other-variant changes (BPF-only)
These have been moved to other PRs
* Remove mismatch guarding (we need a more comprehensive approach)
* Add comment back
* Add template for PA curve
* This is a 5-6W radio!! Add TX_GAIN_LORA
* Trunk fmt because NomDeTom hates emdashes
---------
Co-authored-by: Thomas Göttgens <tgoettgens@gmail.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
The send_bell append writes bytes[size] and bytes[size+1] while the guard only checked size < DATA_PAYLOAD_LEN, overrunning the 233-byte payload array by one when size == DATA_PAYLOAD_LEN - 1. Guard on size + 1.
* Null-check packet allocations in allocForSending and its callers
Sibling of 0ae44d701.
* Null-check allocDataProtobuf, allocAckNak and allocErrorResponse callers
Second tier of the same nullable contract.
* Keep telemetry sleep scheduling on allocation failure
Allocation failure now marks the telemetry invalid instead of returning
early, so power-saving SENSOR nodes still arm deep sleep.
- t5s3-epaper: the board's 3.3V octal (AP_3v3) PSRAM is unreliable at the qio_opi
default of 80MHz (Total PSRAM reads 0, display dead / boot-loop); clock it at 40MHz.
- EInkParallelDisplay: skip EInk init and no-op display ops when the PSRAM framebuffer
is unavailable, so the node boots headless instead of hard-faulting on a NULL buffer.
- src/platform/esp32: weak software __atomic_*_{1,2,4} so esp32s3 links on toolchains
(e.g. macOS) that lack the sized libcalls GCC emits under -mdisable-hardware-atomics.
Removed:
- MessageStore::addFromPacket and addFromString, superseded by
tryAddFromPacket
- GeoCoord rangeRadiansToMeters, distanceTo, bearingTo
- Router::rawSend, declared virtual with no override and no caller
- ContentHandler handleHotspot, handleFs, handleAdminSettings,
handleAdminSettingsApply, handleDeleteFsContent and their commented
route registrations, plus the now unreachable htmlDeleteDir and the
handleUpdateFs declaration that had no definition
- ContentHelper replaceAll
- OnScreenKeyboardModule popup chain: showPopup, clearPopup, drawPopup,
drawPopupOverlay and their state, unreachable since the frame based UI
was replaced by baseUI
- DebugRenderer drawDebugInfoTrampoline, drawDebugInfoSettingsTrampoline
and the orphaned drawFrameSettings
- NodeListRenderer calculateMaxScroll, drawColumns and a stale extern
haveGlyphs declaration with no definition
- UIRenderer::haveGlyphs, Screen::blink,
NotificationRenderer::showKeyboardMessagePopupWithTitle,
VirtualKeyboard::getInputText
- InkHUD touchNavLeft, touchNavRight, Applet::getActiveNodeCount,
ThreadedMessageApplet::saveMessagesToFlash
- TwoButton::setHandlerUp, TwoButtonExtended setHandlerUp,
setJoystickDownHandlers, setJoystickUpHandlers
- CannedMessageModule LaunchRepeatDestination, isCharInputAllowed,
hasMessages
- TrafficManagementModule resetStats, recordRouterHopPreserved,
saturatingIncrement
- UnitConversions::MetersPerSecondToMilesPerHour
- EncryptedStorage getSessionRemainingSeconds
- BMI270Sensor::writeRegisters, GPS::hasFlow, FSCommon copyFile,
SerialConsole consolePrintf, buzz playLongPressLeadUp,
memGet displayPercentHeapFree
* stm32wl: add hardware RTC support infrastructure
Wires the STM32WL chip's internal RTC (running off the LSE 32.768kHz
crystal) into meshtastic's existing time-of-day framework
(perhapsSetRTC()/readFromRTC()), following the same pattern already
used for I2C RTC chips (RV3028, PCF8563/85063, RX8130CE).
LSE is started and polled manually before ever calling into the
STM32RTC library, with our own bounded timeout - the library's own
internal LSE startup path has no bounded fallback and hangs forever
via Error_Handler() if the crystal never locks, so this is required
for a board with a missing/faulty crystal to boot normally rather
than hang.
Gated behind a new HAS_LSE variant flag (currently unset everywhere,
so this is inert until a variant opts in - see follow-up commit).
Signed-off-by: Andrew Yong <me@ndoo.sg>
Assisted-by: Claude Sonnet 5 <noreply@anthropic.com>
* gps: qualify RTC.h includes to avoid case-insensitive filesystem collision with STM32RTC
The stm32duino STM32RTC library (added to lib_deps in a follow-up
commit) ships its own src/rtc.h. On case-insensitive filesystems
(the macOS default), an unqualified #include "RTC.h"/<RTC.h> from
any file outside src/gps/ resolves to the library's rtc.h instead of
src/gps/RTC.h, since PlatformIO's LDF puts lib_deps include paths
ahead of the project's own -Isrc/gps.
Qualify every include as gps/RTC.h so it can't collide with any
same-named header a future dependency might ship, regardless of
filesystem case sensitivity. Purely mechanical, no behavior change.
Signed-off-by: Andrew Yong <me@ndoo.sg>
Assisted-by: Claude Sonnet 5 <noreply@anthropic.com>
* stm32wl(rak3172): enable hardware RTC support
Opts rak3172 into the HAS_LSE infrastructure added previously: sets
STM32WL_LSE_DRIVE to a conservative default and pulls in the
STM32RTC library. rak3172 has ~63KB flash headroom going in;
build-verified at 76.7% flash usage after this change (up from a
73.8% baseline), well within budget.
wio-e5 is not opted in here despite sharing the same STM32WLE5 chip
- it's already at 96.8% flash usage today (GPS + I2C sensor support
compiled in, unlike rak3172), leaving too little headroom to safely
add STM32RTC without first trimming something else.
Signed-off-by: Andrew Yong <me@ndoo.sg>
Assisted-by: Claude Sonnet 5 <noreply@anthropic.com>
* stm32wl: add docstrings for LSE/RTC setup functions
Addresses CodeRabbit's docstring coverage check on PR #10961.
Signed-off-by: Andrew Yong <me@ndoo.sg>
Assisted-by: Claude Sonnet 5 <noreply@anthropic.com>
* stm32wl: address CodeRabbit nitpicks on PR #10961
- Brace the single-statement HAS_LSE branch in perhapsSetRTC() to
match the sibling readFromRTC() branch's style.
- Quote the RTC.h include in PhoneAPI.cpp for consistency with every
other qualified include site.
Signed-off-by: Andrew Yong <me@ndoo.sg>
Assisted-by: Claude Sonnet 5 <noreply@anthropic.com>
---------
Signed-off-by: Andrew Yong <me@ndoo.sg>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
* Snake!
* Add spiLock to snake score saving
* Check fixes
* More careful locking
* WIP: Big Display Node
* Update src/graphics/HUB75Display.cpp
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
* Add HUB75 Native
* Add Tetris game module with core logic and UI integration
- Implement TetrisGame class for game logic, including piece movement, rotation, and line clearing.
- Create TetrisModule class to manage game state, input handling, and rendering on OLED display.
- Introduce high score tracking with persistence and optional mesh broadcasting.
- Define UI states for title, playing, paused, game over, and high scores.
- Implement input handling for game controls and state transitions.
- Add rendering functions for the game board, high scores, and title screen.
* feat(snake): add snake graphics and update display logic in SnakeModule
* Prompt for Initials for Tetris, too
* refactor games module
* Games refactor
* hub75 native double buffer
* Games tuning
* Make joystick repeat events on held button
* Add clouds and colors
* Fix breakout and more color
* difficulty tuning
* trunk
* refactor game announcements, etc
* Scale chirpy gravity as game speed increases
* Portduino, check for hub75 display before reading in hub75 options
* Final(?) games tuning
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* Properly ignore input when games screen not shown
* Fail gracefully when HUB75 is selected but not supported.
---------
Co-authored-by: Thomas Göttgens <tgoettgens@gmail.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: Ixitxachitl <kramerfm@gmail.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* fix(display): don't leak a TFT driver instance on every screen wake
On variants whose Screen::handleSetOn() re-runs ui->init() on each display
wake (Heltec Tracker V1.x/V2, VTFT_LEDA and ST7796 boards, MUZI/Cardputer
via dispdev->init()), OLEDDisplay::init() unconditionally re-invokes
connect(), and TFTDisplay::connect() allocated a fresh driver instance
each time without freeing the old one. Each OFF->ON transition orphaned a
full LGFX device (measured: exactly 1,260 bytes per wake on a Heltec
Wireless Tracker V2). Since PowerFSM wakes the screen on every received
text message, a device on a busy channel leaked tens of KB per day -
matching field reports of heap climbing from 78% to 92% within a day.
Null-guard the driver allocations (matching the existing linePixelBuffer/
repaintChunkBuffer guards below them) so re-entry re-runs tft->init() on
the existing instance. Bench-validated on a Tracker V2: free heap now flat
across 13 consecutive wake cycles, with the panel still re-initializing
and rendering on every wake.
* fix(ble): don't leak BluetoothPhoneAPI and callbacks on BLE re-enable
NimbleBluetooth::setup()/setupService() re-run when Bluetooth is
re-enabled after deinit(), but BLEDevice::deinit() only frees the GATT
objects - the caller-owned allocations were re-created unguarded on every
cycle:
- bluetoothPhoneAPI: ~3.5KB PhoneAPI object per cycle, and the orphaned
instance is an OSThread that stays registered with the scheduler, so a
duplicate thread kept servicing the same static queues
- toRadioCallbacks / fromRadioCallbacks / security + server callbacks:
one small object each per cycle
- the BLESecurity shim was heap-allocated and never freed even on first
boot; it only forwards to static setters, so use a stack instance
Reuse the existing objects on re-setup; the setCallbacks() calls still
run every time since the characteristics themselves are new.
* chore: remove unused PacketCache
PacketCache landed in #8341 but no consumer was ever wired up - repo-wide,
the only references to PacketCache/packetCache are in its own two files.
As designed it also malloc()s per cached packet with no eviction or size
cap, so it should be re-reviewed for bounds before any future use. Remove
the dead code; git history preserves it if a bounded revival is wanted.
* fix(ble): reset stale session state when reusing BluetoothPhoneAPI
Review follow-up: reusing bluetoothPhoneAPI across BLE enable cycles could
hand the next session the previous session's dirty state. The only full
cleanup path (onDisconnect) is skipped when deinit()'s bounded disconnect
wait expires before the event lands (its 2s cap matches the connection
supervision timeout, so a phone that walked out of range makes this a
coin flip): the reused object then enters the next session mid-config,
with stale queue contents served to the new phone and a stale connection
handle that defeats the checkConnectionTimeout self-heal.
Factor onDisconnect's cleanup into resetBleSessionState() and run it from
setupService() when reusing the instance, restoring the old fresh-object
invariant. Also switch the four stateless callback objects to function-
local statics (the resolved framework BLE wrapper stores plain pointers
and never frees them, so static instances are safe and avoid the guarded
heap allocations), correct the comment that claimed deinit() frees the
GATT objects (it deletes only the BLEServer itself; the services and
characteristics it created remain a small library-side leak per cycle),
and fix the inverted connection check in getRssi() that made BLE RSSI
always read 0 on ESP32-S3/C6.
* refactor(display): construct-once guard around the whole driver ladder
Review follow-up: one if (!tft) around the #if/#elif/#else construction
block instead of a guard per branch, so a future display family can't
reintroduce the per-wake leak by copying an unguarded branch. Make the
HACKADAY bus pointer local to the construction (it was a write-only
global), and point the comment at the Screen::handleSetOn gates instead
of hand-listing boards that would go stale.
* fix(ble): use-after-free notifying a freed characteristic after deinit()
deinit() nulled bleServer and BatteryCharacteristic but left
fromNumCharacteristic dangling after BLEDevice::deinit(true) freed the
GATT graph, and it never detached the PhoneAPI fromNumChanged observer.
When deinit()'s bounded 2s disconnect wait expires before onDisconnect
runs (the same stale-bond / host-reset race resetBleSessionState was
built for), close() is skipped, so the observer stays attached with
state == STATE_SEND_PACKETS. After BLE is off, the next mesh packet
drives MeshService fromNumChanged -> PhoneAPI::onNotify -> onNowHasData
-> fromNumCharacteristic->notify() on freed memory (the framework BLE
wrapper's notify() dereferences the freed server via getConnectedCount).
Unlike sendLog(), onNowHasData() had no isConnected guard.
deinit() now calls resetBleSessionState() to detach the observer and
reset session state unconditionally (also forcing the conn handle to
NONE so checkConnectionTimeout can't be fooled by the stale handle),
nulls fromNumCharacteristic/logRadioCharacteristic like the other freed
pointers, and onNowHasData() bails on a null characteristic.
Reachable on all ESP32/S3/C6 NimBLE boards via admin disable-bluetooth
or a sleep transition while a phone is connected. Found by a follow-up
lifecycle audit of the re-enable changes in this PR.
Decree of the Cabinet of Ministers of Ukraine dated February 25, 2026,*
No. 262, on amendments to Section 2 of the Plan for the Allocation and
Use of the Radio Frequency Spectrum in Ukraine, has harmonised
Ukrainian regulations on the 868 MHz spectrum with the EU's, thereby
making UA_868 legally obsolete.
* https://zakon.rada.gov.ua/laws/show/262-2026-п
Co-authored-by: Tom <116762865+NomDeTom@users.noreply.github.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>
* 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.
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>
* first pass tests
* more tests
* Fix two crafted-admin-packet crashes found by the E5 fuzzer
Both are reachable from an authorized admin (local from==0, admin channel,
or PKC) - remote DoS:
1. SIGFPE in LoRa config validation. A set_config LoRaConfig with
use_preset=false and bandwidth=0 makes freqSlotWidth 0, so numFreqSlots
is 0 and `hash(name) % numFreqSlots` (RadioInterface.cpp) divides by
zero. Guard the modulo; the existing channel_num check then rejects/
clamps the config.
2. Stack overflow in Channels::getKey. A SECONDARY channel at the primary
slot with an empty PSK recursed into getKey(primaryIndex) forever. Skip
the primary-key borrow when chIndex == primaryIndex.
Re-enable the E5 admin fuzzer to hit both triggers again (use_preset both
ways incl. bandwidth 0, plus the set_channel tag) as regression guards.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Correct fuzz-test invariants after the crash fixes
- E5 admin fuzz: node eviction under a filling NodeDB is legitimate, so
assert only the bounded-count invariant, not that a specific seed node
survives 6000 mutating ops.
- TMM blitz: scope off the nodeinfo direct-response send path (it needs a
fully-wired MeshService/phone queue the fixture doesn't provide; the
deterministic directResponse tests cover it). The crafted-nodenum
rate/unknown/position cache stress is unchanged.
clod helped too
* realistic tests
* test: dedup fuzz RNG into shared test/support/DeterministicRng.h
The four in-tree fuzz suites (test_fuzz_decode, test_fuzz_packets,
test_hop_scaling, test_traffic_management) each carried a byte-identical
copy of the seeded 64-bit LCG (rngSeed/rngNext/rngByte/rngRange). Hoist
it into one shared header so there is a single generator to reason about
and no risk of the copies drifting. static inline keeps per-suite state
per translation unit and avoids -Wunused-function for suites that don't
use every helper. Also corrects a stale comment in test_traffic_management
(the blitz's nodeinfo direct-response path is intentionally left off).
No behavioral change: same constants, same per-suite seeds.
clod helped too
* test: fuzz uncovered ProtobufModule handlers and the MQTT downlink ingress
Extend the in-tree fuzz coverage to packet sources that previously had
none:
- test_fuzz_packets E8/E9/E10: drive PositionModule, DeviceTelemetryModule
and NeighborInfoModule at handleReceivedProtobuf directly (via using-shims,
bypassing the ProtobufModule reply/send path so no router is needed). The
fixture already stands up nodeDB/service/channels, and nodeStatus/powerStatus
are auto-initialized in main.cpp, so no new globals are required. Adds a
shared fuzzRxHeader() helper for crafting adversarial RX packet headers.
- test_fuzz_decode: add meshtastic_KeyVerification to the decode table. The
KeyVerification and StoreForward handler paths are documented as decode-level
only, with the concrete reason each is intrinsic (private-state gating /
PSRAM + self-pointer wiring), not a fixture gap.
- test_mqtt: test_receiveFuzzServiceEnvelope blitzes the non-RF broker-push
ingress (onReceiveProto) two ways - raw garbage bytes that must fail envelope
decode cleanly, and a well-formed ServiceEnvelope wrapping a crafted inner
MeshPacket over crafted channel_id/gateway_id - exercising the channel match,
isFromUs, XEdDSA receive policy and perhapsDecode chain. Adds a deliverRaw()
passthrough to MQTTUnitTest.
All under the coverage env (ASan/LSan). No firmware/src changes. Full sweep
GREEN 27/27, 544 cases.
clod helped too
* Harden LoRa/channel config against crafted admin messages; consolidate test helpers
Production (review findings on the hot-fuzz crash fixes):
- Clamp bandwidth at the source (clampBandwidthKHz) in checkOrClampConfigLora
and applyModemConfig so numFreqSlots can never be 0 for any consumer; a
bandwidth-0 set_config previously passed validation and re-armed the SIGFPE
on the next applyModemConfig.
- Guard applyModemConfig's hash % numFreqSlots (the validator's sibling modulo
was fixed earlier but this one was still unguarded).
- Enforce the primary-channel invariant in Channels::onConfigChanged: a config
demoting every slot now re-promotes the stale SECONDARY slot (keeping its
key) or restores the default channel if the slot is DISABLED, instead of
leaving every getPrimaryIndex() reader on a non-primary slot. The getKey
recursion guard stays as defense-in-depth.
Tests:
- New test/support/MockMeshService.h and AdminModuleTestShim.h replace four
byte-identical mocks and three divergent admin shims (test_mqtt's capturing
mock is genuinely different and stays).
- DeterministicRng.h: add rngFill() (replaces 14 hand-rolled fill loops) and
rngEdgeNodeNum() (unifies the three NodeNum boundary pools).
- Extract fuzzChannelSettings() shared by the set_channel case and fuzzBeacon.
- fuzzBeacon: the un-terminated branch now fills the whole buffer with non-NUL
bytes so the strnlen bound is actually stressed (~50% of iterations, not ~4%).
- E6 beacon fuzz: replace the TEST_ASSERT_TRUE(true) tautology with real
invariants (handler never consumes; offers land in lastReceivedOffer keyed
to the sender).
- Trim the seven over-long comment blocks flagged against the 1-2 line rule;
the FINDINGS trailer moves to this commit message (see production notes).
Full native suite GREEN 27/27 under the coverage (ASan/LSan) env.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Clamp UTF-8 char length in the emote walkers; add test_fuzz_emotes
A TEXT_MESSAGE payload is opaque protobuf bytes, so PB_VALIDATE_UTF8 never
screens it - invalid UTF-8 and truncated multi-byte lead bytes reach the
emote/width render path verbatim. EmoteRenderer's walkers advanced by
utf8CharLen(lead) without clamping to the bytes actually remaining, so a
truncated lead (e.g. a lone 0xF0, which claims 4 bytes) near the end of the
buffer made getUtf8ChunkWidth's memcpy read past the string. ASan confirms a
heap-buffer-overflow READ from measureStringWithEmotes.
Add utf8CharLenClamped() and use it at every walk site (width measure,
truncation cut-loop, and the draw-path text-run/chunk builders); the one
already-guarded site (matchAtIgnoringModifiers) is unchanged.
New test/test_fuzz_emotes drives measureStringWithEmotes and truncateToWidth
over adversarial byte strings (biased to embed/end in truncated multi-byte
leads) in exact-sized heap buffers so any over-read is a hard ASan fault. Its
headless display uses a synthetic font (firstChar 0, fontData centered in a
large buffer) so the stock OLEDDisplay::getStringWidth - which indexes the
font jump table with a signed char and over-reads for any byte >= 0x80 - does
not mask the finding. native-suite-count bumped 27 -> 28.
Full native suite GREEN 28/28 under the coverage (ASan/LSan) env.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Keep emote width measurement in-bounds for non-ASCII bytes
OLEDDisplay::getStringWidth (the utf8=false path EmoteRenderer uses on default
builds) indexes the font jump table by (c - firstChar) with a signed char and
no bounds check, so any byte outside printable ASCII - high bytes from UTF-8
text, but also a stray control byte like 0x0A - reads outside the font array.
On-device this reads adjacent flash and returns a garbage width; under ASan the
test_fuzz_emotes fuzzer flags it as a global-buffer-overflow, and it made the
non-ASCII width measurement meaningless either way.
The OLED driver is a pinned upstream dependency, so guard it firmware-side in
EmoteRenderer's getStringWidth helper: measure a sanitized copy where any byte
outside [0x20, 0x7E] counts as a '?' placeholder. Printable ASCII is unchanged
and the UA/RU lookup path is untouched.
test_fuzz_emotes now drives a real ArialMT font instead of the synthetic
in-bounds font it needed before this fix, so the suite exercises the true
production width path (utf8CharLen clamp + this sanitizer) end to end. The same
fuzzer tripped the global-buffer-overflow before this change.
Full native suite GREEN 28/28 under the coverage (ASan/LSan) env.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* Add MemAudit: per-subsystem heap accounting in the boot log
The 2.8.0 nRF52840 heap-exhaustion field reports had to be diagnosed by
hand, reconstructing each subsystem's heap footprint from source and
build flags one report at a time. This makes every future report
self-diagnosing from the serial log: a tiny fixed-size registry
(src/memory/MemAudit.*) that big long-lived allocations report into,
printed as one line at the end of setup() and alongside the periodic
"Heap free:" log:
MemAudit[boot]: tmm=2500 warm=4000 pkthist=5824 nodedb=13440
msgstore=2200 pktpool(live)=3270 total=31234
Instrumented: NodeDB hot vector (nodedb) + satellite maps (satmaps,
rb-tree overhead estimated), WarmNodeStore (warm), PacketHistory records
and hash index (pkthist), TrafficManagement caches (tmm/tmm_ni),
MessageStore text pool (msgstore), TFT line/repaint buffers (display),
and live in-flight packets (pktpool(live)) via an optional audit tag on
the packet pool allocator - the one hot path, counted with a relaxed
32-bit atomic add (single instructions on Cortex-M, no locks).
Cost: 128 B RAM for the 16-tag table, well under 1 KB flash on rak4631.
MESHTASTIC_MEM_AUDIT=0 compiles it out to inline no-op stubs (call
sites need no ifdefs); STM32WL, the tightest flash target, defaults off.
New native suite test_mem_audit covers add/set/snapshot arithmetic, tag
reuse (pointer and cross-TU strcmp fallback), null/unknown tags, and
table-full behavior; test/native-suite-count bumped to 28.
* native-wasm: add src/memory/ to the curated source filter
The wasm env denies all sources and adds an explicit file list; MemAudit
callers (main, MeshService, NodeDB, PacketHistory) are in that list but
src/memory/MemAudit.cpp was not, so wasm-ld failed on undefined
memaudit:: symbols.
The Python MCP server + hardware test harness that lived under mcp-server/
now has its own home at https://github.com/meshtastic/meshtastic-mcp
(published, versioned independently). Remove the in-tree copy and wire the
firmware repo to the standalone server externally.
- Delete mcp-server/ (96 files) and the 8 harness-coupled AI workflow files
under .claude/commands/ and .github/prompts/ that drove ./mcp-server/
run-tests.sh — those workflows now ship with meshtastic-mcp as skills.
- .mcp.json: register the server via
`uvx --from git+https://github.com/meshtastic/meshtastic-mcp meshtastic-mcp`
instead of a local ./mcp-server/.venv, keeping MESHTASTIC_FIRMWARE_ROOT="."
so the MCP tools still work from this checkout with no local build.
- Repoint the remaining references (AGENTS.md, CLAUDE.md,
.github/copilot-instructions.md, bin/regen-*.sh, docs, Screen.h,
userPrefs.jsonc, test/fixtures/nodedb/README.md, .trunk/configs/.bandit)
at the standalone repo. The MCP tool surface is unchanged — only the
pytest harness moves out; run it from a meshtastic-mcp checkout with
MESHTASTIC_FIRMWARE_ROOT pointed here.
No build/CI/platformio coupling existed, so nothing in the firmware build
changes.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* Add define for screen I2C frequency
* Add functions to get I2C screen frequency and port
* Make reclock function aware of screen-set I2C speeds
* Refurbish ReClockI2C API
* Make ReClockI2C API class
* Use new API in AirQualityTelemetry module
* Minor changes on some logs
* Update esp8266-oled library
* Fix esp8266 library
* Minor logging changes
* Improve setClock and restoreClock cases
* Make getter functions const, fix capitalisaiton of new functions
* Minor typo, remove pragma once
* Low prio debug fixes
* Mark getter functions as const (properly)
* Fix LOG based on coderabbit feedback
---------
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
* clarify channel number formatting and fix the fixed-width padded nodeIDs
* use the explicit 0x%08x for packets and nodeIDs throughout
* LLM instructions
* Disable gps thread on startup if lora region is unset
There is little reason to waste battery on the gps if the data cannot
yet be used.
* fix goobered merge
Refactor GPS enabling logic and remove duplicate code.
* trunk
---------
Co-authored-by: Jonathan Bennett <jbennett@incomsystems.biz>
* InkHUD: map tile background, zoom controls, and GPS live tracking
- Map background tiles are now rendered on the display when available, compressed with LZ4 to keep flash usage low
- If no map tiles are loaded, the map applets behave exactly as before
- Zoom in, zoom out, and reset zoom (back to auto-fit) are accessible from the menu, and only appear when the menu is opened from a map screen
- The map updates automatically whenever GPS gets a new position or your phone shares a location
- The Positions and Favorites map applets now also refresh when mesh position packets arrive for your own node
- The Favorites map now shows your position on the map even if you have no favorites yet
* README Update
* Update MapApplet.cpp
* Zoom fixed
* Zoom with no tiles fix
* CI fix
* NodeDB: 3-tier node store with persistent warm tier (long-tail identity retention)
Introduces a tiered NodeDB so the device retains identity (public key,
last_heard) for far more nodes than fit in the full-record hot store,
without growing heap or the persisted nodes.proto unboundedly.
- Hot store: full NodeInfoLite, MAX_NUM_NODES (120 on nRF52).
- Satellite maps: position/telemetry/environment/status capped at
MAX_SATELLITE_NODES (40 freshest); eviction via enforceSatelliteCaps /
evictSatelliteOverCap.
- Warm tier (WarmNodeStore): 40 B {num,last_heard,public_key} records for
evicted nodes so DMs to/from long-tail nodes keep encrypting/decrypting.
Persisted to /prefs/warm.dat, or on nRF52840 a dedicated 12 KB raw-flash
record-ring below LittleFS (3x4 KB pages; see linker scripts + the
nrf52_warm_region.py post-link guard).
NodeDB::getOrCreateMeshNode now demotes evicted nodes into the warm tier and
re-admits them (restoring key/last_heard). Router PKI decrypt/encode resolve
the peer key via NodeDB::copyPublicKey (hot store, then warm tier).
NodeInfoLite gains snr_q4 (sint32, Q4-encoded dB); the float snr is zeroed on
disk. NodeInfoLite grows 105 -> 112 B; backup 2432 -> 2468 B.
Note: the snr_q4 .proto change still needs to land in the protobufs submodule
(generated header is updated here; submodule pointer left at upstream).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* NodeDB: robust receive + retention for blocked (ignored) nodes
Hardens how ignored/favourite nodes are received over admin and retained,
closing paths where a block could be lost or accidentally cleared.
- Blocking keeps the node's public key (admin set_ignored_node and
addFromContact no longer zero it / drop the warm-tier key), so a blocked
peer stays a verifiable identity.
- set_ignored_node creates the node if absent, so a block by node ID sticks
even for a node we've never heard from (e.g. pushed by a remote admin) with
no NodeInfo or key.
- Eviction protection (favourite/ignored/manually-verified) now also applies to
the load-time hot-store migration and is never undone by cleanupMeshDB, which
previously purged ignored nodes that lacked user info.
- The hot-store migration leaves our own node (index 0) in place and prefers to
demote non-protected nodes, like the runtime eviction scan.
Caps the protected set (favourite + ignored + verified) at MAX_NUM_NODES-2 via
NodeDB::setProtectedFlag(), so at least two evictable slots always remain and
getOrCreateMeshNode can always make room — replacing the previous unconditional
append that could run off the end of the node vector when every node was
protected. A locally-set favourite/ignore that hits the cap reports back to the
phone via a ClientNotification.
Adds test_nodedb_blocked covering the migration, favourite/ignored eviction
protection, ignored-survives-cleanup, and the protected-node cap. The
maintenance methods stay private in production; the test reaches them through a
PIO_UNIT_TESTING-guarded friend shim.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
# Conflicts:
# src/mesh/NodeDB.h
* fix copilot comments
* once again
* WarmNodeStore: fix cppcheck warnings (uninitvar, constVariablePointer)
Zero-initialise `stranded[]` and `seqs[]/order[]` VLAs so cppcheck can
verify there are no unguarded reads of uninitialised memory (the guards
exist but are not visible to static analysis). Mark two local pointers
`const` where the pointed-to entry is never mutated after assignment.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* self-care added to assist 2.7 and 2.8 nodedb migration
* Tidy warm-store/self-care: comments, guards, log + flash cleanup
Style/cleanup pass over the branch (no behavior change except the noted
preprocessor simplifications, which are semantically identical):
- Comments: move function descriptions to the headers, cap in-function
comments at ~3-4 lines, drop leading-number step markers, label stacked
#endif blocks, de-decorate banner comments.
- dumpToLog: fully gate decl + definition + AdminModule call site behind
MESHTASTIC_NODEDB_MIGRATION_VERBOSE so it compiles out when disabled
(~1.2 KB when off).
- mesh-pb-constants: drop the dead nRF52832 WARM_NODE_COUNT branch and trim
the macro docs.
- WarmNodeStore: simplify the redundant `ARCH_NRF52 && NRF52840_XXAA` guards
to `NRF52840_XXAA`, add a kNoPage sentinel for the ring page state.
- Shorten the always-on LOG_WARN strings (~120 B flash).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* more tidying up, aligning with docs and undoing other-arch regressions
* Update protobufs (#19)
Co-authored-by: NomDeTom <116762865+NomDeTom@users.noreply.github.com>
* made the migration pathway cleareer
* address copilot review
* fixed a copilot review on a downstream PR.
* Address Copilot review comments for PR #10705 (warmstore/nodedb)
- WarmNodeStore.h: default MIGRATION_VERBOSE to 0 (suppress info-level
chatter on production builds; opt in with =1)
- WarmNodeStore.cpp load(): move memset to top of function so all
failure paths (header-read fail, invalid header) leave entries clear
- WarmNodeStore.cpp save(): replace manual spiLock lock/unlock around
mkdir with LockGuard covering the full SafeFile sequence, matching
the lock discipline in load()
- Router.cpp: memcpy(&p->public_key.bytes, ...) -> memcpy(p->public_key.bytes,
...) — pass decayed uint8_t* rather than pointer-to-array
- AdminModule.cpp: check setProtectedFlag return for PKC auto-favorite;
log cap-refusal warning instead of unconditional "auto-favoriting"
- nrf52_warm_region.py: error message references both v6.ld and v7.ld
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* NodeDB: formatting cleanup (blank lines after preprocessor blocks)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Lukewarm store
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
* Fixes
* Remove BATTERY_LPCOMP_THRESHOLD
BATTERY_LPCOMP_THRESHOLD is dead code — in main-nrf52.cpp it's inside #ifdef BATTERY_LPCOMP_INPUT, which this board intentionally doesn't define. The threshold value is never reached.
* Trunk fix
* Update MotionSensor.cpp
* fix
---------
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
Co-authored-by: Jonathan Bennett <jbennett@incomsystems.biz>