Commit Graph
100 Commits
Author SHA1 Message Date
Ben MeadorsandGitHub b08c04f264 Add Meshnology W12 (WiFi LoRa 32 V5) LR2021 board support (#10910)
* Add Meshnology W12 (WiFi LoRa 32 V5) variant with LR2021 radio

ESP32-S3R8 + 16 MB flash + Semtech LR2021 dual-band LoRa + SSD1315 OLED,
Heltec V3-style pinout.

Pin map comes from the vendor pin sheet and vendor Arduino demos, confirmed
on hardware: I2C scan finds the OLED at 0x3C on SDA17/SCL18, and the LR2021
answers on NSS=8 SCK=9 MISO=11 MOSI=10 with BUSY=13/NRESET=12 verified by
reset-pulse probing.

The IRQ line needs LR2021_IRQ_DIO_NUM 8. RadioLib defaults irqDioNum to 5,
but DIO5 is not bonded out on this board - the two IRQ lines are DIO7 -> IO7
and DIO8 -> IO14, established by driving each radio DIO as a GPIO output and
watching which ESP32 pin followed. Left at the default, every interrupt is
routed to a floating pin: RX/TX still appear to work because
RadioLibInterface::pollMissedIrqs() falls back to polling the IRQ status
register, but the blocking scanChannel() waits on the pin itself and never
returns.

A shadow pins_arduino.h is needed because the generic esp32s3 one defines
RGB_BUILTIN, which pulls the Arduino RMT HAL into the link and fails against
this build's FreeRTOS config.

* Address review: add W12 variant metadata, trim comments

Declares the custom_meshtastic_* metadata the build manifest expects. Without
it the emitted .mt.json carried no hwModel, slug, display name or support
level, so the board was invisible to the flasher - and board_level = extra
kept it out of PR builds entirely, which is why it never appeared in this
PR's board list. It now matches the W10 sibling at board_level = pr with
support level 1. hwModel stays 255/PRIVATE_HW until a W12 enum lands in the
protobufs.

Also trims the header comments to the one-or-two-line limit in AGENTS.md;
the vendor pin sheet and probing evidence live in the PR description.
2026-07-24 03:29:44 +00:00
Ben Meadors 876962e8f2 Protos 2026-07-23 06:49:09 -05:00
Ben MeadorsandGitHub d9150e8dc7 ci: warn and skip the pio check job on transient network failures (#11145)
* ci: warn and skip the pio check job on transient network failures

The `check` matrix job intermittently fails before it analyses anything:
`pio check` has to fetch the platform package first, and that download gets
reset mid-flight by the CDN often enough to be a nuisance. Three of the last
~200 CI runs died this way, all identical and all ~1s into the run:

    Checking station-g3 > cppcheck (board: station-g3; platform:
    https://github.com/pioarduino/platform-espressif32/.../platform-espressif32.zip)
    requests.exceptions.ConnectionError: ('Connection aborted.',
        ConnectionResetError(104, 'Connection reset by peer'))

cppcheck never ran, so there is no signal at all - just a red X someone has to
re-run by hand.

`pio check` exits 1 for both a dead socket and a real defect, so only the output
can tell them apart. check-all.sh now tees the run and classifies the failure:

  * cppcheck reported something (summary table or a `file:line: [severity]`
    defect line) -> real, propagate the exit status. This is checked first, so
    no amount of network noise elsewhere in the log can mask a genuine defect.
  * transport-level error and nothing else -> log a ::warning:: annotation
    naming the board and the underlying error, and exit 0 under CI (exit 2
    locally, so a local run never quietly reports itself clean).
  * anything else -> propagate the exit status.

404 / 403 / UnknownPackageError are deliberately not treated as transient: a
missing or forbidden package is a reproducible break from a bad pin in a .ini,
not weather.

No workflow change is needed - gh-action-firmware's entrypoint runs
/workspace/bin/check-all.sh for MT_TARGET=check, so the whole fix lives in this
repo and applies to local runs too.

Verified by replaying the captured CI logs through the script with a stubbed
`pio`: the station-g3 connection-reset log skips with a warning under CI and
exits 2 locally, the rak11200 `Total 1 0 0` defect log still fails, that same
defect log with the reset traceback appended still fails, and a 404 still fails.

* ci: pass board names to pio as an argument array

Addresses CodeRabbit review feedback on the previous commit (and the SC2086 the
line has carried since it was written): BOARDS was a space-joined string and
$CHECK was expanded unquoted, so a board override containing whitespace or a
glob character could turn into extra pio arguments.

BOARDS and CHECK are arrays now, and pio is invoked with "${CHECK[@]}", so board
names reach it as literal arguments. The two skip messages use "${BOARDS[*]}" -
a bare "${BOARDS}" would have quietly named only the first board.

shellcheck -x is clean on the file. Re-verified with a stubbed pio that records
its argv: 'tb*' and 'tbeam extra' each arrive as one literal argument, the
default list still expands to 15 -e pairs, a two-board skip names both boards,
and all six classification cases (network/CI, network/local, real defects,
defects with a reset traceback appended, 404, clean pass) are unchanged.
2026-07-23 06:25:18 -05:00
efc9708236 Add userpref for the packet signature policy default (#11144)
Co-authored-by: Austin <vidplace7@gmail.com>
2026-07-23 10:05:02 +02:00
Ben MeadorsandGitHub 8a9980576f Merge pull request #11154 from RCGV1/codex/fix-packet-policy-excluded-test
test: cover packet policy coercion on excluded builds
2026-07-22 15:52:27 -05:00
Ben MeadorsandGitHub 1804fd1886 Merge pull request #11150 from meshtastic/always-exclude-rangetest
Always exclude range test
2026-07-22 12:20:06 -05:00
Ben Meadors cad3a3a5c7 Always exclude range test
Range test is excluded on every target as of 2.8, so set
MESHTASTIC_EXCLUDE_RANGETEST=1 globally in [env] build_flags and drop the
now-redundant per-variant flags from esp32, rak11200 and stm32.

Guard RangeTestModule.cpp with the same condition used in Modules.cpp so the
translation unit compiles to nothing rather than relying on --gc-sections to
strip it, and always report RANGETEST_CONFIG in the device metadata
excluded_modules bitmask so clients hide the config.
2026-07-22 10:25:54 -05:00
Ben MeadorsandGitHub 9662c818b5 Merge pull request #11148 from NomDeTom/flash-clawback
claw back some space from signing policies
2026-07-22 08:54:23 -05:00
Ben MeadorsandGitHub f90c1340de Merge pull request #11117 from NomDeTom/nailed-test-number
Nailed native test numbers so PRs don't forget to increment
2026-07-22 06:06:08 -05:00
Ben MeadorsandGitHub c263a9e4a3 Merge pull request #11136 from meshtastic/renovate/rak13800-w5100s-1.x
Update RAK13800-W5100S to v1.0.3
2026-07-21 19:40:27 -05:00
Ben MeadorsandGitHub 1d6f8a8f34 Merge pull request #11130 from meshtastic/ci-native-tests-by-area
Run native PlatformIO tests by area for readable failures
2026-07-21 19:39:58 -05:00
Ben MeadorsandGitHub 5e3d1ea606 Merge pull request #11129 from RCGV1/agent/include-statusmessage-config
phoneapi: send status message config
2026-07-21 19:37:35 -05:00
Ben MeadorsandGitHub c6669a4ed8 Merge pull request #11128 from RCGV1/agent/keyverification-allocation-safety
keyverification: handle response allocation failure
2026-07-21 19:36:19 -05:00
Ben MeadorsandGitHub 6e37b53324 Merge pull request #11134 from RCGV1/codex/verify-packet-auth-config-docker
fix(security): round-trip packet policy defaults
2026-07-21 17:50:43 -05:00
Ben MeadorsandGitHub 72aac12b8d Merge branch 'develop' into agent/keyverification-allocation-safety 2026-07-21 16:35:38 -05:00
Ben MeadorsandGitHub df971c2921 Merge branch 'develop' into agent/include-statusmessage-config 2026-07-21 16:34:50 -05:00
Ben MeadorsandGitHub 6e48fac741 Merge pull request #11125 from meshtastic/baseui_fixindicator 2026-07-21 13:50:34 -05:00
Ben MeadorsandGitHub 5548bd3195 Merge pull request #10967 from RCGV1/codex/packet-auth-policy 2026-07-21 11:32:40 -05:00
Ben MeadorsandGitHub fdb7668c15 Merge pull request #11124 from meshtastic/fix/admin-pin-tests-simradio 2026-07-21 11:31:44 -05:00
Ben Meadors 6fc7c1b1db test: unset force_simradio so admin request pinning is exercised
The native test harness boots Portduino in simulated mode, and
wouldEncryptWithPKC() short-circuits to false whenever
portduino_config.force_simradio is set. noteOutgoingAdminRequest() derives its
pin from that predicate:

    keyValid = haveDestKey && wouldEncryptWithPKC(&p, p.channel, haveDestKey);

so under the harness no outgoing admin request is ever key-pinned, and
responseIsSolicited() admits a plaintext response to a PKC-pinned request.
test_pinned_request_keeps_its_key_after_an_unpinned_request and
test_request_to_keyed_node_pins_the_stored_key have therefore failed since
#11092 added them, on develop and on every branch that merges it.

Instrumenting the predicate shows every other term already satisfied
(haveDestKey=1, isFromUs=1, private_key=32, unicast, ADMIN_APP, channel
LongFast) with wouldEncryptWithPKC=0, leaving only the simradio guard.

Clear the flag in setUp so the fixture models a real device. Skipping PKC under
force_simradio is correct for a simulated radio - there is no PKC to pin - so
the predicate is left alone rather than relaxed to make a test pass. The only
sim-mode path in this suite is AdminModule's exit_simulator intercept, which no
test here exercises.

Native suite: 38 suites, 723/723 cases, no sanitizer findings.
2026-07-21 10:49:35 -05:00
Ben MeadorsandGitHub a58daf015d Don't misdetect the ES7210 audio codec as an INA219 (#11120)
The T-Deck's ES7210 audio ADC straps to 0x40, which is INA_ADDR. It
answers none of the INA226/INA260/SHT2X checks, so it fell into the bare
"assume INA219" fallback and PowerTelemetry then drove an audio codec as
a power monitor, panicking the board into a boot loop.

The INA219 has no manufacturer or die ID register, so it can only ever be
inferred. Positively identify the ES7210 instead, via its chip ID
registers 0x3D/0x3E (0x72/0x10), and leave the device unclaimed.

Also guard the INA219 fallback on the type rather than chaining it to the
SHT2X check: that else bound to the SHT2X if, so a positively identified
INA226/INA260 was overwritten with INA219 right after being found.

Fixes #11115
2026-07-21 07:24:01 -05:00
Ben MeadorsandGitHub e11635baa2 Merge branch 'develop' into codex/packet-auth-policy 2026-07-21 07:18:02 -05:00
Ben Meadors 7d954e668d Merge branch 'develop' into codex/packet-auth-policy
Resolve conflicts against the NodeDB signer/key primitives (#11050) and the
admin-key PKI decrypt budget (#11100).

- NodeDB: drop this branch's hasSeenXeddsaSigner in favour of develop's
  isKnownXeddsaSigner. They answer the same question, but develop's reads the
  dedicated warm signer bit (warmSignerOf) rather than the WarmProtected
  category, and TrafficManagementModule already depends on it. Keep develop's
  copyPublicKey/copyPublicKeyAuthoritative, isVerifiedSignerForKey and
  commitRemoteKey/KeyCommitTrust.
- checkXeddsaReceivePolicy: keep this branch's Strict/Balanced/Compatible
  policy, which is a superset of develop's balanced-only downgrade gate, and
  call isKnownXeddsaSigner from it. develop's !pki_encrypted term is dropped
  because the policy returns early for PKI packets before that check.
- perhapsDecode: keep develop's key resolution (NodeDB then pending-key, only
  for real PKI candidates) plus its admin-key token bucket, and re-apply this
  branch's pkiAttempted flag feeding the DECODE_OPAQUE verdict. Keep both
  passesRoutingAuthGate and adminKeyFallbackAllowed/Refund.
- test_A17: model eviction the way NodeDB actually does it, passing the warm
  signer bit as well as the XeddsaSigner category, since isKnownXeddsaSigner
  reads the former.

Native suite: 38 suites, 743/743 cases, no sanitizer findings.
2026-07-21 06:09:57 -05:00
Ben Meadors e247f70c6d test: drain toPhoneQueue in MQTT test to fix ASan leak
sendLocal() now dispatches local packets through handleReceived() directly
instead of the mock-overridden enqueueReceivedMessage(). The MQTT implicit
ACK-to-self therefore runs the full receive pipeline (RoutingModule ->
MeshService::handleFromRadio -> sendToPhone), enqueuing pooled MeshPacket
copies into toPhoneQueue. Production drains that queue via PhoneAPI, but the
test has no phone reader, so the copies leaked at teardown (LeakSanitizer:
42824 bytes / 101 objects across test_receiveFuzzServiceEnvelope and
test_receiveAcksOwnSentMessages).

Give MockMeshService a destructor that drains toPhoneQueue like the phone
would. Test-only; the real firmware does not leak here.
2026-07-21 05:27:23 -05:00
Ben MeadorsandGitHub 0165e914a9 fix: gate module replies by request port (#11094) 2026-07-21 10:19:38 +02:00
Ben MeadorsandGitHub 8b0f29026a fix: include board PSRAM class in the HybridCompile sdkconfig cache key (#11112) 2026-07-21 08:58:28 +02:00
Ben Meadors 1317176f78 test: accept DECODE_OPAQUE/DECODE_POLICY_REJECT in perhapsDecode fuzz assertion
The packet-auth-policy change extends the DecodeState enum with DECODE_OPAQUE
and DECODE_POLICY_REJECT. test_E1_perhaps_decode_fuzz drives arbitrary
ciphertext through perhapsDecode and asserted the verdict was one of the
original three states; random ciphertext that matches no channel with no PKI
attempt now returns DECODE_OPAQUE, tripping the assertion. Broaden the check to
accept all five valid verdicts, matching the test's stated 'any verdict is
fine' contract.
2026-07-20 19:12:28 -05:00
Ben MeadorsandGitHub beb9a3095d Merge branch 'develop' into codex/packet-auth-policy 2026-07-20 17:43:32 -05:00
Ben MeadorsandGitHub 5cf346311c fix: don't wipe admin keys when regenerating the keypair (#11088)
A client's "regenerate keys" action sends a blank SecurityConfig carrying
only the new private key rather than the config it read from the device,
so assigning it wholesale cleared admin_key, is_managed, serial_enabled,
debug_log_api_enabled, admin_channel_enabled and packet_signature_policy.
Losing the admin keys locks the owner out of remote admin with no recourse
but a physical connection to the node.

Detect that bare-rotation shape and swap in just the keypair, leaving the
rest of the security config intact. Deliberately clearing admin keys still
works through a SET that leaves the private key alone.

Fixes #11073
2026-07-20 07:19:29 -05:00
Ben MeadorsandGitHub 829ff80a09 fix(t5s3-epaper): 40MHz octal PSRAM; guard EInk init; esp32s3 sw-atomic shim (#11062)
- t5s3-epaper: the board's 3.3V octal (AP_3v3) PSRAM is unreliable at the qio_opi
  default of 80MHz (Total PSRAM reads 0, display dead / boot-loop); clock it at 40MHz.
- EInkParallelDisplay: skip EInk init and no-op display ops when the PSRAM framebuffer
  is unavailable, so the node boots headless instead of hard-faulting on a NULL buffer.
- src/platform/esp32: weak software __atomic_*_{1,2,4} so esp32s3 links on toolchains
  (e.g. macOS) that lack the sized libcalls GCC emits under -mdisable-hardware-atomics.
2026-07-20 12:13:50 +02:00
Ben Meadors 4726375282 Update protos 2026-07-19 06:55:26 -05:00
b20d89974a Stop accelerometer thread when double-tap/wake-on-motion disabled at runtime (#11025)
* Stop accelerometer thread when double-tap/wake-on-motion disabled at runtime

double_tap_as_button_press and wake_on_tap_or_motion are applied live: the
OFF->ON edge calls accelerometerThread->start(), but there was no ON->OFF
branch, so turning either flag off left the sensor thread running (polling
I2C, drawing power) until reboot. Worse, because enabled stayed true, a later
OFF->ON edge was a no-op (the enabled==false guard blocked re-start), leaving
the feature un-restartable without a reboot.

Add the symmetric ON->OFF branch in both handlers. When a flag goes true->off
and the other consumer of the shared thread is also off, call
accelerometerThread->disable() (stops runOnce polling and clears enabled so a
later re-enable can start() again). Each branch checks the other flag first so
disabling one feature never stops the sensor while the other still needs it.

* Keep accelerometer thread running when its sensor drives the compass

* Guard accelerometer thread config toggles against a null thread pointer

* AdminModule: factor shared accelerometer start/stop into a helper

The device and display config handlers had mirror-image blocks reconciling the
shared accelerometer thread. Extract reconcileAccelerometerThread(wasOn, nowOn,
otherFeatureOn) so the null guard, edge logic, compass (providesHeading) guard,
and rationale live in one place; each call site is now a single call. Behavior
is unchanged. Also drops the redundant per-field assignment that the
whole-struct `config.device = ...` / `config.display = ...` overwrites anyway.

---------

Co-authored-by: Thomas Göttgens <tgoettgens@gmail.com>
2026-07-18 06:26:20 -05:00
Ben Meadors 6e0e9c174d Protobufs 2026-07-17 09:45:56 -05:00
Ben MeadorsandGitHub f6f3284cfc fix(lockdown): re-lock serial console admin auth on USB link drop (#11028)
On lockdown builds (MESHTASTIC_PHONEAPI_ACCESS_CONTROL, nRF52) the SerialConsole
is a process-lifetime singleton, so the per-connection admin-auth slot keyed by
its inherited PhoneAPI* is reused for every USB/serial client for the whole boot.
An operator's admin unlock stayed latched across serial client swaps: an attacker
plugging into the USB/serial port before the prior session's 15-minute inactivity
timeout inherited admin authorization -- the serial analog of the BLE stale-session
reuse bug closed by resetting state in onConnect()/onDisconnect().

Sample the USB-CDC host link (DTR/mount) state each runOnce(); on the link-drop
edge call close(), which frees the auth slot and resets PhoneAPI state so whoever
connects next re-locks via handleStartConfig()'s !isConnected() branch on their
first want_config -- the same physical-link boundary BLE enforces in onConnect().
On the nRF52 TinyUSB (Adafruit) core, (bool)Port == tud_cdc_n_connected(), which
goes false on cable unplug or host port-close. Console transports without a real
DTR line fall back to the existing inactivity timeout, no worse than before.

Entirely nRF52-lockdown-gated; non-lockdown builds are byte-identical.
2026-07-17 07:05:11 -05:00
Ben MeadorsandGitHub 5cf85d523a fix: heap leaks on display wake and BLE re-enable; remove dead PacketCache (#11019)
* fix(display): don't leak a TFT driver instance on every screen wake

On variants whose Screen::handleSetOn() re-runs ui->init() on each display
wake (Heltec Tracker V1.x/V2, VTFT_LEDA and ST7796 boards, MUZI/Cardputer
via dispdev->init()), OLEDDisplay::init() unconditionally re-invokes
connect(), and TFTDisplay::connect() allocated a fresh driver instance
each time without freeing the old one. Each OFF->ON transition orphaned a
full LGFX device (measured: exactly 1,260 bytes per wake on a Heltec
Wireless Tracker V2). Since PowerFSM wakes the screen on every received
text message, a device on a busy channel leaked tens of KB per day -
matching field reports of heap climbing from 78% to 92% within a day.

Null-guard the driver allocations (matching the existing linePixelBuffer/
repaintChunkBuffer guards below them) so re-entry re-runs tft->init() on
the existing instance. Bench-validated on a Tracker V2: free heap now flat
across 13 consecutive wake cycles, with the panel still re-initializing
and rendering on every wake.

* fix(ble): don't leak BluetoothPhoneAPI and callbacks on BLE re-enable

NimbleBluetooth::setup()/setupService() re-run when Bluetooth is
re-enabled after deinit(), but BLEDevice::deinit() only frees the GATT
objects - the caller-owned allocations were re-created unguarded on every
cycle:

- bluetoothPhoneAPI: ~3.5KB PhoneAPI object per cycle, and the orphaned
  instance is an OSThread that stays registered with the scheduler, so a
  duplicate thread kept servicing the same static queues
- toRadioCallbacks / fromRadioCallbacks / security + server callbacks:
  one small object each per cycle
- the BLESecurity shim was heap-allocated and never freed even on first
  boot; it only forwards to static setters, so use a stack instance

Reuse the existing objects on re-setup; the setCallbacks() calls still
run every time since the characteristics themselves are new.

* chore: remove unused PacketCache

PacketCache landed in #8341 but no consumer was ever wired up - repo-wide,
the only references to PacketCache/packetCache are in its own two files.
As designed it also malloc()s per cached packet with no eviction or size
cap, so it should be re-reviewed for bounds before any future use. Remove
the dead code; git history preserves it if a bounded revival is wanted.

* fix(ble): reset stale session state when reusing BluetoothPhoneAPI

Review follow-up: reusing bluetoothPhoneAPI across BLE enable cycles could
hand the next session the previous session's dirty state. The only full
cleanup path (onDisconnect) is skipped when deinit()'s bounded disconnect
wait expires before the event lands (its 2s cap matches the connection
supervision timeout, so a phone that walked out of range makes this a
coin flip): the reused object then enters the next session mid-config,
with stale queue contents served to the new phone and a stale connection
handle that defeats the checkConnectionTimeout self-heal.

Factor onDisconnect's cleanup into resetBleSessionState() and run it from
setupService() when reusing the instance, restoring the old fresh-object
invariant. Also switch the four stateless callback objects to function-
local statics (the resolved framework BLE wrapper stores plain pointers
and never frees them, so static instances are safe and avoid the guarded
heap allocations), correct the comment that claimed deinit() frees the
GATT objects (it deletes only the BLEServer itself; the services and
characteristics it created remain a small library-side leak per cycle),
and fix the inverted connection check in getRssi() that made BLE RSSI
always read 0 on ESP32-S3/C6.

* refactor(display): construct-once guard around the whole driver ladder

Review follow-up: one if (!tft) around the #if/#elif/#else construction
block instead of a guard per branch, so a future display family can't
reintroduce the per-wake leak by copying an unguarded branch. Make the
HACKADAY bus pointer local to the construction (it was a write-only
global), and point the comment at the Screen::handleSetOn gates instead
of hand-listing boards that would go stale.

* fix(ble): use-after-free notifying a freed characteristic after deinit()

deinit() nulled bleServer and BatteryCharacteristic but left
fromNumCharacteristic dangling after BLEDevice::deinit(true) freed the
GATT graph, and it never detached the PhoneAPI fromNumChanged observer.
When deinit()'s bounded 2s disconnect wait expires before onDisconnect
runs (the same stale-bond / host-reset race resetBleSessionState was
built for), close() is skipped, so the observer stays attached with
state == STATE_SEND_PACKETS. After BLE is off, the next mesh packet
drives MeshService fromNumChanged -> PhoneAPI::onNotify -> onNowHasData
-> fromNumCharacteristic->notify() on freed memory (the framework BLE
wrapper's notify() dereferences the freed server via getConnectedCount).
Unlike sendLog(), onNowHasData() had no isConnected guard.

deinit() now calls resetBleSessionState() to detach the observer and
reset session state unconditionally (also forcing the conn handle to
NONE so checkConnectionTimeout can't be fooled by the stale handle),
nulls fromNumCharacteristic/logRadioCharacteristic like the other freed
pointers, and onNowHasData() bails on a null characteristic.

Reachable on all ESP32/S3/C6 NimBLE boards via admin disable-bluetooth
or a sleep transition while a phone is connected. Found by a follow-up
lifecycle audit of the re-enable changes in this PR.
2026-07-16 15:13:26 -05:00
Ben MeadorsandGitHub 43084873a6 nRF52 BLE: restore pairing passkey callback on re-enable (#11027)
shutdown() installs onUnwantedPairing to actively refuse pairing (used on
the factory-reset / BT-disable teardown path), but the correct callback
(onPairingPasskey) is installed only in setup(). Re-enabling BLE on an
already-constructed nrf52Bluetooth goes through resumeAdvertising(), not
setup() (main-nrf52.cpp), which only re-armed advertising and never
restored the pairing callback. So a shutdown()->resumeAdvertising() cycle
without a reboot left the device silently refusing all pairing until the
next reboot.

Restore the correct pairing passkey callback in resumeAdvertising(),
guarded by the same config.bluetooth.mode check setup() uses. Only PIN
modes drive a passkey-display callback; NO_PIN (Just Works) never invokes
it, so no restore is needed there.

Reachable today only via the PowerStress module's BT_OFF/BT_ON opcodes
(normal runtime BT-disable paths reboot), so low severity, but a real
teardown-latches / re-enable-doesn't-restore asymmetry.
2026-07-16 13:02:43 -05:00
Ben MeadorsandGitHub f7fd058308 Fix packet-pool slot leak in canned message destination picker (#11017)
updateDestinationSelectionList() allocated a MeshPacket via
allocDataPacket() that was never sent or released, permanently
consuming one packetPool slot every time the destination-selection
picker was rebuilt.

On non-PSRAM targets packetPool is a static 70-slot BSS pool, so
repeated picker use exhausts it and eventually blocks all packet
allocation (TX/RX failures). On PSRAM/portduino (MemoryDynamic)
targets it is a true heap leak of ~424B per rebuild.

The allocation and its two field writes (pki_encrypted, channel) were
a copy/paste artifact of the PKI setup in sendText() and had no effect
in this function. Remove the dead allocation.
2026-07-16 08:22:42 -05:00
Ben MeadorsandGitHub 0902c13473 feat: build-flagged frame injection into the RX pipeline for testing (#11011)
MeshService::injectAsReceived (gated by MESHTASTIC_ENABLE_FRAME_INJECTION, off by default) extends
the portduino SimRadio SIMULATOR_APP path to real hardware: a client-supplied frame, wrapped in a
Compressed envelope on the SIMULATOR_APP portnum, is delivered through the real receive pipeline
(router->enqueueReceivedMessage) as if it arrived off the LoRa chip. It therefore exercises from!=0
enforcement, channel/PKC decryption, remote-admin authorization, and hop/dedup/module dispatch -
paths the toRadio API cannot reach (it forces from=0). from==0 is dropped to match real RX.

This forges over-the-air traffic, so the flag must never ship enabled. Drive it with the
meshtastic-mcp inject_frame tool / cli/meshinject.py. Documents the technique in the agent-facing
copilot-instructions.md + AGENTS.md.
2026-07-14 20:48:54 -05:00
Ben MeadorsandGitHub c5355641d3 Add MEDIUM_TURBO modem preset (#10988)
* Protobufs

* Wire up MEDIUM_TURBO modem preset

MEDIUM_TURBO (500 kHz, SF9, CR 4/5) already existed in the protobuf enum but
was never wired into firmware, so selecting it silently fell through to the
LONG_FAST default and rendered an "Invalid" display name.

Add its bw/sf/cr mapping (modemPresetToParams), display name (MediumTurbo/MedT),
PRESETS_STD membership (standard regions only — 500 kHz does not fit EU868's
250 kHz band, so it stays out of PRESETS_EU_868 and is rejected/clamped there),
and the MEDIUM SNR-grading bucket. Includes positive coverage in test_radio,
EU868-reject + US-accept coverage in test_admin_radio and test_mesh_beacon,
the STD preset count 9->10, an extended fuzz range, and the client-spec doc.

* Address review feedback on MEDIUM_TURBO tests

- test_mesh_beacon: assert has_mesh_beacon before checking the invalid preset was
  cleared, so the EU868-cleared test can't pass on a dropped message (matches the
  existing SHORT_TURBO test).
- test_fuzz_packets: draw modem presets from _ModemPreset_ARRAYSIZE instead of a
  hard-coded 17 so the fuzz range tracks future enum additions automatically.
2026-07-11 08:24:35 -05:00
Ben MeadorsandGitHub 8abae90838 Give the 4MB T3-S3 boards an app partition that fits (#10978)
* fix(device-install): flash littlefs at the offset from firmware metadata

device-install.sh read the spiffs offset out of the .mt.json metadata into
SPIFFS_OFFSET, but flashed the littlefs image at $OFFSET -- a separate
variable still holding the hardcoded 0x300000 default. The metadata value
was never used, so any board with a non-default partition table had its
filesystem written to the wrong place.

Unify on SPIFFS_OFFSET, and guard both metadata overrides so a table that
omits ota_1 or spiffs falls back to the built-in default instead of handing
esptool an empty offset. Quote both offsets at the call site: the spiffs one
is newly consumed from jq, and a multi-line result would otherwise word-split
into esptool's argv and silently mis-pair address with file.

device-install.bat already does all of this correctly; this brings the shell
script to parity.

* fix(tlora-t3s3): give the 4MB T3-S3 boards an app partition that fits

tlora-t3s3-v1 and tlora-t3s3-epaper have overflowed the shared 4MB
partition-table.csv app slot (ota_0 = 0x250000 = 2,424,832 bytes) on every
develop build since 22072c5f4 (2026-06-20). The last green build,
ca7d82629, cleared it by 881 bytes; develop is now ~38.7KB over. These envs
have no board_level, so they only build in the full matrix on push to
develop -- which is why no PR ever caught it.

Add partition-table-t3s3.csv, dedicated to the 4MB ESP32-S3FH4R2 T3-S3
boards, and point all three t3s3 envs at it:

  app      ota_0  0x010000  0x290000   (was 0x250000, +256KB)
  flashApp ota_1  0x2A0000  0x0A0000   (unchanged size)
  spiffs          0x340000  0x0C0000   (was 0x100000, -256KB)

The headroom comes out of spiffs, not flashApp: the unified BLE OTA image is
636,544 bytes against a 655,360 byte slot, so ota_1 has nothing to give. The
table is contiguous, ends exactly on the 4MB boundary, keeps both app
partitions 64KB-aligned, and leaves the littlefs image these boards ship well
inside the remaining 768KB.

Verified with a Docker build of all three envs:

  tlora-t3s3-v1             2,463,504  91.7%
  tlora-t3s3-epaper         2,458,487  91.5%
  tlora-t3s3-epaper-inkhud  2,400,851  89.4%

Trimming was considered and rejected. The entire emoji set is 6,304 bytes
(measured A/B build), and cutting traffic management plus the paxcounter,
storeforward, rangetest and atak modules recovers 46,636 -- enough to land at
99.7% full, i.e. weeks of headroom at develop's observed growth rate, in
exchange for shipping a feature-reduced board. The slot was simply mis-sized
for a board this full.

Note this changes flash layout: existing T3-S3 units must be erased and
factory-flashed once to pick up the new offsets. They already cannot run a
current develop build, so no working configuration regresses.
2026-07-10 10:18:09 -05:00
Ben MeadorsandGitHub b3ddeca9d0 ci: nightly develop build published to github.io firmware-nightly/ (#10957)
Adds a scheduled (09:00 UTC) run of the CI build off develop that does
everything a workflow_dispatch build does except create a GitHub release.
Instead it refreshes a single, stable firmware-nightly/ folder on
meshtastic.github.io with the current develop build, leaving that folder's
hand-maintained release_notes.md untouched so it can be edited manually.

On a nightly run (the schedule, or a manual nightly=true dispatch) only the
firmware build + gather-artifacts + the new publish-nightly job run; the
tests/docker/wasm/macOS/debian-src/size jobs and the three release jobs are
skipped, so no release or tag is created.

publish-nightly downloads the per-board artifacts, generates the
firmware-<version>.json release manifest and an index.json version pointer
(read by the web-flasher), then publishes to firmware-nightly/ via the same
peaceiris/actions-gh-pages action used for releases. keep_files:false is scoped
to destination_dir, so it clears stale nightly binaries while leaving sibling
release folders untouched; the hand-maintained release_notes.md is fetched and
carried forward first (fail-closed) so it is never clobbered.
2026-07-09 11:52:30 -05:00
Ben Meadors 515fe8b94f Clamp bw for malformed use_preset false with empty bandwidth 2026-07-09 10:30:11 -05:00
Ben MeadorsandGitHub 168f669e61 esp32: fix NimBLE use-after-free crash when disabling bluetooth while connected (#10950)
* esp32: fix NimBLE use-after-free crash when disabling bluetooth while connected

Saving certain configs over BLE (e.g. MQTT module config) calls
disableBluetooth() -> NimbleBluetooth::deinit() while the phone is still
connected. deinit() called BLEDevice::deinit(true) straight away, which
deletes the BLEServer before nimble_port_stop(). Stopping the NimBLE
port with a live connection makes the host synthesize unsubscribe events
via ble_gatts_connection_broken() and dispatch them into the freed
BLEServer -> LoadProhibited panic in BLEServer::handleGATTServerEvent
(iterating server->m_notifyChrVec on freed memory).

Backtrace tail:
  BLEServer::handleGATTServerEvent (SUBSCRIBE)
  <- ble_gatts_subscribe_event <- ble_gatts_connection_broken
  <- ble_gap_conn_broken <- ble_gap_rx_disconn_complete

It was reliably preceded by our onRead callback pinning the NimBLE host
task in its up-to-20s polling loop ('BLE onRead: timeout after 19920 ms,
4000 tries'), so the host task couldn't process the teardown until long
after the objects under it were freed.

Fix: drain before demolition in deinit(). Clear
onReadCallbackIsWaitingForData so an in-flight onRead returns
immediately; disconnect the peer and wait (bounded, ~2s) for the host
task to run onDisconnect (which clears nimbleBluetoothConnHandle) so the
unsubscribe/GATT teardown lands on the still-live server; only then
BLEDevice::deinit(true). isDeInit is raised after the drain (onDisconnect
early-returns when set and would otherwise never clear the handle), the
deferred advertising restart is dropped (the stack is going away), and
the now-dangling bleServer pointer is nulled.

deinit() runs on the main task, so waiting on the host task's callbacks
is safe; the waits are bounded regardless.

Upstream ordering bug (delete BLEServer before nimble_port_stop) to be
reported to espressif/arduino-esp32 separately.

* esp32: gate onRead during BLE teardown and use Throttle for drain wait

Addresses review feedback on the NimBLE deinit UAF fix:

- Set a bleDraining flag before disconnecting so onRead bails immediately
  instead of arming the up-to-20s wait. Without this, a read that re-arms
  after deinit() clears onReadCallbackIsWaitingForData could pin the NimBLE
  host task, block onDisconnect from clearing the conn handle, and make the
  2s drain expire. onRead now skips the wait when draining, and the wait
  loop also observes the flag so an in-flight read is released.
- Use Throttle::isWithinTimespanMs() for the bounded drain wait instead of
  a raw millis() subtraction, per coding guidelines.

* esp32: reset BLE teardown guards on re-init

deinit() latches bleDraining (new) and isDeInit (pre-existing) as teardown
guards, but setup() never cleared them. AdminModule's disable-bluetooth admin
command calls deinit() directly without a reboot, and PowerFSM can re-enable
via setBluetoothEnable(true) -> setup() on the same boot (isActive() is false
because deinit() nulls bleServer). Without resetting the flags, the re-initialized
stack has onRead permanently bailing on the drain path (empty reads) and
onDisconnect early-returning without clearing the connection handle.

Clear both flags at the top of setup() so a re-init recovers cleanly.
2026-07-09 09:42:35 -05:00
Ben MeadorsandGitHub b4b1ece8f1 nrf52: fix loop-task stack overflow causing reset on BLE pairing/first-sync (#10944)
The Arduino loop task runs Meshtastic's entire cooperative OSThread
scheduler on the Adafruit core's stock 4 KB (1024-word) stack. The 2.8
first-sync path - AdminModule::handleSetConfig -> saveChanges ->
configChanged -> RadioInterface::reloadConfig -> LR11x0 reconfigure ->
SPI Lock::lock, with vsnprintf/USB-CDC logging frames stacked on top -
overflows it.

SWD fault capture on a T1000-E (pyocd vector_catch=h) proved it: the
loop task's SP was driven 40 bytes BELOW its own pxStack base, the
stack paint was consumed to the floor, and the crash was a BusFault in
xQueueSemaphoreTake dereferencing a semaphore handle that adjacent-heap
corruption had overwritten with log text (0x3f3f207c = "| ??"). The
core's HardFault handler is a bare NVIC_SystemReset, so in the field
this presents as a silent reboot ~seconds after a phone pairs (or a 30 s
zombie hang -> supervision timeout 0x8 when the corruption lands on task
TCBs instead). Also explains the set_time_only -> immediate-NodeInfo
crash variant reported on empty-DB nodes: same task, different deep
chain. 2.7.26 is unaffected because the deep frames (XEdDSA, satellite
map conversion, replay engine) did not exist.

Fix: set -DLOOP_STACK_SZ=2048 (words = 8 KB) for all nrf52840 targets.
Requires the #ifndef guard from meshtastic/Adafruit_nRF52_Arduino#7;
until that merges the flag is a harmless redefinition warning. Validated
on hardware: pairing + full sync completes, stack paint shows healthy
margin under load.

Also fix the boot-time reset-reason log: the core's init() caches
RESETREAS and W1C-clears the register before setup() runs, so the raw
read here has always printed 0. Use readResetReason() instead.
(preFSBegin's RESETREAS==0 gate is intentionally left as-is - its
degenerate GPREGRET-only behavior is what makes lfs recovery work.)
2026-07-08 17:11:32 -05:00
Ben Meadors 9b5b2889e7 Remove triage workflows 2026-07-08 14:44:59 -05:00
Ben MeadorsandGitHub 8cff9e0ac3 Setup renovate for nrf52 arduino platform package (#10946) 2026-07-08 13:57:35 -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
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 24c1dccf00 CI: track RAM (.data+.bss) in size reports and gate on per-env budgets (#10899)
* CI: track RAM (.data+.bss) in size reports and gate on per-env budgets

The 2.8.0 nRF52840 heap regression (99% heap in field reports) shipped
invisibly because CI only tracked flash. On nRF52840 the heap arena is the
linker gap after .bss, so every byte of static .data+.bss growth shrinks the
usable heap 1:1 - RAM needs the same guardrails flash already has.

What's added:

- bin/platformio-custom.py emits ram_bytes (.data + .bss from the ELF, via
  the toolchain size tool) into each .mt.json manifest. Heap/stack
  placeholder sections are deliberately excluded.
- bin/collect_sizes.py records {flash_bytes, ram_bytes} per env;
  bin/size_report.py grows RAM and RAM-delta columns in the PR size report.
  Older artifacts without ram_bytes (and legacy int-schema baselines)
  degrade to "n/a" instead of crashing.
- bin/ram_budgets.json: per-env RAM/flash budgets, enforced only for envs
  listed there. Seeded with rak4631: ram 113,000 (current 110,948 + ~2 KB
  slack), flash 786,000 (current 765,192 + ~20 KB; the app region is
  0x27000..0xEA000 = 798,720 and the image must stay clear of the
  warm-store ring guard in extra_scripts/nrf52_warm_region.py).
- New size-budget-gate CI job runs size_report.py --enforce-budgets and
  fails the build on violation; the informational firmware-size-report job
  now also renders budget usage into the PR comment.
- src/main.cpp: opt-in boot heap watermark (-DMESHTASTIC_HEAP_WATERMARK_CHECK)
  logs LOG_ERROR when less than 20% of the heap is free at the end of
  setup(). Off by default; skipped on platforms without heap accounting.

How budgets are raised: deliberately, never automatically. If a change needs
more headroom, bump the env's limit in bin/ram_budgets.json in the same PR
and justify the increase in the PR description.

Verified: python3 bin/test_size_scripts.py (23/23 pass, including ram_bytes
parsing, n/a fallback, and over/under/missing-env budget-gate cases).

* Address review: fail the budget gate closed, fix RAM section matching

- size-budget-gate workflow: drop continue-on-error on the manifest
  download and the empty-dir fallback, so the job fails when the data it
  gates on cannot be fetched.
- size_report.py --enforce-budgets now fails closed on every
  missing-data path instead of trivially passing: empty collected sizes,
  a budgeted env that was not built, or a manifest without the budgeted
  metric. Report-only mode keeps rendering those as n/a.
- load_budgets() rejects zero/negative/non-integer budgets with a clear
  error (a typo'd budget could previously crash budget_markdown with
  ZeroDivisionError or silently skip the check); the percentage render
  keeps a defensive guard for direct callers.
- compute_ram_bytes(): count RISC-V small-data sections (.sdata/.sbss)
  and exclude ESP-IDF .rtc.* sections, which live outside the
  heap-competing SRAM.
- Trim the heap-watermark comment in main.cpp to two lines.

bin/test_size_scripts.py: 27/27 - the two fail-open assertions are
flipped to fail-closed, with new report-only counterparts plus cases for
empty sizes under enforcement and malformed budgets.
2026-07-06 14:52: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
Ben MeadorsandGitHub 00685a4e61 nrf52840: right-size the SoftDevice RAM reservation (+8 KB heap arena) (#10903)
The app RAM ORIGIN in both nrf52840 linker scripts has been a hard-coded
0x20006000 (24 KB reserved for S140) since they were introduced, but
sd_ble_enable() with our fixed Bluefruit configuration (one peripheral
link, BANDWIDTH_MAX / ATT MTU 247, 0x1000 attribute table) requires
substantially less. The SoftDevice cannot use the gap and the app image
is linked above it, so every byte between the true requirement and the
ORIGIN is simply unusable RAM - on a part where 2.8.0 field reports
showed the heap arena at 99% use.

Lower the ORIGIN to 0x20004000 in both s140 v6 and v7 scripts. The heap
arena is the linker gap on this platform, so the change is worth exactly
+8,192 B of free heap on every nRF52840 board (verified: rak4631 .heap
section 124,572 -> 132,764 B; both v6- and v7-script boards link with
.data at 0x20004000).

Safety net: Bluefruit.begin()'s return value - previously discarded -
is now checked. If a future SoftDevice or Bluefruit config change raises
the requirement past the reservation, the node logs a critical error
instead of silently running without BLE, with instructions to re-measure
via CFG_DEBUG=1 ("SoftDevice's RAM requires: 0x...").
2026-07-06 14:13:18 -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
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
510e9796f9 Extract mcp-server to its own repo (meshtastic/meshtastic-mcp) (#10861)
The Python MCP server + hardware test harness that lived under mcp-server/
now has its own home at https://github.com/meshtastic/meshtastic-mcp
(published, versioned independently). Remove the in-tree copy and wire the
firmware repo to the standalone server externally.

- Delete mcp-server/ (96 files) and the 8 harness-coupled AI workflow files
  under .claude/commands/ and .github/prompts/ that drove ./mcp-server/
  run-tests.sh — those workflows now ship with meshtastic-mcp as skills.
- .mcp.json: register the server via
  `uvx --from git+https://github.com/meshtastic/meshtastic-mcp meshtastic-mcp`
  instead of a local ./mcp-server/.venv, keeping MESHTASTIC_FIRMWARE_ROOT="."
  so the MCP tools still work from this checkout with no local build.
- Repoint the remaining references (AGENTS.md, CLAUDE.md,
  .github/copilot-instructions.md, bin/regen-*.sh, docs, Screen.h,
  userPrefs.jsonc, test/fixtures/nodedb/README.md, .trunk/configs/.bandit)
  at the standalone repo. The MCP tool surface is unchanged — only the
  pytest harness moves out; run it from a meshtastic-mcp checkout with
  MESHTASTIC_FIRMWARE_ROOT pointed here.

No build/CI/platformio coupling existed, so nothing in the firmware build
changes.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 14:33:04 -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
Ben MeadorsandGitHub 23bb55d965 chore(deps): forward-port LovyanGFX + audio-driver renovate bumps from master (#10826)
Two more renovate updates landed on master (2.7) after #10824; reconcile
develop to their values (value reconcile, consistent with #10824):
- LovyanGFX 1.2.21 -> 1.2.24 (#10783) -- 18 lovyan03 custom.pio pins
- pschatzmann arduino-audio-driver v0.2.1 -> v0.3.0 (#10770) -- tlora-pager, tracksenger

Left untouched (deliberate develop-only pins; master's renovate didn't bump them):
- lovyan03/LovyanGFX@1.2.19 (single variant pin)
- mverch67/LovyanGFX fork (seeed-sensecap-indicator variant)
2026-06-30 13:42:59 -05:00
Ben MeadorsandGitHub d8884125b6 chore(deps): forward-port renovate updates from master to develop (#10824)
Reconciles develop to master's latest renovate values for deps bumped on the
2.7 line but never back-merged, so the develop->master 2.8 promotion (#10777)
doesn't regress them. Done as a value reconcile, not a cherry-pick: several
master commits are superseded (5 device-ui bumps, 2 ststm32 bumps), and
develop's stale archive/refs/tags/ URL form actually blocked renovate from
bumping these (the regex expects archive/<version>.zip).

GitHub Actions:
- actions/checkout v6 -> v7 (35 refs)
- actions/cache v5 -> v6
- actions/github-script v8 -> v9 (3 refs)
- actions/stale v10.2.0 -> v10.3.0

Build / platform:
- alpine 3.23 -> 3.24 (alpine.Dockerfile)
- platformio/ststm32 19.5.0 -> 19.7.0
- platformio/nordicnrf52 10.11.0 -> 10.12.0

Libraries:
- Adafruit SSD1306 2.5.16 -> 2.5.17
- SparkFun MMC5983MA v1.1.4 -> v1.1.5
- Sensirion I2C SCD30 1.0.0 -> 1.1.1
- meshtastic esp8266-oled-ssd1306 6bfd1f1 -> 2e26010

Deliberately excluded:
- meshtastic/device-ui digest: coupled to firmware protobuf/API and develop has
  diverged hard (NodeDB v25); left for a separate maintainer bump + visual check.
- libpax: develop uses the mverch67 fork (pinned by Arduino-3.x migration #9122),
  master uses dbinfrago -- a fork divergence, not a version bump; not reconciled.
- platform-native digest: develop already at 61067ac (equal to master).
2026-06-30 12:18:01 -05:00
Ben Meadors 8345c2b6a9 Protos 2026-06-29 08:20:55 -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
Ben Meadors ad132e9540 Protobufs update 2026-06-26 07:19:33 -05:00
Ben MeadorsandGitHub 88e0b78017 Merge pull request #10734 from NomDeTom/copilot-instructions
docs: update test instructions to prefer bin/run-tests.sh;
2026-06-25 07:46:49 -05:00
Ben MeadorsandGitHub 8f425ad3f5 Merge pull request #10776 from jessm33/fix/native-unknown-module
Update native to exit when configured with an unknown module
2026-06-25 05:40:50 -05:00
Ben MeadorsandGitHub 7aa004c1eb Merge pull request #10780 from meshtastic/forward-port-t-impulse-plus
Forward-port: Add Lilygo T-Impulse-Plus (#10497) to develop
2026-06-24 20:11:33 -05:00
Ben MeadorsandGitHub 8e72b16693 Merge pull request #10779 from meshtastic/forward-port-master-fixes
Forward-port master-only fixes to develop
2026-06-24 20:10:58 -05:00
Ben Meadors 41cbb45cdc Fix build on picomputer
(cherry picked from commit 22d08b4303a9bd41872420b35fe523b5a811e4eb)
2026-06-24 16:50:44 -05:00
Ben Meadors 9e3a9c370d Enhance RTC handling with unit test support for system time fallback (#10642)
* Enhance RTC handling with unit test support for system time fallback

* 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>

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
(cherry picked from commit c7f17a80b26a4a0ef151f5c1ba9342e7e1e205ed)
2026-06-24 16:49:29 -05:00
Ben MeadorsandGitHub c4537f2038 Merge branch 'develop' into fix/native-unknown-module 2026-06-24 16:45:36 -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
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
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
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
Ben Meadors 2291b672c4 Protos 2026-06-17 06:14:40 -05:00
Ben Meadors a944e1c908 Update security documentation with detailed cryptographic mechanisms and known limitations 2026-06-17 05:43:24 -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
Ben Meadors 9ea1f0065a Update protos 2026-06-13 07:30:56 -05:00
Ben Meadors eb719f6fca Refine IPv6 address logging for CH390 driver in WiFiAPClient 2026-06-11 15:37:22 -05:00
Ben Meadors 02081dc85d Fix Ethernet handling and dependencies for CH390 driver 2026-06-11 15:36:13 -05:00
Ben Meadors ed52e3019d Change handleSetOwner parameter to const reference and improve long name handling 2026-06-11 14:24:12 -05:00
Ben Meadors c2bcec93d0 Fix long name clamping and adjust related structures for compatibility 2026-06-11 12:18:26 -05:00
Ben Meadors 83c7e4ede3 Add board_level configuration for Heltec V4, RAK WisMesh Tag, and Seeed Wio Tracker L1 2026-06-11 07:21:25 -05:00
Ben Meadors a14f7afe87 fix(workflows): expand trusted author criteria for flasher comments 2026-06-10 20:04:40 -05:00
Ben Meadors 1490daa7ca Update runner configuration to use GitHub-hosted runners for checks 2026-06-10 19:12:32 -05:00
Ben Meadors a4001d71d5 Improve PR resolution logic for web flasher link comments 2026-06-10 17:54:24 -05:00
Ben Meadors 6da9f5f20e Add placeholder comment for web flasher during PR builds 2026-06-10 17:28:30 -05:00
Ben Meadors 2541db2bef fix(workflows): update artifact selection to exclude expired firmware size artifacts 2026-06-10 10:01:12 -05:00
Ben Meadors f875518b28 Flasher link fix 2026-06-10 08:00:05 -05:00
Ben Meadors 334ad9b313 Restrict web flasher link comments to organization members only 2026-06-10 06:33:33 -05:00
Ben Meadors 0953706e9e Add GitHub Action to post web flasher link comments on successful PR workflows 2026-06-10 05:48:39 -05:00
Ben Meadors 309d51a3e8 fix(NodeInfoModule): update user handling in allocReply to prevent global state clobbering 2026-06-09 21:00:40 -05:00
Ben Meadors 93f87c57b9 MacOS fixes 2026-06-09 21:00:05 -05:00
Ben MeadorsandGitHub 94ef2ae451 Revert "Automated version bumps (#10667)" (#10672)
This reverts commit abef0d85a2.
2026-06-09 20:04:26 -05:00
bf68b9e597 NRF52 LTO flags (#10655)
* Add LTO support for nrf52840 while preserving interrupt handlers

* nrf52840: enable whole-image LTO on all targets via nrf52_base

Moves -flto + the nrf52_lto.py exclusion middleware from the rak4631 env
(771018ca8) up to [nrf52_base], so every nrf52840 target inherits it.

nrf52_lto.py keeps the interrupt handlers out of LTO (framework core +
TinyUSB USBD_IRQHandler) -- they're referenced only from the asm vector
table, so whole-program LTO would otherwise drop them and the chip hangs.

HW-validated: RAK4631 (SX1262, -60KB) and muzi-base (LR1121) both boot and
init their radios. Build-verified on canaryone (fresh board, base-inherited).

Caveat: the RAK "1-Watt" variant's radio does not tolerate global-LTO
(SX126x init fails); it shares the rak4631 binary, so keep that hardware on
src-only or split it into a separate target.

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

* nrf52840: move -fmerge-all-constants to nrf52_base (all targets)

It had been trialed on rak4631 only; it's a general image-wide flag (the
same one stm32 uses globally, ~0.7KB), so move it up to [nrf52_base] next
to -flto so every nrf52840 target gets it instead of just rak4631.

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

* nrf52_lto.py: normalize path separators for Windows (Copilot review)

get_abspath() returns backslash-separated paths on Windows, so the
"/FrameworkArduino/" / "/cores/nRF5/" substring matches would miss and the
ISR-owning objects would still be LTO'd -> hang on first IRQ. Replace
backslashes with forward slashes before matching. No-op on macOS/Linux.

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

* Fix  directory handling for LTO

* Add post-link guard to check for dropped ISR handlers in nrf52 LTO

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Jason P <applewiz@mac.com>
2026-06-09 16:09:25 -05:00
Ben Meadors 8a1d6d9285 Add stable uid generation for PLI entities in allocAtakPli function 2026-05-31 12:26:00 -04:00
Ben Meadors 1971e5ab13 Remove now unused payload variant settings in allocAtakPli function 2026-05-29 06:15:11 -05:00
Ben Meadors e2fda6598c Update protobufs 2026-05-28 21:01:30 -05:00
Ben Meadors f9fea562aa Add TAKTALK message and room data structures to support voice/text chat 2026-05-27 09:05:38 -05:00