Commit Graph
2773 Commits
Author SHA1 Message Date
Benjamin Faershtein d6b12ea3f1 feat(security): enforce packet authenticity policies 2026-07-09 17:48:55 -07:00
Ben Meadors 515fe8b94f Clamp bw for malformed use_preset false with empty bandwidth 2026-07-09 10:30:11 -05:00
Andrew YongandGitHub 0ae44d7017 fix(router): null-check packetPool/clientNotificationPool allocations (#10948)
On platforms where these pools are heap-backed (MemoryDynamic - used
whenever there isn't enough static RAM for a fixed pool, e.g.
ARCH_STM32WL or BOARD_HAS_PSRAM), allocCopy()/allocZeroed() return
nullptr on allocation failure and already log a warning, but most
callers dereferenced the result unconditionally. Under real heap
pressure this reliably produced a HardFault - reproduced on STM32WL
hardware under sustained mesh traffic, including the RX entry point
(RadioLibInterface::handleReceiveInterrupt) where every received
packet is allocated.

Adds null checks at all call sites that were missing one, mirroring
the guard pattern already used correctly elsewhere in the same files
(e.g. RadioInterface.cpp's sendErrorNotification). On allocation
failure, callers now skip the send/retransmission/notification and
log nothing further (the allocator already did) rather than crash.

Assisted-by: Claude Sonnet 5 <noreply@anthropic.com>

Signed-off-by: Andrew Yong <me@ndoo.sg>
2026-07-09 04:36:36 -05:00
Thomas GöttgensandGitHub 38074f584f Fix SENSOR power saving deep sleep truncating TX and skipping sleep on failed reads (#10939)
* Fix SENSOR power saving deep sleep behavior

Deep sleep could be entered while a telemetry packet was still queued
or on air, truncating the transmission. canSleep() gained a deepSleep
parameter and now vetoes in that case; light sleep is unchanged.
Telemetry modules defer a pending deep sleep (bounded to 30s) until the
radio is idle and no longer let the sensor polling interval override
the 5s pre-sleep grace period. A failed sensor read still arms deep
sleep instead of leaving the node awake for a full telemetry interval.

Fixes #10890
Fixes #10932

* Deduplicate telemetry deep sleep deferral logic

Move the radio-busy deferral and its counter into BaseTelemetryModule
and add an isPowerSavingSensor() helper. Removes the telemetry-specific
counter from OSThread. The sleep arming block stays per module because
it needs protected OSThread members not visible to the base class.
2026-07-08 07:30:46 -05:00
Ben MeadorsandGitHub 8d20606203 feat(config): make position & telemetry broadcast opt-in (#10929)
Position sharing was opt-out (the default primary channel shipped
position_precision=13) while device telemetry was already opt-in, and a
normal firmware upgrade preserves saved config, so existing nodes stayed
position-on. This makes both broadcasts opt-in, both on a fresh flash and via
a one-time migration for upgrading nodes.

- Fresh default: initDefaultChannel now sets position_precision=0.
- One-time migration in loadFromDisk, gated on a dedicated
  POSITION_TELEMETRY_OPTIN_VER (26) watermark kept separate from
  DEVICESTATE_CUR_VER (bumping that would re-run the NodeDatabase v24
  legacy decoder on already-migrated v25 DBs): disables position broadcast
  on PUBLIC/default-PSK channels and forces every device-telemetry
  mesh-broadcast flag plus the MQTT map-report location off.
- Private-PSK channels are left untouched: a channel with a custom key is a
  deliberate trusted-group setup, so its configured precision is preserved.
- Idempotent: ordinary saves never re-stamp .version, so a user who
  re-enables sharing is not re-disabled on the next boot.
- Added pure channelFileUsesPublicKey() (operates on the raw ChannelFile,
  since the channels singleton isn't initialized during loadFromDisk) and
  refactored Channels::usesPublicKey() to delegate to it.
- New test/test_optin_migration native suite (14 cases).
2026-07-07 14:58:58 -05:00
d16ae2b098 Rename power.h to Power.h for casing consistency with Power.cpp (#10919)
Every other .cpp/.h pair in src/ (350 of 351) uses identical
capitalization between the two files. Power.cpp/power.h was the sole
outlier; this aligns it with the rest of the codebase. No functional
change — all includes already resolved this file the same way on
case-sensitive filesystems.

Assisted-by: Claude Sonnet 5 <noreply@anthropic.com>

Signed-off-by: Andrew Yong <me@ndoo.sg>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2026-07-07 10:15:01 -05:00
Ben MeadorsandGitHub 97302f1845 Add Meshnology W10 AIOT Dev Kit (SX1262 via MCP23017 I2C expander) (#10911)
* 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.
2026-07-07 09:58:54 -05:00
Ben Meadors f250cf3cdc Update protos 2026-07-07 06:34:22 -05:00
Ben MeadorsandGitHub 12b2d973a4 Add central memory-class ladder (MemClass.h) with fail-safe-small defaults (#10901)
* Right-size nRF52 heap tiers after 2.8.0 heap-exhaustion field reports

Field reports on 2.8.0 show nRF52840 devices at 99% heap (114/115 KB)
within minutes of boot; operator new asserts on OOM, so these devices
are one allocation from a reboot. The 2.8.0 cache sizing ladders gave
nRF52 the largest non-PSRAM tiers on the assumption that a BLE-only
part has a roomy heap - the arena is actually ~125 KB shared with the
FreeRTOS task stacks.

Per-target retiers (nRF52840 unless noted):
- Traffic Management cache 1000 -> 250 entries (10 KB -> 2.5 KB); the
  unclassified fallthrough drops 1000 -> 400 to match the classic-ESP32
  tier (also affects RP2040/RP2350)
- Warm node store 200 -> 100 entries (8 KB -> 4 KB); the non-XXAA
  fallthrough drops 320 -> 100 so an unclassified RAM-constrained part
  can't boot-allocate 12.8 KB
- MESSAGE_HISTORY_LIMIT 20 -> 10 (text pool 4.4 KB -> 2.2 KB), the tier
  classic ESP32 already ships
- MAX_RX_TOPHONE 32 -> 16, shrinking the static packet pool 70 -> 54
  slots (~6.6 KB of .bss returned to the heap arena)
- PacketHistory hash index off arch-wide (1 KB); O(n) over 240 records
  is negligible at LoRa packet rates
- OLEDDISPLAY_REDUCE_MEMORY arch-wide (~1 KB OLED back buffer); the five
  TFT variants -U it because TFTDisplay.cpp needs buffer_back for
  dirty-window diffing
- Drop the stale "for testing" 1024-entry TMM override on T1000-E

Measured on rak4631: heap arena grows 124,572 -> 131,180 B and boot
allocations drop ~15.7 KB, roughly +22 KB free heap on the field-report
device class.

Migration: the nRF52840 warm flash ring replays through place() (LRU),
so the newest 100 identities survive the shrink; the file backend
rejects oversized snapshots cleanly (new test covers this). Native
suites pass (536/536 Docker, 13/13 native-macos warm store); rak4631,
heltec-mesh-node-t114 (TFT) and tracker-t1000-e build green.

* Add central memory-class ladder (MemClass.h) with fail-safe-small defaults

The 2.8.0 nRF52840 heap exhaustion happened because each RAM-sized cache
picked its per-platform tier from its own chip #ifdef ladder, and every
ladder's fallthrough default was its largest non-PSRAM tier - nRF52 was
never named, so it silently got 1000-entry caches on a ~115 KB arena.

This introduces src/memory/MemClass.h: a single MESHTASTIC_MEM_CLASS
(TINY / SMALL / MEDIUM / LARGE) ranked by usable app heap after platform
overheads, with the deliberate property that an unclassified chip lands
in SMALL - a new target boots with small caches until someone opts it up
in one visible place.

The TMM cache, warm store, MAX_RX_TOPHONE and MAX_SATELLITE_NODES
ladders in mesh-pb-constants.h now key off the class; branches pinned by
something other than RAM stay explicit and say why (nRF52840's SoftDevice
arena, RP2040's warm.dat watchdog bound). MAX_NUM_NODES intentionally
stays separate - it is flash-shaped (nodes.proto vs LittleFS), not
heap-shaped.

A per-class MESHTASTIC_BOOT_CACHE_BUDGET static_assert now covers the
three big boot-allocated caches, so the next cache-adding PR that would
blow a small platform's budget fails to compile instead of exhausting
heap in the field.

No values change for any existing target: rak4631, tbeam, rak11310 and
wio-e5 build byte-identical before/after; all ladders remain
#ifndef-guarded so variant overrides keep working.

* Address review: fix RP2350 class-table doc, add PacketRecord static_assert

- MemClass.h's class table claimed RP2350 was MEDIUM while the mapping
  ladder classifies it SMALL (with RP2040) - the table now matches the
  ladder, with a note that RP2350 is a MEDIUM candidate whenever someone
  wants to tune it up (kept SMALL here so this header stays a behavioral
  no-op).
- The boot-cache budget comment referenced a static_assert pinning
  PacketHistory::PacketRecord at 20 B that did not exist (only a layout
  comment). Add the real static_assert so the budget math in
  mesh-pb-constants.h fails to compile if the record layout changes.

Also merges develop (the base #10898 landed there as a squash, which is
what made this stacked branch conflict); develop's mesh-pb-constants.h
is byte-identical to this branch's base, so the resolution keeps the
MemClass ladder unchanged. rak4631 and wio-e5 build green;
test_packet_history 47/47.

* Address review: share PACKETHISTORY_MAX, trim policy comments

- Hoist PACKETHISTORY_MAX from PacketHistory.cpp into
  mesh-pb-constants.h (next to the MAX_NUM_NODES it derives from) so the
  constructor clamp and the boot-cache budget static_assert use one
  definition instead of hand-mirrored arithmetic that could drift. The
  expression stays valid where MAX_NUM_NODES resolves at runtime
  (ESP32-S3, portduino); the pointless 2.0 double math becomes integer.
- Trim the MemClass.h header (36 -> 16 comment lines) and the budget /
  sizing-policy comments per the repo comment-length guideline, keeping
  the class table, the fail-safe-small rule, the override mechanism, and
  the include-order constraint.

rak4631 (compile-time MAX_NUM_NODES) and heltec-v3 (runtime) build
green; test_packet_history 47/47.
2026-07-06 14:51:41 -05:00
d846780a9b More fuzz tests and small fixes for the findings (#10864)
* 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>
2026-07-06 13:19:01 -05:00
Ben MeadorsandGitHub 6c7ee8afc7 Add MemAudit: per-subsystem heap accounting in the boot log (#10900)
* Add MemAudit: per-subsystem heap accounting in the boot log

The 2.8.0 nRF52840 heap-exhaustion field reports had to be diagnosed by
hand, reconstructing each subsystem's heap footprint from source and
build flags one report at a time. This makes every future report
self-diagnosing from the serial log: a tiny fixed-size registry
(src/memory/MemAudit.*) that big long-lived allocations report into,
printed as one line at the end of setup() and alongside the periodic
"Heap free:" log:

  MemAudit[boot]: tmm=2500 warm=4000 pkthist=5824 nodedb=13440
  msgstore=2200 pktpool(live)=3270 total=31234

Instrumented: NodeDB hot vector (nodedb) + satellite maps (satmaps,
rb-tree overhead estimated), WarmNodeStore (warm), PacketHistory records
and hash index (pkthist), TrafficManagement caches (tmm/tmm_ni),
MessageStore text pool (msgstore), TFT line/repaint buffers (display),
and live in-flight packets (pktpool(live)) via an optional audit tag on
the packet pool allocator - the one hot path, counted with a relaxed
32-bit atomic add (single instructions on Cortex-M, no locks).

Cost: 128 B RAM for the 16-tag table, well under 1 KB flash on rak4631.
MESHTASTIC_MEM_AUDIT=0 compiles it out to inline no-op stubs (call
sites need no ifdefs); STM32WL, the tightest flash target, defaults off.

New native suite test_mem_audit covers add/set/snapshot arithmetic, tag
reuse (pointer and cross-TU strcmp fallback), null/unknown tags, and
table-full behavior; test/native-suite-count bumped to 28.

* native-wasm: add src/memory/ to the curated source filter

The wasm env denies all sources and adds an explicit file list; MemAudit
callers (main, MeshService, NodeDB, PacketHistory) are in that list but
src/memory/MemAudit.cpp was not, so wasm-ld failed on undefined
memaudit:: symbols.
2026-07-06 13:18:49 -05:00
Ben MeadorsandGitHub ed03a69555 Right-size nRF52 heap tiers after 2.8.0 heap-exhaustion field reports (#10898)
* Right-size nRF52 heap tiers after 2.8.0 heap-exhaustion field reports

Field reports on 2.8.0 show nRF52840 devices at 99% heap (114/115 KB)
within minutes of boot; operator new asserts on OOM, so these devices
are one allocation from a reboot. The 2.8.0 cache sizing ladders gave
nRF52 the largest non-PSRAM tiers on the assumption that a BLE-only
part has a roomy heap - the arena is actually ~125 KB shared with the
FreeRTOS task stacks.

Per-target retiers (nRF52840 unless noted):
- Traffic Management cache 1000 -> 250 entries (10 KB -> 2.5 KB); the
  unclassified fallthrough drops 1000 -> 400 to match the classic-ESP32
  tier (also affects RP2040/RP2350)
- Warm node store 200 -> 100 entries (8 KB -> 4 KB); the non-XXAA
  fallthrough drops 320 -> 100 so an unclassified RAM-constrained part
  can't boot-allocate 12.8 KB
- MESSAGE_HISTORY_LIMIT 20 -> 10 (text pool 4.4 KB -> 2.2 KB), the tier
  classic ESP32 already ships
- MAX_RX_TOPHONE 32 -> 16, shrinking the static packet pool 70 -> 54
  slots (~6.6 KB of .bss returned to the heap arena)
- PacketHistory hash index off arch-wide (1 KB); O(n) over 240 records
  is negligible at LoRa packet rates
- OLEDDISPLAY_REDUCE_MEMORY arch-wide (~1 KB OLED back buffer); the five
  TFT variants -U it because TFTDisplay.cpp needs buffer_back for
  dirty-window diffing
- Drop the stale "for testing" 1024-entry TMM override on T1000-E

Measured on rak4631: heap arena grows 124,572 -> 131,180 B and boot
allocations drop ~15.7 KB, roughly +22 KB free heap on the field-report
device class.

Migration: the nRF52840 warm flash ring replays through place() (LRU),
so the newest 100 identities survive the shrink; the file backend
rejects oversized snapshots cleanly (new test covers this). Native
suites pass (536/536 Docker, 13/13 native-macos warm store); rak4631,
heltec-mesh-node-t114 (TFT) and tracker-t1000-e build green.

* Drop stale OLEDDISPLAY_REDUCE_MEMORY -U on t114 / mesh-solar-tft

Only USE_TFTDISPLAY variants (t1, t096, wismeshtap) compile
TFTDisplay.cpp and need the lib's buffer_back; t114 and mesh-solar-tft
render through the meshtastic-st7789 driver, which handles the
reduced-memory configuration fine - as #10894 (merged from develop)
already established by defining the flag there. Remove the -U guard and
the per-variant -D (redundant with the arch-wide define in nrf52_base
on this branch). Both variants verified building.
2026-07-06 13:18:25 -05:00
1e66ecff60 Fix Ethernet MQTT reconnect handling (#10896)
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2026-07-06 10:21:24 -05:00
Jonathan BennettandGitHub fe6bf91343 Add tx_power to userprefs (#10889) 2026-07-05 16:04:04 -05:00
TomandGitHub c8f2ea16f0 ugh (#10874) 2026-07-03 18:57:42 -05:00
AustinandGitHub 397ff8523a Add 70cm amateur radio region support (#10627) 2026-07-03 10:00:14 +02:00
TomandGitHub fc7043f117 second pass adding beacons admin (#10839)
* second pass adding beacons admin

* Bump protobufs submodule to merged MESHBEACON_CONFIG (PR #970)

clod helped.

* Gate MeshBeacon module config behind admin auth in PhoneAPI

The FromRadio module-config sync copied moduleConfig.mesh_beacon unconditionally. MeshBeaconConfig embeds two ChannelSettings (broadcast_offer_channel, broadcast_on_channel) that carry PSKs, so an unauthorized client could exfiltrate channel PSKs when MESHTASTIC_PHONEAPI_ACCESS_CONTROL is enabled - bypassing the redaction already applied to mqtt/network/security config.
Gate the payload copy on getAdminAuthorized(), mirroring the mqtt case; unauth clients now receive an empty MeshBeaconConfig.

clod helped
2026-07-02 23:06:33 +01:00
Ben MeadorsandGitHub b4dd76a4db Harden against crafted-packet crashes + adversarial fuzzing (#10862)
Audit and fuzzing of the RF-packet decode -> dispatch -> display/phone paths for
the "crash a node or phone with a crafted packet" surface, beyond the XEdDSA
authenticity work.

Crash fixes (reproduced under AddressSanitizer / UBSan):
- GeoCoord::latLongToUTM/latLongToMGRS read fixed letter tables out of bounds on
  extreme latitude_i/longitude_i from a received Position, and narrowed
  out-of-range easting/northing doubles to unsigned (float-cast-overflow UB).
  Clamp the UTM zone, the easting/northing narrowing, and the band/col/row
  indices. Regression: test_geocoord_extreme_coords_no_oob.
- EnvironmentTelemetry/AirQualityTelemetry render attacker floats via
  String(float), which on nRF52/RP2040/STM32/portduino formats into a fixed
  char[33] (dtostrf) and overflows near FLT_MAX. Clamp the rendered metrics via
  UnitConversions::displaySafeFloat (finite + magnitude <= 1e9), unit-tested in
  test_type_conversions.

Defense-in-depth + robustness:
- TraceRouteModule::printRoute: fix an snr_back[-1] OOB read (wrong count in the
  guard) and stop formatting the INT8_MIN "unknown SNR" sentinel as a dB value.
- WaypointModule/NodeDB: sanitize untrusted strings before the OLED renderer and
  the phone-facing ClientNotification (belt-and-suspenders vs PB_VALIDATE_UTF8).
- MeshService::sendToPhone: withhold NODEINFO/WAYPOINT packets whose nested string
  won't cleanly decode, protecting strict phone protobuf decoders without
  affecting mesh relay.

Tests: new test_fuzz_decode (protobuf decode + UTF-8 sanitizer fuzz) and
test_fuzz_packets (perhapsDecode / module-handler / traceroute / phone-gate fuzz),
all under AddressSanitizer; native-suite-count 25 -> 27. Full suite 515/515 green.
2026-07-02 16:50:49 -05:00
Ben MeadorsandGitHub 0e84c1a827 Harden XEdDSA unsigned-packet policy and add coverage (#10858)
Audit of the XEdDSA packet-signing implementation (#10478) surfaced several
issues in when unsigned packets are accepted on receive or emitted on send.
This fixes them and adds regression coverage.

- Unicast NodeInfo exchange no longer breaks against signer nodes: the
  NodeInfoModule downgrade drop is gated to broadcasts, since senders never
  sign unicast (want_response replies, directed exchanges).
- Replace the payload-size sign heuristic with an exact encoded-size gate
  (signedDataFits) and mirror it on the receive side, removing a dead band
  where 167-168 B broadcasts were signed then failed TOO_LARGE.
- Extract the receive policy into checkXeddsaReceivePolicy() and apply it to
  plaintext-MQTT decoded downlink, which previously skipped signature
  verification and downgrade protection entirely.
- Reject signatures whose length is neither 0 nor 64 as malformed, so a
  crafted partial signature can't inflate the size estimate and dodge the
  unsigned-downgrade drop.
- Hold cryptLock on the MQTT verify path (shared Ed25519 key cache).
- Clear any client-preset signature on packets we originate, on all builds.
- Randomized (hedged) signing per the Signal XEdDSA spec: bump the
  meshtastic/Crypto pin to the build where XEdDSA::sign mixes 32 bytes of
  caller randomness into the nonce as Z (meshtastic/Crypto#3), and seed those
  bytes in xeddsa_sign from HardwareRNG (checked, with a seeded-CSPRNG
  fallback). test_crypto pins that repeated signs differ and both verify.

Adds test coverage: test_packet_signing groups A-E (receive matrix, send
policy, NodeInfo backstop, encoding invariants, decoded-ingress policy),
test_mqtt end-to-end downlink cases, and a test_crypto randomization check.
2026-07-02 11:48:58 -05:00
Ethac.chenandGitHub 4e36f4271e fix(tap_v2): disable Bluetooth by default on TFT device (#10857)
TAP V2 has a TFT screen and belongs to the same device category as
T-Deck / T-Watch S3 / HELTEC_V4_TFT -- Bluetooth should default to OFF
to avoid unwanted BLE connections and reduce power consumption. Users
can re-enable BT via the TFT settings menu when needed.
2026-07-02 08:01:53 -05:00
TomandGitHub 3becaf2d95 emdashes begone (#10847) 2026-07-01 19:01:27 -05:00
dee94e0758 Fix SX126x daemon abort when Tx power exceeds chip limit (#10711)
* Fix SX126x daemon abort when tx power exceeds chip limit

reconfigure() asserted on setOutputPower() failure. `power` is derived from
operator-settable config (tx_power, SX126X_MAX_POWER); a value above the SX126x
RadioLib limit (+22 dBm) returns RADIOLIB_ERR_INVALID_OUTPUT_POWER. The assert
aborts meshtasticd mid-reconfigure -- before reloadConfig() persists the change --
so the offending config silently reverts and the daemon crash-loops. Record a
critical error and continue instead, matching the adjacent setFrequency() handling.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Address Copilot review: per-chip power limit, log requested power

- Comment no longer states a universal "+22 dBm for SX126x"; notes the
  limit is the driver's max (e.g. +22 on SX1262/SX1268, lower on SX1261).
- Log the rejected `power` value and that the previous Tx power is retained,
  so the invalid setting and the resulting config/radio drift are visible.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Address CodeRabbit nit: trim inline comment to repo guideline

Collapse the setOutputPower failure rationale from six lines to two,
per the repo's "one or two lines at most" comment guideline. The full
incident history lives in the PR and issue #10710.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Convert pre-existing em-dashes to ASCII (trunk ascii-dash)

Two em-dashes in this file (from bbcc35e) fail the repo's ascii-dash
formatter once the file is in a PR's modified set. `trunk fmt` rewrites
them to ASCII hyphens; applied here so the Trunk Check passes. No
functional change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2026-07-01 19:01:01 -05:00
ManuelandGitHub 18d64bf544 replace EVENT_CONTACT_FROM_PHONE (#10840) 2026-07-01 16:04:50 -05:00
e64d20548c feat(eth): port mesh/http to RP2350 + W5500 (HTTP/80 + HTTPS/443) (#10573)
* add watchdog to rp2xx0

* feat(eth-api): phase 0 skeleton — HTTP API server on TCP/80 (503 only)

First step of porting mesh/http/ to RP2350 + W5500 (today ESP32-only).
Phase 0 stands up the listener over the existing arduino-libraries/Ethernet
stack (no Mongoose — its built-in TCP/IP would conflict with EthernetServer
and break OTA/MQTT/NTP). Skeleton parses request line, logs it, replies 503.
Real handlers (/api/v1/{info,fromradio,toradio}) come in later phases.

- New mesh/eth/ethApiServer.{h,cpp} gated by HAS_ETHERNET_API
- Wired into ethClient init+loop in parallel with existing ethHttpOTA (port 4244)
- Enabled in both wiznet_5500_evb_pico2_e22p and pico2_w5500_e22 variants
- Build impact on wiznet: +~9 KB flash (63.0% used), <1 KB RAM

* feat(eth-api): phase 1 — implement /api/v1/{fromradio,toradio} with PhoneAPI

Replace phase 0 skeleton with the real handlers that bridge the HTTP transport
to PhoneAPI, mirroring mesh/http/ContentHandler (ESP32) semantics.

- EthHttpAPI : public PhoneAPI (api_type=TYPE_HTTP, checkIsConnected=true),
  outside the MESHTASTIC_EXCLUDE_WEBSERVER gate so it builds on RP2350.
- Minimal HTTP parser: method/path/query/Content-Length, no allocations beyond
  Arduino String, capped at 32 header lines x 256 bytes (anti-DoS).
- OPTIONS preflight -> 204 with CORS + X-Protobuf-Schema.
- GET /api/v1/fromradio[?all=true]: stream-write up to 64 protobufs from
  webAPI.getFromRadio(), Connection: close framing (HTTP/1.0 style).
- PUT /api/v1/toradio: read Content-Length bytes (<=512), call
  webAPI.handleToRadio(), echo body back.
- 404 default for unknown paths, 405 for wrong method, 400 for bad body.

Validated e2e against wiznet @ 192.168.1.143:
- OPTIONS /api/v1/fromradio -> 204 + correct CORS headers
- PUT ToRadio{want_config_id=1} (2 bytes) -> 200 + echo
- GET /api/v1/fromradio?all=true -> 200 + 3476 bytes (config+channels+nodeinfo)
- GET /api/v1/nonexistent -> 404 unknown endpoint
- OTA HTTP on port 4244 untouched and still responsive

Build impact on wiznet_5500_evb_pico2_e22p: +2 KB flash (63.1%), +1.8 KB RAM
(17.6%) vs phase 0.

* fix(eth-api): move accept loop to dedicated OSThread (20ms tick)

The API server was being polled from the Ethernet client Periodic which runs
every 5s. Measured impact before this fix on wiznet @ 192.168.1.143:

  req 1: ttfb=6.458s total=6.509s    (matches the 5s tick + handler overhead)
  req 2: connection refused          (W5500 sockets exhausted)
  req 3: connection refused

After: same hardware, same network, back-to-back requests:

  req 2: ttfb=0.287s
  req 3: ttfb=0.015s
  req 4: ttfb=0.022s
  req 5: ttfb=0.020s

The web client (meshtastic/web served from localhost) was visibly stalling
mid-handshake — it had pulled 109 nodes + 19 messages but channels and device
info never arrived. With the periodic-only polling, every request takes 5s
and the W5500's 4 hardware sockets fill up under the burst.

EthApiServerThread mirrors the WebServerThread pattern from ESP32
mesh/http/WebServer.cpp: adaptive interval — 20ms when there's recent
traffic, 100ms after 5s of idle, 500ms after 30s. Auto-registers with the
OSThread scheduler on construction.

ethHttpOTA still ticks from the periodic; left unchanged because OTA is one
large transfer that tolerates the latency, and minimizing scope here to one
behaviour change at a time.

* refactor(eth-api): extract handlers to shared module via IStreamReadWrite

Phase 2.0 of the TLS port — separate transport from request handling so
the upcoming HTTPS server can reuse the exact same parser + routing logic.

- New ethApiHandlers.{h,cpp}: Request, parser, CORS helpers, fromradio/toradio
  handlers, EthHttpAPI : PhoneAPI subclass. All driven by a single
  IStreamReadWrite interface that inherits Print (so client.print(...) keeps
  working transparently).
- ethApiServer.cpp slimmed to ~90 LOC: now just the EthernetServer(80) +
  OSThread + an EthernetClientStream adapter that forwards reads/writes to
  the underlying EthernetClient. No behaviour change.

Validated on wiznet @ 192.168.1.143: 5 curl requests TTFB 18-110ms (same
as pre-refactor), protobuf round-trip PUT 200 + GET ?all=true 3499 bytes.
Flash impact: +200 bytes (63.2%); RAM unchanged (17.6%).

* feat(eth-tls): ECDSA P-256 self-signed cert generation for HTTPS API

mbedTLS-based cert generation module that produces a SAN=IP self-signed
ECDSA P-256 server certificate, persisted under LittleFS so subsequent
boots reuse the same key. Generation runs once on a dedicated OSThread
so the ECDSA keygen path (~430 ms) never blocks the Periodic stack or
the Ethernet reconnect loop.

  - mbedTLS 3.6.2 sources compiled in via scripts/add_mbedtls_sources.py
    (BuildSources of pico-sdk/lib/mbedtls/library/*.c, all 108 files).
    MBEDTLS_USER_CONFIG_FILE injected as CPPDEFINES tuple in the script —
    build_flags shell escape mangles the embedded quotes on Windows.
  - src/mbedtls_user_config.h undefs MBEDTLS_HAVE_TIME, HAVE_TIME_DATE,
    TIMING_C, NET_C, FS_IO, PSA_ITS_FILE_C, PSA_CRYPTO_STORAGE_C, and
    defines MBEDTLS_NO_PLATFORM_ENTROPY (entropy_poll.c uses a raw
    platform check, not gated by a flag).
  - src/mesh/eth/ethCert.{h,cpp}: ECDSA P-256 keypair + cert with
    SAN(IP=current) using pico-sdk get_rand_64() directly as f_rng,
    bypassing mbedtls_entropy. DER buffers heap-allocated to keep the
    OSThread stack within budget. Cert + key + ip persisted under
    /prefs/eth_*.der so subsequent boots reuse the same identity.
  - Gated by HAS_ETHERNET_TLS_API. Standalone phase: generation only;
    HTTPS server (TCP/443) wired up in the follow-up commit.

* feat(eth-tls-api): phase 2.2 — HTTPS server on TCP/443 reusing handlers

Brings up an mbedTLS server on port 443 that defers to the ECDSA P-256
self-signed cert produced by [[ethCert]]. Reuses the HTTP request /
response handlers from [[ethApiHandlers]] via the IStreamReadWrite
interface — there is exactly one code path for routing / CORS / PhoneAPI
integration regardless of whether the transport is plain TCP or TLS.

EthTlsApiServerThread (OSThread):
  - Phase A: poll isEthCertReady() every 500 ms while the cert worker
    runs. Once true, parse cert chain + key, build ssl_config (TLS server,
    stream transport, default preset, VERIFY_NONE since we are the cert
    issuer), install own cert, run ssl_setup, bind tlsServer on 443.
  - Phase B: standard adaptive accept loop (20 / 100 / 500 ms tick),
    identical to the plain-HTTP server.

Per-connection flow:
  1. session_reset on the static ssl context (1 in-flight session — multi-
     session pool is Phase 3 if needed)
  2. set_bio routes mbedtls I/O to two C callbacks (netSend / netRecv)
     that bridge to the live EthernetClient via the void* ctx
  3. handshake loop (sync, blocking, with 10 s recv timeout) — mbedTLS
     errors are logged with mbedtls_strerror
  4. wrap (ssl, client) in MbedTlsStream → handleApiClient(stream)
  5. close_notify + client.stop

Stack budget continues the Phase 2.1-bis discipline: every large buffer
(ssl context, cert chain, pk_key, ssl_config) lives in BSS as a static
global. The OSThread stack only holds the per-connection EthernetClient
adapter and small mbedtls return codes.

Validated on wiznet 192.168.1.143:
  - cert load-from-FS path: 13 ms (regen skipped on second boot)
  - cert gen path: 290 ms (first boot only)
  - ssl_setup chain: parse cert, parse key, ssl_config_defaults,
    conf_own_cert, ssl_setup all return 0
  - end-to-end: curl -k https://192.168.1.143/api/v1/fromradio → 200 OK
    with application/x-protobuf, CORS, X-Protobuf-Schema headers, server
    initiates close_notify cleanly

Footprint vs 2.0:
  - flash 70.5% → 87.3% (+220 KB for mbedtls_ssl + x509 server-side code)
  - RAM static 17.6% → 19.8% (+11 KB BSS for ssl_context + cert chain)
  - heap at runtime adds ~32 KB per active session (mbedtls in/out
    record buffers, default MBEDTLS_SSL_*_CONTENT_LEN=16384)

Next: validate from Firefox direct (https://192.168.1.143 → warning
accept → JSON visible), then from client.meshtastic.org hosted to
confirm the mixed-content block is gone.

* fix(eth-tls): add KeyUsage + EKU serverAuth and cap TLS 1.2 for browser compat

Two browser-compat fixes that surfaced in Firefox validation:

1. Cert v1 only had Basic Constraints + SAN. NSS / Firefox refuse to
   treat a cert as a TLS server cert without an Extended Key Usage
   extension naming id-kp-serverAuth since 2023 — the error surfaces as
   a non-overridable 'Secure Connection Failed' with no 'Accept the
   Risk' path. Add KeyUsage(digitalSignature + keyEncipherment, critical)
   and ExtendedKeyUsage(serverAuth, critical). Bump cert/key/ip file
   paths to '_v2' so live boards regenerate on next start instead of
   loading a v1 cert that the browser silently refuses.

2. pico-sdk mbedtls defines MBEDTLS_SSL_PROTO_TLS1_3 in its default
   config, but the server-side 1.3 plumbing in this vendored build is
   incomplete: Firefox and openssl-3.5's s_client default to 1.3 and
   the handshake dies 4 ms in with MBEDTLS_ERR_ERROR_GENERIC_ERROR
   (-0x0001). curl/SChannel happened to default to 1.2 so it masked the
   issue earlier. Cap min/max_tls_version to TLS 1.2 — clients downgrade
   transparently and we keep the ECDHE-ECDSA + AES-GCM / CHACHA20-POLY1305
   suites that already work end-to-end.

Validated on wiznet 192.168.1.143:
  - cert v2 dumps with the 4 extensions visible (openssl x509 -text):
    BasicConstraints CA:FALSE, KeyUsage(critical) digitalSignature +
    keyEncipherment, ExtendedKeyUsage(critical) serverAuth, SAN IP.
  - openssl s_client (no version flag): downgrades to TLS 1.2, handshake
    completes, verify_return=18 (self-signed, expected).
  - Firefox: warning self-signed -> Advanced -> Accept Risk -> handshake
    OK in 632 ms with CHACHA20-POLY1305, request reaches handleApiClient
    and returns the protobuf body.

* perf(eth-api): HTTP/1.1 keep-alive — one handshake per session, not per request

Before this change /fromradio used 'Connection: close' framing (HTTP/1.0
style with no Content-Length), forcing each poll to redo the full TLS
handshake. client.meshtastic.org needs ~80 sequential /fromradio polls
during initial sync (one per packet from the config-replay state
machine: MyInfo, channels, every Config_*, every ModuleConfig_*, every
NodeInfo), so the user-visible load time was ~50 s of pure handshake
overhead (80 requests * ~625 ms ECDSA P-256 each).

Three coordinated changes:

1. handleApiClient() now loops on the same connection until the peer
   closes or parseRequest hits its 3 s idle timeout. requestsServed
   counter keeps the 'bad/timeout request' debug log from firing on
   the natural idle close after a keep-alive sequence.

2. handleFromRadio() buffers all packets into a std::vector before
   writing, so it can emit a real Content-Length and 'Connection:
   keep-alive'. Buffer is dynamic — common 1-packet response only
   allocates ~256 B; ?all=true keeps the 64-packet cap which tops out
   around 16 KB. handleToRadio + sendPreflight switched to keep-alive
   too (they already had real Content-Length). sendError keeps close —
   errors are terminal.

3. ethTlsApiServer netRecv RECV_TIMEOUT_MS dropped from 10 s to 3 s so
   mbedtls_ssl_read can't outlast the handler's idle deadline (a quiet
   browser leaving the socket open would otherwise wedge the OSThread
   for 10 s past the natural close).

Measured on wiznet 192.168.1.143 against client.meshtastic.org:
  - one TLS handshake (641 ms, CHACHA20-POLY1305)
  - ~80 requests pipelined over the same session
  - full config + 40 NodeInfos in ~6-8 s (vs ~50 s before)
  - per-request latency post-handshake: ~10-25 ms
  - curl -kv with two URLs: 'Reusing existing https: connection',
    server returns 'Connection: keep-alive' explicitly.

* fix(eth-tls): watchdog + Chrome compat — handshake stability across browsers

Phase 3 keep-alive shipped a working Firefox flow but client.meshtastic.org
loops + Chrome 'Test Connection' both rebooted the board. Four distinct
issues; collectively they kept the OSThread inside serveClient() too long
or spin-looping without yielding, and pico-sdk mbedtls' TLS 1.3 code path
choked on Chrome's modern ClientHello.

1. Watchdog reset during keep-alive idle. Once a client drained the
   replay queue and entered 3 s poll mode, the OSThread sat inside
   netRecv()'s busy-wait waiting for the next request. Two consecutive
   3 s waits plus prior handler time crossed the 8 s RP2350 hardware
   watchdog. Pet the watchdog inside netRecv()'s poll loop (every 2 ms)
   so a quiet client can never starve the watchdog. Same fix in the
   ethApiHandlers per-request yield path.

2. Cap session at 64 requests + yield() between. Defense-in-depth: a
   pathological client can't monopolize serveClient indefinitely; after
   the cap it just re-handshakes (~625 ms), still vastly cheaper than
   the per-request handshake we had before keep-alive.

3. TLS 1.3 code compiled out of mbedtls entirely
   (#undef MBEDTLS_SSL_PROTO_TLS1_3 in mbedtls_user_config). Capping
   max_tls_version=TLS1_2 at runtime is enough for Firefox / openssl
   (they downgrade cleanly), but Chrome's ClientHello carries TLS 1.3
   extensions — post-quantum key shares, Encrypted ClientHello,
   etc. — that the vendored mbedtls 1.3 parser crashes on before the
   downgrade decision happens. Removing the 1.3 sources sidesteps the
   parser; ServerHello just announces TLS 1.2 and Chrome accepts.

4. netSend infinite WANT_WRITE spin. When W5500's TX buffer momentarily
   filled mid-handshake (Chrome draining slower than Firefox during
   ServerKeyExchange), EthernetClient::write() returned 0, our netSend
   returned MBEDTLS_ERR_SSL_WANT_WRITE without delay, mbedtls retried
   immediately, repeat at ~180k iter/sec until ... well, until the
   board's other threads got nothing done. Log signature: ret=-0x6880
   tight-looping in the handshake iter trace. Rewrite netSend to block
   with delay(2) + watchdog_update() and a 3 s timeout — same shape as
   netRecv. Return MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY on disconnect
   (was incorrectly returning WANT_WRITE).

Also added granular per-iter handshake logging gated on first 20 iters
+ every 50th after that, so any future regression localizes itself in
COM9 without RTT JLink.

Validated on wiznet 192.168.1.143:
  - Firefox: client.meshtastic.org full sync + idle poll stable (no
    reset during the previously-crashing 'replay drain complete' phase)
  - Chrome: 'Test Connection' accepts the cert prompt and connects
  - Edge: same as Chrome
  - openssl s_client default + tls1_2 forced: both negotiate TLS 1.2
    with ECDHE-ECDSA + AES-GCM / CHACHA20-POLY1305, verify=18 (self-
    signed, expected)

* chore(eth-tls): drop verbose debug logs from cert + TLS init paths

The cert pipeline + TLS context init had step-by-step LOG_INFOs and
ubiquitous Serial.flush() that were essential while diagnosing the
Phase 2.1-bis stack overflow, the Chrome handshake crash, and the
keep-alive watchdog reset. Once those bugs were fixed the logs just
clutter COM9 on every boot.

Kept on hand:
  - cert: 'loaded from FS', 'generating ...', 'generated N B in T ms',
    'persisted to LittleFS', plus all LOG_ERROR / LOG_WARN paths
  - tls: 'server listening on TCP port 443', 'client connected from',
    'handshake OK in N ms ciphersuite=', 'handshake failed -0xXXXX (...)',
    plus all init LOG_ERRORs

Dropped:
  - cert: 'step 1/8 pk_setup' through 'step 8/8 copy key DER', 'thread
    woke', 'pipeline OK' (now silent on success)
  - tls: 'cert is ready, initializing', 'parsing cert chain', 'parsing
    key', 'ssl_config_defaults', 'conf_own_cert', 'ssl_setup',
    'server worker scheduled', and the per-iter handshake trace
  - obsolete 'Optional mbedtls debug bridge' commented-out stub
  - all Serial.flush() that were added defensively for the
    debug-the-crash phase

Bin shrinks ~40 KB (logs + format strings). Validated on wiznet
192.168.1.143: HTTP 200 round-trip works post-flash, no regression.

* fix(eth): harden API/TLS build guards (review #10573)

- ethApiHandlers.{h,cpp}: gate on (HAS_ETHERNET_API || HAS_ETHERNET_TLS_API)
  so the shared handleApiClient/IStreamReadWrite still compile for an
  HTTPS-only variant (the TLS server depends on them).
- ethTlsApiServer.{cpp,h}, ethCert.{cpp,h}: require ARCH_RP2040 (they use Pico
  SDK get_rand_64()/watchdog_update()); the ethClient.cpp TLS include + call
  site are tightened to match, so enabling the flag on a non-RP2040 Ethernet
  target is a clean no-op instead of a cryptic build break.
- clang-format (16) the touched eth files to satisfy Trunk Check.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(eth): address review — listener/cert recovery + keep-alive framing

- Rebind HTTP/80 and HTTPS/443 after a W5500 chip reset. reconnectETH() now
  tears the API/TLS servers down (deInitEthApiServer/deInitEthTlsApiServer) so
  the restart path recreates them; the singleton guards previously left both
  bound to dead sockets until reboot. The workers are kept (only the listener
  is dropped) so nothing is deleted from another thread's runOnce.

- Stop the keep-alive loop after a handler error. /fromradio and /toradio
  return whether the connection may stay open; a 400/405/408 response
  (Connection: close) now ends the loop so unread body bytes can't be parsed
  as the next request.

- Validate the cached cert/key before trusting it (parse both DERs), and clear
  the IP commit-marker before rewriting the pair and write it last, so a reset
  mid-persist regenerates instead of handing the TLS server a truncated or
  mismatched pair (which would hard-disable HTTPS).

- Track a cert generation counter so the TLS server reloads and rebinds when a
  DHCP lease change regenerates the cert for a new IP (the SAN must follow, or
  browsers reject the new address). The cert worker is no longer one-shot.

- Silence the SCons F821/E402 lint on add_mbedtls_sources.py to match the other
  extra_scripts.

Validated on a W5500 board: a forced chip reset rebinds 80 and 443 without an
MCU reboot; a truncated cached cert regenerates and HTTPS recovers; a simulated
lease change reissues the cert with the new IP in the SAN.

* fix(eth): clear cert readiness on regen failure + verify cached pair

Follow-up to the review:
- Reset ready_/certIp_ when ensureCertForIp() fails, so isReady() no longer
  reports true with empty material — otherwise a later TLS reload fails
  initTlsContext() and stays disabled instead of retrying on the next poll.
- certKeyParse() now also checks the cached cert and key form a matching pair
  (mbedtls_pk_check_pair), rejecting an independently-parseable but mismatched
  cert/key (e.g. if the IP commit-marker survived a failed clear).

Validated on a W5500 board: a valid cached pair still loads from FS, and a
regenerated cert reloads cleanly.

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 05:51:14 -05:00
cfc2e457a9 Update protobufs (#10832)
Co-authored-by: caveman99 <25002+caveman99@users.noreply.github.com>
2026-07-01 09:33:20 +02:00
TomandGitHub fd62322876 Log tidy (#10798)
* clarify channel number formatting and fix the fixed-width padded nodeIDs

* use the explicit 0x%08x for packets and nodeIDs throughout

* LLM instructions
2026-06-30 12:44:25 -05:00
0cf6fe72fd fix self-nodeID change (#10822)
* fix se0fl-nodeID change

* address review: avoid redundant satellite erase + add braces in createNewIdentity

removeNodeByNum() already drops the satellite stores (and warm-tier copy) when it
removes the node, so only erase satellites directly in the rare case the lite entry
was already absent. Brace the removal block.

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2026-06-30 12:17:42 -05:00
Jonathan BennettandGitHub fd8ba4fa4e Chop down tft-related loooong ifdefs chains (#10803)
* Chop down tft-related loooong ifdefs chains

* Fix accidental sweep-up of portduino into screen #ifdef refactor
2026-06-29 21:23:25 -05:00
Ben Meadors 8345c2b6a9 Protos 2026-06-29 08:20:55 -05:00
b44ed4552f Fix spiLock deadlocks / frequent watchdog restarts in WarmNodeStore::save() (#10809)
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2026-06-29 06:07:04 -05:00
Ben MeadorsandGitHub 0488a46a3c fix: stop unexpected NodeNum regeneration from PKI key loss (#10808)
* fix: stop unexpected NodeNum regeneration from PKI key loss

A node's NodeNum is derived from its PKI public key
(my_node_num == crc32(public_key)), so it changes only when the keypair is
regenerated -- which happens only when a keygen path runs while
config.security.private_key.size != 32. Two paths could trigger that without
the user intending a new identity:

1. Boot: loadFromDisk() collapsed every non-success loadProto() result for the
   config file into installDefaultConfig() (preserveKey=false), which memset()s
   config and wipes the private key. loadProto() distinguishes DECODE_FAILED
   (file present but undecodable/undecryptable) from OTHER_FAILURE (absent), so
   a transient/corrupt read silently minted a new identity. A new
   configDecodeFailed flag now gates the boot keygen and the boot config-save
   directly (not via region, so it survives the userprefs/region overrides that
   run later in loadFromDisk): identity is frozen, the unreadable file is left
   intact for a later clean boot to recover, and the node boots radio-silent
   (region UNSET, TX off). A genuinely-absent config still gets defaults + keygen.

2. AdminModule security SET: config.security = c.payload_variant.security
   wholesale-clobbered the keypair, then regenerated whenever the incoming
   private_key.size != 32. A client editing an unrelated security field without
   round-tripping the private key would re-mint identity. The existing keypair
   is now preserved when the incoming config omits it; first provisioning and
   key import are unchanged. Intentional reset goes through factory_reset.

Native PlatformIO unit suite (Docker): 499/499 test cases pass.

* test: cover security keypair preservation; clarify load-failure comment

Addresses PR review feedback:
- Add native unit tests (test_admin_radio) asserting handleSetConfig preserves the
  existing identity keypair when a security SET omits the private key, and applies a
  full supplied keypair (key import). Guards against reintroduced NodeNum/keypair
  regeneration. AdminModuleTestShim (now a friend) defers saves so the lightweight
  harness doesn't saveToDisk an uninitialized node database.
- Clarify the non-DECODE_FAILED config-load comment: OTHER_FAILURE / NO_FILESYSTEM
  cover an absent or unopenable file, not just first boot.
2026-06-28 18:48:24 -05:00
0baf8b1db6 Portduino WASM hello world (still experimental) (#10793)
* Add ARCH_PORTDUINO_WASM build: meshtasticd in WebAssembly over WebUSB

Compile the full portduino firmware to WebAssembly (Emscripten) so a real node runs in a browser tab or headless Node, driving a LoRa radio over WebUSB through a CH341 — the desktop Ch341Hal path with its libusb backend swapped for a WebUSB one. The native/desktop portduino build is unchanged.

New build env under src/platform/portduino/wasm/ (excluded from the native build_src_filter): WebUSB libpinedio backend, config/FS/region/MAC/PhoneAPI glue, wasm setup/loop, JS WebUSB runtime, and build stubs. bin/build-portduino-wasm.sh runs a standalone cached emcc build to build/wasm/meshnode.{mjs,wasm}.

Six firmware sources gain #ifdef ARCH_PORTDUINO_WASM guards (single-threaded cooperative emscripten_sleep loop, continuous RX, US region default, std RNG, no-popen exec); none affect non-wasm builds.

* PortDuino WASM: CI build job, cross-platform libdeps, configurable adapter

bin/build-portduino-wasm.sh: auto-detect native-macos (macOS) vs native (Linux/CI) libdeps with a NATIVE_ENV override, and use the SAME env's Crypto with an XEdDSA-present guard. Drops the heltec-v3/Crypto borrow — the meshtastic/Crypto pin already ships XEdDSA; the native libdeps cache was just stale. No env pin change.

Add .github/workflows/build_portduino_wasm.yml: build the ARCH_PORTDUINO_WASM target in CI (ubuntu + emsdk, native libdeps) so it can't silently bit-rot; asserts build/wasm/meshnode.{mjs,wasm}.

src/platform/portduino/wasm: add wasm_set_lora_* setters so the JS host can configure any CH341 LoRa adapter (module, USB ids, DIO/TCXO, SPI speed, pins); wasm_config_apply falls back to the MeshToad defaults when unset.

* WASM: build as a first-class [env:wasm] via platform-wasm (retire emcc script)

Replace the standalone emcc build (bin/build-portduino-wasm.sh) with a normal
PlatformIO env, `pio run -e wasm`, using the new meshtastic/platform-wasm
platform (emcc/em++ + Asyncify, the WASM sibling of platform-native). The
portduino WebAssembly node is now built the same way as every other target.

- variants/native/portduino/platformio.ini: add [env:wasm] (platform pinned to
  platform-wasm, board wasm). Translates the script's source set into a curated
  build_src_filter, the ~30 EXCLUDE_* defines, and the lib set; defines
  ARCH_PORTDUINO_WASM in-repo so correctness doesn't hinge on the platform's
  board.json.
- extra_scripts/wasm_link_flags.py: the firmware-specific emcc *link* settings
  (EXPORT_NAME, EXPORTED_RUNTIME_METHODS, EXPORTED_FUNCTIONS, ASYNCIFY_IMPORTS).
  PlatformIO feeds build_flags to compile only, so these must ride LINKFLAGS;
  without it the WebUSB Asyncify seam and the JS host's runtime methods are
  dropped (the _wasm_* exports survive only via EMSCRIPTEN_KEEPALIVE).
- PortduinoGlue.{h,cpp}: guard the yaml-cpp dependency out of the WASM build
  (#ifndef ARCH_PORTDUINO_WASM around the include, emit_yaml/loadConfig/
  readGPIOFromYaml). The browser node configures via the wasm_set_lora_* setters
  and dead-strips the YAML path; this drops the host yaml-cpp build dependency
  entirely. Native is unchanged (guards are inert there).
- portduino_glue_wasm.cpp / portduino_main_wasm.cpp: repair EM_ASM JS that a
  formatter had mangled (!== -> != =, regex split) in the prior landing; the
  emcc link succeeds regardless, so CI now runs `node --check meshnode.mjs`.
- .github/workflows/build_portduino_wasm.yml: build via `pio run -e wasm`
  (artifacts under .pio/build/wasm/), trigger on the shared inputs the env
  inherits (root platformio.ini, bin/platformio-*.py).
- NodeDB.cpp: drop the dead ARCH_PORTDUINO_WASM region-default branch (region
  now defaults the same as native).
- Crypto renovate pins: add the missing gitBranch so they track upstream.

Output: .pio/build/wasm/meshnode.{mjs,wasm} (ES module, factory createMeshNode).
Verified: pio run -e wasm (against the published platform archive), node --check,
module instantiates in Node with all exports; native-macos + Docker native unit
tests (450/450) still pass.

* Fix name on the Piggystick

* wasm: pin platform-wasm at the GPL-3.0-relicensed commit

platform-wasm's LICENSE was always GPLv3 (matching this firmware), but its
platform.json/README still declared Apache-2.0 (mis-copied from platform-native).
That's fixed upstream in b83fa5b; bump the [env:wasm] pin to it. Build output is
unchanged (license metadata only). Verified: pio run -e wasm against b83fa5b.

* wasm: make reboot() actually restart the node (was a no-op)

In wasm the reboot path is live (main.cpp -> Power::powerCommandsCheck ->
Power::reboot), but Power::reboot's ARCH_PORTDUINO arm tore down SPI/Wire/Serial
and then called the no-op ::reboot() stub — leaving the node running with a dead
radio until the tab was manually reloaded. Triggers include an admin/phone
reboot, factory reset, the "reconfigure failed" path, and the 60 s stuck-TX
hardware watchdog (RadioLibInterface).

- Power::reboot(): add an ARCH_PORTDUINO_WASM arm (before ARCH_PORTDUINO, since
  the wasm build defines both) that skips the host teardown and just calls
  ::reboot(). notifyReboot already let modules persist.
- ::reboot() (glue): hand off to the JS host — browser reloads the tab (NodeDB
  state survives via IDBFS, same identity returns); headless calls Module.onReboot
  if provided, else logs. Loose !=/== so clang-format doesn't mangle the EM_ASM JS.
- README: document the reboot handoff + the Module.onReboot hook.

Verified: pio run -e wasm + node --check (EM_ASM intact); native-macos unaffected.

* wasm: rename env to native-wasm and run it in the main CI matrix

Rename [env:wasm] -> [env:native-wasm] for consistency with the portduino
native family (native, native-macos, native-tft). The build dir follows to
.pio/build/native-wasm/ (artifact is still meshnode.{mjs,wasm}); the PIOENV
guard in extra_scripts/wasm_link_flags.py, the README, and the companion wrapper
move with it. The board stays `wasm`.

Also wire the build into normal CI: build_portduino_wasm.yml becomes a reusable
workflow (workflow_call) invoked as the `build-wasm` job of main_matrix.yml, so
the WebAssembly node is built like every other platform instead of on a separate
path trigger.

* native-wasm: auto-locate the Emscripten SDK (pre-build script)

`pio run -e native-wasm` failed with "emcc not found" whenever it was invoked
from a shell that hadn't sourced emsdk_env.sh — a VS Code task, an IDE build
button, a bare terminal. Add a pre: extra script that probes the usual emsdk
locations ($EMSDK_ENV, $EMSDK, ~/emsdk, ./.emsdk, the sibling companion
checkout), sources emsdk_env.sh, and imports the resulting environment so the
platform builder and emcc see PATH/EMSDK/EM_CONFIG. No-op when emcc is already
reachable (CI), silent when no SDK is found (the platform emits its own error).

* wasm: address PR review feedback

- js/bridge.js: import CH341 from "./ch341.js" (sibling in this layout), not
  "../src/ch341.js" which doesn't resolve here.
- js/ch341.js: a zero-length transferIn while MISO bytes are still outstanding
  now throws instead of breaking out with a partially-filled buffer — silent SPI
  corruption becomes a loud error, matching the comment above it.
- libpinedio_webusb.c: webusb_set_auto_cs honors the AUTO_CS option (? 1 : 0)
  instead of the always-on ? 1 : 1. Runtime behavior is unchanged — Ch341Hal sets
  AUTO_CS=0 right after pinedio_init (RadioLib drives the active-low NSS); the
  option just isn't set yet at init, so this now correctly defaults off.
- SX126xInterface.cpp: the RX-start error log now names the method actually
  called (startReceive vs startReceiveDutyCycleAuto) instead of hardcoding the
  duty-cycle name in the WASM branch.

* native-wasm: drop the emsdk bootstrap shim (now in platform-wasm)

The Emscripten SDK auto-location moved into the platform-wasm builder, so the
firmware no longer needs its own pre: extra script. Remove
extra_scripts/wasm_emsdk_env.py and bump the platform pin to the build that
carries the bootstrap. The wasm_link_flags.py post script stays — those exported
fns / runtime methods / Asyncify import seam are firmware-app-specific.

* wasm: use the canonical companion name (meshtasticd-wasm-node)

The companion repo was renamed meshtastic-web-node -> meshtasticd-wasm-node; fix
the stale name in the wasm README and bump the platform pin to the build that
promotes the canonical name in its emsdk auto-location.

* wasm: re-entrancy guard for the API/region entry points + flaky-open retry

Two robustness fixes for the browser node:

- Re-entrancy guard. The node is single-threaded + Asyncify: while setup()/loop()
  is suspended inside a WebUSB transfer, the JS event loop is free, so a stray
  DOM/timer callback that re-enters a wasm_* entry point starts a second Asyncify
  unwind ("async operation already in flight" abort) or clobbers shared PhoneAPI
  state (observed as a "PhoneAPI::available unexpected state" flood). Add a
  g_wasm_in_firmware flag set around setup()/loop() (portduino_main_wasm.cpp); the
  wasm_set_region / wasm_api_to_radio / wasm_api_from_radio / wasm_api_available
  entry points now reject a mid-tick call (return busy) instead of corrupting or
  aborting. The host must still call them between ticks — this is the safety net
  the design lacked, not a substitute for the JS queue.

- CH341 open retry (js/bridge.js). First-connect WebUSB is flaky — the interface
  is briefly unclaimable right after the grant, or held by a prior session,
  giving a transient "Could not open SPI: -1". Retry the open with a short
  backoff, closing the device between attempts so claimInterface starts clean.

* wasm: exclude emscripten-only sources from cppcheck

The `check` board matrix runs `pio check` (cppcheck) over all of src/,
including src/platform/portduino/wasm/. cppcheck can't parse the EM_ASYNC_JS/
EM_JS macros (Syntax Error: AST broken at libpinedio_webusb.c:39,
internalAstError) and these sources are not part of any checked board build
([env:native-wasm] is board_level=extra, compiled by the build-wasm CI job).
Suppress the wasm dir in suppressions.txt, the same way generated/ and .pio/
are already excluded.

* wasm: coalesce FS.syncfs so two never run at once

IDBFS syncfs is async; the explicit wasm_fs_sync (5s timer + post-save +
beforeunload) could overlap a prior in-flight sync, warning "2 FS.syncfs
operations in flight at once". Serialize: if a sync is running, mark a pending
re-sync and let the in-flight one chain it on completion — at most one in flight,
trailing writes still flushed. (Companion drops IDBFS autoPersist so this is the
single persistence path.)

* wasm: silence false-positive SAST on the emscripten glue

- extra_scripts/wasm_link_flags.py: restore the trunk-ignore-all(ruff/F821,
  flake8/F821) header every other SCons extra_script carries; Import/env are
  SConscript-injected globals, so ruff/flake8 flag them as undefined.
- .semgrepignore: exclude src/platform/portduino/wasm/js/ (browser WebUSB glue,
  not part of the firmware binary). The unsafe-formatstring rule false-positives
  on its benign retry/diagnostic console logs.

* Update .github/workflows/build_portduino_wasm.yml

Co-authored-by: Austin <vidplace7@gmail.com>

---------

Co-authored-by: Austin <vidplace7@gmail.com>
2026-06-28 06:50:35 -05:00
TomGitHubSteve GilberdDarafei Praliaskouskigithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>Claude Opus 4.8
ec5d230305 Feat/mesh beacon (#10618)
* Tips robot virtual node / relayer to different LoRa modes & channels

Note that this commit has details hardcoded for the Wellington (NZ)
mesh, and also requires the following patch to the protobufs:

-----
diff --git a/meshtastic/mesh.proto b/meshtastic/mesh.proto
index 03162d8..ec54c99 100644
--- a/meshtastic/mesh.proto
+++ b/meshtastic/mesh.proto
@@ -1393,6 +1393,21 @@ message MeshPacket {
    * Set by the firmware internally, clients are not supposed to set this.
    */
   uint32 tx_after = 20;
+
+  /*
+   * The modem preset to use fo rthis packet
+   */
+  uint32 modem_preset = 21;
+
+  /*
+   * The frequency slot to use for this packet
+   */
+  uint32 frequency_slot = 22;
+
+  /*
+   * Whether the packet has a nonstandard radio config
+   */
+  bool nonstandard_radio_config = 23;
 }

 /*
-----

* fix: repair mesh tips CI build

* feat: add MeshBeacon module (Phase 1 — proto + generated code + initial stub)

* feat(beacon): implement broadcaster + listener (phases 2-5)

* feat(beacon): wire RadioLibInterface hooks + admin validation (phases 6-7)

* fix(beacon): fix LocalModuleConfig flat access (no payload_variant), add localonly proto field

* feat(beacon): fix broadcaster inheritance, add preset/region validation + proto cache

- MeshBeaconBroadcastModule now inherits ProtobufModule<meshtastic_MeshBeacon>
  (alongside private MeshBeaconModule + OSThread), giving it allocDataPacket()
  and setStartDelay() without extra includes.

- Payload cache: rebuildCache() encodes the MeshBeacon protobuf once and stores
  it in payloadCache[]/payloadCacheSize; sendBeacon() only calls rebuildCache()
  when payloadCacheDirty==true. AdminModule calls invalidateCache() after saving
  new config so the next broadcast picks up changes.

- Region/preset validation in handleSetModuleConfig (mesh_beacon_tag):
  broadcast_on_preset is validated against the device's current region via
  RadioInterface::validateConfigLora(); broadcast_offer_region is validated via
  RadioInterface::validateConfigRegion(). Invalid values are zeroed with a
  LOG_WARN before saving.

* feat(beacon): add unit tests for MeshBeaconModule and AdminModule configuration validation

* remove old meshtips

* more  validation in NodeDB and AdminModule, and userprefs for baked in goodness

* copilot is my gravity

* mmmmm... beacon

* oops

* Enhance unit tests for MeshBeaconModule with detailed validation checks and output formatting

* new lines. Why not?

* finally

* legacy mode activate!

* Update protobufs (#17)

Co-authored-by: NomDeTom <116762865+NomDeTom@users.noreply.github.com>

* better logic, fixed a test

* updated for packet signing
fixed a test
added guards for licensed/ham mode

* channel numbers

* beacon: encrypt on the beacon channel PSK; fix split note

When broadcast_on_channel overrides the primary channel's name/PSK, the
beacon was encrypted with the PRIMARY PSK: perhapsEncode keys encryption
off the primary slot, but the radio-thread channel switch happens only
after encryption. sendBeaconPacket() now installs the beacon channel into
the primary slot for the synchronous duration of send() (cooperative
threading => no interleaving) so encryption/hash use the beacon channel,
then restores it. A shared beaconChannelSettings() helper builds the
channel for both the encrypt-time swap and the RF-time swap so the
key+hash cannot drift.

Also: correct the legacy-split comments (both packets go out on the same
beacon radio settings, not the normal config) and merge the two
consecutive `if (hasText)` blocks in the listener (cppcheck
duplicateCondition).

Tests: add channelPskOverride_swapsBeaconChannelAndRestores and
noChannelOverride_doesNotSwapPrimary; MockRouter snapshots the primary
channel at send() time.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test/beacon: drain toPhoneQueue in tearDown to fix LSan leak abort

The listener delivers received text via MeshService::sendToPhone(), which
enqueues the packet into toPhoneQueue and takes ownership. Nothing dequeues
it in tests, so the three listener tests carrying message text stranded a
MeshPacket each — 1272 bytes / 3 allocations that LeakSanitizer flagged at
process exit, aborting the coverage run (surfaced by pio as [ERRORED] /
SIGHUP even though all 40 assertions passed).

Drain the phone queue in tearDown (getForPhone()/releaseToPool) so the
packets return to packetPool. Suite is now GREEN with no sanitizer abort.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* legacy hop override for zero-hoppers

* ever more beacons

* beacon: comment out broadcast_send_as_node pending further review

Functionality preserved in comments with full signing/has_bitfield notes
for when it is re-enabled. Proto tag 3 retained on the wire.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test/beacon: fix fromIsCustomNodeWhenSet now that send-as-node is disabled

broadcast_send_as_node is commented out; from is always the local node.
Update the test assertion and doc comment to match current behaviour.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Update protobufs (#21)

Co-authored-by: NomDeTom <116762865+NomDeTom@users.noreply.github.com>

* flags for beacons

* beacon: do more with less — slot-index targets + validation

Multi-target beacons embedded a full ChannelSettings in every BroadcastTarget,
blowing ModuleConfig past the 512-byte BLE FromRadio budget so the firmware would
not compile. Targets now reference an existing channel-table slot by channel_index
and the broadcaster resolves it via channels.getByIndex() at TX time. Net effect:
the same multi-target capability for a fraction of the bytes —
FromRadio 609 -> 510 B, MeshBeaconConfig 596 -> 324 B, AdminMessage 615 -> 511 B.

- proto: BroadcastTarget.channel (embedded) -> channel_index (uint32 ref); regen all
  generated headers (size constants propagate to admin/localonly/deviceonly/mesh).
- broadcaster: resolve channel_index from the channel table; an out-of-range or blank
  slot falls back to the default channel for the target preset rather than borrowing
  the primary's name/PSK.
- AdminModule: validate broadcast_targets entries on write (region/preset sanitised
  like the single-target fields; channel_index range-checked).
- userPrefs: TARGET_<n>_CHANNEL_{NAME,NUM,PSK} collapse to a single CHANNEL_INDEX.
- docs: two-step (set_channel -> set_module_config) multi-target setup, inline-vs-
  reference distinction, and single-/multi-target are equal (not "legacy") options.
- tests: target validation + channel-index resolution incl. blank-slot fallback
  (47/47 green on `./bin/run-tests.sh -e native -f test_mesh_beacon`).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NRAF5csgsMn6p1zEcFL8Qz

* throttling after reboot

* address copilot review

* simplify

* fix(beacon): use 0x%08x for node/packet IDs in logs; register test suite

The %#08lx log specifiers passed uint32_t (NodeNum/PacketId) to a %lx
length modifier — undefined behaviour on 64-bit (native test) targets and
non-standard width. Switch to the project-standard 0x%08x. Also bump
test/native-suite-count to 25 for the added test_mesh_beacon suite.

clod helped too

* copilot & clarity
clod helped too

* refactor(beacon): use auto for the sanitized config copy

clod helped too

* fix(beacon): guard empty-payload sends; gate has_mesh_beacon on build flag; document ISR_TX pre-switch

clod helped too

---------

Co-authored-by: Steve Gilberd <steve@erayd.net>
Co-authored-by: Darafei Praliaskouski <me@komzpa.net>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 08:20:51 -05:00
Ben Meadors ad132e9540 Protobufs update 2026-06-26 07:19:33 -05:00
Ben MeadorsandGitHub b93239bb0a fix(phone-api): skip manifest scan for node-info-only requests; bound getFiles (#10778)
Forward-port of #10754 and #10757 from master (2.7) into develop, so the
develop->master 2.8 promotion (#10777) doesn't drop them.

#10754: PhoneAPI no longer walks the filesystem to build the file manifest on
node-info-only config requests (SPECIAL_NONCE_ONLY_NODES), which never consume
it. getFiles() is now bounded (default 64 entries, depth 3) via collectFiles(),
takes an optional wasLimited out-param, and reserves capacity with a bad_alloc/
length_error fallback. The manifest vector is freed via swap (releaseFilesManifest).

#10757: getFiles()/collectFiles() now guard against empty file names returned by
the Adafruit LittleFS nRF52 glue (issue 4395).

Ported by hand rather than cherry-picked: master had reflowed FSCommon.cpp to a
different brace style (every line conflicted), #10754 already subsumes #10757,
and develop carries a MESHTASTIC_EXCLUDE_FILES_MANIFEST path (nRF54L15) that
master lacks. The exclude path is preserved and now also short-circuits + frees
the manifest. Verified: native Docker suite 448/448, clang-format clean.
2026-06-24 12:38:46 -05:00
CopilotGitHubcopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>thebenternBen MeadorsClaude Opus 4.8
fcfe329091 Guard TCP API writes after Wi-Fi reconnects (#10505)
* Guard TCP API writes against dead sockets

Agent-Logs-Url: https://github.com/meshtastic/firmware/sessions/ebeb38d5-7339-4eac-b310-4b6dd9d40758

Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>

* fix: apply review fixes for server write checks and stream locking

Agent-Logs-Url: https://github.com/meshtastic/firmware/sessions/215c659d-bc17-4b30-89f4-78e3f9cdb3c3

Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>

* style: format TCP API write check files

* fix(api): keep TCP API open on write backpressure; drop broken writability gate

canWriteFrame() treated a full transmit buffer (availableForWrite() == 0) as a
dead socket and closed the connection, so every healthy client was dropped and
the native simulator integration test timed out waiting for config. A zero
availableForWrite() is normal backpressure, not a disconnect; only a dropped
link should refuse a write. Reduce canWriteFrame() to the reliable
!client.connected() check -- genuine write failures are still caught after the
fact by onFrameWriteFailed().

Remove the now-unused ClientWriteChecks.h writability helper and its unit test
(test_client_write_checks, which also failed to link), and restore
ServerAPI.cpp to the project clang-format style (fixes the Trunk check).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 05:31:17 -05:00
Thomas GöttgensandGitHub c4fdf374e1 fix: right-size warm tier for constrained platforms (#10746, #10705) (#10759)
* fix: right-size warm tier for constrained platforms and feed RP2040 watchdog during NodeDB save

* fix: size warm tier and traffic cache per-MCU RAM, lowering no-PSRAM ESP32 (classic/S2/C3) tiers

* docs: document nRF52/RP2040 #else fall-through in warm tier and TM cache cascades
2026-06-22 06:21:50 -05:00
3501f620a3 fix: restore fixed position into localPosition on boot (#10670)
* NodeDB: restore fixed position into localPosition at boot

Fixed-position nodes without a GPS stop broadcasting their position (and
publishing MQTT map reports) after a reboot. On boot localPosition is empty,
and NodeDB::hasValidPosition() for the local node only inspects localPosition,
not the persisted node position. PositionModule therefore never sends a
position, so the lazy localPosition backfill in getPositionPacket() never
runs and localPosition stays at (0,0) indefinitely.

Restore the configured fixed position into localPosition during NodeDB
construction so position broadcasts and map reports resume automatically
after a reboot.

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-06-21 08:17:09 -05:00
c51c01607d Traffic Management module: dedup, rate limiting, role-aware policing (#10706)
Adds the Traffic Management module (TMM) plus the NodeDB/warm-store and
next-hop foundations it builds on:

- Unified per-node cache (flat array, 8-bit relative ticks) shared by all
  features; role-aware throttles for tracker / lost-and-found.
- Position deduplication: drop unchanged position rebroadcasts within a
  configurable interval; precision driven off the channel ceiling (clamped to
  the public-key max on well-known channels). Enabled by default at 11h.
- Per-node rate limiting and unknown-packet filtering (config-driven; a
  non-zero companion field enables each feature -- no bool toggles).
- NodeInfo direct response from cache with role-based hop clamps.
- Persistent next-hop overflow store: confirmed hops have no TTL, are seeded
  from NodeInfoLite at boot, and survive hot-store eviction.
- Three-tier sender-role resolution (hot NodeInfoLite -> warm store -> TMM
  cache). Role is cached write-time (seeded on first track, refreshed from
  NodeInfo), pins its cache entry like a next-hop hint, and is evicted last.
- Warm store caches device role + protected category across reboot/eviction.
- PositionModule stationary floor for tracker / lost-and-found.
- PSRAM gating for warm/satellite/TMM cache sizes; STM32WL excluded.

Protobufs: TrafficManagementConfig trimmed to the five uint32 fields actually
used; submodule repointed to protobufs develop.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-20 20:35:14 -05:00
d8d92b7b71 Fix/zerohop and hopscale mqtt (#10753)
* fix for prehopdrop on zerohop packets

* hopscaling: exclude MQTT-origin packets from the node histogram

The hop-scaling histogram gate only checked transport_mechanism == TRANSPORT_LORA,
which excludes packets received from the broker but NOT MQTT-origin packets that a
gateway rebroadcasts onto LoRa (those arrive as TRANSPORT_LORA with via_mqtt set).
Counting them inflates the local mesh-size/density estimate with nodes that aren't
real RF participants — and since the bridged copy usually carries hop_start==0 they
land in the hop-0 bucket, the one that pulls getLastRequiredHop() lowest, over-
shrinking NodeInfo/position/telemetry hop limits. Add `!mp.via_mqtt` to the gate.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-20 17:08:29 -05:00
Ben Meadors 161cd26519 Add conditional compilation for warm node count checks 2026-06-19 20:49:48 -05:00
Ben MeadorsandGitHub dd1ec9d462 Lora region preset map (#10736)
* 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.
2026-06-19 19:56:24 -05:00
22072c5f4b Pr1.5 tmm nexthop (#10745)
* TrafficManagement: flat unified cache + persistent next-hop overflow store

Reworks the TrafficManagementModule cache layer (policing behaviour unchanged
from upstream) and adds a routing-hint overflow store:

- Flatten the ring: replace the cuckoo-hashed unified cache and the bucketed
  PSRAM NodeInfo index with plain flat arrays + linear scan (same idiom as
  WarmNodeStore). At LoRa packet rates an O(n) scan of the cache is negligible,
  and it removes a large amount of hashing/displacement complexity. The cache
  entry is 11 B; timestamps use a uniform +1 presence-offset so a 0 byte always
  means "empty" across every sub-store. Adds rebaseEpoch() so cached state
  survives the ~19 h relative-timestamp horizon instead of being flushed.

- Next-hop overflow cache: setNextHop/getNextHopHint store a confirmed last-byte
  relay for a destination, written only from NextHopRouter's ACK-confirmed
  decision (and mirrored from TraceRoute). NextHopRouter::getNextHop falls back
  to this cache when the hot NodeDB has no hint, so DMs/relays to long-tail
  nodes keep routing after the node ages out of NodeInfoLite.

- Persistence: preloadNextHopsFromNodeDB warm-starts the cache from persisted
  NodeInfoLite hints on first maintenance pass; next_hop entries are kept alive
  across the maintenance sweep (no TTL) and never clobbered by a stale preload.

All packet-policing logic (rate limit, position dedup, unknown-packet drop,
NodeInfo direct response, hop exhaustion) is the existing upstream behaviour,
untouched. HAS_TRAFFIC_MANAGEMENT defaults on so the module is compiled in. (see note).

Tests: upstream policing suite now actually runs (adds the MeshTypes.h include
that gates HAS_TRAFFIC_MANAGEMENT) plus 4 next-hop tests. Role-aware throttles,
politeness, precision clamp, port-interval and mesh-radius gating — and the
rate-limit >255 saturation fix — are deferred to the advanced-TMM branch.

Note: default dedup movement grid moves to ~91m, which also means 1.5km required to end up with the same signature position - coarser and therefore further than before.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* TrafficManagement: fix cppcheck constVariablePointer warning

`node` in preloadNextHopsFromNodeDB() is never written through — mark
it const to satisfy cppcheck's constVariablePointer check in CI.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Add multi-hop NextHop recovery tests and unit tests for routing reliability

- Introduced a new test suite for multi-hop NextHop directed-message delivery and relay recovery in `test_nexthop_multihop_recovery.py`. This includes tests for end-to-end delivery and recovery after relay drop.
- Implemented unit tests in `test_main.cpp` for NextHop routing reliability mitigations, covering:
  - M1: Ambiguity-aware last-byte resolution.
  - M2: NextHopRouter's strict-neighbor gate and hop limit checks.
  - M3: Route-health freshness and failure decay.
- Enhanced mock classes to facilitate controlled testing of node behaviors and routing logic.

* grafting fixed

* Address Copilot review for PR #10735 (NextHop improvements)

- docs/nexthop-routing-reliability.md: update status from "no code
  changes yet" to reflect that mitigations and tests are implemented

RAM pressure and MIGRATION_VERBOSE concerns addressed upstream in
PR2.5 (per-platform TRAFFIC_MANAGEMENT_CACHE_SIZE) and PR2 (verbose
default=0) respectively; (0,0) sentinel fixed in PR2.5.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* CI: fix cppcheck constVariablePointer and test include path

- NextHopRouter.cpp: qualify two RouteHealth *h locals as const — only
  read for stale-route checks, never mutated through the pointer
- Router.cpp: qualify meshtastic_NodeInfoLite *node as const in
  shouldDecrementHopLimit — only read for favorite/role predicate
- test_position_module/test_main.cpp: change bare PositionModule.h to
  modules/PositionModule.h — build_flags sets -Isrc, not -Isrc/modules,
  so the bare form fails to resolve in the native PlatformIO test env

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* WarmStore: cache device role + protected category in last_heard low bits

Steal the low 6 bits of WarmNodeEntry.last_heard to carry an evicted node's
device role (4 bits) and a protected category (2 bits) for the hop-trim path,
at zero record-size cost (entry stays 40 B; no RAM/flash growth). The high bits
remain a real unix-seconds timestamp, quantised to 64 s — ample for warm LRU
ordering of long-tail nodes.

- absorb() packs role/protectedCat; place()/ring replay store the raw word so
  metadata round-trips through flash. LRU compares masked time (warmTimeOf).
- take() rehydration masks the metadata bits and restores the cached role so a
  re-admitted node isn't stuck at CLIENT until its next NodeInfo.
- NodeDB classifies the category (favorite/ignored/verified -> Flag;
  tracker/sensor/tak_tracker -> Role) at each eviction site.
- WarmNodeStore::lookupMeta() exposes role/category to consumers.
- Bump WARM_RING_MAGIC (WRNG->WRN2): old rings read as erased and rebuild;
  warm data is a non-critical evictee cache, so discard-on-upgrade is safe.

Tests: test_warm_store 11/11 (new meta round-trip + quantisation-aware ordering);
NodeDB compiles (test_nodedb_blocked 4/4).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* WarmStore: migrate v1 rings/files by discarding last_heard, not the data

Previously the WRNG->WRN2 magic bump treated old rings as erased, discarding all
warm entries — including the PKI public keys that let evicted nodes keep
decrypting DMs. Instead, read v1 (WRNG / WRM1) records and keep each node's
identity + public key, discarding only last_heard (its low bits would otherwise
be misread as the new role/protected metadata). Records re-rank and re-learn
their role on next contact.

- Ring backend (nRF52840): ringReadHeader accepts both magics and reports v1 via
  an out-param; replay zeroes last_heard for v1 records. If the active head page
  is v1, force a rotation so new v2 records never land in a v1-headered page
  (which would discard their freshly-set role on the next load). Legacy pages
  convert to v2 as the ring rotates.
- File backend (warm.dat): bump WARM_STORE_MAGIC WRM1->WRM2; accept WRM1, verify
  CRC against the stored bytes, then discard last_heard and mark dirty so the
  next save rewrites as v2.

Tests: test_warm_store 12/12 (adds test_ws_v1_migration_discardsLastHeard:
key survives, role/protected reset).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* WarmStore: guard role bit-width + test eviction carries role/protected

- static_assert that the device role enum still fits the 4-bit warm metadata
  field (WARM_ROLE_MASK); fails the build loudly if a new role is added past 15
  rather than silently truncating role on eviction. (Max role today = 12.)
- Add test_migration_carriesRoleAndProtectedIntoWarm: a demoted TRACKER lands in
  the warm tier with its key, role=TRACKER and protected category=Role; a demoted
  CLIENT carries role=CLIENT/None. Exercises the NodeDB eviction path +
  warmProtectedCategory classification (the warm-store unit tests only cover
  absorb() directly).

Tests: test_nodedb_blocked 5/5.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix copilot comments

* fix(test): restore #if HAS_TRAFFIC_MANAGEMENT guard in TMM test

The rebase onto PR1.5 lost the top-level HAS_TRAFFIC_MANAGEMENT guard
that PR1.5 introduced, leaving the #else/#endif tail orphaned and
causing compile errors on non-TMM builds.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2026-06-19 19:52:58 -05:00
f2f23f8978 NimBLE params overhaul and try-fix for incompatible bond cleanup (#10741)
* NimBLE params overhaul and try-fix for incompatible bond cleanup

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Address PR review: remove dead clearNVS(), defer bond-purge log below init

- Delete unused clearNVS() (no callers; should have been removed in #10264).

- Move purgeIncompatibleBleBonds() after the "Init the NimBLE" log so bond-cleanup output doesn't appear to precede module init; it still runs before BLEDevice::init() reads the store.

* Update MAX_SATELLITE_NODES and WARM_NODE_COUNT definitions for ESP32-S3 PSRAM support

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-06-19 12:21:05 -05:00
4f0e2dde98 feat: add Ethernet OTA support for RP2350/W5500 boards (#10136)
* feat: add Raspberry Pi Pico 2 + W5500 + E22-900M30S variant

Adds community variant for Raspberry Pi Pico 2 (RP2350, 4 MB flash)
with external WIZnet W5500 Ethernet module and EBYTE E22-900M30S LoRa
module (SX1262, 30 dBm PA, 868/915 MHz).

Key details:
- LoRa on SPI1: GP10/11/12/13 (SCK/MOSI/MISO/CS), RST=GP15,
  DIO1=GP14, BUSY=GP2, RXEN=GP3 (held HIGH via SX126X_ANT_SW)
- W5500 on SPI0: GP16/17/18/19/20 (MISO/CS/SCK/MOSI/RST)
- SX126X_DIO2_AS_RF_SWITCH: DIO2→TXEN bridge on module handles PA
- SX126X_DIO3_TCXO_VOLTAGE 1.8: TCXO support via EBYTE_E22 flags
- DHCP timeout reduced to 10 s to avoid blocking LoRa startup
- GPS on UART1/Serial2: GP8 TX, GP9 RX
- Reuses WIZNET_5500_EVB_PICO2 code paths for Ethernet init

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* pico2_w5500_e22: rename define and address review feedback

Rename WIZNET_5500_EVB_PICO2 to PICO2_W5500_E22 so the variant-specific
define matches the variant directory name and isn't confused with an
on-board EVB SKU.

Review fixes from PR #10135:
- Gate the 10 s Ethernet DHCP timeout behind PICO2_W5500_E22 so other
  Ethernet builds keep the default 60 s behavior; apply the same timeout
  to reconnectETH() for consistency.
- Drop the unused -D EBYTE_E22 flag; EBYTE_E22_900M30S already selects
  TX_GAIN_LORA / SX126X_MAX_POWER in src/configuration.h.
- Rewrite "on-board W5500" comments to describe the external module.
- Correct README TX_GAIN_LORA value (7, not 10) and drop the EBYTE_E22
  row.

* fix(pico2_w5500_e22): drop DEBUG_RP2040_PORT=Serial

The arduino-pico framework hooks _write() when DEBUG_RP2040_PORT=Serial
is set and dumps raw debug bytes onto USB CDC, corrupting any binary
protobuf stream sent through StreamAPI (e.g. `meshtastic --port COMx`).

The variant excludes BT and WiFi, so the primary client transport is
Ethernet TCP via ethServerAPI — unaffected — but users who configure
the node over USB serial would see protobuf decode failures from
debug-byte interleaving. Removing the flag restores clean USB CDC.

Debug output can still be enabled per-build by adding -D DEBUG_RP2040_PORT=Serial1
to redirect to UART0 instead of USB CDC.

* feat: add Ethernet OTA support for RP2350/W5500 boards

Adds over-the-air firmware update capability for RP2350-based boards
with a WIZnet W5500 Ethernet module (e.g. pico2_w5500_e22).

Protocol (MOTA):
- SHA256 challenge-response authentication with a configurable PSK
  (override via USERPREFS_OTA_PSK; default key ships in source)
- 12-byte header: magic "MOTA" + firmware size + CRC32
- Firmware received in 1 KB chunks, verified with CRC32, written via
  Updater (picoOTA), then device reboots to apply
- Constant-time hash comparison prevents timing attacks on auth
- 30s inactivity timeout + 5s cooldown after failed auth
- Response codes 0x00-0x08 map 1:1 to OTAResponse enum

Firmware side:
- ethOTA.cpp / ethOTA.h: OTA TCP server on port 4243
- ethClient.cpp: wire initEthOTA/ethOTALoop into reconnect loop
- main-rp2xx0.cpp: hardware watchdog (8s, paused during debug)
- pico2_w5500_e22/platformio.ini: HAS_ETHERNET_OTA flag,
  filesystem_size bumped to 0.75m for OTA staging

Host side:
- bin/eth-ota-upload.py: Python uploader with progress and full
  result-code mapping (matches OTAResponse 0x00-0x08)

* style(eth): clang-format ethOTA.cpp per repo .clang-format

Reformat to the repo trunk clang-format config (IndentWidth 4,
ColumnLimit 130). Resolves the Trunk Check 'Incorrect formatting'
failure on PR #10136.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 16:28:10 +02:00
Quency-DandGitHub 68af6b277c Add Heltec tower v2 board. (#10693)
* Add heltec_mesh_tower_v2 board.

* Added automatic detection for high and low power versions.

* Fix low power consumption issues
2026-06-18 07:07:50 -05:00
TomGitHubClaude Opus 4.8github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>Ben Meadors
79a7dcc46c Pr1 nodedb warmstore (#10705)
* NodeDB: 3-tier node store with persistent warm tier (long-tail identity retention)

Introduces a tiered NodeDB so the device retains identity (public key,
last_heard) for far more nodes than fit in the full-record hot store,
without growing heap or the persisted nodes.proto unboundedly.

- Hot store: full NodeInfoLite, MAX_NUM_NODES (120 on nRF52).
- Satellite maps: position/telemetry/environment/status capped at
  MAX_SATELLITE_NODES (40 freshest); eviction via enforceSatelliteCaps /
  evictSatelliteOverCap.
- Warm tier (WarmNodeStore): 40 B {num,last_heard,public_key} records for
  evicted nodes so DMs to/from long-tail nodes keep encrypting/decrypting.
  Persisted to /prefs/warm.dat, or on nRF52840 a dedicated 12 KB raw-flash
  record-ring below LittleFS (3x4 KB pages; see linker scripts + the
  nrf52_warm_region.py post-link guard).

NodeDB::getOrCreateMeshNode now demotes evicted nodes into the warm tier and
re-admits them (restoring key/last_heard). Router PKI decrypt/encode resolve
the peer key via NodeDB::copyPublicKey (hot store, then warm tier).

NodeInfoLite gains snr_q4 (sint32, Q4-encoded dB); the float snr is zeroed on
disk. NodeInfoLite grows 105 -> 112 B; backup 2432 -> 2468 B.

Note: the snr_q4 .proto change still needs to land in the protobufs submodule
(generated header is updated here; submodule pointer left at upstream).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* NodeDB: robust receive + retention for blocked (ignored) nodes

Hardens how ignored/favourite nodes are received over admin and retained,
closing paths where a block could be lost or accidentally cleared.

- Blocking keeps the node's public key (admin set_ignored_node and
  addFromContact no longer zero it / drop the warm-tier key), so a blocked
  peer stays a verifiable identity.
- set_ignored_node creates the node if absent, so a block by node ID sticks
  even for a node we've never heard from (e.g. pushed by a remote admin) with
  no NodeInfo or key.
- Eviction protection (favourite/ignored/manually-verified) now also applies to
  the load-time hot-store migration and is never undone by cleanupMeshDB, which
  previously purged ignored nodes that lacked user info.
- The hot-store migration leaves our own node (index 0) in place and prefers to
  demote non-protected nodes, like the runtime eviction scan.

Caps the protected set (favourite + ignored + verified) at MAX_NUM_NODES-2 via
NodeDB::setProtectedFlag(), so at least two evictable slots always remain and
getOrCreateMeshNode can always make room — replacing the previous unconditional
append that could run off the end of the node vector when every node was
protected. A locally-set favourite/ignore that hits the cap reports back to the
phone via a ClientNotification.

Adds test_nodedb_blocked covering the migration, favourite/ignored eviction
protection, ignored-survives-cleanup, and the protected-node cap. The
maintenance methods stay private in production; the test reaches them through a
PIO_UNIT_TESTING-guarded friend shim.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

# Conflicts:
#	src/mesh/NodeDB.h

* fix copilot comments

* once again

* WarmNodeStore: fix cppcheck warnings (uninitvar, constVariablePointer)

Zero-initialise `stranded[]` and `seqs[]/order[]` VLAs so cppcheck can
verify there are no unguarded reads of uninitialised memory (the guards
exist but are not visible to static analysis). Mark two local pointers
`const` where the pointed-to entry is never mutated after assignment.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* self-care added to assist 2.7 and 2.8 nodedb migration

* Tidy warm-store/self-care: comments, guards, log + flash cleanup

Style/cleanup pass over the branch (no behavior change except the noted
preprocessor simplifications, which are semantically identical):

- Comments: move function descriptions to the headers, cap in-function
  comments at ~3-4 lines, drop leading-number step markers, label stacked
  #endif blocks, de-decorate banner comments.
- dumpToLog: fully gate decl + definition + AdminModule call site behind
  MESHTASTIC_NODEDB_MIGRATION_VERBOSE so it compiles out when disabled
  (~1.2 KB when off).
- mesh-pb-constants: drop the dead nRF52832 WARM_NODE_COUNT branch and trim
  the macro docs.
- WarmNodeStore: simplify the redundant `ARCH_NRF52 && NRF52840_XXAA` guards
  to `NRF52840_XXAA`, add a kNoPage sentinel for the ring page state.
- Shorten the always-on LOG_WARN strings (~120 B flash).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* more tidying up, aligning with docs and undoing other-arch regressions

* Update protobufs (#19)

Co-authored-by: NomDeTom <116762865+NomDeTom@users.noreply.github.com>

* made the migration pathway cleareer

* address copilot review

* fixed a copilot review on a downstream PR.

* Address Copilot review comments for PR #10705 (warmstore/nodedb)

- WarmNodeStore.h: default MIGRATION_VERBOSE to 0 (suppress info-level
  chatter on production builds; opt in with =1)
- WarmNodeStore.cpp load(): move memset to top of function so all
  failure paths (header-read fail, invalid header) leave entries clear
- WarmNodeStore.cpp save(): replace manual spiLock lock/unlock around
  mkdir with LockGuard covering the full SafeFile sequence, matching
  the lock discipline in load()
- Router.cpp: memcpy(&p->public_key.bytes, ...) -> memcpy(p->public_key.bytes,
  ...) — pass decayed uint8_t* rather than pointer-to-array
- AdminModule.cpp: check setProtectedFlag return for PKC auto-favorite;
  log cap-refusal warning instead of unconditional "auto-favoriting"
- nrf52_warm_region.py: error message references both v6.ld and v7.ld

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* NodeDB: formatting cleanup (blank lines after preprocessor blocks)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Lukewarm store

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2026-06-18 08:41:34 +01:00
5ffd30c4a8 Fix PKC on portduino sim by working around region blocks keygen and packet length (#10730)
* Fix PKC on portduino sim by working around region blocks keygen and packet length

* Reserve Compressed framing overhead in sim loopback capacity check

The sim loopback re-encodes the Compressed wrapper back into
decoded.payload.bytes (the same 233-byte field its data is copied from),
so the carried bytes must leave room for protobuf framing. Checking
against sizeof(c.data.bytes) (233) let near-max payloads overflow
pb_encode_to_bytes(), which returns 0 and silently sends an empty
loopback payload. Cap at sizeof(decoded.payload.bytes) minus the
worst-case Compressed framing (meshtastic_Compressed_size - data size)
for both the ciphertext and plaintext paths.

Addresses Copilot review feedback on #10730.

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Run trunk fmt

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-06-17 13:34:29 -05:00
Jonathan Bennett 7424631a27 Beta fixes (#10728)
* Wipe message Store on factory reset

* Check for destination 0 in a new message, and convert to broadcast

* Make sure CHARGE_LED_state gets turned off, to avoid stuck LEDs

* Take the spiLock in MessageStore when clearing messages

* Trunk

* Add thinknode M5 voltage curve

* Fix the oops
2026-06-17 13:29:20 -05:00
Ben MeadorsGitHubcopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>Copilot Autofix powered by AI
d878c81ce8 Clamp position precision on public / known-keys (#10665)
* Clamp position precision on public / known-keys (Compliance)

* Fix review comments: bounds check, all-channel precision clamp, disabled channel guard

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Refactor position precision comments for clarity and update channel configuration handling in AdminModule

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-06-14 18:53:18 -05:00