mirror of
https://github.com/meshtastic/firmware.git
synced 2026-11-06 22:34:13 -05:00
70bd2cc579a503ab4d0f1650f66ea488ec3e9628
7412
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
70bd2cc579 |
TransparentRadio: verify what the device took, and don't rewrite it needlessly
- Read-back compares the settings that decide what is on the air (region, preset or bandwidth/SF, coding rate, slot or frequency, tx_enabled), not just transparent_mode. A device that rejects a LoRa config restores its old one whole, transparent_mode included, so that is now "Refused": retried after 5 minutes or at once on this node's next config change. Only a device that took the settings and still reads transparent_mode false is old firmware. - The same comparison decides whether a device is already set up, so fields the device normalises no longer cause a push on every reconnect. coding_rate, spread_factor and bandwidth are clamped the way the device's AdminModule stores them. - Firmware that reports a release older than 2.8.3 is refused before any set_config reaches it. - With no LoRa region set here, nothing is pushed (it would set the device's region to UNSET and turn its TX off); setting a region pushes. - Frames held for their own radio settings (RadioTxHooks::holdsRadio, e.g. MeshBeacon on another preset) are dropped, since the device can't switch per packet, and the hooks hear about every released frame. - Reconnect attempts back off from 5 s to 60 s: opening a TCP link blocks the main loop. - hop_limit/hop_start are clamped as encodeRadioBuffer() does instead of being refused by the device; slot_parity is no longer forwarded. - Lora.Module transparent leaves spidev empty, so the SPI bus is untouched. - Queued frames are released on destruction; the late-window clamp only reschedules when the moved frame is the front. |
||
|
|
346148b667 |
meshtasticd: use a transparent-mode node as the radio
Lora.Module "transparent" makes meshtasticd drive the LoRa radio of another node running LoRaConfig.transparent_mode, over that node's client API. Lora.TransparentDevice is its serial device path, or host[:port] for its TCP API (port 4403 by default). TransparentRadio speaks the framed client API (the StreamAPI framing) over a serial port (termios, 115200 raw) or a non-blocking TCP socket: - Handshake: want_config with the config-only nonce, then a local admin set_config with this node's LoRa settings plus transparent_mode, then a get_config read-back that must confirm transparent_mode. Firmware that predates the mode is refused. A device already set up is not rewritten. - channel_num is pinned to the slot this node resolved, since the device would otherwise hash its own primary channel name. The device keeps its own frequency_offset and front-end settings. - TX: frames wait out the normal CSMA / SNR-weighted relay delays in this node's own queue, so cancel and late-rebroadcast logic keep working, then go to the device as encrypted MeshPackets carrying only the radio header fields and ciphertext. A full device queue holds frames until it reports space. TX/RX airtime is logged here, since the device leaves airtime to its client. - RX: encrypted frames from the device go to the router once the handshake is done; the device's own decoded traffic does not. - Heartbeats, reconnect on link loss, re-handshake when the device reboots, and re-push when this node's LoRa config changes. Also moves SimRadio's time-on-air formula to RadioInterface::computeTimeOnAirMsec() for both non-RadioLib radios, and teaches --check the new keys. |
||
|
|
51ac6ea7b7 |
Set ClientNotification.type on every firmware notification (#12280)
* Set ClientNotification.type on every firmware notification Every ClientNotification the firmware sends now carries the NotificationType for its message, so clients can localize or act on it without matching the text. `message` is unchanged. The shared senders take the type as a required argument so a new call site cannot omit it: PhoneAPI::sendNotification, AdminModule::sendWarning / sendWarningAndLog / queueChannelWarning, and RadioInterface's sendErrorNotification. The channel-warning queue keeps the type of a lone warning and uses CHANNEL_*_ISSUES for a coalesced one. checkConfigRegion() gains an optional errType out-parameter so validateConfigRegion() can send the reason's type. NodeDB::notifyRefusedIdentity() (added in #12191) has no type yet and stays UNSET until the protobufs add one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AZVEy3QxYqj916trqLEsrb * Fix AdminModule.h formatting Move pendingWarningType below the aligned member block so clang-format leaves the existing trailing-comment alignment alone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AZVEy3QxYqj916trqLEsrb * Honor level in PhoneAPI::sendNotification; test catch-all types sendNotification ignored its level argument and always sent WARNING. Every caller passes WARNING today, so behavior is unchanged. Add test_admin_radio cases for the CHANNEL_PSK_ISSUES and CHANNEL_NAME_AND_PSK_ISSUES catch-all types. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AZVEy3QxYqj916trqLEsrb --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
24c6e139dc |
Implement LoRaConfig.transparent_mode (dumb LoRa modem) (#12283)
Implements meshtastic/protobufs#975. With config.lora.transparent_mode set, the node runs the radio but does no encryption, decryption or mesh routing; the connected client owns all of that, including airtime rules. Frame format both ways is a MeshPacket with the `encrypted` payload: its header fields map one-to-one onto the 16-byte radio header. RX: every LoRa frame goes straight to the client, still encrypted. No ignore lists, dedup, decrypt, NodeDB update, modules, ACKs, relay or MQTT. Non-LoRa ingress (MQTT, UDP) is dropped. The radio driver's usual header handling is unchanged. TX: an encrypted packet from the client is queued on the radio verbatim (from, id, channel hash, hop fields, next_hop, relay_node untouched), skipping PhoneAPI dedup and per-port rate limits and the duty-cycle check. Frames with no sender, hop fields wider than their 3 header bits, a payload past radio capacity, or a no-LoRa destination are refused with BAD_REQUEST/TOO_LARGE in QueueStatus. Local scheduling hints (tx_after, rx_snr/rssi) are cleared so a client cannot stall the TX queue. Decoded packets addressed to this node (admin, so the client can set the LoRaConfig) are still handled locally. Decoded packets for the mesh are refused, nothing the node generates itself goes on the air, and the client gets no QueueStatus for those on-device sends. The mode switches live: the send gates apply from the moment it is set. Also checks the payload variant before the Store & Forward portnum test in sendToPhone, where an encrypted frame's size aliases decoded.portnum. Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
12e81f1087 |
nRF52, RP2xx0: readable assert resets (drain USB console, log context) (#12294)
* nRF52, RP2xx0: drain the USB console before an assert reset
Serial.flush() on both TinyUSB cores only moves the next packet into the
endpoint buffer and returns, so the reset that follows an assert discards
the rest of the CDC TX FIFO and the packet still waiting for the host.
Field logs end mid-line ("assert failed src/platfor"), losing the file
and line.
Wait, bounded to 250 ms, for the FIFO to drain, then 20 ms for the host
to collect the last packet. Skipped with no host connected and where the
USB task can't run.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* nRF52: say more when an assert resets
The assert line alone can't tell an out-of-memory crash apart from heap
corruption or a stack overflow, and for a failed operator new it always
names alloc.cpp.
- After the assert line, log the FreeRTOS task (the log's [thread] tag is
the last OSThread to run), its stack high-water mark, free heap and the
largest free block, or that the free list failed a bounds-checked walk.
- A failed operator new logs the requested size and the caller's address
first, for addr2line.
- MemGet::getMaxAllocHeap() walks newlib-nano's free list on nRF52, so the
periodic heap line reports the largest block there. That line no longer
prints "min 0" where the minimum isn't tracked.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Keep out-of-memory logging from allocating or recursing
RedirectablePrint::log() heap-allocated a copy of every format string to
append the newline, so with the heap exhausted the log calls in a failed
operator new and in the assert handler failed their own allocation and
recursed. Copy usual-length formats on the stack instead (96 B; some tasks
have well under 1 KB of stack free) and only use the heap for long ones.
Guard logAllocFailure() and __assert_func() against re-entry for the paths
that still allocate (long formats, BLE).
getMaxAllocHeap() on nRF52 now reports what malloc() can return (chunk
size less 8 B of header and alignment), and 0 if the free-list walk
fails rather than a partial figure.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* nRF52: drain before a nested assert reset; bound the heap walk by heap size
A nested assert reset at once, dropping the outer assert's lines still
in the USB FIFO; drain first (the drain neither logs nor allocates).
The free-list walk treated more than 2048 chunks as corruption, which a
valid but badly fragmented heap could reach. Bound it by the most chunks
the heap can hold at the 8 B minimum, so passing the bound means a loop.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* nRF52: treat free-list chunks under newlib-nano's minimum as corruption
newlib-nano never frees a chunk smaller than MALLOC_MINCHUNK (12 B on
32-bit), so a smaller size in the list means it is damaged. Reject those,
and derive the walk's chunk bound from the same minimum.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* nRF52: compare heap addresses as integers in the free-list walk
__HeapBase and __HeapLimit are distinct linker objects, so subtracting
them as pointers is undefined, and cppcheck's high-severity
subtractPointers check failed the static-analysis jobs. Do the bounds
arithmetic on uintptr_t, as the STM32 sbrk headroom code already does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
c55fc438f1 |
NodeDB: encode satellite maps in place when saving (#12293)
* NodeDB: encode satellite maps in place when saving saveNodeDatabaseToDisk() copied each satellite map into nodeDatabase's vectors just to encode them, so every save needed a second, contiguous copy: up to ~19 KB at the nRF52 cap of 40, 40 x 312 B for environment alone. On busy meshes this ran nRF52 sensor nodes out of heap (new(10920) failed in saveNodeDatabaseToDisk()). Mirror the decode path: arm per-map encode sources for the duration of the save, and have the NodeDatabase encode callback walk the map, one stack entry at a time. Same entries in the same order, so nodes.proto is byte-identical. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * NodeDB: clear satellite vectors before encoding the save With a satellite DB compiled out, nodes.proto's entries for it still decode into nodeDatabase's vector. The projection used to clear those vectors before encoding; without that, the first save after boot wrote the stale entries back. Clear all four before arming the encode sources; an armed source ignores its vector anyway. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
11f8de309c |
MeshBeacon: wait out the remaining interval after a reboot (#12274)
* MeshBeacon: wait out the remaining interval after a reboot When the transmit history shows a beacon inside the interval, runOnce now sleeps until that interval ends instead of a full interval from boot. Saving mesh_beacon reschedules the broadcaster, so enabling broadcast or changing the interval takes effect within the broadcast start delay rather than on the old period. fixes #12273 * MeshBeacon: retry an airtime-held beacon within the hour A due beacon held by the air-util limit now rechecks after 5 minutes, doubling to a 60 minute cap, instead of waiting a full interval. The history check runs first, so a beacon that is not due no longer logs a skipped send. A reschedule requested while runOnce runs, as from the nRF52 BLE write callback, is now kept rather than overwritten by runOnce's return. Declare the transmit_history.dat write in the state manifest. |
||
|
|
6975a80dd8 |
Unify input debug logging under INPUT_DEBUG (#12287)
InputBrokerDebug (which gated LOG_INPUT) and INPUT_DEBUG (raw #ifdef blocks in ButtonThread and TrackballInterruptBase) were two switches for the same thing. LOG_INPUT now lives in DebugConfiguration.h next to LOG_DEBUG_RADIO and is enabled with -DINPUT_DEBUG; InputBrokerDebug is gone. Per-event input logs that previously ran unconditionally now go through LOG_INPUT: button double/multi/long-release, rotary encoder and TCA6408 turns and presses, touch taps/swipes/long-press, MPR121, Hackaday, matrix and RAK14004 keypad keys, and T5S3 touch coordinates. Several of these logged typed keystrokes on every build. Init, warning and state change logs (mute, backlight, interrupts attached) are unchanged. The ButtonThread "held" line drops from WARN to DEBUG with the rest. Claude-Session: https://claude.ai/code/session_011C7D9f2qD7EhCiM5P74SiV Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
65eb10f0ff |
Leave TCXO powered during standby (#12119)
* Leave TCXO powered during standby To avoid its startup delay * Fix cppcheck warning * Use dedicated result variable for standbyXOSC call --------- Co-authored-by: Jonathan Bennett <jbennett@incomsystems.biz> |
||
|
|
e2258a6945 | Fix the t-impulse touch button (#12286) | ||
|
|
1546ebee2f |
Report the BLE address in DeviceMetadata.ble_address (#12281)
* Report the BLE address in DeviceMetadata.ble_address Fills the field added in meshtastic/protobufs#1159 with the address the BLE stack advertises on, so a client can read it over USB instead of scanning for it (e.g. to print a connect QR code for a fixed-PIN node). - getBluetoothAddr() joins getMacAddr()/getDeviceId() as a per-arch hook. - ESP32: esp_read_mac(ESP_MAC_BT), the controller's public address that NimBLE uses; it differs from the base MAC. None on ESP32-S2. - nRF52: sd_ble_gap_addr_get() while the SoftDevice runs, else the FICR identity address getMacAddr() already derives. - nRF54, RP2xx0, STM32WL, native: no address yet; the field stays empty. - formatMacAddr() renders "AA:BB:CC:DD:EE:FF". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011C7D9ehe42gSGx3n1gXFJw * test(ble_address): run under the native test harness Include TestUtil.h and call initializeTestEnvironment(), as test/README.md requires: the suite calls getDeviceMetadata(), so it is not one of the pure-function suites that skip the harness, and its RUN_TEST then gets the per-test environment and state checks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011C7D9ehe42gSGx3n1gXFJw --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
e6ed4b5f6a |
AdminModule: trust from == 0 only when the dispatcher reports a local origin (#12242)
AdminModule treats from == 0 as a local client, skipping the admin_key and session-passkey checks. That trust rested entirely on every ingress path dropping from == 0 (RadioLibInterface, MQTT downlink, UDP multicast and frame injection all do). Make the handler check it itself: - MeshModule gains a static currentRequestSource, set by callModules() alongside currentRequest, so modules can see the RxSource of the packet they are handling. - AdminModule drops a from == 0 payload unless callModules() is dispatching that exact packet with RX_SRC_USER or RX_SRC_LOCAL. It fails closed for a radio source, for no dispatch context, and for a context that belongs to another packet. The drop is silent (no NAK to a forged sender). Tests: AdminModuleTestShim::handleReceivedProtobuf now runs the handler inside a dispatch context (default RX_SRC_USER, the phone client existing suites stand in for), plus handleWithDispatchContext() for the negative cases. test_admin_session_repro adds six cases: from == 0 via RX_SRC_RADIO, with no context, and with another packet's context are rejected, both directly and through MeshModule::callModules(); the phone-client controls still apply the setter. With the gate removed, the four rejection cases fail. Claude-Session: https://claude.ai/code/session_01Y1BYe4ahUHxyhw3nGTX67D Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
7b2d353eed |
Update protobufs (#12278)
Co-authored-by: jp-bennett <5630967+jp-bennett@users.noreply.github.com> |
||
|
|
274a302b59 |
MUI: adapt to device-ui storage (FATFS) and SD bus classes for SD card (#12249)
* adapt to device-ui #446: storage (FATFS) and SD bus class * Refactor chip variable assignment in ffconf_paths Refactor chip variable assignment to improve clarity. * Update extra_scripts/esp32_fatfs_exfat.py Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Pin device-ui to master a5b80d4 and fix black formatting a5b80d4 is the current device-ui master head and already contains the storage refactor (device-ui #446) this PR adapts to, so this PR now supersedes the Renovate bump in #12058. Re-wrap the chip lookup in esp32_fatfs_exfat.py as black expects, which was failing Trunk Check. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011C7D9dVfgrXSj2xHzyerZi --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
4c89b6937c |
Router: drop received packets that carry our own node number (#12270)
* Router: drop received packets that carry our own node number Every packet we transmit is recorded in PacketHistory when it is sent, so an echo heard back off the mesh is consumed by shouldFilterReceived(): the implicit ACK fires and retransmissions stop. A LoRa frame with our number that the filter does not recognise (one of ours that has aged out of PacketHistory, or that was sent before a reboot) was then processed as new inbound traffic: dispatched to modules and, when addressed to us, forwarded to the client as a fresh packet. MQTT and UDP ingress already drop from == us. Drop such packets in Router::perhapsHandleReceived() right after shouldFilterReceived(), so recognised echoes keep their implicit ACK and retransmission cancel. test_own_number_ingress pushes frames through the real ingress path: an unrecognised own-number frame to us and an unrecognised own-number broadcast reach neither modules nor the phone; a frame from another node still does; and a recognised echo of our own want_ack broadcast still yields the implicit ACK and clears the pending retransmission. Without the drop the two own-number cases fail; with the drop moved ahead of the filter the echo case fails. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y1BYe4ahUHxyhw3nGTX67D * FloodingRouter: skip dupe reprocessing for our own packets The first copy of an unrecognised own-number frame enters PacketHistory before Router::perhapsHandleReceived() drops it, so a second copy takes the dupe path. With hop_start == hop_limit that path reprocesses it as an originator retry (traceroute handling, and an ACK for a want_ack frame to us), and an "upgraded" copy with more hops removes our own queued transmission from the TX queue. Neither applies to our own packets: we never relay them, and their implicit ACK is generated earlier in ReliableRouter::shouldFilterReceived. reprocessPacket() now returns false for them, and perhapsHandleUpgradedPacket() leaves them alone, so the upgraded copy falls through to the own-number drop. test_own_number_ingress adds a repeated want_ack own-number frame (must not be ACKed) and an upgraded own-number copy (our queued packet must stay queued). Both fail without the guards. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y1BYe4ahUHxyhw3nGTX67D * FloodingRouter: consume upgraded copies of our own packets PacketHistory reports an upgraded copy as seen, so when perhapsHandleUpgradedPacket() returned false for an own-number copy it fell through to the ordinary dupe handling. With hop_start > hop_limit that reached perhapsCancelDupe(), which on a client cancels our own queued transmission. Return true for own-number copies instead, so they are consumed without touching the TX queue. test_own_number_ingress adds the hop_start > hop_limit upgrade case (our queued packet must stay queued); it fails without this change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y1BYe4ahUHxyhw3nGTX67D * test_own_number_ingress: follow the implicit ACK to the phone The echo case only counted the ACK request through the harness's stub sendAckNak(). AuthPipelineRoutingModule gains a deliverAcks switch that also runs the real RoutingModule::sendAckNak(), so the ACK travels Router::sendLocal() -> RoutingModule -> MeshService to the phone queue. - The broadcast echo case now requires exactly one frame on the phone: a ROUTING_APP success ACK carrying the original request_id. - New case: an opaque echo of our own want_ack PKI DM (unknown channel hash, OPAQUE_RELAY_ONLY) must cancel the retransmission and deliver the same ACK. Filtering our number at phone delivery (as in #11487) fails both; skipping the opaque implicit ACK (as in #11502) fails the PKI DM case. The switch is off by default and cleared on reset, so test_rebroadcast_mode is unaffected. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y1BYe4ahUHxyhw3nGTX67D --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Ben Meadors <benmmeadors@gmail.com> |
||
|
|
f9c26505ed |
fix(eink): let UP/DOWN scroll the message screen (#12256)
* fix(eink): let UP/DOWN scroll the message screen The e-ink branch of the message screen set scrollY = 0 on every redraw to turn off autoscroll, which also discarded the position UP/DOWN had just set. It now resets only when the user hasn't scrolled. EInkDynamicDisplay::hashImage() XOR-ed buffer[b] << b: from byte 32 on the shift is undefined, and 0 on ARM, so only the first 32 bytes counted and a scrolled message list hashed like the previous frame, so the update was skipped. It is now FNV-1a over every byte (as the Radioddity fork does on the Nodara). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(eink): encoder scrolling and the whole buffer in the frame hash nudgeScroll() (rotary encoder, e.g. Mini ePaper S3) sets manualScrolling, so the next e-ink redraw keeps the position. hashImage() covers displayBufferSize bytes: on 250x122 panels (Heltec Wireless Paper, Vision Master E213) it missed the last 218 of 4,000 bytes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Only enable manualScrolling on e-ink --------- Co-authored-by: Kongduino <Kongduino@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Jonathan Bennett <jbennett@incomsystems.biz> |
||
|
|
eaba8b95be |
Meshnology W12: power and switch the GC1109 front end (#12238)
On the W12 (schematic W12-MB-V0.2, July 2026) the sub-GHz GC1109 was never turned on, so every packet left through a shut-down, unpowered front end about 50 dB below the board's working output. - Its LDO is enabled by IO4 (PA_EN_M), whose pull-up is not fitted: drive it as LR2021_POWER_EN. - Its CTX/CPS/CSD pins are LR2021 DIO9/10/11, not hardwired: add an RF-switch table (LR2021_DIO_AS_RF_SWITCH, rfswitch.h). - Hold IO3 (PA_EN_G, 2.4 GHz front end) low instead of floating. - Add the GC1109 TX gain curve (same front end as the Heltec V4.2), so the working PA stays within the configured and regional power. - Correct the comments that called the enables pulled high, swapped IO3/IO4, and said RF switching was hardwired. Co-authored-by: Ben Meadors <benmmeadors@gmail.com> |
||
|
|
c39728e3fb |
STM32WL: seed random() from the on-chip RNG at boot (#12234)
* STM32WL: seed random() from the on-chip RNG at boot The STM32WL port never called randomSeed(), so newlib's random() started from the same state on every boot and anything drawn from it repeated after a reset. On a bench, a DMShell client reused its session id across reboots and collided with the previous, still-open session on the server. HardwareRNG::fill() gains an STM32WL branch that reads the on-chip TRNG, clocked from MSI for the duration of the read (every STM32WL variant runs SYSCLK from MSI). Before this, STM32WL fell through to std::random_device. stm32wlSetup() now seeds random() from it on every board, folding in the chip UID and boot timer, and starts the LSE as before where HAS_LSE is set. Costs 524-800 bytes of flash per STM32WL env. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * HardwareRNG: log an STM32WL RNG failure where it happens cppcheck flagged the warning's if (!filled) as a duplicate of the fallback condition right after it (duplicateCondition). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
95ff510f47 |
feat(beacon): per-target and offered frequency slots (#12126)
* feat(beacon): per-target and offered frequency slots Implements the slot fields from meshtastic/protobufs#1059 on top of the broadcast_targets scheme from #11646: - BroadcastTarget.frequency_slot pins a target's slot. Unset derives it the way a node on that channel would: the target region's override slot, else the hash of the target channel's name on the target preset. A target on the home radio keeps the home slot. A pin the target region does not hold skips the target. The deprecated ChannelSettings.channel_num is no longer read, and the home node's channel_num no longer leaks into other presets. - broadcast_offer_frequency_slot is advertised as MeshBeacon.offer_frequency_slot only when a receiver could not derive it. An offer pinned to a slot its region lacks is withheld; the text still goes out. - The listener keeps a received offer's slot. - Admin clears slot 0, and a slot past an explicit region and preset. - userPrefs: USERPREFS_MESH_BEACON_OFFER_FREQUENCY_SLOT and USERPREFS_MESH_BEACON_TARGET_<N>_FREQUENCY_SLOT. Also from #11662: - A beacon is addressed at its target's channel index, so perhapsEncode() keys it like any other packet; the primary channel is never overwritten. The TX hook writes the target preset's channel hash for a blank-named channel on another preset. - A beacon whose send() did not queue releases its target entry on every failure, not only ERRNO_SHOULD_RELEASE. - A beacon still queued a broadcast interval after it was armed, or whose entry was evicted from a full table, is dropped at TX instead of going out on the home radio. Supersedes #11662. Co-authored-by: Tom <116762865+NomDeTom@users.noreply.github.com> * fix(beacon): address review of frequency slots - TX gate checks the target preset against its region or EU sibling, not the home slot - Targets and offers run an EU sibling's preset in that sibling, as the switch does - Switch clears override_frequency and restores tx_power; restore keeps fields edited mid-switch - An unusable channel_index skips the target instead of falling back to the primary - Beacon config validation moved to MeshBeaconModule::sanitizeConfig(), also run on load; message cap no longer ends mid-character - Evicted-id ring sized to the TX queue * fix(radio): frequencySlotCount() reports no slots for UNSET UNSET's table span is a placeholder; an unknown region code resolves to it too. * fix(beacon): slot count and restore for custom bandwidth - frequencySlotCount() counts a custom bandwidth wider than the region at the region default, as the clamp runs it - resolveFrequencySlot() falls back to the hash when the region override slot exceeds the count - The beacon switch restores a custom bandwidth applyModemConfig() clamped for the target region * fix(beacon): run a target's named preset on a node with custom modem params The switch sets use_preset for a target that names a preset and restores it after TX. A target naming no preset keeps the custom params. * fix(beacon): keep an out-of-range target channel_index sanitizeConfig() cleared has_channel_index for an index past the channel table. That turns the entry into an unset-index target, and sendBeacon() sends those on the primary channel - under a name and key the operator never named. sanitizeConfig() now also runs from loadFromDisk(), so a stored or userPrefs index did that at every boot, not only on the write that set it. Keep the index and only warn. sendBeacon() already bounds-checks it against channels.getNumChannels(), which is tighter than MAX_NUM_CHANNELS, before getByIndex(), and skips the target - so nothing can index out of bounds, which is what the clear was there for. test_adminValidation_targetChannelIndexOutOfRange_isCleared becomes _isKeptAndSkipped: the index survives the write, and the stored config emits no beacon rather than one on the primary. Reported by NomDeTom on #12126. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SwNPp17REf1c9je42aWyJa --------- Co-authored-by: Tom <116762865+NomDeTom@users.noreply.github.com> Co-authored-by: Jonathan Bennett <jbennett@incomsystems.biz> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
e039e34171 |
Reset into BLE OTA DFU instead of jumping from the DFU callback (#12220)
The Adafruit BLEDfu handler switches to the main stack and jumps to the bootloader. Since the nRF52 toolchain moved to GCC 12.3.1 (#12027) that call is compiled as a tail call whose epilogue pops the task frame from the main stack, past the end of RAM, and the CPU locks up; the bootloader then starts with GPREGRET 0xB1 and no SoftDevice, faults, and resets into the app. Handle START_DFU here instead: write the same peer data and leave through enterOTADfu(), a reset with the 0xA8 magic that every bootloader handles. |
||
|
|
34cb8d5828 |
Rework XEdDSA signing policy: Compatible stops signing, AES-CCM overrides, Strict exemptions (#12191)
* Rework XEdDSA signing policy: Compatible stops signing, AES-CCM overrides Signing costs 66 bytes per packet. On a crowded mesh that matters, and packets on an AES-CCM channel were paying it on top of the CCM tag. Sending (perhapsEncode): - Compatible, the default, no longer signs. Balanced and Strict sign broadcasts as before; licensed (ham) nodes also sign unicasts, but now only under Balanced or Strict. - Packets on an AES-CCM (use_aead) channel are never signed. This also removes the case where a signed CCM packet passed signedDataFits() but then failed the CCM size check with TOO_LARGE. Receiving (checkXeddsaReceivePolicy): - Only Strict drops a packet for lacking a signature. Balanced no longer drops unsigned broadcasts from known signers; that downgrade rule and its canonical-sizing helper are removed. - An AES-CCM channel overrides Strict: its unsigned or unverifiable packets are accepted, since the CCM tag authenticates them. - A malformed or failing signature still drops the packet in every mode. Identity (new NodeDB::refusesUnsignedIdentity, used by updateUser, NodeInfoModule and TrafficManagementModule): - Balanced and Strict still refuse an unsigned NodeInfo that would change a known signer's identity, except on an AES-CCM channel. - Compatible accepts it, since firmware before 2.8 strips signatures when relaying. Channels::isAEADEnabled() now returns false for an out-of-range index instead of logging, since receive paths pass a packet's index unchecked. XEDDSA_SIGNATURE_FIELD_BYTES lost its only user and is removed. Tests: test_packet_signing drops the cases that pinned the removed downgrade sizing (A7-A9, A18-A19, E5, E10-E13, D1), flips A4 and E1 to acceptance, and adds AES-CCM, Compatible and ham cases (A18-A20, B18-B20, N12-N13). test_mqtt and test_traffic_management pin the same rules on the plaintext-MQTT and NodeInfo-cache paths. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011DjtCrF8NLMGs9DtEVQkUK * Sign NodeInfo unicasts, forward signed requests, check signed NodeInfo keys A NodeInfo request or reply carries the key a peer will use for PKI direct messages, but off ham mode only broadcasts were signed. A Strict node therefore dropped every NodeInfo reply and could only learn a new peer's key from a signed broadcast, hours apart. - perhapsEncode: under Balanced and Strict, NodeInfo is signed even when unicast (still never on an AES-CCM channel, never in Compatible). - TrafficManagementModule: a NodeInfo request that carries a signature is forwarded instead of answered from the cache. Its sender may need a signed reply, and a reply sent on the target's behalf can never be signed. - checkXeddsaReceivePolicy: a signed NodeInfo verified against a stored key must advertise that same key, and the key must hash to the sender's node number, as first contact already requires. Otherwise it is dropped. This stops a valid signature vouching for another key, and stops a key planted on trust under the wrong node number proving itself with fresh signatures. Tests: B7 now expects NodeInfo unicasts signed; B21 pins signing per policy; A21-A23 pin the key-consistency check; a traffic-management case pins that a signed request is forwarded while the same request unsigned is answered. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011DjtCrF8NLMGs9DtEVQkUK * Strict: accept unsigned acks and traceroutes; never sign traceroutes Strict dropped every unsigned packet that was not PKI-encrypted. Acks and traceroutes are never signed, so a Strict node saw no acks, retransmitted every reliable packet to exhaustion, and could not take part in traceroutes. - checkXeddsaReceivePolicy: Strict no longer drops an unsigned ROUTING_APP packet; an ack carries its own proof (AckProof.h). Traceroutes are accepted in every mode, and any signature on one is ignored rather than verified. - perhapsEncode: a traceroute is never signed, including in ham mode, which signs other unicasts. Every relay rewrites the route, so a signature fails verification from the second hop on, and a bad signature drops the packet in every mode. - TraceRouteModule::alterReceivedProtobuf: strip any signature when rewriting the route, so a traceroute signed by older firmware is not dropped by older receivers further along. Tests: A24-A26 (Strict accepts unsigned acks and traceroutes; a traceroute signature is ignored), B22 (traceroutes never signed), B7 updated, and test_traceroute_nexthop pins the signature strip on rewrite. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011DjtCrF8NLMGs9DtEVQkUK * Drop the traceroute signature strip Only 2.8.0 and 2.8.1 pre-release nodes ever signed a traceroute: ham mode, which signs every unicast, and client-sent broadcast traceroutes. Stripping the signature on rewrite only helped those pre-releases interoperate, which does not justify the code. Receivers still ignore a traceroute signature, and traceroutes are still never signed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011DjtCrF8NLMGs9DtEVQkUK * Sign unicast positions under Balanced and Strict A position request or reply is PKI-excluded, so a signature is its only authentication, but off ham mode only broadcasts were signed. Strict dropped every position request and reply, and under Balanced any channel member could fake another node's location with an unsigned reply. perhapsEncode now signs POSITION_APP unicasts under Balanced and Strict, as it does NodeInfo. Nothing rewrites a position in flight - precision is applied by the originator before signing - so the signature survives relaying, and it binds request_id so a reply cannot be retargeted. Compatible and AES-CCM channels still sign nothing. Tests: B23 pins position unicast signing per policy and Strict acceptance; B7 expects position unicasts signed; B2 and B5 use PRIVATE_APP as their unsigned unicast now that position is signed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011DjtCrF8NLMGs9DtEVQkUK * Strict: accept unsigned key verification messages Key verification's bootstrap messages (M1, M2) go out channel-encrypted because the peer's key is not known yet, so they cannot carry a signature the receiver could verify. Strict dropped them, so two Strict nodes that did not already hold each other's keys could never run key verification. The handshake does not rely on the transport: hash1 binds both public keys to the security number the users exchange, and a key is only stored after the numbers match and the user accepts. A forged request can at most raise a verification prompt, bounded by the single session slot and the 60 s remote cooldown - as it already can under Balanced. Test: A27 pins that Strict accepts an unsigned key verification message. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011DjtCrF8NLMGs9DtEVQkUK * test_nodedb_identity_hygiene: pin the identity refusal to Balanced Two cases expected updateUser() to refuse an unsigned update from a known signer under a zeroed config. Zeroed is Compatible, which now accepts it, so they failed in CI. Run them under Balanced, and add the Compatible counterpart so both halves of the rule are pinned at the updateUser level. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011DjtCrF8NLMGs9DtEVQkUK * Ham mode: sign in place of PKI, authenticate acks with the ack proof (#12200) Ham mode signed every unicast under Balanced and Strict, acks included. A signed ack from a node whose key a Strict receiver lacks is dropped, so the sender retransmits to exhaustion, and the signature costs 66 bytes where the ack proof costs about 10. Ham mode now signs the unicasts PKI would cover in regular mode, plus broadcasts, NodeInfo and position as before. Acks are no longer signed; they carry the ack proof, which needs only the two nodes' keys, not PKI. Strict already accepts unsigned acks. Claude-Session: https://claude.ai/code/session_011DjtCrF8NLMGs9DtEVQkUK Co-authored-by: Claude <noreply@anthropic.com> * Notify the client when an unsigned identity update is refused Balanced and Strict refuse an unsigned NodeInfo that would change a known signer's name or role, but the refusal only reached the device log, so the user saw a stale name with no reason. Send a ClientNotification naming the node (by the name already held, never the refused claim) and how to accept its updates again: remove the node. Rate limited to once per node per boot, over the last 8 nodes, and to one notice per minute overall, so a signer relayed through pre-2.8 firmware or a burst of them cannot flood the client. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011DjtCrF8NLMGs9DtEVQkUK * Refused identity notice: only for a name or role change, drop the warnings Notify only when the refused NodeInfo would have changed the stored long name, short name or role, compared through CopyUserToNodeInfoLite so truncation and sanitizing cannot read as a change. A signer held only in the warm tier has no stored name to compare and is not reported. Remove the two LOG_WARNs at the refusal sites; the notice replaces them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011DjtCrF8NLMGs9DtEVQkUK * Refused identity notice: name the security setting that refused it Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011DjtCrF8NLMGs9DtEVQkUK * Refused identity notice: one global hourly throttle, no per-node tracking Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011DjtCrF8NLMGs9DtEVQkUK --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
7fcbbe15d0 |
Add the Nodara nRF52840 board (#12211)
* Add the Nodara nRF52840 board skeleton with e-ink init. * Fix the Nodara e-ink driver leak and rename the board macro to NODARA. * Nodara: use the stock S140 v6 linker script The board-specific script had the same flash layout as nrf52840_s140_v6.ld (app 0x26000-0xEA000) and differed only in the RAM origin (0x20006000 vs 0x20004000). The SoftDevice's RAM requirement is set by the shared Bluefruit config, not the board, so the extra 8 KB was unused. Tested on hardware: BLE comes up, and a phone connects and syncs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Nodara: build the same modules as other nRF52 boards Drop the variant's module exclusions and the -<mqtt/> source filter. nRF52 boards keep MQTT for the phone client proxy, and audio/paxcounter are already excluded for the whole platform in nrf52.ini. Flash use is 89.5%, in line with t-echo (88.7%). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Nodara: probe SX1262/SX1268 and TCXO/XTAL at boot Define both radio types and TCXO_OPTIONAL so initLoRa() picks whichever chip answers (RadioLib tells them apart by the version string) and falls back from a DIO3 TCXO to the crystal. The boot log reports the result. The test unit reports "SX1262 init success, XTAL, Vref 0.0V". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Nodara: map the six keys to the trackball and cancel buttons Use the existing trackball driver for up/down/left/right/select (B key) and CANCEL_BUTTON_PIN for the A key, which also gets the SENSE wake from System OFF. Same pattern as the Wio Tracker L1 e-ink. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Nodara: power on/off through the power MCU Nodara's power button goes to an always-on power MCU, which reports it as Modbus RTU frames on a receive-only 1200 baud line (Serial2, P1.00). Handle it in extra_variants: a shutdown request takes the standard shutdownAtMsec path, and on a power-on reset boot waits up to 5.5 s for the power-on confirm the MCU sends once the button has been held long enough. Also give Serial2 a valid TX pin so the core doesn't index the pin map with -1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Nodara: fix cppcheck and millis() deadline lint in the power MCU link Zero-initialize the frame buffers and use Throttle::deadlinePassed() for the power-on wait. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Nodara: report the NODARA hardware model Map NODARA to meshtastic_HardwareModel_NODARA and add the flasher metadata (hw model 151, slug, display name, tags, support level). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Nodara: power MCU link review fixes Scan every buffered frame for the power-on confirm, poll the idle line every 250 ms instead of 20 ms, and name the boot wait as the uptime deadline it is. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Nodara: menu shutdown points at the power button A software shutdown leaves the board powered, since the power MCU only cuts power on its own button. Add POWER_BUTTON_SHUTDOWN_HINT: when a variant defines it, the Shutdown menu shows it as a notification instead of the confirmation. Nodara sets it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: steve.su <steve.su@sainstore.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
7c302675a3 |
Update protobufs (#12214)
Co-authored-by: jp-bennett <5630967+jp-bennett@users.noreply.github.com> |
||
|
|
a10c96f790 |
T-Beam 1W: support the LR1121 and LR2021 radio modules (#12204)
* T-Beam 1W: support the LR2021 radio module LilyGo ships the T-Beam 1W with either an SX1262 or an LR2021 module on the same pins. Build both drivers into the t-beam-1w env; initLoRa() probes the SX1262 first and keeps whichever radio answers, so SX1262 boards are unchanged. LR2021 settings follow LilyGo's t_beam_1w_lr2021 documentation: IRQ on radio DIO10 (GPIO1), TCXO at 3.0 V, and PA/LNA switching on radio DIO5-DIO8 (rfswitch.h). The 2.4 GHz PA must not see more than 8 dBm, so LR2021_MAX_POWER_HF is 8. GPIO21 is radio DIO11 on this module; the SX1262 probe only claims it as RXEN after a successful begin(), so it is never driven on an LR2021 board. TX_GAIN_LORA stays at the SX1262 module's 10 dB. The LR2021 module's gain is about 8 dB, so it is driven slightly below its maximum. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * T-Beam 1W: release SX126X_RXEN before the LR2021 probe SX126xInterface::init() calls setRfSwitchPins() before setCurrentLimit(), setCRC() and setOutputPower(), so a begin() that succeeds before one of those fails leaves RXEN configured as an output driven low. initLoRa() then drops the interface and probes the LR2021, which drives the same pin itself - GPIO21 is radio DIO11 on the T-Beam 1W's LR2021 module. Release the pin on that path, for the boards that can carry either radio and wire an RXEN. * T-Beam 1W: also support the LR1121 radio module LilyGo also ships the T-Beam 1W with an LR1121 module on the same pins. Add it to the t-beam-1w env; initLoRa() probes SX1262, LR1121, LR2021 in that order and keeps whichever answers. LR1121 settings follow LilyGo's t_beam_1w_lr1121 documentation and factory example: TCXO at 3.0 V, PA/LNA switching on radio DIO6-DIO8 (sub-GHz PA on DIO8 in TX_HP, 2.4 GHz PA on DIO7, 2.4 GHz LNA on DIO6). LilyGo warns that more than 1 dBm into the 2.4 GHz PA can damage it, so LR1120_MAX_POWER (the LR11x0 LORA_24 clamp) is 1. On an LR2021 board the LR1121 probe returns RADIOLIB_ERR_CHIP_NOT_FOUND after about 3.2 s and the LR2021 then initialises normally. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * T-Beam 1W LR1121: IRQ on GPIO21, force the high-power PA The LR1121 module's IRQ is radio DIO11 on GPIO21 (LilyGO utilities.h: RADIO_DIO_IRQ_PIN = RADIO_DIO11_PIN), not GPIO1. With GPIO1 the first listen-before-talk CAD waited forever for CAD_DONE and froze the node. The module's sub-GHz antenna is only reachable through the high-power PA path (external PA enabled on DIO8 in MODE_TX_HP), but RadioLib selects the low-power PA at 14 dBm and below, where every switch is off. Add an opt-in LR11X0_FORCE_HP_PA that calls setOutputPower(power, true), clamped to the HP PA's -9 dBm floor, at init and in programModemParams(), and enable it for this board. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * T-Beam 1W LR1121: list RF switch DIOs from DIO5 RadioLib's LR11x0::setRfSwitchTable() sets each mode's bits by the pin's position in the array, counting from DIO5, while the enable mask uses the DIO number. A table starting at DIO6 therefore drove every line one DIO low: sub-GHz TX raised DIO7 (the 2.4 GHz PA) instead of DIO8, so the sub-GHz PA and TX/RX switch never left RX and the signal only leaked out. List DIO5 (unused, always low) first, as LilyGo's documentation and factory firmware do. Measured at 10 dBm against an SX1262 node: peer RSSI -100 dBm before, -29 dBm after; 20/20 each way. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * T-Beam 1W: also release SX126X_RXEN before the LR1121 probe GPIO21 is the LR1121 module's IRQ (radio DIO11), so an RXEN left driven low by a partly failed SX1262 init would mask its interrupts and hang the first channel scan. Extend the release to boards that build the LR1121. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
329b7ea918 |
Fail the native simulator job on a sanitizer fault, and fix the one it found (#12205)
* Fail the native simulator job on a sanitizer fault, and fix the one it found The integration step backgrounded meshtasticd and ended in `wait "$PID" 2>/dev/null || true`, which discards the simulator's exit status. The coverage env is built with -fsanitize=address, so a fault there prints a full report and exits non-zero on the way out, after the Python harness has already printed its own success - and the job concluded green with an AddressSanitizer heap-use-after-free sitting in the log. Gate it: capture the simulator's output, read the status `wait` reports, and scan the output for a sanitizer report. The pattern moves to bin/lib/sanitizer.sh so bin/run-tests.sh and the new bin/check-sanitizer-log.sh share one matcher rather than drifting apart; the output is uploaded as an artifact, since a fault on the exit path prints after the last test has passed. Leak detection is off, as in bin/test-config-check.sh - exit_simulator exit()s with the node's globals still live. The fault it had been hiding: OSThread::run() points currentThread at the running thread for the whole of runOnce(), and exit_simulator calls exit(0) from AdminModule while ServerAPI is that thread. ServerAPI is declared `public StreamAPI, private OSThread`, so its Thread base frees ThreadName first and PhoneAPI::~PhoneAPI's own LOG_DEBUG then reads it back through currentThread. Clear the pointer in ~OSThread when it names the thread being destroyed, which covers any thread deleted from inside its own runOnce, not just this one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JjAvRt6xuztC3hKqWkW9Dw * Detach observers when an Observable is destroyed The new sanitizer gate turned the simulator job red on a second heap-use-after-free on the same exit path, this one in Observable::removeObserver. ~Observer removes itself from every observable it watches, but Observable has no destructor and dies with its watchers still pointing at it. exit_simulator exit()s from AdminModule, and static destruction takes the global xModem - whose packetReady is an Observable that PhoneAPI::handleStartConfig attached to - before the heap-allocated ServerAPI that is watching it. PhoneAPI::close() then calls unobserve(&xModem.packetReady) on the dead observable, and ~Observer would walk it again afterwards. Both halves of the contract, so neither side has to outlive the other: ~Observable drops itself from each watcher's list, and unobserve() checks membership before reading through the pointer it is given. Also take CodeRabbit's grep -m 4 in bin/check-sanitizer-log.sh: with more than four matching lines, head closing the pipe is a SIGPIPE that pipefail reports and set -e exits on, so the script gave 141 where it documents 1. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JjAvRt6xuztC3hKqWkW9Dw --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
e069a8d491 |
LR2021: time received frames on the configured coding rate (#12183)
* LR2021: time received frames on the configured coding rate The LR2021's reported RX coding rate can pick up extra bits at a bad status read right after our own TX and keep them until a hard reset. Explicit-header frames still decode, since the rate travels in the header, but the wrong value inflates RX airtime: a latched 4/8 against a configured 4/5 times a 200-byte frame about 55% long. That feeds channel utilisation, the CSMA contention window and the reliable-router retransmit timers. computePacketTime() gains useRxHeaderInfo; LR20x0Interface passes false, so received frames are timed on the configured coding rate and CRC, and the readout task no longer reads the header info on that chip. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GTiifXrVc4YdonmLRCeWhw * LR2021: describe the coding-rate accumulator in the comments The reported RX coding rate was described as latching after a post-TX status read. That attribution was wrong: GetLoraPacketStatus accumulates the bitwise OR of every received header's coding rate since reset, which an SX1262 injector reproduces on demand - frames at 4/5, 4/6 and 4/7 each OR their bit in, a frame at the configured rate changes nothing, and the field reaches values DS rev 2.2 Table 9-4 does not define. It is invisible on a single-coding-rate mesh, since cr | cr == cr. No behaviour change: comments only. --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
20dc8e31e5 |
Key verification: refuse in licensed (ham) mode (#12201)
The handshake's third message must be PKI-encrypted, and licensed operation neither sends nor decrypts PKI. A licensed node still opened a session, raised the security-number prompt, and waited until it timed out. A licensed node now refuses to start a session, from the admin message or the screen, and tells the client why. It ignores a remote node's request without a NAK, as a busy node does. The screen's node menu hides the Key Verification entry in licensed mode. Claude-Session: https://claude.ai/code/session_011DjtCrF8NLMGs9DtEVQkUK Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
97bf6e48c0 |
fix(motion): include NodeDB.h so MotionSensor always sees the global config (#12192)
* fix(motion): include NodeDB.h for the global `config` MotionSensor.cpp uses config.display unconditionally (compass orientation, wake-on-motion) but only reached the global `config` through a fragile transitive include via Power.h/Screen.h. On a screenless board that also excludes the WiFi/web stack, that transitive path disappears and the file fails to compile with 'config' was not declared in this scope. Include mesh/NodeDB.h directly so MotionSensor always compiles regardless of which modules a variant excludes. Co-authored-by: Cursor <cursoragent@cursor.com> * cleaning up comments Removed comments explaining the inclusion of NodeDB.h. --------- Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Ben Meadors <benmmeadors@gmail.com> |
||
|
|
8ebb6988b5 |
fix(gps): match Airoha idle_int sleep to the documented sequence (#12167)
* fix(gps): match Airoha idle_int sleep to the documented sequence For boards that wake the AG3352 through idle_int (ThinkNode M8 and M9): - Drop $PAIR382,1 before the sleep command. It locks the CPU out of sleep, which works against both $PAIR003 and $PAIR650. - Hold idle_int low 50 ms before sending, and wait 50 ms after the command takes before EN cuts the core, as Elecrow's reference does. - A successful $PAIR650 need not reply, so also accept NMEA going quiet for a full cycle as success; resend once otherwise. Boards with RTC_INT are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * ThinkNode M8: reset the GPS out of backup mode at boot If the MCU resets while the AG3352 is in backup mode, the GPS stays there and the cached-probe check gets no answer, forcing a full re-probe. Pulse RESETB in earlyInitVariant(), as the M5 does with its wake pulse in #12113. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
31bcb2ae0a |
Fix RF95 optional pins and CannedMessage initialization (#11913)
* Fix RF95 optional pins and CannedMessage initialization * style: clang-format CannedMessageModule else branch --------- Co-authored-by: Tom <116762865+NomDeTom@users.noreply.github.com> Co-authored-by: caveman99 <tgoettgens@gmail.com> Co-authored-by: Jonathan Bennett <jbennett@incomsystems.biz> |
||
|
|
72edb49e1f |
Key verification: create the responder's NodeDB entry on commit (#12194)
* Key verification: create the responder's NodeDB entry on commit An initiator that did not hold the responder in NodeDB could not complete manual key verification: DO_VERIFY logged "Attempted to commit key, but unknown node" and the key was never marked verified. KeyVerificationModule consumes the responder's hash2 reply, so module dispatch stops before RoutingModule sniffs the packet into NodeDB and no entry is created. commitVerifiedRemoteNode() assumed one existed. When it does not, commit the pending key the reply carried (the key the user has just confirmed) through commitRemoteKey(), which creates the entry. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Key verification: commit only from the final-confirmation state DO_VERIFY with a matching nonce, and the on-screen Accept callbacks, called commitVerifiedRemoteNode() in any state. The app learns the nonce at the security-number prompt, so an early DO_VERIFY (or one after a wrong number) committed the unchecked pending key as ManuallyVerified. Require SENDER_AWAITING_USER or RECEIVER_AWAITING_USER before committing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011DjtCrF8NLMGs9DtEVQkUK --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
9c119da2e3 |
AG3352 GPS update for ThinkNode M5 (#12113)
* initial commit for L76K/AG3352 dual GNSS detection * GPIO fixes according firmware demo * resolve rabbits * define PIN_GPS_SWITCH * fix wake button and power pin * clear response to avoid logging junk data * increase IO07 samples to more reliably detect version * ThinkNode M5: read the v1.0 GPS switch on GPIO10 On a v1.0 (L76K) board the GPS switch is sensed on GPIO10, not GPIO18. Watching GPIO18 there meant flipping the switch did nothing. Pick the pin by revision, as the GPS UART pins already are; m5HasAg3352 is set in earlyInitVariant() before createGps() configures and starts watching it. Tested on a v1.0 board: switch off and on now turns the GPS off and on. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * ThinkNode M5: trunk fmt variant.h Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Jonathan Bennett <jbennett@incomsystems.biz> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
6770269176 |
SX126x: follow RadioLib's silent XTAL fallback and log the chip's state after init (#11995)
* SX126x: follow RadioLib's XTAL fallback and log the chip's state after init Since 7.5.0 (92e2f333f1) RadioLib retries config() on the XTAL inside begin() when the TCXO will not start, zeroes its own Vref and returns success. The TCXO_OPTIONAL ladder therefore never builds its XTAL instance, and Meshtastic kept reporting "TCXO, Vref 1.8" on crystal boards. - reinitChip(): take RadioLib's zeroed Vref after a successful begin() - init(): one line with the oscillator in use, chip mode, command status and device errors - the ladder's SX1262/SX1268 success lines report the oscillator actually in use - drop the embedded pre-init "DIO3 as TCXO Vref" debug line, which only stated the attempt * tronk * SX126x: don't report a stale chip state when the diagnostic read fails status is a function-local static filled by parseStatusCb, so an SPIreadStream() that fails before parsing left the previous call's byte in it, and the init log reported that as the current mode and command status. Check the read and log the failure instead. * fix(sx126x): keep RadioLib's status validation on the diagnostic read The stand-in parseStatusCb captured the status byte but always returned RADIOLIB_ERR_NONE, so a failed command or a 0x00/0xFF (no chip) byte was logged as chip state. Chain to the saved parser so those fail the read. * SX126x: comment out the post-init GetDeviceErrors read The raw SPI read and parseStatusCb swap run after startReceive(), outside a RadioSequence, so they can race the RX readout task. Kept as a commented-out debug aid for init failures. --------- Co-authored-by: Tom <116762865+Nestpebble@users.noreply.github.com> |
||
|
|
a12618ffbf |
fix esp32 shutdown: RTCIO number error and button pullup behavior (#12159)
* fix-rtcio-number-error * c3 fix |
||
|
|
200aa4270e |
Stop treating BATTERY_PIN -1 as a battery ADC (#12091)
* Stop treating BATTERY_PIN -1 as a battery ADC Six variants defined BATTERY_PIN -1 to mean "no battery sense", but every guard is `#ifdef BATTERY_PIN`, so the analog battery read compiled in anyway. On the Seeed Xiao S3 it samples an unwired GPIO1 and reports a battery that is not there: 0% at 3112 mV on one board, charging at 6310 mV on another. On nRF52, analogRead(-1) indexes past the pin map. Leave BATTERY_PIN undefined on those boards, which takes the existing external-power path, and fail the build when a variant defines a negative pin. * Keep the INA battery path on boards without a battery ADC pin AnalogBatteryLevel is the only reader of a battery INA configured through power.device_battery_ina_address, and analogInit() only installed it when BATTERY_PIN was defined. Dropping the -1 placeholder from six variants therefore also dropped their INA path. Install AnalogBatteryLevel on any board without a pin when the environmental sensors are compiled in, and let the PowerFSM treat such a board as externally powered only while no battery source has been found. |
||
|
|
6d03617210 |
Use github zip for ZinggJM/GFX_Root (#12174)
Switch from git reference to github .zip for ZinggJM/GFX_Root library in InkHUD. This should help with transient failures in inkhud builds. |
||
|
|
57467f028f |
Fix Store & Forward first-enable crash (#12168)
Admin writes can enable Store & Forward before the scheduled reboot constructs its module. Check the singleton before calling isServer() so the configuration response reaches the phone without a null dereference and pending settings transactions can commit. Validated tbeam-s3-core builds and USB first-enable YAML/standalone tests against an unpatched baseline, enabled controls, and reboot persistence. The full native coverage suite and Trunk checks pass; native tests do not execute this ESP32-only condition. |
||
|
|
7579bfe4d3 |
InkHUD: Feature/sdcard eink maps (#12118)
* InkHUD: load offline map tiles from SD cards InkHUD map backgrounds previously came only from MapTile.h compiled into the firmware image. This commit expands that to read map tiles from an SD Card. * Firmware-compiled tiles remain the first choice, with SD Card secondary. * SD Card MapTiles' header position table and payload descriptors loaded into PSRAM. * The index is cached in PSRAM when allocation succeeds; otherwise streamed index access. * Validates the file magic, layout, counts and index/data bounds before use. * Added an MTB1 reader for /Map/MapTile.bin * Supports sparse and grid indexes together with white, black and raw LZ4 block entries. * Moved the tile format constants and LZ4 decoder out of MapApplet. * Reuses the applet's existing decoded-tile buffer. Compressed tile imagery remains on the card. Rendering resolves coordinates from the cached index, takes spiLock for a bounded open/seek/read/close operation, releases the bus, then decompresses the tile. Memory use therefore stays bounded by the index and one compressed and decoded tile rather than the size of the map file. The T5S3 E-Paper V2 connects its SD card and LoRa radio to the same SPI pins. Drop SDCARD_USE_SPI1 only from the InkHUD environment so both use the default SPI instance and the existing shared lock. Keep SD map enablement and preload setup in the T5S3 variant; the non-InkHUD V2 environment retains its existing SPI1 configuration. Update MapApplet to walk only the visible tile-coordinate range, wrap tile X coordinates across the antimeridian and check embedded tiles before requesting an SD tile. A missing, invalid or temporarily busy SD source simply leaves that background tile undrawn without changing the existing map applets or their compiled-in tile behavior. * PowerFSM: service non-input GPIO wakeups after light sleep Before |
||
|
|
4964a0c6a5 |
feat(raksensorhub): SensorHub uplink for WisMesh Hub (RAK2560) (#11817)
* feat(raksensorhub): add SensorHub uplink driver for WisMesh Hub Add HAS_RAKHUB env rak_wismesh_sensorhub with 1-Wire bus + IPSO cache. Ship standard environment metrics only; soil/water chemistry stays gated until SoilWaterMetrics lands in firmware. * fix(raksensorhub): harden RX length, MODBUS log, and power TTL Reject oversized frames without uint16 wrap, validate MODBUS before head[] access, TTL hub power like other scalars, and set board_level for CI matrix. * fix(raksensorhub): track HubPower freshness per field Use separate timestamps for voltage, current, and capacity so one IPSO cannot keep stale siblings fresh, and percent no longer depends on volMv. * fix(raksensorhub): stamp 0-unset cache times with Time::skipZero A wrap or boot 0 tick must not look like never-updated; freshness reads use Time::stampMillis() too. --------- Co-authored-by: Thomas Göttgens <tgoettgens@gmail.com> |
||
|
|
dc9999280f |
Don't let a stale RX flag re-arm RX over a live frame (#12153)
* Don't let a stale HEADER_ERR re-arm RX over a live frame
Before: a header that fails on noise, a collision or a foreign frame
latches HEADER_ERR and no RX_DONE follows it. `checkStaleRxFlags()`
counts HEADER_VALID and HEADER_ERR as one boolean, so the error starts
the max-packet window that re-arms RX. A good frame arriving inside that
window raises HEADER_VALID beside the error, which does not restart the
window - a header was already up. One max packet after the error was
first seen, the poll re-arms, and `startReceive()`'s standby aborts the
frame still on the air.
After: the window is timed from the valid header alone. A HEADER_ERR
beside a HEADER_VALID is retired at once and decides nothing, because one
header raises one flag or the other (SX1262 DS rev 2.1 table 13-29), so
the two together are two frames and the error is the older one.
A lone stale HEADER_ERR past its window is now cleared rather than
re-armed on every chip but SX128x. The re-arm exists for the SX1280 wedge
of DS rev 3.3 16.2, which a clear does not unwedge; on SX126x, LR11x0 and
LR20x0 the demodulator is already hunting for a preamble again, and a
clear never aborts a frame that may be in one. `headerErrWedgesRx()` is
the per-chip hook, true only on SX128x. SX126x already clears HEADER_ERR
unconditionally in `resumeRunningReceive()` for the same reason.
The busy decision is unaffected: `receiveDetected()` reads only
HEADER_VALID and PREAMBLE_DETECTED, so the cost of the bug was a dropped
reception, never a talk-over.
How: `staleRxFlagAction()` takes HEADER_VALID and HEADER_ERR separately
plus the per-chip wedge flag, and gains a ClearHeaderErr verdict.
`checkStaleRxFlags()` keeps a second stamp bit for "the window was
started by a valid header", so a valid header restarts a failed one's
window the way develop already has a header restart a bare preamble's.
Five native cases cover it, two of them the regression itself.
Found and analysed by Tom (NomDeTom), who also wrote the commit the
behaviour came from,
|
||
|
|
e9c046547d |
LoRa: size the preamble hold for the worst-case frame, and let the CAD settle an old bare preamble (#12125)
* LoRa: size the preamble hold for the frame that could be arriving The hold ran for getPacketTime(DATA_PAYLOAD_LEN + sizeof(PacketHeader)): 249 bytes at our own coding rate. A sender's header carries its own CR, up to 4/8, and that header is unreadable when the hold starts, so a legal 255-byte frame at CR 4/8 with CRC outlives the hold and we key up over it. That is the case the hold exists for, since LoRa we cannot decode - a foreign sync word - never produces HEADER_VALID at all. maxRxFrameMsec() times the 255-byte PHY frame at CR 4/8 with CRC from the configured modem, through the radio's own calculateTimeOnAir(), and falls back to getPacketTime(MAX_LORA_PAYLOAD_LEN) if the radio answers an error code (#11935). At SHORT_TURBO that is 159 ms rather than 100 ms, at LONG_FAST 3.3 s rather than 2.1 s. The HEADER_VALID ageing in receiveDetected() takes the same bound, for the same reason. checkStaleRxFlags() and checkCadHandoffTimeout() keep the shorter window: they decide a latched flag is dead and recover the chip, where waiting longer delays the recovery rather than protecting a frame. Three test cases in test/test_radio pin the sizing, the error fallback and that the hold outlives the flag clear that starts it. FakeIrqRadio now answers calculateTimeOnAir() and honours a clear. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CStem6K6MQRBNR8YCFgtCE * LoRa: let the pre-TX CAD settle an old bare-preamble hold A hold runs for one max frame whether or not anything is really arriving: 159 ms at SHORT_TURBO, but 2.1 s at LONG_FAST and 14 s at LONG_SLOW. For all of it the TX path only re-rolls its backoff - canSendImmediately() is false, so the pre-TX CAD never runs and nothing re-examines the channel. A noise preamble therefore costs seconds of TX and can overflow the Tx queue, which is what @GUVWAF objected to on #11968. A frame we could decode latches HEADER_VALID within the rest of the preamble plus the ~12.25 symbols of sync word, SFD and explicit header. Once that grace has passed with no header, the sighting is noise or LoRa we cannot read, and neither deserves the rest of the hold: preambleHoldPeekable() lets the TX path fall through to its CAD and send if the channel is clear. At LONG_FAST the grace is 248 ms against the old code's own 262 ms deadline, and 8 ms at SHORT_TURBO, so a spurious preamble now blocks TX for slightly less than develop did before the hold existed - and then actually listens. HEADER_VALID keeps the full hold: that is a real frame on our own modem. receiveDetected() notes a header even while the hold short-circuits, so a frame whose header arrives mid-hold is never peeked past, and a new hold forgets the previous one's header. Our own transmit ends the hold, since the CAD has just judged the channel. preambleHeaderGraceMsec() computes the grace from sf and bw rather than RadioInterface::preambleTimeMsec, which is whole milliseconds: above 2 GHz a 12-symbol preamble is 0.95 ms, so that value is 0 and the grace would be 0, making every sighting peekable the moment it was seen. Six more cases in test/test_radio: the grace at wide-LoRa and sub-GHz presets, its rounding, and the peek inside the grace, after it, behind a header and across a new hold. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CStem6K6MQRBNR8YCFgtCE * LoRa: ask for the frame bound before reading back the CR it timed test_preambleHold_isSizedForTheWorstCaseFrame read phy.lastCodingRate straight after the first preamble sighting, where nothing has yet asked the chip to time anything: preambleHoldActive() short-circuits on the unset stamp, so the sighting that starts a hold never reaches calculateTimeOnAir() and the fake still reports a coding rate of 0. Ask maxRxFrameMsec() for the bound first, then assert the CR and CRC it was asked for. The case pins the same thing it always meant to - that the hold is timed at CR 4/8 with CRC rather than at our own CR. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CStem6K6MQRBNR8YCFgtCE * LoRa: scale the preamble hold's airtime fallback to CR 4/8 When calculateTimeOnAir() cannot time the worst-case frame, maxRxFrameMsec() falls back to getPacketTime(), which times it at our own coding rate. A configured rate below 4/8 then produced a hold shorter than the frame the hold exists to cover. Scale that figure up to 4/8; the preamble does not scale with CR, so the result runs long, which is the safe direction for a hold. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013mdEBL5t2xdysijGG6ejQR --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
ed072eb4b5 |
ThinkNode M8: switch the GPS to the AG3352Q (#12151)
* ThinkNode M8: switch the GPS to the AG3352Q Later M8 units replace the ATGM336H with an Airoha AG3352Q. Drop the ATGM336H standby pin and wire up idle_int (P1.07) so the existing GPS_SLEEP_INT path stops the engine with $PAIR003 before GPS_EN cuts the 0.8 V core, as on the ThinkNode M9. Default baud is now 115200. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * ThinkNode M8: put the AG3352 into backup mode for sleep $PAIR003 only stops the GNSS engine, so with the 1.8 V rail always on the receiver never reached low power (1.5 mA measured by Elecrow). Add GPS_AIROHA_RESET_WAKE for boards without RTC_INT that have RESETB: sleep sends $PAIR650,0 (with idle_int low) before EN goes low, and wake pulses RESETB once EN is back up. Enable it on the M8. At shutdown, hold idle_int high and RESETB released instead of letting the variant's pin sweep float them through System OFF. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
bda9f8c22d |
Add configurable USB console light sleep veto for ESP32 (#12160)
* Add a build option to ignore the USB console's light-sleep veto #12141 vetoes light sleep while a USB host has the hardware USB-serial/JTAG console open, because no shipping chip keeps the USJ pad alive across a sleep and the host gives up before we surface again. That is right for a user, and it also means light sleep can no longer be watched in a debug log over USB, which is how the path is normally developed. mverch67 asked for a way to turn it off on #12141. MESHTASTIC_LIGHT_SLEEP_IGNORE_USB_CONSOLE does that. It gates the veto rather than the detector, so usbConsoleHostAttached() still answers honestly and the only thing that changes is whether lsIdle() lets the sleep through - and, with it, the "Skip light sleep: USB console host attached" line, which would otherwise be logged once per stay in LS by a build that is deliberately sleeping anyway. Nothing defines it. A build that does accepts losing the console to the first sleep until the board is power cycled. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GTiifXrVc4YdonmLRCeWhw * Make the light-sleep USB opt-out a single defaulted define The helper added a function to say what the condition can say itself. Define MESHTASTIC_LIGHT_SLEEP_IGNORE_USB_CONSOLE to 0 by default and test it inline, so there is one name to grep for and a variant can override it with a -D. Behaviour is unchanged: with the define at its default the condition reduces to the same !usbConsoleHostAttached(), and the detector is still untouched, so usbConsoleHostAttached() keeps answering honestly for any other caller. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GTiifXrVc4YdonmLRCeWhw --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
2d61ca6736 |
Latch the LoRa IRQ skip-sleep give-up until the line is seen low (#12155)
doLightSleep() skips a light sleep when the LoRa IRQ line is already asserted, and bounds that to LORA_IRQ_SKIP_SLEEP_MAX consecutive skips so a line stuck high cannot stop the node sleeping for good. But the counter was reset on the sleep it finally allowed, so the bound never stuck: a genuinely stuck-high line produced five skips, one sleep, five skips, one sleep, for as long as it stayed high. Each of those skipped attempts is not free. It runs waitEnterSleep() and the full notifyLightSleep / notifyLightSleepEnd observer pass, which detaches, arms and restores every wake pin - the path #12100 and #12131's interrupt storm live on - so the loop costs far more than the sleep it is protecting. Reset the counter only when the line is actually read low, which is the condition that says the radio is clearing its IRQs again. Nothing changes for the transient case the skip exists for: the line goes low as soon as the main loop services the packet, and the next sleep clears the count. Claude-Session: https://claude.ai/code/session_01GTiifXrVc4YdonmLRCeWhw Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
660680cea5 |
Prepare the LoRa wake pin on every ESP32 variant, not just ESP32/S2/S3 (#12154)
enableLoraInterrupt() had two branches for the same job. The first did the DIO1 pull-down, the LORA_RESET and LORA_CS pull-ups and the HAS_LORA_FEM sleep-RX setup before arming the wake pin; the second armed the pin and did none of that. The condition choosing between them was SOC_PM_SUPPORT_EXT_WAKEUP, an IDF backwards-compatibility alias defined only on ESP32, S2 and S3 - the same alias main-esp32.cpp:427 already warns about for the ext1 deep-sleep path - so C3, C6, H2 and P4 compiled the second one. On those chips DIO1 therefore entered light sleep with no pull-down, which on a line that is not externally pulled reads high and wakes the node from every sleep; LORA_CS and LORA_RESET entered unpulled; and a LoRa FEM was not put into its sleep RX mode. Nothing here uses ext wakeup: it is a GPIO light-sleep wake source, and gpio_wakeup_enable() and the pin pulls are available on every ESP32 variant. So the first branch now compiles everywhere and the second is gone. The restore half of the same path was already unconditional - its #if SOC_PM_SUPPORT_EXT_WAKEUP / #else carried two identical branches, vestigial from #12131 - so that is collapsed too. Claude-Session: https://claude.ai/code/session_01GTiifXrVc4YdonmLRCeWhw Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
1c366c2d05 |
Radio: end a CAD handoff whose RX hits a damaged header (#12144)
* Radio: end a CAD handoff whose RX hits a damaged header When CAD hands off to RX and that RX ends on a damaged header (CRC_ERR or HEADER_ERR with no RX_DONE), the RX readout task cleared those flags and returned. Nothing was notified and cadHandoffRxStart stayed set, so the radio thread never re-armed. Where the chip had stopped listening, the node stayed deaf until the main loop's backstop logged "CAD>RX timeout" and re-armed: 200 ms to over a second later. Before the readout task, the same edge went to handleReceiveInterrupt(), which read the error, cleared the flags and re-armed. The bench saw this on an LR2021 (X1) running develop: 5 "CAD>RX timeout" lines across 3 runs, where pre-#11983 develop logged none in 4 runs. Now the task hands every frameless end of a handoff's RX (TIMEOUT, CRC_ERR, HEADER_ERR) to the radio thread, which clears the flags through handleReceiveInterrupt() and re-arms, as it already did for TIMEOUT. Co-Authored-By: Jonathan Bennett <jbennett@incomsystems.biz> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01REkPVFh6kvG4AZJ5A8AtM9 * Radio: don't strand a damaged header's flags when its notification is dropped Three follow-ups to the handoff end, from review of the first commit. The readout task's notify(ISR_RX, !rxArmedBeforeTxDone) deliberately does not overwrite a pending TX notification, so the RX notification can be dropped. That is pre-existing for the TIMEOUT case, and the ISR_TX path does put the chip back into RX, but the first commit moved the clearing of CRC_ERR / HEADER_ERR to the radio thread, so in the dropped case nothing clears them until checkCadHandoffTimeout(). notify() returns false exactly when it dropped, so clear them at that site in that case, as the code did before, and leave the open handoff to the backstop. handleReceiveInterrupt() logged "CAD>RX pkt" for a frameless end, and read a length that is not a frame's. A damaged header now takes the same exit as an empty window, with its own log line, so a capture says which of the two happened and getPacketLength() is not asked about a packet that never arrived. rxBad is unchanged either way: neither case counted before. An empty window inside a handoff can carry a damaged header's flags alongside the timeout, both routed to DIO1, so that exit clears them together. Plain RX is untouched there and still leaves HEADER_ERR to checkStaleRxFlags(). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GTiifXrVc4YdonmLRCeWhw --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
a2984a5d28 |
ThinkNode M8: make button E the cancel button (#12150)
Button E was PIN_BUTTON1 with a hand-rolled FunctionButton map in InputBroker (single press: ping, double: privacy toggle, 5 s hold: shutdown). The rotary encoder already drives the UI, so define the pin as CANCEL_BUTTON_PIN instead and drop the M8 special case: a press now cancels and a 4 s hold shuts down, through the generic cancel button. variant_shutdown() follows the rename so the wake-on-press pin is still parked and sensed. Claude-Session: https://claude.ai/code/session_01B7bFRZ9bBMRr7KPBF8rmzu Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
0af1396ea3 |
Let a configured ls_secs survive the phone API round trip (#12145)
The config-send path copies config.power and then overwrites one field with
the default, unconditionally:
fromRadioScratch.config.payload_variant.power = config.power;
fromRadioScratch.config.payload_variant.power.ls_secs = default_ls_secs;
The note at the end of that state explains why the effective value is sent:
0 means "use the default" and the app needs to know what the device will
actually do. But the write is not conditional on the stored value being 0, so
a configured value is replaced on the way out. Every client, the CLI included,
reads back default_ls_secs (IF_ROUTER(ONE_DAY, 5 * 60)) no matter what is
stored, and a set that took effect looks like a set that was refused.
It also loses the setting. AdminModule's power case stores what it is sent
(src/modules/AdminModule.cpp:967), so a client that reads this config and
writes it back - which is what happens when a user changes any other field in
the same message - stores the default over the configured value.
Observed on an ESP32 node: ls_secs set to 60, read back as 300, and the node
then light slept on the 60 s cadence, so the stored value had taken all along.
Default::getConfiguredOrDefault() keeps the behaviour the note describes, since
0 is exactly the case it falls through on, and changes only the readback of a
non-zero stored value. Note that config.power.ls_secs is initialised to
default_ls_secs in NodeDB::installDefaultConfig() (src/mesh/NodeDB.cpp:1366)
and consumed raw in PowerFSM's lsIdle() (src/PowerFSM.cpp:133), so a stored 0
is not how a default-configured node looks in the first place.
Claude-Session: https://claude.ai/code/session_01GTiifXrVc4YdonmLRCeWhw
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
afdaccc25f |
Makerfabs nomad terminal (#12112)
* Add Makerfabs Nomad Terminal variant * Configure external power detect pins before probing battery gauges * TFTDisplay: add ILI9488 reset pin and colour inversion options * Makerfabs Nomad Terminal: add BaseUI env, early power latch, soft-start LCD/GPS rail, GPS wake * I2C scan: tell FocalTech touch from AHT10 at 0x38 and BMM150 from VEML7700 at 0x10 * Run a BMM150 next to an IMU on the magnetometer thread * Makerfabs Nomad Terminal: BaseUI touch * BMM150: add hard/soft-iron calibration, read at 10 Hz * Makerfabs Nomad Terminal: fix MUI build, restart Wire after MUI touch init * BMM150: seed calibration from the first valid sample * Makerfabs Nomad Terminal: combined BaseUI+MUI -tft firmware, SD defines in build flags * Makerfabs Nomad Terminal: reset touch controller at init, wake touch thread on INT * BMM150: poll every 200 ms * Makerfabs Nomad Terminal: scale CW2015 SOC to the on-USB full reading * BMM150: end an expired calibration when the sample is rejected |
||
|
|
04cb888511 |
Wio Tracker L2 fixes (#12133)
* fix WakeKey not responding when pressed during boot * fix I2C error address 0x48 during scan * enable GT911 gesture recognition in MUI * shutdown_variant() * disable io expander INT during light sleep * fix BaseUI wakeup * prevent bootloop in case DIO1 fires * fix ADS1X15 scan type * fix power down sequence * external keyboard support * fix I2C port 1 define; skip false positive |