* fix(radio): put the beacon restore back inside completeSending's if (p)
Reverts the RadioLibInterface and RadioInterface changes from #11573
(ac330e6a6). Hoisting MeshBeaconModule::reconfigureForBeaconTX() out of the
if (p) block changed its meaning from "a send completed" to "the radio went to
standby, for any reason" - and every driver's setStandby() calls
completeSending() unconditionally: on the pre-TX LBT scan, on startReceive(),
and inside reconfigure().
Two shipping faults followed, both confirmed on hardware the next day.
Every beacon transmitted on the wrong preset. isChannelActive() standbys the
radio immediately before each transmit, so the restore ran between the switch
and the key-up. The packet went out carrying the beacon channel hash with home
modem settings - inaudible to listeners on the target preset, an unknown hash
to listeners on the home one. Inert in both directions.
And unbounded recursion: the restore calls iface->reconfigure(), which
standbys, which calls completeSending(), which restores again, each level
running a full applyModemConfig(). It terminated in a HardFault and a silent
reboot (Reset reason 0x4 on nRF52, no panic output). The crash masked the
misdirection - the node died before Started Tx, so the wrong preset was
invisible until the recursion was fixed.
completeSending() clears sendingPacket at the top, so any nested call sees
p == NULL. The if (p) block was an accidental re-entrancy guard, and nothing
named it as such; removing it created both faults at once. Name it now.
This also reverts the beginSending() failure return that motivated the move,
and the startSend() scaffolding built to reach the restore on that path. The
payload bounds check it replaced is reinstated in the next commit, at a point
where refusing a packet is already a supported outcome.
* fix(radio): bound the payload at the radio queue, not mid-transmit
#11573 replaced beginSending()'s assert with a runtime check that logged,
released the packet and returned 0. beginSending() had never returned 0
before, so startSend() gained a failure path it had to unwind - and the
release moved ownership of the packet out of the caller that held it. That
new return value is what made hoisting the beacon restore look necessary.
The check itself is worth keeping. MeshPacket.encrypted has a nanopb maximum
of 256 bytes against a 240-byte radio buffer, and beginSending() is on the
path for relayed frames and phone-sourced packets, neither under our control.
Asserts are commonly compiled out in release builds, so what shipped was an
unchecked 256-into-240 memcpy driven by remote input.
Move it to Router::send(), immediately before iface->send(p) - the single
funnel for every over-the-air transmit. Refusing a packet there is already a
supported outcome: it returns TOO_LARGE, which is what perhapsEncode() already
returns for the same condition on the decoded path, and releases or NAKs
exactly as the duty-cycle limit above it does. Nothing radio-side has happened
at that point, so there is no half-started transmit to tear back down.
perhapsEncode()'s existing check does not cover this case: relayed and
phone-sourced frames arrive already encrypted and never reach it.
beginSending() keeps a last line of defence, but clamps rather than failing,
so it stays a call that always succeeds. Adds MAX_RADIO_PAYLOAD_LEN so both
sites name the same number instead of recomputing it.
Nothing about a beacon can trigger any of this - broadcast_message is
admin-truncated to 100 bytes, the whole MeshBeacon protobuf tops out at 180,
and observed beacons run to 106 - which is why this is separated from the
beacon changes rather than carried with them.
Tests: Router::send() refuses an oversized payload and still sends one that
exactly fills the buffer; beginSending() clamps instead of rejecting, and
leaves ordinary traffic whole.
* fix(beacon): guard the radio switch/restore against re-entry and early restore
Two checks in reconfigureForBeaconTX(), both independent of radio state, so
the switch/restore state machine no longer rests on sendingPacket's lifetime -
which is exactly the implicit coupling that let #11573 through.
A re-entrancy guard. Both branches end in iface->reconfigure(), whose
setStandby() runs completeSending(), which calls straight back in here. While
one call is applying a config, a nested call returns false and leaves it
alone. This covers the switch branch too, which had the same exposure with a
quieter symptom: a second switch before the restore would take the re-entrant
call as a restore and undo the switch still being applied, sending the beacon
on the home channel instead of its target.
A restore gate. The restore now waits for the packet that armed the switch to
actually finish, tracked by id against our own target table rather than by
asking the radio. Every caller that completes or abandons a beacon clears that
packet's target settings first, so a live entry means the TX has not happened
yet. cancelSending() now clears too, which is what keeps a cancelled beacon
from pinning the radio on the beacon config.
Together these make explicit the invariant completeSending()'s if (p) block
was carrying by accident: a future hoist of that call gets a logged no-op
instead of a crash and a misdirected beacon.
Also sets radioSwitched before reconfigure() rather than after, in both
branches, so the flag never describes a radio state that is not yet true.
Diagnostics, because every step of this dance was previously silent about its
own state. Count consecutive switches with no restore between them and log the
depth on both sides, so a change-change-change-restore run reads off the log;
switch #2 onwards prints the held home snapshot, which is the value that has
to survive a second switch. The restore names the config it is restoring to,
so a stale snapshot is visible directly. The re-entrancy guard logs when it
fires - expected exactly twice per beacon, so a burst means something new is
re-entering rather than a silent reboot. And setTargetRadioSettings() now
warns on the slot eviction that previously left a packet to key up on whatever
config was running - no crash, no log, wrong channel.
Reachable only with beacon broadcast enabled (the default flags are
LISTEN_ENABLED | LEGACY_SPLIT, so broadcast is off) and a target differing
from the running config; an identical target takes the early return and never
switches.
Tests: three re-entrancy cases against a RadioInterface whose reconfigure()
re-enters exactly as completeSending() does - bounded, so a regression fails
an assertion instead of overflowing the stack and taking the runner with it -
plus a restore that must defer until the beacon it switched for completes.
* fix(beacon,radio): address review findings on #11596
Payload ceiling was one byte too generous. RadioBuffer::payload is 240 bytes
because the buffer reserves MAX_LORA_PAYLOAD_LEN + 1, but the PHY caps a whole
frame at 255 and beginSending() adds a 16-byte header - so a 240-byte payload
produced a 256-byte frame. Define the ceiling as MAX_LORA_PAYLOAD_LEN -
sizeof(PacketHeader), matching what perhapsEncode() already enforces, with a
static_assert that it still fits the buffer.
Target-table eviction could unblock the restore gate. With every slot live,
setTargetRadioSettings() overwrote slot 0 - and if that slot held the packet the
outstanding switch is gated on, the restore came unblocked and put the home
config back under a beacon that had not keyed up. Skip that entry when choosing
a victim, and refuse the target outright if every slot is in flight. Needs
radioSwitched/switchedForId at file scope so the setter can see them.
Restore on every abandon path, not just the clear. cancelSending() dropped a
queued packet's target without restoring, so a beacon pre-switched by onNotify()
and then cancelled left the radio receiving on the beacon config;
removePendingTXPacket() did neither. Both now route through
abandonBeaconTarget(), as does startSend()'s tx-disabled branch. The restore
gate makes it a no-op when the abandoned packet is not the one we switched for.
No NAK on the oversize drop. p->channel is a wire hash by that point, not an
index, and Channels::getIndexByHash() is declared but never defined. Only
already-encrypted ingress can reach the gate anyway - perhapsEncode() bounds
everything it encodes - and those carry no index to answer on. Release and log.
Tests clear sendingPacket before releasing their packet, and assert against the
payload ceiling rather than the buffer size.
* fix(beacon): route the invalid-target drop through abandonBeaconTarget
onNotify()'s invalid-config drop was the one packet-abandonment path still
clearing the target directly instead of going through abandonBeaconTarget(),
so a packet that armed the radio switch and then failed validation would be
released with the radio left on the beacon config and nothing to restore it.
The helper's restore gate (targetRadioSettingsLive(switchedForId)) makes the
call a no-op for any packet that did not arm the switch, so this closes the
gap without risking a premature restore.
Also trims the switch-state comment to the two-line limit.
* fix(radio): take the abandoned packet as a pointer to const
cppcheck's constParameterPointer failed the check matrix on every board:
abandonBeaconTarget() only forwards the packet to clearTargetRadioSettings(),
which already takes a const pointer, so the parameter should be const too.
* refactor(radio): drive the beacon radio switch through TX hooks
RadioLibInterface named MeshBeaconModule at six call sites behind
MESHTASTIC_EXCLUDE_BEACON guards, so the driver carried per-packet beacon
state: when to switch preset, when a target config was invalid mid-transmit,
and when not to listen on a busy channel. Review on #11596 asked for the
module dependency to come out.
RadioTxHook is what the driver knows instead - beforeTransmit() returning
send/defer/drop, holdsRadio(), packetReleased() - on a self-registering
intrusive list, so nothing is allocated and a build without the beacon module
registers nothing and every call is a no-op. The four abandon paths (cancel,
remove-pending, TX disabled, completeSending) collapse onto one
packetReleased(), and the tri-state means the driver no longer has to know why
a packet wanted a re-delay or a drop.
MeshBeaconTxHook wraps the existing statics; the switch/restore logic, its
re-entrancy guard and its restore gate are untouched. It is created in
Modules.cpp inside the existing exclusion guard, so MESHTASTIC_EXCLUDE_BEACON
now works by nothing registering rather than by #ifdefs in the driver.
Behaviour is unchanged. The invalid-config LOG_DEBUG moves into the module and
the driver logs a generic refusal. Four tests cover the send/defer/drop mapping
and that an empty hook list is a no-op; native:test_mesh_beacon is 59/59.
Also notes in sendBeaconPacket that beacons uplink to MQTT on the primary
slot's uplink_enabled, and that the topic follows the beacon channel under the
crypto-override swap - both intentional.
* fix(beacon): restore the home config for a packet that jumps the queue
The restore gate added in 9cb7b96c9 refused to put the home config back while
the beacon that armed the switch was still live. That is right for a release -
completeSending() runs on every setStandby(), and restoring there would undo
the switch before the beacon had keyed up - but it also caught the case where
the driver is asking about a different packet it is about to transmit.
MeshPacketQueue::enqueue() inserts by priority (std::upper_bound over
CompareMeshPacketFunc), so an ACK or routing packet queued during the beacon's
deferred transmit delay lands ahead of it. beforeTransmit() then saw an
untagged packet, found the beacon still queued, skipped the restore and
returned PRETX_SEND - and the packet transmitted on the beacon's preset, slot
and region. It was encrypted and hashed for the home channel, so no receiver
on either preset could use it.
Apply the gate only to a null p. A non-null untagged packet is the driver
about to key up, which always restores; the restore returns PRETX_DEFER, so
the driver re-runs the delay and the channel scan on the config it will
actually transmit on. beforeTransmit() is the only caller that passes a
non-null untagged packet, so nothing else changes.
Found by CodeRabbit on #11596. native:test_mesh_beacon 60/60, including a
regression test for the queue transition; the four re-entrancy tests still
cover the null-p gate.
---------
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
* Fix for MeshBeacon packet leakage
* fix: add runtime payload size check against radiobuffer
* review fix for PR#11573: clear target radio settings before MeshBeacon packet release
* add unit test for radio buffer capacity check, removing related assert for the test
* review fix for PR#11573: add explicit verifaction against rejected packets
* feat(lora): state a pinned userPrefs preset as the unset region's intent
A vendor build can pin USERPREFS_LORACONFIG_MODEM_PRESET while leaving the
region unset, so a fresh flash comes up as region UNSET plus a deliberate
preset. Stock installs come up as region UNSET plus the LONG_FAST placeholder,
and nothing in FromRadio told the two apart - so clients treat every
unset-region node as factory-fresh and replace its preset with the region
default as soon as the user picks a region. A mesh pinned to SHORT_TURBO loses
every new node to LONG_FAST or LONG_TURBO, silently.
getRegionPresetMap() now emits an UNSET entry when, and only when, the build
pins a preset, stating that preset as both the group's sole entry and its
default. Stock builds are unchanged on the wire: no UNSET entry, which clients
already read as unconstrained.
This is intent, not enforcement. supportsPreset() still accepts any known
preset while the region is unset (#11496) and the radio is held silent either
way, so the device continues to honour whatever the user or an admin sets.
Costs one group slot and one region slot on pinned builds only (6->7 of 8,
34->35 of 38); exhaustion is logged and degrades to the existing unconstrained
behaviour.
* Trim comments to the project's one-to-two-line limit
- WiFiServerAPI/ethServerAPI apiPort and ethApiServer's listener are
create/destroy cycles that repeat across WiFi teardown and W5500
chip resets; the manual delete+null bookkeeping becomes reset().
(ethTlsApiServer's listener is left for a follow-up: that file is
already touched by the partial-init fix PR and converting it here
would conflict.)
- ContentHandler::handleFormUpload held its body parser raw with
delete on four separate exit paths of a per-request handler; any
future early return was a silent leak. unique_ptr removes all four.
- The portduino ch341Hal global becomes unique_ptr. The LoRa-error
recovery loop's delete/null/new sequence was correct only by
hand-preserved ordering; it becomes reset()/make_unique. RadioLibHAL
keeps a non-owning raw pointer, as before.
No behavior change.
* logging: strip redundant punctuation, level prefixes, and 'successfully' from log strings
The logger already appends a newline and prints the level tag, so
trailing '.', '!', '...', literal \n, and 'Error:'/'Warning:' prefixes
inside format strings are wasted flash bytes. Same for 'successfully'
(the affirmative form already implies it).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LBiZc9sfPrH1MZ2L3Fxgt1
* logging: tighten verbose log strings in modules, radio, platform, and system code
Rewrite wordy log messages to terser equivalents - drop filler words
(articles, 'attempting', 'due to', 'please'), use 'Can't X'/'X failed'
phrasing, and abbreviate where the codebase already does (config, init,
msg, BT). Format specifiers and argument lists are unchanged; distinctive
greppable tokens are preserved.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LBiZc9sfPrH1MZ2L3Fxgt1
* logging: tighten verbose log strings in telemetry sensors and GPS
Same terseness pass: drop filler, 'Can't X'/'X failed' phrasing, common
abbreviations (temp, msg). Specifiers, arguments, and sensor-name
prefixes unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LBiZc9sfPrH1MZ2L3Fxgt1
* logging: tighten verbose log strings in mesh core
Same terseness pass over NodeDB, Router, MeshService, PhoneAPI,
RadioInterface, NextHopRouter, and PacketHistory: 'X failed'/'Can't X'
phrasing, imperative verbs, dropped filler. Specifiers and arguments
unchanged; duplicate literals kept identical to preserve linker string
dedup.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LBiZc9sfPrH1MZ2L3Fxgt1
* logging: 'Unable to/Could not/Cannot' -> "Can't" in log strings
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LBiZc9sfPrH1MZ2L3Fxgt1
* logging: clang-format rewrap after string shortening
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LBiZc9sfPrH1MZ2L3Fxgt1
* logging: restore boot-logo trailing newline and progress-dot strings
The terseness pass over-trimmed: the Meshtastic ASCII boot logo kept its
blank line via a trailing \n, and three bare "." progress ticks were
reduced to empty strings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LBiZc9sfPrH1MZ2L3Fxgt1
* Update src/mesh/wifi/WiFiAPClient.cpp
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
* logging: address review feedback on the terseness audit
- Node/packet IDs use the repo's 0x%08x convention in NextHopRouter,
NodeDB, AdminModule and CannedMessageModule. The sibling log in each
if/else pair is converted too, so a pair isn't split across two formats.
next_hop stays 0x%x - it's the last-byte relay hint, not a NodeNum.
- RTC: the read-path and set-path "not found" warnings were byte-identical,
so the linker deduped them and the log couldn't say which one fired.
Split into "RTC read:" / "RTC set:". (The four sites live in mutually
exclusive #ifdef branches, so the RTC family was never ambiguous.)
- SCD4X getAmbientPressure()/setAmbientPressure() logged "altitude", and
SCD30 getASC() logged "Can't send command" for a read. Both now name the
operation they actually perform.
- LOG_ERROR already carries the level: ". Error: %u" -> ", rc=%u" (matching
the existing rc=%d house style) and "Error executing X()" -> "X() failed".
- Typos and wording: "OTA partiton. (Reason" -> "OTA partition (reason",
"CST3530 not response ~" -> "CST3530 no response", "Packet received with
to: of 0" -> "to=0", HostMetrics "Error decoding" -> "Can't decode", and
the dangling ": " on the NextHopRouter retransmission line.
Printf specifier sequences are byte-identical on all 37 touched lines apart
from the 6 deliberate %x/%u -> %08x node-ID widenings, all on uint32_t args.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
* add LR 2021 to portduino, and allow Framebuffer devices to rotate the screen from config. Requires https://github.com/meshtastic/device-ui/pull/355 and supersedes https://github.com/meshtastic/firmware/pull/10567 and https://github.com/meshtastic/firmware/pull/11138
Many thanks to the original authors https://github.com/a-li3n and https://github.com/jessm33
* Build LR2021Interface.cpp in the wasm env
initLoRa() constructs LR2021Interface for Lora.Module: lr2021, so excluding
the file from the native-wasm source filter left the constructor undefined at
link time. LR20x0Interface.cpp stays excluded; it is template-only and comes in
via the InterfacesTemplates.cpp amalgamation.
Also replace the non-UTF-8 degree signs in the framebuffer rotation comment and
correct the rfswitch alias cleanup comment.
* Trim comments
* Report setenv failure for the framebuffer rotation
* Add 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
Decree of the Cabinet of Ministers of Ukraine dated February 25, 2026,*
No. 262, on amendments to Section 2 of the Plan for the Allocation and
Use of the Radio Frequency Spectrum in Ukraine, has harmonised
Ukrainian regulations on the 868 MHz spectrum with the EU's, thereby
making UA_868 legally obsolete.
* https://zakon.rada.gov.ua/laws/show/262-2026-п
Co-authored-by: Tom <116762865+NomDeTom@users.noreply.github.com>
* Protobufs
* Wire up MEDIUM_TURBO modem preset
MEDIUM_TURBO (500 kHz, SF9, CR 4/5) already existed in the protobuf enum but
was never wired into firmware, so selecting it silently fell through to the
LONG_FAST default and rendered an "Invalid" display name.
Add its bw/sf/cr mapping (modemPresetToParams), display name (MediumTurbo/MedT),
PRESETS_STD membership (standard regions only — 500 kHz does not fit EU868's
250 kHz band, so it stays out of PRESETS_EU_868 and is rejected/clamped there),
and the MEDIUM SNR-grading bucket. Includes positive coverage in test_radio,
EU868-reject + US-accept coverage in test_admin_radio and test_mesh_beacon,
the STD preset count 9->10, an extended fuzz range, and the client-spec doc.
* Address review feedback on MEDIUM_TURBO tests
- test_mesh_beacon: assert has_mesh_beacon before checking the invalid preset was
cleared, so the EU868-cleared test can't pass on a dropped message (matches the
existing SHORT_TURBO test).
- test_fuzz_packets: draw modem presets from _ModemPreset_ARRAYSIZE instead of a
hard-coded 17 so the fuzz range tracks future enum additions automatically.
* Add software LoRa IRQ polling and an MCP23017 expander HAL for radios behind an I2C expander
Some boards route the SX126x control lines (NRESET/DIO1/BUSY) through an I2C
GPIO expander instead of native GPIOs, and don't wire the expander's /INT to
the MCU, so RadioLib cannot take a hardware DIO1 interrupt.
Two reusable pieces, both gated behind USE_MCP23017 / LORA_DIO1_SOFTWARE_POLL
so builds that don't define them are unaffected:
- ExtensionIOMCP23017 + MCP23017LockingArduinoHal: a RadioLib HAL that maps
virtual pins (MCP23017_VPIN_BASE .. +15) onto an MCP23017's GPIO, so
RESET/BUSY/DIO1 reads and writes become I2C transactions while the real SPI
GPIOs (SCK/MOSI/MISO/CS) pass straight through. A failed I2C read skips the
read-modify-write rather than clobbering the rest of the bank.
- LORA_DIO1_SOFTWARE_POLL: with no DIO1 interrupt available, the SX126x
interface polls the radio IRQ status register from the radio thread and
synthesizes the ISR_TX/ISR_RX events, filtering noisy preamble/header IRQs so
they can't starve TX. A 1 ms ISR_POLL_TICK drives the poll; TX timers may
overwrite the pending tick (pollMissedIrqs() is the backup that bounds the
latency of the busy-Rx contention window). Behaviour is unchanged on all
boards that keep the hardware interrupt.
* Add Meshnology W10 AIOT Dev Kit variant (SX1262 via MCP23017 expander)
ESP32-S3R8 + EBYTE E22-900MM22S (SX1262) + AXP2101 PMIC + Quectel L76KB GPS +
SPI TFT, with the radio's RESET/DIO1/BUSY and the LCD reset routed through an
MCP23017 I2C expander at 0x20. The expander's /INT is not wired to the ESP32,
so DIO1 uses the software poll. Pins come from the board schematic and the
vendor firmware; the expander is an MCP23017 despite the V1.1 schematic's stale
'TCA9555' label (the V1.2 placement diagram and all vendor code use the MCP23017
register map).
A local pins_arduino.h shadows the generic esp32s3 variant to drop RGB_BUILTIN,
which otherwise pulls the Arduino RMT HAL into the link and fails against this
build's trimmed FreeRTOS config.
Reports HardwareModel 140 (MESHNOLOGY_W10, already present in the protobufs).
Hardware-verified on a real W10 AIOT Dev Kit: AXP2101 PMU, MCP23017, SX1262
init + a full over-the-air TX/RX round trip through the expander, PCF85063 RTC,
SHT41 and QMI8658 auto-detected, and an L76KB GPS fix.
* meshnology-w10: enable ES8311 speaker for notification tones
Bring up the ES8311 codec (I2C 0x18) -> NS4150 amp -> speaker so the board can
play notification tones / ringtones over the I2S buzzer path (the
use_i2s_as_buzzer external-notification option). Codec2 voice stays out of
scope: it is SX1280-only and this is a sub-GHz board.
- variant.h: HAS_I2S + DAC_I2S pins (MCLK=1, BCK=2, WS=4, DOUT=5, DIN=3)
- platformio.ini: arduino-audio-driver + ESP8266Audio + ESP8266SAM
- extra_variants/meshnology_w10/variant.cpp: lateInitVariant() configures the
ES8311, mirroring the other Meshtastic ES8311 boards. Kept codec-only (no
main.h) so the audio driver's 'using namespace audio_driver' does not pull a
conflicting GpioPin into scope alongside Meshtastic's class GpioPin.
- AudioThread.h: toggle the NS4150 amp (MCP23017 EXIO_PA_CTRL) around playback.
Verified on hardware: a test tune played audibly through the speaker.
* meshnology-w10: address review feedback on the MCP23017 driver
- ExtensionIOMCP23017: serialize register access with a mutex, since the radio
HAL (BUSY/DIO1/RESET) and AudioThread (amp enable) now reach the expander from
different threads and the read-modify-write paths are not atomic.
- digitalRead: return a fail-safe HIGH on a failed read instead of LOW, so a
transient I2C error on the LoRa BUSY line can't look like 'ready' and let
RadioLib start an SPI transaction early. (DIO1 is polled via the radio IRQ
register, not this pin.)
- enablePinChangeInterrupt: use the checked readReg() overload and skip on a
failed read, matching the other read-modify-write helpers.
- meshnology_w10 variant.cpp: log a warning if an ES8311 register write NACKs
instead of silently leaving the codec half-configured.
- RadioInterface.cpp: guard the MCP23017 HAL branch with ARCH_ESP32 to match the
include and driver-file guards.
* Replace board-model audio/sleep ifdefs with reusable variant capability macros
Two shared-code spots keyed off specific hardware models; move the board-specific
detail into opt-in macros the variants define, so the core code stays generic and
new boards can opt into the behavior class without touching shared files.
- AudioThread amp control: drop the T_LORA_PAGER / MESHNOLOGY_W10 ifdefs. A board
with an I2S amp now defines AUDIO_AMP_ENABLE(on) in its variant.h (T-LoRa Pager
and Meshnology W10 do), and AudioThread just calls it around playback. The
expander includes are keyed on USE_XL9555 / USE_MCP23017 (capabilities) instead
of the model name.
- Light-sleep / DIO1 wakeup: drop the SENSECAP_INDICATOR check. Boards whose
LORA_DIO1 is an expander pin (not an ESP32 GPIO) define LORA_DIO1_EXTENDED_IO
(SenseCAP Indicator and Meshnology W10), and sleep.cpp keys off that for both
the light-sleep skip and the DIO1 GPIO-wakeup skip. This also fixes a latent
issue on the SenseCAP: it previously reached the GPIO-wakeup path and called
gpio_pulldown_en() on a virtual expander pin; it now skips that cleanly.
Builds verified on meshnology_w10, tlora-pager, and seeed-sensecap-indicator.
* meshnology-w10: silence cppcheck constParameterPointer on the ISR-callback helper
isIsrTxCallback() takes a function pointer it only compares; cppcheck's
constParameterPointer wants it 'pointer to const', which is meaningless for a
function pointer (the codebase already globally suppresses the sibling
constParameterCallback). Inline-suppress it to keep the check green.
* HopScaling: qualify the member assignment as this->count
cppcheck (CI's version) flags 'count = newCount;' at the end of trimIfNeeded()
as uselessAssignmentArg ('assignment of function parameter has no effect') - a
false positive, since count is a member that outlives the call. Write it as
this->count (matching the Step-1 assignment above) so the analyzer sees a member
write. This finding comes in via the develop merge, not this board; fixing it
here to unblock the PR's cppcheck check.
* first pass tests
* more tests
* Fix two crafted-admin-packet crashes found by the E5 fuzzer
Both are reachable from an authorized admin (local from==0, admin channel,
or PKC) - remote DoS:
1. SIGFPE in LoRa config validation. A set_config LoRaConfig with
use_preset=false and bandwidth=0 makes freqSlotWidth 0, so numFreqSlots
is 0 and `hash(name) % numFreqSlots` (RadioInterface.cpp) divides by
zero. Guard the modulo; the existing channel_num check then rejects/
clamps the config.
2. Stack overflow in Channels::getKey. A SECONDARY channel at the primary
slot with an empty PSK recursed into getKey(primaryIndex) forever. Skip
the primary-key borrow when chIndex == primaryIndex.
Re-enable the E5 admin fuzzer to hit both triggers again (use_preset both
ways incl. bandwidth 0, plus the set_channel tag) as regression guards.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Correct fuzz-test invariants after the crash fixes
- E5 admin fuzz: node eviction under a filling NodeDB is legitimate, so
assert only the bounded-count invariant, not that a specific seed node
survives 6000 mutating ops.
- TMM blitz: scope off the nodeinfo direct-response send path (it needs a
fully-wired MeshService/phone queue the fixture doesn't provide; the
deterministic directResponse tests cover it). The crafted-nodenum
rate/unknown/position cache stress is unchanged.
clod helped too
* realistic tests
* test: dedup fuzz RNG into shared test/support/DeterministicRng.h
The four in-tree fuzz suites (test_fuzz_decode, test_fuzz_packets,
test_hop_scaling, test_traffic_management) each carried a byte-identical
copy of the seeded 64-bit LCG (rngSeed/rngNext/rngByte/rngRange). Hoist
it into one shared header so there is a single generator to reason about
and no risk of the copies drifting. static inline keeps per-suite state
per translation unit and avoids -Wunused-function for suites that don't
use every helper. Also corrects a stale comment in test_traffic_management
(the blitz's nodeinfo direct-response path is intentionally left off).
No behavioral change: same constants, same per-suite seeds.
clod helped too
* test: fuzz uncovered ProtobufModule handlers and the MQTT downlink ingress
Extend the in-tree fuzz coverage to packet sources that previously had
none:
- test_fuzz_packets E8/E9/E10: drive PositionModule, DeviceTelemetryModule
and NeighborInfoModule at handleReceivedProtobuf directly (via using-shims,
bypassing the ProtobufModule reply/send path so no router is needed). The
fixture already stands up nodeDB/service/channels, and nodeStatus/powerStatus
are auto-initialized in main.cpp, so no new globals are required. Adds a
shared fuzzRxHeader() helper for crafting adversarial RX packet headers.
- test_fuzz_decode: add meshtastic_KeyVerification to the decode table. The
KeyVerification and StoreForward handler paths are documented as decode-level
only, with the concrete reason each is intrinsic (private-state gating /
PSRAM + self-pointer wiring), not a fixture gap.
- test_mqtt: test_receiveFuzzServiceEnvelope blitzes the non-RF broker-push
ingress (onReceiveProto) two ways - raw garbage bytes that must fail envelope
decode cleanly, and a well-formed ServiceEnvelope wrapping a crafted inner
MeshPacket over crafted channel_id/gateway_id - exercising the channel match,
isFromUs, XEdDSA receive policy and perhapsDecode chain. Adds a deliverRaw()
passthrough to MQTTUnitTest.
All under the coverage env (ASan/LSan). No firmware/src changes. Full sweep
GREEN 27/27, 544 cases.
clod helped too
* Harden LoRa/channel config against crafted admin messages; consolidate test helpers
Production (review findings on the hot-fuzz crash fixes):
- Clamp bandwidth at the source (clampBandwidthKHz) in checkOrClampConfigLora
and applyModemConfig so numFreqSlots can never be 0 for any consumer; a
bandwidth-0 set_config previously passed validation and re-armed the SIGFPE
on the next applyModemConfig.
- Guard applyModemConfig's hash % numFreqSlots (the validator's sibling modulo
was fixed earlier but this one was still unguarded).
- Enforce the primary-channel invariant in Channels::onConfigChanged: a config
demoting every slot now re-promotes the stale SECONDARY slot (keeping its
key) or restores the default channel if the slot is DISABLED, instead of
leaving every getPrimaryIndex() reader on a non-primary slot. The getKey
recursion guard stays as defense-in-depth.
Tests:
- New test/support/MockMeshService.h and AdminModuleTestShim.h replace four
byte-identical mocks and three divergent admin shims (test_mqtt's capturing
mock is genuinely different and stays).
- DeterministicRng.h: add rngFill() (replaces 14 hand-rolled fill loops) and
rngEdgeNodeNum() (unifies the three NodeNum boundary pools).
- Extract fuzzChannelSettings() shared by the set_channel case and fuzzBeacon.
- fuzzBeacon: the un-terminated branch now fills the whole buffer with non-NUL
bytes so the strnlen bound is actually stressed (~50% of iterations, not ~4%).
- E6 beacon fuzz: replace the TEST_ASSERT_TRUE(true) tautology with real
invariants (handler never consumes; offers land in lastReceivedOffer keyed
to the sender).
- Trim the seven over-long comment blocks flagged against the 1-2 line rule;
the FINDINGS trailer moves to this commit message (see production notes).
Full native suite GREEN 27/27 under the coverage (ASan/LSan) env.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Clamp UTF-8 char length in the emote walkers; add test_fuzz_emotes
A TEXT_MESSAGE payload is opaque protobuf bytes, so PB_VALIDATE_UTF8 never
screens it - invalid UTF-8 and truncated multi-byte lead bytes reach the
emote/width render path verbatim. EmoteRenderer's walkers advanced by
utf8CharLen(lead) without clamping to the bytes actually remaining, so a
truncated lead (e.g. a lone 0xF0, which claims 4 bytes) near the end of the
buffer made getUtf8ChunkWidth's memcpy read past the string. ASan confirms a
heap-buffer-overflow READ from measureStringWithEmotes.
Add utf8CharLenClamped() and use it at every walk site (width measure,
truncation cut-loop, and the draw-path text-run/chunk builders); the one
already-guarded site (matchAtIgnoringModifiers) is unchanged.
New test/test_fuzz_emotes drives measureStringWithEmotes and truncateToWidth
over adversarial byte strings (biased to embed/end in truncated multi-byte
leads) in exact-sized heap buffers so any over-read is a hard ASan fault. Its
headless display uses a synthetic font (firstChar 0, fontData centered in a
large buffer) so the stock OLEDDisplay::getStringWidth - which indexes the
font jump table with a signed char and over-reads for any byte >= 0x80 - does
not mask the finding. native-suite-count bumped 27 -> 28.
Full native suite GREEN 28/28 under the coverage (ASan/LSan) env.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Keep emote width measurement in-bounds for non-ASCII bytes
OLEDDisplay::getStringWidth (the utf8=false path EmoteRenderer uses on default
builds) indexes the font jump table by (c - firstChar) with a signed char and
no bounds check, so any byte outside printable ASCII - high bytes from UTF-8
text, but also a stray control byte like 0x0A - reads outside the font array.
On-device this reads adjacent flash and returns a garbage width; under ASan the
test_fuzz_emotes fuzzer flags it as a global-buffer-overflow, and it made the
non-ASCII width measurement meaningless either way.
The OLED driver is a pinned upstream dependency, so guard it firmware-side in
EmoteRenderer's getStringWidth helper: measure a sanitized copy where any byte
outside [0x20, 0x7E] counts as a '?' placeholder. Printable ASCII is unchanged
and the UA/RU lookup path is untouched.
test_fuzz_emotes now drives a real ArialMT font instead of the synthetic
in-bounds font it needed before this fix, so the suite exercises the true
production width path (utf8CharLen clamp + this sanitizer) end to end. The same
fuzzer tripped the global-buffer-overflow before this change.
Full native suite GREEN 28/28 under the coverage (ASan/LSan) env.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* clarify channel number formatting and fix the fixed-width padded nodeIDs
* use the explicit 0x%08x for packets and nodeIDs throughout
* LLM instructions
* Added lora region and preset maps
* Protos
* Address PR review feedback
- Log (and break/skip) when the region preset map exceeds its array bounds
instead of silently dropping regions
- Derive test bounds from the generated nanopb array sizes via sizeof()
instead of hard-coded magic numbers
- Fix want_config sequence comment (missing comma, STATE_SEND_MODULECONFIG)
- Specify a language on the spec's fenced code blocks (markdownlint MD040)
* Fix want_config stall: handle STATE_SEND_REGION_PRESETS in PhoneAPI::available()
available() had a separate per-state switch that wasn't updated for the new
state, so it returned false ('unexpected state 5') and getFromRadio() was
never called - the config handshake stalled after metadata and the client
timed out. Verified via the native simulator integration test.
Move default frequency (slot) override from RegionProfile to RegionInfo (set per-region).
This is usually set to `0`, but will be especially useful for Ham modes where each region default must fit within a band plan.
Co-authored-by: Copilot <copilot@github.com>
* Enable Lite and Narrow regions and introduce getEffectiveDutyCycle for Lite profiles
* Add TrafficType enum and extend getConfiguredOrDefaultMsScaled to manage based on regionProfile settings
* Refactor telemetry modules to include TrafficType in getConfiguredOrDefaultMsScaled calls
* Update submodule protobufs to latest commit
* Add support for new region presets and modem presets in menu options
* Add new LoRa region codes and modem presets for EU bands
* boof
* Add modem presets for LITE and NARROW configurations
* Update subproject commit reference in protobufs
* Update protobufs
* Refactor modem preset definitions to use macro for consistency and clarity
* Refactor modem preset cases to use PRESET macro for consistency
* fix: update LoRa region code for EU 868 narrowband configuration
Co-authored-by: Copilot <copilot@github.com>
* Fix test suite failure
Co-authored-by: Copilot <copilot@github.com>
* Add override slot override - for when one override isn't enough.
Co-authored-by: Copilot <copilot@github.com>
* address copilot comments
---------
Co-authored-by: Copilot <copilot@github.com>
* LR2021 radio on NRF_Promicro
Co-authored-by: Copilot <copilot@github.com>
* Refactor LR2021 interface includes and conditional compilation for improved clarity
Co-authored-by: Copilot <copilot@github.com>
* Refactor LR20x0 interface: remove unused includes and update comments for clarity
* Fix LR2021 max power definitions and add radio type detection tests
* remove potato radio type detection tests
* Include placeholder for DCDC - currently requires godmode
* Added godmode features - not enabled by default
---------
Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
* Add powerlimits to reconfigured radio settings as well as init settings.
* Refactor preamble length handling for wide LoRa configurations
* Moved the preamble setting to the main class and made the references static
* Enhance LoRa configuration with modem presets and validation logic
* Rename bootstrapLoRaConfigFromPreset tests to validateModemConfig for clarity and consistency
* additional tidy-ups to the validateModemConfig - still fundamentally broken at this point
* Enhance region validation by adding numPresets to RegionInfo and implementing validateRegionConfig in RadioInterface
* Add validation for modem configuration in applyModemConfig
* Fix region unset handling and improve modem config validation in handleSetConfig
* Refactor LoRa configuration validation methods and introduce clamping method for invalid settings
* Update handleSetConfig to use fromOthers parameter to either correct or reject invalid settings
* Fix some of the copilot review comments for LoRa configuration validation and clamping methods; add tests for region and preset handling
* Redid the slot default checking and calculation. Should resolve the outstanding comments.
* Add bandwidth calculation for LoRa modem preset fallback in clampConfigLora
* Remove unused preset name variable in validateConfigLora and fix default frequency slot check in applyModemConfig
* update tests for region handling
* Got the synthetic colleague to add mock service for testing
* Flash savings... hopefully
* Refactor modem preset handling to use sentinel values and improve default preset retrieval
* Refactor region handling to use profile structures for modem presets and channel calculations
* added comments for clarity on parameters
* Add shadow table tests and validateConfigLora enhancements for region presets
* Add isFromUs tests for preset validation in AdminModule
* Respond to copilot github review
* address copilot comments
* address null poointers
* fix build errors
* Fix the fix, undo the silly suggestions from synthetic reviewer.
* we all float here
* Fix include path for AdminModule in test_main.cpp
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* More suggestion fixes
* admin module merge conflicts
* admin module fixes from merge hell
* fix: initialize default frequency slot and custom channel name; update LNA mode handling
* save some bytes...
* fix: simplify error logging for bandwidth checks in LoRa configuration
* Update src/mesh/MeshRadio.h
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* refactor: update throttling factor calculation and add unit tests for scaling behavior
* refactor: adjust throttling factor calculation for improved accuracy in different configurations
* Update src/mesh/Default.h
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* Update src/mesh/Default.h
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* refactor: enhance throttling factor calculation and introduce pow_of_2 utility function
* refactor: improve expected ms calculation in unit tests for Default::getConfiguredOrDefaultMsScaled
* refactor: improve scaling logic for routers and sensors in computeExpectedMs function
---------
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* simplify the observer pattern, since all the called functions are const getters.
* use arduino macro over std: for numerical values and refactor local variables in drawScrollbar()
* oh, so Cppcheck actually complained about const pointers not being const.
* slowly getting out of ifdef hell
* fix inkHUD warnings as well
* last 2 check warnings
* git checks should fail on low defects from now on
I thought git would be smart enough to understand all the whitespace changes but even with all the flags I know to make it ignore theses it still blows up if there are identical changes on both sides.
I have a solution but it require creating a new commit at the merge base for each conflicting PR and merging it into develop.
I don't think blowing up all PRs is worth for now, maybe if we can coordinate this for V3 let's say.
This reverts commit 0d11331d18.