* 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
* Strip the default PSK when licensed defaults are installed
* Only record rate-limited portnums from the phone
* Bound payload reads by the received size
* Include warm-tier signers in the identity update gate
* Fail the send when PKI encryption fails
* Require signatures on licensed unicasts
* Include warm-tier signers in the NodeInfo downgrade drop
* Clamp hop fields on UDP multicast ingress
* Address review comments on signing hardening
Condense the updateUser rationale to two lines and stop calling the
Balanced-mode drop a broadcast now that licensed unicasts reach it.
* Add explicit presence for MeshPacket.rx_time (arrival time)
rx_time is now proto3 optional with a has_rx_time presence bit, matching
the rx_rssi treatment. A node with no GPS and no phone connected yet has
no time source at all, so a bare 0 was indistinguishable from a genuine
1970-01-01 reading; downstream consumers (replay packets, JSON
serialization) now check has_rx_time instead of the value.
* Dedupe rx_time stamping into a shared helper; trim a debug log string
Extract the repeated haveTime/rx_time/has_rx_time stamp logic (5 call
sites across Router.cpp, MeshBeaconModule.cpp, MeshService.cpp) into
Router::computeRxTimeStamp()/stampRxTime(). Also shorten the new RTC.cpp
LOG_DEBUG string. Saves 48 bytes of flash on rak4631 (measured), no
behavior change.
* Fix has_rx_rssi presence carried unconditionally through StoreForward replay
preparePayload() set has_rx_rssi = true unconditionally on replay, regardless
of whether the packet's rx_rssi at store time was a genuine measurement (e.g.
MQTT-relayed packets carry no real RSSI). Store the presence bit alongside
rx_rssi in PacketHistoryStruct and restore it on replay instead.
Flagged by Copilot on #11271 (same root cause the has_rx_time explicit
presence work fixes) but never addressed before that PR merged.
* Trim comment blocks to the repo's 1-2 line guideline
.github/copilot-instructions.md:338 caps code comments at 1-2 lines; several
blocks added across the rx_time explicit-presence work ran well past that.
Also consolidates Time.cpp's file-level doc comment into Time.h, where the
rest of the Time:: API contract already lives.
No behavior change.
* Add rx_time explicit-presence test coverage
- test_meshpacket_serializer: has_rx_time=false fixture plus tests asserting
JsonSerialize/JsonSerializeEncrypted emit 0 rather than leaking the
millis() placeholder, alongside the has_rx_time=true baseline.
- test_stream_api: two tests driving a real PhoneAPI handshake (want_config_id
through STATE_SEND_PACKETS) that simulate a phone time-giving transaction
arriving before vs. after a queued packet is drained - covering both the
reconciled and the ships-with-placeholder-absent paths of
MeshService::reconcilePendingRxTimes().
* Fix three correctness issues flagged in review
- Time.h: drop the reserved-identifier include guard (_MT_TIME_H); pragma
once already covers it, matching convention elsewhere (e.g. RTC.h).
- Time.cpp: rebase getMillis64()'s wrap accumulator when the test seam
swaps clock sources, so a real<->injected clock jump isn't miscounted
as a genuine 32-bit wrap.
- NodeInfoModule: the 12h reply-suppression window is a local dedup
duration, not a wall-clock reading - switch it to Time::getMillis64()
so RTC-quality jumps and replayed packets' stale rx_time can't perturb
it.
- StoreForwardModule: has_rx_time was derived from *current* RTC quality
at replay time rather than stored at capture time, so a history entry
saved while time-blind could be misreported as a valid epoch once the
clock later improved. Persist the presence bit in PacketHistoryStruct
instead.
* tryfix CI
* post review fixes
* more test fixes
* feat: resolve event mode hop limit
* feat: bake event mode hop limit
* fix: honor event mode hop cap in routing
* docs: expose event mode hop limit preference
* fix: enforce event hop defaults across routing
* docs: clarify event hop override behavior
* refactor: simplify event mode hop preference
* fix: cap equal event hop limit
* Position: stream our own position to the phone/UI while mesh sharing is opt-in
Position broadcasts became opt-in in 2.8 (#10929): with every public channel
at position_precision 0, sendOurPosition() finds no eligible channel and
returns without queueing anything, so the connected phone or on-device UI
never sees the node's own GPS fix ("GPS looks dead" on standalone MUI
devices even though the receiver has a lock).
Mirror device telemetry's local delivery: once a minute, when the toPhone
queue is idle, stream our own position to the connected client at full
precision. The packet is handed straight to sendToPhone() and never touches
the mesh, so the per-channel opt-in and the public-channel precision clamp
still govern everything on the air.
Also stop logging "Send pos ... to mesh" before the channel scan has found
an eligible channel; when sharing is disabled everywhere the skip is now
logged explicitly instead of pretending a send happened.
The cadence gate is a pure static (shouldSendPositionToPhone) alongside the
existing broadcast-policy helpers, with unit tests covering the first-send,
cadence, gating, and millis() rollover cases.
* Review: only advance phone cadence on a queued packet; drop the ms==0 sentinel
sendOurPositionToPhone() now reports whether a packet actually reached the
phone queue, and runOnce() stamps the cadence only on success, so a guard or
allocation failure retries on the next tick instead of waiting out a minute.
The never-sent state is a dedicated hasSentPositionToPhone flag rather than
lastPhoneSendMs == 0, so a send stamped exactly at millis() == 0 still holds
the cadence. New regression test covers that case; existing cases updated to
the explicit flag.
* Review: align the rollover test fixture with its documented elapsed times
lastSent now sits exactly 30,000 ms before the uint32 wrap, so the two cases
are precisely 70,000 ms (sends) and 40,000 ms (held) - the previous comments
claimed 70s/20s against actual elapsed values of 70,001/40,001 ms.
The rak_wismesh_tap_v2-tft link failed on CI with "sh: Argument list too
long": the device-ui bump in #11214 grew the MUI object list enough that
the linker command line crossed the kernel exec arg limit (this env's long
name lengthens every object path, so it tipped first).
extra_scripts/ld_response_file.py already solves this for esp32p4; promote
it to esp32_common so every ESP32 target links through a response file,
and drop the now-inherited entry from esp32p4.ini.
Verified by building rak_wismesh_tap_v2-tft at the failing commit plus this
fix: links clean (macOS's exec arg limit is smaller than the Linux one that
tripped CI, so an unwrapped link could not have passed).
relayOpaquePacket() allocates a copy and returns Router::send(relay) == ERRNO_OK, discarding ERRNO_SHOULD_RELEASE. The interface returns that for NODENUM_BROADCAST_NO_LORA, so the copy is never freed and one pool slot leaks per frame.
The opaque path is reached for packets on a channel we have no key for, so no key or PSK is needed: a frame with an unknown channel hash, to=NODENUM_BROADCAST_NO_LORA, a nonzero id and hop_limit>0 leaks a slot, and roughly MAX_PACKETS of them exhaust the pool until reboot.
#11087 fixed this pattern in perhapsRebroadcast and the retransmission paths but did not cover relayOpaquePacket, which was added separately with the opaque relay path.
sendLog() notified once per firmware log line with no backpressure. With
debug_log_api_enabled and a subscribed client, a logging burst drains the
msys_1 mbuf pool that every GATT notification and ATT response allocates
from, so ble_gatts_notify_custom starts returning BLE_HS_ENOMEM (rc=6) --
for the fromNum doorbell too, not just the log line that exhausted it.
Each failure then prints an error over serial at ~12ms apiece, which is
itself enough to throttle the main loop during a burst.
Register a callback on the log characteristic so the host's own
ERROR_GATT verdict pauses log notifies for 250ms and lets the pool
refill. Keying off the failure rather than a free-block count keeps this
correct however the pools are sized.
Also restore CONFIG_BT_NIMBLE_MSYS_1_BLOCK_COUNT to the IDF default of
12. Pool selection is by block size and a notify request always resolves
to the smallest pool, so msys_1 is the only pool GATT draws from; 8 was
thin once anything chains or fragments. msys_2 and the ACL/EVT transport
pools are left trimmed -- neither is on the notify path, and restoring
the full defaults would re-spend the contiguous allocation that #10741
was fixing on heap-tight boards.
sendLog() also lacked the null check onNowHasData() already has, so a log
line arriving during BLE teardown dereferenced a freed characteristic.
Fixes#11245
blockLen() rounds numBytes up to the AES block size, so 241..256 returns 256. Stored in a uint8_t that truncates to 0, giving a zero-length encBuf that Process() then writes numBytes bytes into, smashing the stack with attacker-controlled ciphertext.
encrypted.size is a 256-byte protobuf field and encryptPacket admits numBytes up to MAX_BLOCKSIZE, so sizes above the 239-byte LoRa frame limit arrive via the MQTT and UDP multicast ingress paths, which pass the packet to the router without clamping. Only the hardware AES path for keys of 16 bytes or less is affected, which includes the default channel.
allocAckNak returns nullptr when the packet pool is exhausted, but allocErrorResponse passed the result straight to setReplyTo, which dereferences it. Reachable unauthenticated: an ADMIN_APP packet on a known-key channel takes the NOT_AUTHORIZED path, so a flood that empties the pool crashes the node. MemoryDynamic::alloc no longer aborting on failure made this reachable on PSRAM, STM32WL and Portduino targets. Callers already treat a null reply as no response. Same fix for the two RemoteHardwareModule sites.
begin_edit_settings sets a bool that only the matching commit ever
clears. If the commit never arrives -- the client dropped its link
partway through a bulk config import, or went away entirely -- the
transaction stays open indefinitely, and every later config write from
any client is applied to RAM, acknowledged, and then never saved. The
writes look successful and are visible in get_config, but the node
reverts all of them at the next boot, and only a reboot clears it.
Give the transaction a one minute idle timeout. Each deferred write
restarts the clock, so the window bounds the gap between writes rather
than the length of the edit; a bulk import sends them milliseconds
apart. The next admin message after it lapses retires the transaction,
persisting the segments it had deferred and flushing the warnings it was
holding. The check runs after the auth gates and before the switch, so
every case sees consistent state and the recovery's flash write happens
in the main loop rather than a disconnect callback.
It saves rather than rolls back because there is nothing to roll back
to: each write already took effect in RAM and was acknowledged, so the
transaction only ever deferred the save. That also makes an early expiry
cheap -- the worst case for a client that really was still going is that
its remaining writes are saved individually instead of batched. Closing
on disconnect instead was tempting, but the reporter's captures show iOS
dropping and reconnecting within half a second several times per
session, which would abort imports that currently survive the blip.
Also drops the file-scope hasOpenEditTransaction, shadowed by the class
member at every use site and referenced nowhere else in the tree.
Reported in #11245
nrf52_promicro_diy_tcxo has been failing the nrf52 warm-region guard on
develop: the image ends at 0xEA0C0, 192 bytes past the 12 KB WarmNodeStore
record-ring reserved at 0xEA000. This variant compiles four radio driver
families (SX126x/LLCC68, SX127x/RF95, LR11x0/LR1121, LR2021) so any module
can be soldered on, which makes it the largest nrf52 image we ship.
Building it with -D EXCLUDE_EMOJI saves 6,808 bytes and puts the image at
0xE8618, 6.6 KB clear of the warm region.
EXCLUDE_EMOJI was not previously usable: graphics::emotes[] becomes empty,
but the canned-message emote picker never checked for that. Entering the
picker clamped emotePickerIndex to numEmotes - 1 (i.e. -1), and selecting
read emotes[-1].label into a String - an out-of-bounds read of whatever
precedes the array in flash. Guard both entry points instead: refuse to
open the picker when there are no emotes, and bounce back to freetext if
the picker state is somehow reached anyway.
Under whole-image LTO those guards fold to constants, so the picker draw
and input paths dead-strip entirely on builds that set EXCLUDE_EMOJI -
which is where the savings come from beyond the bitmap data itself.
Received messages containing emoji render as text on this variant, and
the emote-list key is a no-op. Verified rak4631 (emoji enabled) is
unaffected: 0xE46D8, 22 KB clear.
* 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>
* feat: isolate event radio profiles
* fix: preserve identity on degraded config boot
* fix: guard event profile storage
* style: align event profile storage names
* fix: discard event profile on normal boot
* refactor: simplify event profile cleanup
* fix: limit inactive profile migration to event firmware
* fix(event): make event-profile capacity check portable across filesystems
The USERPREFS_EVENT_MODE storage preflight called FSCom.totalBytes() and
FSCom.usedBytes(), which only exist on ESP32's LittleFS wrapper. Every other
backend failed to compile once event mode was enabled:
error: 'class InternalFileSystem' has no member named 'totalBytes' (nRF52)
error: no member named 'totalBytes' in 'fs::FS' (Portduino)
This was not caught earlier because the event block is behind
#if USERPREFS_EVENT_MODE, and the existing unit tests only cover the pure
helpers, which compile identically either way.
Add fsTotalBytes()/fsUsedBytes() to FSCommon and implement them per backend:
littlefs v1 traversal for nRF52 (Adafruit_LittleFS) and STM32
(STM32_LittleFS), FSInfo for RP2040, statvfs for Portduino, and the native
methods for ESP32 and nRF54L15. The nRF52/STM32 path reports "full" if the
traversal errors so the capacity check fails safe.
Verified with USERPREFS_EVENT_MODE=1: native-macos test_event_profile_storage
passes 5/5, and heltec-v3, rak4631, rak11310 and rak3172 all build clean.
* fix(event): use std::filesystem for Portduino capacity, bump native-suite-count
Two CI fixes:
- native-windows has no <sys/statvfs.h>, so the Portduino branch of
fsTotalBytes()/fsUsedBytes() broke the Windows build. Switch to
std::filesystem::space(), which is already used under ARCH_PORTDUINO in
HostMetrics.cpp and works on both POSIX and MinGW. Errors report "full"
so the capacity check still fails safe.
- This PR adds test/test_event_profile_storage, taking test/ from 40 to 41
suite directories, which trips the native-suite-count reconciliation check.
* fix(event): scope boot-write deferral to the radio profile, address review
Review feedback:
- Copilot: saveProto()'s boot-write deferral applied to every file, so
loadFromDisk()'s recovery writes (e.g. restoring owner fields into
devicestate) were silently dropped and never retried, because the ctor CRC
baselines are computed after loadFromDisk(). Deferral now applies only to
the radio-profile files, via isRadioProfileFile().
- jp-bennett: eventConfigFromStandard() wrapped a struct copy plus one field
assignment in a header helper with its own unit test. Inlined at its single
call site and removed; the behaviour is covered end-to-end by hardware
validation instead (RAK4631 normal -> event -> normal preserves NodeNum and
public key while swapping LoRa, and restores the original channel
byte-identically).
- jp-bennett: the active-backup encrypted-storage migration ran for normal
builds too, which changed non-event behaviour and contradicted the PR's
stated event-only scope. Scoped to USERPREFS_EVENT_MODE; the adjacent block
already covers the inactive standard backup in event builds.
New tests (all verified to fail under mutation, not vacuous):
- test_event_paths_never_collide_with_standard_or_shared_files: the core
safety property. A path-table slip would make event firmware overwrite the
user's real config, channels or backup, or capture a shared file like
devicestate/nodedb. Nothing asserted this before.
- test_only_radio_profile_files_defer_boot_writes: guards the deferral fix
above so re-widening it fails loudly.
- test_event_reservation_fits_smallest_supported_filesystem: the reservation
is compile-time but must fit filesystems as small as 14 KiB (STM32WL) and
28 KiB (nRF52840). Protobuf growth pushing it past those would silently
stop event profiles persisting - the exact failure mode confirmed by
fault injection on a RAK4631.
Verified with USERPREFS_EVENT_MODE both on and off: 7/7 tests pass, and
rak4631, heltec-v3, rak11310 and rak3172 all build clean.
* fix: preserve event profile storage recovery
---------
Co-authored-by: Jonathan Bennett <jbennett@incomsystems.biz>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
Co-authored-by: Benjamin Faershtein <benjaminfaershtein@Benjamins-MacBook-Pro-2.local>
AdminModule::handleReceivedProtobuf carried a 3008-byte stack frame - 37% of
the 8 KB Arduino loopTask stack. GCC inlines the eight handleGet* helpers into
it, each of which builds a whole meshtastic_AdminMessage (480 B), so the
dispatcher reserved a slot for every one of them at once even though only one
case ever runs. Two more full AdminMessages lived directly in the function.
That left any admin write short of stack for what runs beneath it:
NodeDB::saveToDisk -> saveProto -> pb_encode -> SafeFile -> LittleFS -> flash.
On heltec-v4 the canary fired at 7824 of 8192 bytes while writing
/prefs/config.proto, so no setting ever persisted and the device rebooted.
Mark the response builders NOINLINE and move the module-API and observer
responses into their own functions. Pure refactor; measured on heltec-v4 the
dispatcher frame drops 3008 -> 384 bytes, taking the crashing path from 368
bytes of headroom to 2992.
Fixes#11237
checkov/CKV_SECRET_6 flags the commented-out "large4cats" example, which
is the public default credential for the meshtastic.org MQTT broker rather
than a real secret. Ignore the file in .trunk/trunk.yaml so the example
config stays free of lint-suppression comments.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
glob and os do not depend on the SCons-injected env, so importing them at
the top of the file removes the flake8 E402 findings and lets the E402
ignore-all directives (which were no longer suppressing anything) go away.
Setting TAK team/role ACKed and rebooted but stored nothing:
handleSetModuleConfig had no tak case, saveToDisk never set has_tak,
and handleGetModuleConfig had no TAK_CONFIG case. Add all three, plus
a native suite sweeping every ModuleConfig submessage through
set -> save -> load -> get and a TAK value-fidelity suite.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix(mesh): don't assert on malloc() failure in MemoryDynamic::alloc()
MemoryDynamic<T>::alloc() called assert(p) right after malloc(), instead
of returning nullptr like the static MemoryPool<T,N>::alloc() already
does on exhaustion. All of packetPool's callers were already hardened to
null-check allocCopy()/allocZeroed() (#10948, #10951), but that path is
unreachable on real OOM here: assert() fires first, one level down,
before the caller ever gets a chance to check anything.
On STM32WL (packetPool is MemoryDynamic there - not enough static RAM
for the fixed pool), assert() failures are wrapped to an infinite
`while(true);` loop rather than aborting or resetting, so the thread
just hangs forever instead of returning nullptr. Reproduced on wio-e5
hardware under a burst of incoming DMs, with free heap draining toward
zero right before the hang.
Assisted-by: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Andrew Yong <me@ndoo.sg>
* fix: null-check the two allocUnique*() call sites missed by prior audits
#10948/#10951 null-checked every raw-pointer packetPool.allocCopy()/
allocZeroed() call site, but missed the two spots using the
UniquePacketPoolPacket (unique_ptr) wrapper: UdpMulticastHandler::onReceive()
and MQTT::onReceive() both dereferenced the allocation result unconditionally.
Previously unreachable in practice: MemoryDynamic::alloc() asserted before
ever returning null, so nothing downstream saw it. Fixed alongside that
assert removal (this branch) since these two are now reachable with a
genuine null - same fix as everywhere else, just an unchecked pointer that
turns up under OOM.
Assisted-by: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Andrew Yong <me@ndoo.sg>
* fix(SimRadio): don't leave isReceiving stuck true on allocation failure
startReceive() set isReceiving = true before allocCopy(), so a failed
allocation (now reachable with a real nullptr instead of hanging in
assert()) left the simulated radio permanently marked as receiving:
handleReceiveInterrupt() returns immediately on a null receivingPacket,
before ever reaching the code that would clear isReceiving.
Assisted-by: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Andrew Yong <me@ndoo.sg>
---------
Signed-off-by: Andrew Yong <me@ndoo.sg>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
* Fix nRF52 freeze + watchdog reset when saving config over BLE
Since #10967 phone-originated admin messages are handled synchronously on
Bluefruit's BLE event task. NRF52Bluetooth::disconnect() busy-waited for
BLE_GAP_EVT_DISCONNECTED, which only that same task can process, so any
config save that requires a reboot (e.g. position) deadlocked the device
until the 90s watchdog fired. Bound the wait to 1s and sleep instead of
spinning so lower-priority tasks (including the watchdog feed) keep
running; the SoftDevice completes the link termination on its own.
* Address review: use Throttle helper, tighten comment
* Name the disconnect timeout constant
* Log unconfirmed BLE disconnect at WARN with elapsed time
Since #10967 made Router::sendLocal handle self-addressed packets
synchronously, the entire phone-API chain for a BLE client runs inline in
the Bluefruit characteristic write callback: toRadioWriteCb ->
PhoneAPI::handleToRadio -> admin set-config -> radio reconfigure ->
NodeDB::saveToDisk. That callback executes on the Bluefruit BLE FreeRTOS
task, whose stock stack is 5 KB (CFG_BLE_TASK_STACKSIZE = 256*5 words) -
not the Arduino loop task that #10944 already raised to 8 KB. The loop-task
fix therefore protects the wrong task for BLE-originated writes.
On a Seeed Wio Tracker L1 the 5 KB stack overflows during pairing
first-sync, resetting the device mid-LittleFS-write, every single time.
Repeated mid-write resets tear the LittleFS metadata, lfs_assert fires on
the next boot, and the corruption handler formats the whole filesystem:
region, channels, module config, and the node's keypair are all lost
(critical fault #13, new node identity on next region set). Reproduced
end-to-end tonight on stock develop 6908d27; with this change the same
device pairs, serves config screens, and survives back-to-back
config.proto saves over BLE.
Raise the BLE task to the same 2048 words (8 KB) as LOOP_STACK_SZ, for the
same reason. bluefruit.cpp's #ifndef guard makes the -D take effect with no
framework patch. Costs 3 KB of RAM on nrf52840 targets only.
Credit where due: Ixitxachitl independently established in #11155 testing
that the save-path crash persists after #11185 and that re-queueing
sendLocal (moving the pipeline back to the Router thread) makes it go away
- which corroborates this diagnosis from the other direction. This commit
is the minimal capacity-side fix; #11155's relocation of the pipeline off
the BLE task remains the right architectural follow-up, and this guard
stays correct even after it lands.
Likely also explains #10905 (L1 display-thread crash when a client
requests full configuration) and the 2.8 field reports of idle nodes
losing region and keys after a BLE session.
* Deliver locally-generated replies addressed to us to the phone
Config get/set from the phone times out on every device: the client sends an
admin request, the node handles it, and the response is silently dropped
before it reaches the phone queue.
#10967 changed Router::sendLocal's isToUs branch from enqueueReceivedMessage()
to handleReceived(p, src), so a local packet keeps its RxSource instead of
being relabeled RX_SRC_RADIO by the queue round-trip. That is the right call
for the new policy gates, but module replies go out through
MeshService::sendToMesh() with the default RX_SRC_LOCAL, and a reply to a
phone-originated request is addressed to our own node (setReplyTo resolves
from == 0 to ourNodeNum). Those replies now re-enter callModules as
RX_SRC_LOCAL, where the loopback gate skips every module whose loopbackOk is
false - including RoutingModule, whose promiscuous sniff is the only path that
moves a received packet into toPhoneQueue. The reply is released, never sent.
Requests still work, because the phone's own packets arrive as RX_SRC_USER and
pass the gate, so a set_config is applied and only its acknowledgement is lost.
That is why a client can connect and download config but times out on every
config screen and every setter.
Deliver the phone's copy from sendToMesh() instead: for a local packet
addressed to us, the loopback gate is doing its job in keeping the packet away
from module re-dispatch, and the phone copy is exactly what is missing. Setting
loopbackOk on RoutingModule would instead echo every locally-generated
broadcast back to the phone, and relabeling replies RX_SRC_RADIO would undo the
origin separation #10967 added.
Also stop reporting ERRNO_SHOULD_RELEASE (35) to the phone in the QueueStatus
for these packets. It means "caller frees", not a send failure, and the same
hunk changed it from the 0 the phone used to see.
* Address review: trim comments, assert the QueueStatus count
Condense the added comments to the one-or-two-line house style; the rationale
lives in the commit message and PR.
The reply test drained QueueStatus records in a while loop, which would have
passed just as happily on an empty queue. Count them and require both the
request's and the reply's.
meshtasticd must run as root to reach GPIO/I2C/SPI hardware, so the final
USER root is deliberate. The existing file-level ignore used the old trivy
code (DS002); rename it to DS-0002 and add checkov/CKV_DOCKER_8 so the
last-USER-root findings are suppressed with an explicit justification.