Commit Graph

12380 Commits

Author SHA1 Message Date
vidplace7
7510c0e037 NodeDB: Change pointer to const for mesh node retrieval in loadFromDisk
Addresses cppcheck
src/mesh/NodeDB.cpp:2431: [low:style] Variable 'us' can be declared as pointer to const [constVariablePointer]
2026-07-31 19:56:23 -04:00
Austin
5bfad255e0 Remove Ubuntu 'questing' series from PPA and release channel workflows (#11308)
questing went EOL July 9th and no longer builds on PPA
2026-07-31 17:36:13 +00:00
Jason P
3d8ff48fab BaseUI: Clean up dead code (#11306)
* Clean up dead code

* Fix up the WiFi macro to be more directly called
2026-07-31 17:29:21 +00:00
Austin
44dbcaac3d Actions: Add caching for PlatformIO in native tests workflow (#11309)
Also switch to arm64 runners for the tests themelves. In my experience so far, they are faster (and these jobs are heavyy)
2026-07-31 17:35:20 +00:00
Austin
76f4340f3a Add explicit board_level = release (#11305)
Relying on board_level = <empty> was causing some inheritence footguns.
Let's be explicit about what's being released.
2026-07-31 16:04:24 +00:00
Austin
33a0f8ffb7 Add libcurl as a build dependency for meshtasticd packages (#11303)
Followup to #11214
Adds libcurl as a dependency in Debian and Fedora packages.
2026-07-31 13:40:38 +00:00
Ben Meadors
e2460b546e MUI: stop holding the SPI bus across the whole UI cycle (#11278)
tft_task_handler held spiLock for the entire LVGL cycle. Most of that
cycle is timer work and rendering into the draw buffer, which issues no
SPI at all - but on boards where the TFT, SD card and LoRa radio share
one bus (T-Deck), every radio operation on the main loop still waited it
out. That is tens to hundreds of milliseconds whenever the UI animates,
felt as mesh RX/TX latency.

device-ui now takes the lock around its own transfers instead
(meshtastic/device-ui#356), so the coarse hold here can go and the bus is
contended only during real traffic.

Lend it spiLock through a reentrant adapter. device-ui nests its guards -
SdFsCard::usedBytes() calls cardSize() and freeBytes(), each of which
takes the lock - while spiLock is a plain binary semaphore that would
self-deadlock on the second take, so track the owning task and only touch
the underlying lock on the outermost acquire.

Requires the device-ui pin bump included here.

Tested on T-Deck: flush, touch, panel init, SD detect, powersave sleep
and wake all exercised; LoRa RX decoding under a live UI, no deadlocks,
no watchdog resets. Also builds seeed-sensecap-indicator-tft.

Co-authored-by: Manuel <71137295+mverch67@users.noreply.github.com>
2026-07-31 10:49:39 +00:00
Tom
e26b6bfc5a TMM & Warmstore key source naming (#11119)
* one becomes two

* warmstore clarify

* Address PR review: key-provenance terminology consistency

- Log line now says "not key-proven" (gate is XEdDSA OR manual, not just signer)
- Rename markKeySignerProvenForTest -> markKeyXeddsaSignedForTest (sets only the XEdDSA bit)
- Docs + test comments: "signer bit" -> "XEdDSA-signed bit"

clod helped too

* Rename signer-proven -> key-proven for broadened provenance predicate

Address PR #11119 review: the copyPublicKey()/copyUser() out-parameter and
the cache-path replay gate now report entry->keyProven() (XEdDSA-signed OR
manually verified), so the "signerProven" name and "signer-proven" comments
were misleading. Rename the public out-param to keyProven, the local
cachedKeySignerProven to cachedKeyProven, and update coupled callers, log
strings, docs headings, and comments to say "key-proven".

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

* nitpicks

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-31 10:39:39 +00:00
Thomas Göttgens
6367132919 Fix the NMEA checksum offset and harden the buffer writes around it (#11293)
* Checksum NMEA sentences from the $ delimiter

The PositionLite printWPL() format begins with a CRLF, so the fixed start offset of 1 folded the newline and the $ into the checksum and every sentence went out with a wrong value. Locate the $ instead and stop at the terminator or a \*.

* Clamp truncated writes and harden the remaining fixed buffers

snprintf returns the length it would have written, so a truncated NMEA sentence
made buf + len point past the buffer and bufsz - len underflow into a huge size
for the checksum append. Clamp after each write.

Also pulls in the rest of #11236: the two remaining Dropzone sprintf calls, the
dead strcpy in mt_sprintf that wrote one byte past a zero-size allocation for an
empty format, and the 10-byte errcode buffer that INT32_MIN overflows.

Co-Authored-By: Andrew Yong <me@ndoo.sg>

* Bail out on a zero-sized buffer and cast err for %ld

snprintf writes nothing at all when bufsz is 0, not even a terminator, so the
checksum helper would run strchr over whatever the buffer already held. Return
before touching it.

int32_t is not long on every target, so cast before formatting with %ld.

Co-Authored-By: Andrew Yong <me@ndoo.sg>

* Add NMEA sentence regression tests

Covers checksum computation from the $ delimiter for both printWPL
overloads and printGGA, zero-sized buffers, and truncated buffers down
to one byte.

Co-Authored-By: Andrew Yong <me@ndoo.sg>

* Tighten checksum parsing and pin the WPL fixture checksum

Require exactly two hex digits followed by the sentence terminator, and
assert both WPL overloads against a known checksum instead of comparing
them to each other.

* Bump native suite count to 43

---------

Co-authored-by: Andrew Yong <me@ndoo.sg>
2026-07-31 08:20:56 +00:00
Jason P
575788ae7b Implement Fixes for Meshnology W12's Two Color Display (#11288)
* Round 1 Fixes

* Update GPS icon

* Update calculations for revised GPS icon after regression testing

* Add BICOLOR_OLED_DISPLAY and actually use it

---------

Co-authored-by: Austin <vidplace7@gmail.com>
2026-07-31 04:54:54 +00:00
Thomas Göttgens
ecd59e3120 Package meshtasticd for Windows as an MSI (#11289)
* Package meshtasticd for Windows as an MSI

Adds a --service flag connecting meshtasticd to the Service Control
Manager, a WiX MSI installing it as an auto-start LocalSystem service with
config in %ProgramData%\Meshtastic, and a CI step attaching the MSI to
releases.

* Address review comments

Bind workflow expressions to env vars in run: bodies, and build the
service status per call with an atomic checkpoint.

* Fix service stop state and CI lint

Latch the stop under a mutex so a startup report cannot walk the state
back. Ignore the new workflows in semgrep and checkov, as main_matrix
already is.

* Drop the checkov ignore for the winget workflow

Resolve the newest release inside the job instead of taking
workflow_dispatch inputs, so CKV_GHA_7 no longer fires and checkov stays
active on the file.

* Carry the MSI architecture into the winget manifest

Parse it from the asset name instead of defaulting to x64, and fail on a
multi-arch release rather than validating one at random.

* Restore release/.gitignore

* Leave the main matrix alone

Release attachment moves to the matrix rework in #11151. The MSI is still
built and uploaded as a CI artifact.

---------

Co-authored-by: Austin <vidplace7@gmail.com>
2026-07-31 04:53:52 +00:00
Austin
02ea5baad4 Pioarduino 3.3.11 (#11238)
* Pioarduino 3.3.11

Update pioarduino from 3.3.9 to 3.3.11
There was no 3.3.10 pioarduino release

Release notes:
- https://github.com/pioarduino/platform-espressif32/releases/tag/55.03.311
- https://github.com/espressif/arduino-esp32/releases/tag/3.3.10
- https://github.com/espressif/arduino-esp32/releases/tag/3.3.11

* update sensecap indicator to 3.3.11

---------

Co-authored-by: mverch67 <manuel.verch@gmx.de>
2026-07-30 23:53:13 +00:00
Austin
2192087579 Actions: Add explicit ci-gate, cleanup conditionals (#11296)
Add explicit ci-gate to the matrix workflow, and cleanup conditionals to make them more readable.
Stop gathering artifacts for PRs/merge-queue, as they are not needed and just take up time/space.
2026-07-30 23:13:49 -04:00
Austin
a0c4987c75 Narrow "PR" candidates (now only 17) (#11294)
These boards should be built when changes are made to the Meshtastic Core. Narrowed signficantly to stop wasting time.
2026-07-30 17:20:54 -04:00
Thomas Göttgens
597f6767b5 Add Elecrow ThinkNode M8 board support (thinknode_m8) (#11226)
* Add Elecrow ThinkNode M8 variant scaffold (thinknode_m8)

nRF52840 + SX1262 + 2.4" e-paper + ATGM336H-5NR32 GPS.
All pins resolved from ThinkNode_M8_V0.3.sch; cross-checked
against meshtastic/firmware#9181 (Elecrow V0.1 reference).

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

* Add Elecrow ThinkNode M8 board support (nRF52840/SX1262, 1.54in e-ink, ATGM336H GNSS, SC7A20, EC04 encoder)

* Address review: keep the stored backlight level out of blanking, match only the SC7A20 WHO_AM_I byte, and transfer detents atomically

* Use std::atomic for the press-and-turn detent counter so native builds compile

* Drop the ThinkNode M8 LED_BUILTIN redefinition that warned on every translation unit

---------

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-30 15:59:35 +00:00
brad112358
84322af4e0 Add support for a new nRF variant constructed by soldering an E22 (#7579)
* Add support for an alternate pin assignment to the nrf52_promicro_diy variant
constructed by soldering a Pro Micro type nRF52840 board directly to an E22 module.

* Fix GPS connection documentation and Buzzer pin

---------

Co-authored-by: Tom <116762865+NomDeTom@users.noreply.github.com>
2026-07-30 15:46:59 +00:00
Tom
06577e6b36 tying up some loose ends (#11292) 2026-07-30 13:33:31 +00:00
Thomas Göttgens
a5940b4c6c Guard the deferred local queue and depth counter (#11284)
* Guard the deferred local queue and depth counter

* Close the drain and enqueue race on the deferred queue

* Route the raced loopback through handleReceived

* Make the last-frame check and depth decrement atomic

* Correct the handleReceived doc comment
2026-07-30 11:13:37 +00:00
Thomas Göttgens
fc67590317 Lockdown PIN redaction, a build guard, and favorite compaction (#11285)
* Redact the pairing PIN from unauthorized lockdown clients

* Fail the build when PacketAPI would bypass the lockdown gate

* Compact kept favorites when resetting the node database

* Make the compaction loop reference const
2026-07-30 11:13:11 +00:00
Tom
21e3a583bd Yaml check for Meshtasticd (#11224)
* feat(portduino): add `meshtasticd --check` config validator

Users hand-writing files in /etc/meshtasticd/config.d/ get no feedback when a
key is misplaced, misspelled or duplicated: meshtasticd silently ignores what
it does not read, so a broken config looks identical to a working one.

Add a --check mode that loads the configuration exactly as startup does, then
reports what it found and exits:

- Duplicate keys, via the yaml-cpp Parser/EventHandler stream. The Node API
  cannot see them because the map is already collapsed by the time it exists,
  and yaml-cpp keeps the FIRST occurrence, so a later override is discarded.
- Unknown or misnested keys, against a schema mirroring what loadConfig()
  reads, with a hint naming the section a stray key actually belongs to.
- rfswitch_table validation: unrecognised pins, mode rows whose length does not
  match the pin list, values that are not HIGH/LOW, and unknown modes.
- Cross-file overlap: every .yaml in the config directory merges into one
  portduino_config, so the file loaded LAST wins, the opposite of the
  within-file rule. Those files are read in filesystem order, not alphabetical.
- A warning when more than one file defines a Lora section: spidev, spiSpeed,
  gpiochip, DIO2_AS_RF_SWITCH, DIO3_TCXO_VOLTAGE and USB_PID/VID/Serialnum are
  assigned unconditionally with a default every time one is seen, so any of
  them not repeated in the last file loaded is silently reset.
- The resolved gpiochip/line for each pin, since a line that exists on the
  wrong chip is claimed successfully and then silently does nothing.

Exits non-zero when errors were found so it can also gate CI over
bin/config.d/**, keeping one implementation rather than a second schema.

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

* fix(portduino): flag pins that resolve to -1 in --check

A pin key whose value will not convert to a number falls back to RADIOLIB_NC
(-1) while still being marked enabled, and initGPIOPin() then trips an
assertion inside LinuxGPIOPin rather than failing cleanly. YAML indentation
makes this easy to hit by accident: a stray line under "CS: 8" folds into the
value as a multi-line scalar, so the file parses, the daemon crashes with a
stack trace from a library file, and --check reported "Configuration looks
good" while printing "pin -1" two lines above.

Report it as an error naming the likely cause instead.

Also correct a comment claiming unparseable config.d files are skipped
silently. They are not: loadConfig() prints "*** Exception ..." with the line
and column. It is the discarded return value, not the diagnostic, that makes
the file's absence from the merged config easy to miss.

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

* test(portduino): cover `meshtasticd --check` with fixtures and a fuzz suite

Adds the tests the config validator was missing, and the checks and fixes that
writing them turned up. The theme throughout is configuration that the YAML
parser accepts but that does not mean what it looks like it means.

Tests
-----

bin/test-config-check.sh - 57 assertions driving a built meshtasticd against
test/fixtures/portduino-config (50 fixtures plus two config.d trees). A shell
test rather than a Unity suite because both behaviours under test are properties
of the process: --check is judged by its exit status and printed report, and the
"a normal run rejects a bad config" path ends in exit() inside portduinoSetup(),
neither of which is reachable from a suite that links one translation unit.
Every fixture carries a comment header naming its planted fault and the expected
finding, so it can be read on its own. Coverage:

  * a clean config for each of the ten radio module families (RF95, sx1262,
    sx1268, LLCC68, sx1280, lr1110, lr1120, lr1121, sim, auto), asserted both
    findings-free and resolving to that module, so a silent fallback to sim
    cannot pass
  * LR11xx rfswitch tables: unrecognised pins, rows longer and shorter than the
    pin count, levels that are not exactly HIGH, a missing pins list, more than
    five pins, a scalar table, unknown MODE_ keys, a MODE_ row stranded one
    level out, and a legal partial table
  * the PA gain table in both accepted shapes, entries outside the uint16 range
    it is stored in, and more than the 22 points that are kept
  * values of the wrong type, split by consequence: the two settings read with
    no fallback stop meshtasticd starting, everything else is silently replaced
    by its default
  * out-of-range and unit mistakes: TCXO voltage written in millivolts, ports
    outside their usable range, an over-long StatusMessage
  * MAC sources: both keys set at once, a malformed address, an interface that
    does not exist
  * structural faults: duplicate keys, non-mapping and unknown sections, a key
    left at the top level, a sequence at the document root, an empty file,
    unreadable pins, unparseable YAML
  * cross-file behaviour over a config.d directory, including the switch tables
    that do not override each other
  * five configs run WITHOUT --check, each of which must still be refused, so
    check mode cannot quietly make the normal path permissive

test/test_fuzz_config - adversarial fuzzing of the checker itself, the "the tool
meant to diagnose your config crashes on it" failure mode. Scope is deliberately
narrow: yaml-cpp does the parsing and is fuzzed upstream, so what is exercised
here is our code above the parse, above all the duplicate-key detector, which is
the one hand-rolled piece and walks the raw parser event stream with its own
stack. Groups: the checked-in fixtures as a seed corpus, 3000 byte mutations of
them (flips, truncation, insertion, splicing, deletion), and structural torture
(nesting to 4096 in flow and block style, duplicate keys at depth, anchors,
aliases and merge keys, 64KB keys, 256KB scalars, multi-document files). A
fourth group of random bytes is present but disabled behind
FUZZ_CONFIG_RANDOM_BYTES: it was half the runtime for the least return, since
uniform noise is rejected on the first token. The contract is crash-freedom and
termination under AddressSanitizer, not any particular finding.

CI runs the shell test in the existing native simulator job; the fuzz suite is
picked up by the existing ^test_fuzz_ area rule. native-suite-count 40 -> 41.
The fixtures are exempt from trunk in .trunk/trunk.yaml, since prettier rejects
the duplicate keys and bad indentation that are the point of them.

Checker fixes found while writing the tests
-------------------------------------------

--check reported a clean exit 0 on configs meshtasticd then refuses to boot, the
worst failure a diagnostic tool can have. Four hard exits inside loadConfig()
killed the report before it printed: an unparseable file, an unknown Lora.Module,
MACAddress and MACAddressSource both set, and HUB75 on a build without it. All
are now reported as findings, and all are still refused on a normal run.

New validation: Lora.Module against the accepted spellings, which are matched
exactly and inconsistently cased, with a suggestion when only case differs; a
per-key value type table covering ~85 keys, tested by asking yaml-cpp to perform
the same conversion loadConfig() will so it cannot drift; the PA gain table;
DIO3_TCXO_VOLTAGE, which is in volts and multiplied by 1000, so the millivolt
value everything else uses silently asks for 1800V; APIPort and Webserver.Port
ranges; MaxNodes; StatusMessage truncation; MAC address and source; and an
unreadable ConfigDirectory.

Also fixes a crash: a ConfigDirectory that cannot be read threw an uncaught
filesystem_error from directory_iterator and aborted meshtasticd with SIGABRT,
taking --check down with it. It now fails cleanly.

Two smaller ones: cppcheck's uselessCallsSubstr on the ancestor walk, which was
failing every check job; and the duplicate-key detector's stack pop, which was
unguarded and relied on yaml-cpp emitting balanced events.

Switch tables are the one place "the file loaded last wins" is false. The loader
only ever writes HIGH and never writes LOW back, so a HIGH from an earlier file
survives a later file that clears it and the radio drives the OR of every table
loaded. Confirmed with --output-yaml. Reported as an error for now; the loader
itself is left alone, as that changes RF behaviour.

* fix(portduino): report CH341 pins as adapter indexes, not gpiochip lines

--check printed "Resolved GPIO lines (what meshtasticd will try to claim)" for
every config, listing a gpiochip and line for each Lora pin and advising they be
confirmed against gpiodetect and gpioinfo. For spidev: ch341 every part of that
is false. portduinoSetup() skips initGPIOPin() for every Lora pin when spidev is
ch341 and hands the raw numbers to Ch341Hal, so nothing is claimed from a
gpiochip -- and on Windows and macOS, where a USB adapter is the only way to
attach a radio, there is no gpiochip, gpiodetect or gpioinfo to check against in
the first place. The checker had no ch341 coverage at all: not one fixture used
it, so the whole USB-SPI path went unexercised.

The summary now splits on the transport. A ch341 device gets its pins listed as
adapter indexes with the gpiod advice dropped, and a gpiochip or line mapping
written alongside it is reported: those are read, stored, and never used.

Also: "RF switch table: not set" read as a gap on an SX126x, where there is
nothing to set. setRfSwitchTable() is only ever called for an LR11xx, so absence
is now "not needed for this module" everywhere else, and "not resolved yet" for
auto, which has no module to judge against.

Fixtures: usb-ch341.yaml (clean, the meshstick shape) and ch341-gpiochip.yaml.

CI fix
------

test-native was RED on "config.d overrides are reported", which wanted 2
warnings and got 1. The fixture's two config.d files name different modules, so
which one wins -- and whether the LR11xx-without-a-switch-table warning fires --
depends on the order the filesystem returns them in. That is the very thing the
fixture exists to demonstrate, so the count is no longer asserted; the report's
own order caveat is asserted instead.

Review fixes
------------

The unreadable-ConfigDirectory diagnostic was the one new print in
PortduinoGlue.cpp not gated behind !configCheck, so it landed ahead of the report
header and broke the clean output the rest of the change is careful to keep.

Docs: rfswitch-valid.yaml carries seven modes, not eight, and empty-file.yaml is
comments-only rather than zero bytes.

* style(portduino): trim --check comment blocks and reconcile suite count

Condense the multi-paragraph comment blocks in the --check validator to the
one-to-two-line convention, and bump test/native-suite-count to 42 for the
test_fuzz_config suite added here.

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 11:06:24 +00:00
Benjamin Faershtein
9a37250438 fix(router): clear failed reliable send retries (#11267)
* fix(router): clear failed reliable send retries

* test(router): cover failed interface enqueue

* fix(router): retain retries after duty cycle limits
2026-07-30 11:00:32 +00:00
Thomas Göttgens
047c4e9feb add LR 2021 to portduino, and allow Framebuffer devices to rotate the screen from config (#11252)
* add LR 2021 to portduino, and allow Framebuffer devices to rotate the screen from config. Requires https://github.com/meshtastic/device-ui/pull/355 and supersedes https://github.com/meshtastic/firmware/pull/10567 and https://github.com/meshtastic/firmware/pull/11138

Many thanks to the original authors https://github.com/a-li3n and https://github.com/jessm33

* Build LR2021Interface.cpp in the wasm env

initLoRa() constructs LR2021Interface for Lora.Module: lr2021, so excluding
the file from the native-wasm source filter left the constructor undefined at
link time. LR20x0Interface.cpp stays excluded; it is template-only and comes in
via the InterfacesTemplates.cpp amalgamation.

Also replace the non-UTF-8 degree signs in the framebuffer rotation comment and
correct the rfswitch alias cleanup comment.

* Trim comments

* Report setenv failure for the framebuffer rotation
2026-07-30 10:44:52 +00:00
github-actions[bot]
16765dd089 Upgrade trunk (#11251)
Co-authored-by: vidplace7 <1779290+vidplace7@users.noreply.github.com>
2026-07-30 06:11:23 -05:00
Thomas Göttgens
9c260ad792 Signature key resolution and a send-path leak (#11283)
* Verify signatures against authoritative keys only

* Release the packet on the unset-variant send error
2026-07-29 22:30:30 +00:00
Thomas Göttgens
daf1213580 Licensed channel defaults, phone map growth, and payload read bounds (#11286)
* Strip the default PSK when licensed defaults are installed

* Only record rate-limited portnums from the phone

* Bound payload reads by the received size
2026-07-29 22:29:12 +00:00
Thomas Göttgens
df6e67f70b Signing and ingress hardening (#11282)
* Include warm-tier signers in the identity update gate

* Fail the send when PKI encryption fails

* Require signatures on licensed unicasts

* Include warm-tier signers in the NodeInfo downgrade drop

* Clamp hop fields on UDP multicast ingress

* Address review comments on signing hardening

Condense the updateUser rationale to two lines and stop calling the
Balanced-mode drop a broadcast now that licensed unicasts reach it.
2026-07-29 22:28:25 +00:00
renovate[bot]
50a37b3a19 chore(deps): update meshtastic/device-ui digest to bcb327f (#11276)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-29 14:03:47 +00:00
Tom
2024bb8384 Arrival time fix perhaps (#11274)
* Add explicit presence for MeshPacket.rx_time (arrival time)

rx_time is now proto3 optional with a has_rx_time presence bit, matching
the rx_rssi treatment. A node with no GPS and no phone connected yet has
no time source at all, so a bare 0 was indistinguishable from a genuine
1970-01-01 reading; downstream consumers (replay packets, JSON
serialization) now check has_rx_time instead of the value.

* Dedupe rx_time stamping into a shared helper; trim a debug log string

Extract the repeated haveTime/rx_time/has_rx_time stamp logic (5 call
sites across Router.cpp, MeshBeaconModule.cpp, MeshService.cpp) into
Router::computeRxTimeStamp()/stampRxTime(). Also shorten the new RTC.cpp
LOG_DEBUG string. Saves 48 bytes of flash on rak4631 (measured), no
behavior change.

* Fix has_rx_rssi presence carried unconditionally through StoreForward replay

preparePayload() set has_rx_rssi = true unconditionally on replay, regardless
of whether the packet's rx_rssi at store time was a genuine measurement (e.g.
MQTT-relayed packets carry no real RSSI). Store the presence bit alongside
rx_rssi in PacketHistoryStruct and restore it on replay instead.

Flagged by Copilot on #11271 (same root cause the has_rx_time explicit
presence work fixes) but never addressed before that PR merged.

* Trim comment blocks to the repo's 1-2 line guideline

.github/copilot-instructions.md:338 caps code comments at 1-2 lines; several
blocks added across the rx_time explicit-presence work ran well past that.
Also consolidates Time.cpp's file-level doc comment into Time.h, where the
rest of the Time:: API contract already lives.

No behavior change.

* Add rx_time explicit-presence test coverage

- test_meshpacket_serializer: has_rx_time=false fixture plus tests asserting
  JsonSerialize/JsonSerializeEncrypted emit 0 rather than leaking the
  millis() placeholder, alongside the has_rx_time=true baseline.
- test_stream_api: two tests driving a real PhoneAPI handshake (want_config_id
  through STATE_SEND_PACKETS) that simulate a phone time-giving transaction
  arriving before vs. after a queued packet is drained - covering both the
  reconciled and the ships-with-placeholder-absent paths of
  MeshService::reconcilePendingRxTimes().

* Fix three correctness issues flagged in review

- Time.h: drop the reserved-identifier include guard (_MT_TIME_H); pragma
  once already covers it, matching convention elsewhere (e.g. RTC.h).
- Time.cpp: rebase getMillis64()'s wrap accumulator when the test seam
  swaps clock sources, so a real<->injected clock jump isn't miscounted
  as a genuine 32-bit wrap.
- NodeInfoModule: the 12h reply-suppression window is a local dedup
  duration, not a wall-clock reading - switch it to Time::getMillis64()
  so RTC-quality jumps and replayed packets' stale rx_time can't perturb
  it.
- StoreForwardModule: has_rx_time was derived from *current* RTC quality
  at replay time rather than stored at capture time, so a history entry
  saved while time-blind could be misreported as a valid epoch once the
  clock later improved. Persist the presence bit in PacketHistoryStruct
  instead.

* tryfix CI

* post review fixes

* more test fixes
2026-07-29 14:03:25 +00:00
Benjamin Faershtein
0fef83d434 Add configurable event mode hop limit (#11275)
* feat: resolve event mode hop limit

* feat: bake event mode hop limit

* fix: honor event mode hop cap in routing

* docs: expose event mode hop limit preference

* fix: enforce event hop defaults across routing

* docs: clarify event hop override behavior

* refactor: simplify event mode hop preference

* fix: cap equal event hop limit
2026-07-29 10:54:48 +00:00
renovate[bot]
b4ff1df864 chore(deps): update platformio/ststm32 to v19.7.1 (#11263)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-29 00:52:26 +00:00
Austin
d87cbec45b Remove esp32c6 guard precluding NonBlockingRTTTL (#11273) 2026-07-28 18:47:02 +00:00
Tom
2c57a17124 Phantom node fix perhaps (#11271)
* trying to fix phantom nodes

* fix comment spam

* oops - missed one
2026-07-28 18:32:18 +00:00
Ben Meadors
a8623a60c5 Stream our own position to the phone/UI while mesh position sharing is opt-in (#11270)
* Position: stream our own position to the phone/UI while mesh sharing is opt-in

Position broadcasts became opt-in in 2.8 (#10929): with every public channel
at position_precision 0, sendOurPosition() finds no eligible channel and
returns without queueing anything, so the connected phone or on-device UI
never sees the node's own GPS fix ("GPS looks dead" on standalone MUI
devices even though the receiver has a lock).

Mirror device telemetry's local delivery: once a minute, when the toPhone
queue is idle, stream our own position to the connected client at full
precision. The packet is handed straight to sendToPhone() and never touches
the mesh, so the per-channel opt-in and the public-channel precision clamp
still govern everything on the air.

Also stop logging "Send pos ... to mesh" before the channel scan has found
an eligible channel; when sharing is disabled everywhere the skip is now
logged explicitly instead of pretending a send happened.

The cadence gate is a pure static (shouldSendPositionToPhone) alongside the
existing broadcast-policy helpers, with unit tests covering the first-send,
cadence, gating, and millis() rollover cases.

* Review: only advance phone cadence on a queued packet; drop the ms==0 sentinel

sendOurPositionToPhone() now reports whether a packet actually reached the
phone queue, and runOnce() stamps the cadence only on success, so a guard or
allocation failure retries on the next tick instead of waiting out a minute.

The never-sent state is a dedicated hasSentPositionToPhone flag rather than
lastPhoneSendMs == 0, so a send stamped exactly at millis() == 0 still holds
the cadence. New regression test covers that case; existing cases updated to
the explicit flag.

* Review: align the rollover test fixture with its documented elapsed times

lastSent now sits exactly 30,000 ms before the uint32 wrap, so the two cases
are precisely 70,000 ms (sends) and 40,000 ms (held) - the previous comments
claimed 70s/20s against actual elapsed values of 70,001/40,001 ms.
2026-07-28 14:17:09 +00:00
Ben Meadors
2a20ee5954 ESP32: link via @response files across the family, not just P4
The rak_wismesh_tap_v2-tft link failed on CI with "sh: Argument list too
long": the device-ui bump in #11214 grew the MUI object list enough that
the linker command line crossed the kernel exec arg limit (this env's long
name lengthens every object path, so it tipped first).

extra_scripts/ld_response_file.py already solves this for esp32p4; promote
it to esp32_common so every ESP32 target links through a response file,
and drop the now-inherited entry from esp32p4.ini.

Verified by building rak_wismesh_tap_v2-tft at the failing commit plus this
fix: links clean (macOS's exec arg limit is smaller than the Linux one that
tripped CI, so an unwrapped link could not have passed).
2026-07-28 08:47:46 -05:00
Manuel
5f51bb1c1e MUI: Portduino curl maps (#11214)
* add libcurl, fix missing SDL2

* device-ui commit reference PR353

* add curl dependencies to CI build

* switch to gnutls curl version

* Update device-ui library dependency URL

* Update device-ui dependency to new version

* Fix formatting in Dockerfile package installation

* Update device-ui library dependency URL
2026-07-28 11:09:40 +00:00
vidplace7
dafa583f8b Actions: Do not build meshtasticd for event/ branches
Do not build MacOS, Windows, WASM, or Docker for event branches.
2026-07-27 16:33:48 -04:00
Thomas Göttgens
cdd996248c Release the opaque relay copy the interface declines to send (#11262)
relayOpaquePacket() allocates a copy and returns Router::send(relay) == ERRNO_OK, discarding ERRNO_SHOULD_RELEASE. The interface returns that for NODENUM_BROADCAST_NO_LORA, so the copy is never freed and one pool slot leaks per frame.

The opaque path is reached for packets on a channel we have no key for, so no key or PSK is needed: a frame with an unknown channel hash, to=NODENUM_BROADCAST_NO_LORA, a nonzero id and hop_limit>0 leaks a slot, and roughly MAX_PACKETS of them exhaust the pool until reboot.

#11087 fixed this pattern in perhapsRebroadcast and the retransmission paths but did not cover relayOpaquePacket, which was added separately with the opaque relay path.
2026-07-27 15:57:39 -04:00
Ben Meadors
134fe5ec3e Stop BLE log streaming from starving the NimBLE notify pool (#11253)
sendLog() notified once per firmware log line with no backpressure. With
debug_log_api_enabled and a subscribed client, a logging burst drains the
msys_1 mbuf pool that every GATT notification and ATT response allocates
from, so ble_gatts_notify_custom starts returning BLE_HS_ENOMEM (rc=6) --
for the fromNum doorbell too, not just the log line that exhausted it.
Each failure then prints an error over serial at ~12ms apiece, which is
itself enough to throttle the main loop during a burst.

Register a callback on the log characteristic so the host's own
ERROR_GATT verdict pauses log notifies for 250ms and lets the pool
refill. Keying off the failure rather than a free-block count keeps this
correct however the pools are sized.

Also restore CONFIG_BT_NIMBLE_MSYS_1_BLOCK_COUNT to the IDF default of
12. Pool selection is by block size and a notify request always resolves
to the smallest pool, so msys_1 is the only pool GATT draws from; 8 was
thin once anything chains or fragments. msys_2 and the ACL/EVT transport
pools are left trimmed -- neither is on the notify path, and restoring
the full defaults would re-spend the contiguous allocation that #10741
was fixing on heap-tight boards.

sendLog() also lacked the null check onNowHasData() already has, so a log
line arriving during BLE teardown dereferenced a freed characteristic.

Fixes #11245
2026-07-27 15:56:23 -04:00
Thomas Göttgens
3e2c98420b nRF52: don't truncate the AES-CTR block length to uint8_t (#11261)
blockLen() rounds numBytes up to the AES block size, so 241..256 returns 256. Stored in a uint8_t that truncates to 0, giving a zero-length encBuf that Process() then writes numBytes bytes into, smashing the stack with attacker-controlled ciphertext.

encrypted.size is a 256-byte protobuf field and encryptPacket admits numBytes up to MAX_BLOCKSIZE, so sizes above the 239-byte LoRa frame limit arrive via the MQTT and UDP multicast ingress paths, which pass the packet to the router without clamping. Only the hardware AES path for keys of 16 bytes or less is affected, which includes the default channel.
2026-07-27 15:51:39 -04:00
Thomas Göttgens
80f972655d Null-check reply allocations in allocErrorResponse and RemoteHardware (#11260)
allocAckNak returns nullptr when the packet pool is exhausted, but allocErrorResponse passed the result straight to setReplyTo, which dereferences it. Reachable unauthenticated: an ADMIN_APP packet on a known-key channel takes the NOT_AUTHORIZED path, so a flood that empties the pool crashes the node. MemoryDynamic::alloc no longer aborting on failure made this reachable on PSRAM, STM32WL and Portduino targets. Callers already treat a null reply as no response. Same fix for the two RemoteHardwareModule sites.
2026-07-27 14:54:57 -04:00
Ben Meadors
ae16c052d5 Time out an abandoned admin edit transaction (#11254)
begin_edit_settings sets a bool that only the matching commit ever
clears. If the commit never arrives -- the client dropped its link
partway through a bulk config import, or went away entirely -- the
transaction stays open indefinitely, and every later config write from
any client is applied to RAM, acknowledged, and then never saved. The
writes look successful and are visible in get_config, but the node
reverts all of them at the next boot, and only a reboot clears it.

Give the transaction a one minute idle timeout. Each deferred write
restarts the clock, so the window bounds the gap between writes rather
than the length of the edit; a bulk import sends them milliseconds
apart. The next admin message after it lapses retires the transaction,
persisting the segments it had deferred and flushing the warnings it was
holding. The check runs after the auth gates and before the switch, so
every case sees consistent state and the recovery's flash write happens
in the main loop rather than a disconnect callback.

It saves rather than rolls back because there is nothing to roll back
to: each write already took effect in RAM and was acknowledged, so the
transaction only ever deferred the save. That also makes an early expiry
cheap -- the worst case for a client that really was still going is that
its remaining writes are saved individually instead of batched. Closing
on disconnect instead was tempting, but the reporter's captures show iOS
dropping and reconnecting within half a second several times per
session, which would abort imports that currently survive the blip.

Also drops the file-scope hasOpenEditTransaction, shadowed by the class
member at every use site and referenced nowhere else in the tree.

Reported in #11245
2026-07-27 15:22:33 +00:00
Ben Meadors
f61f65891c fix(promicro): exclude emoji to fit under the warm-store region (#11256)
nrf52_promicro_diy_tcxo has been failing the nrf52 warm-region guard on
develop: the image ends at 0xEA0C0, 192 bytes past the 12 KB WarmNodeStore
record-ring reserved at 0xEA000. This variant compiles four radio driver
families (SX126x/LLCC68, SX127x/RF95, LR11x0/LR1121, LR2021) so any module
can be soldered on, which makes it the largest nrf52 image we ship.

Building it with -D EXCLUDE_EMOJI saves 6,808 bytes and puts the image at
0xE8618, 6.6 KB clear of the warm region.

EXCLUDE_EMOJI was not previously usable: graphics::emotes[] becomes empty,
but the canned-message emote picker never checked for that. Entering the
picker clamped emotePickerIndex to numEmotes - 1 (i.e. -1), and selecting
read emotes[-1].label into a String - an out-of-bounds read of whatever
precedes the array in flash. Guard both entry points instead: refuse to
open the picker when there are no emotes, and bounce back to freetext if
the picker state is somehow reached anyway.

Under whole-image LTO those guards fold to constants, so the picker draw
and input paths dead-strip entirely on builds that set EXCLUDE_EMOJI -
which is where the savings come from beyond the bitmap data itself.

Received messages containing emoji render as text on this variant, and
the emote-list key is a no-op. Verified rak4631 (emoji enabled) is
unaffected: 0xE46D8, 22 KB clear.
2026-07-27 15:20:57 +00:00
Austin
a640d44b5c Add T-Beam BPF - 144-148 Mhz LoRa @ ~37 dBm (5 Watts) (#10558)
* Add T-Beam BPF (144-148 Mhz LoRa)

* minor correction to fix compiler warnings

* Add T-Beam BPF (144-148 Mhz LoRa)

* minor correction to fix compiler warnings

* Add ITU regions for this device and make GPS work.

* Switch pin after defining it as output

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

* Lora CS is indeed 1, SD Card CS is 10

* Include the back option.

* Fix compilation with pioarduino (USB_MODE)

* Default ham to narrow_fast

* Default PROFILE_HAM to slot 17

This is an appropriate default in the USA but not the EU.

The slot override really should follow the region itself, not the regionprofile.

* Fix for ITU 2/3 split

* Add ITU region options to MenuAction enum

* Add HAS_HAM_2M definition to variant headers for 2M support

* Re-add PROFILE_HAM regionprofile

Accidentally removed in last merge

* Trunk fmt

* Initial default slots

* Switch to TinyFast

Still need to flesh out the default channels

* Adjust slotOverrides for TinyFast

* RadioLib doesn't accept 15.625 kHz

Use 15.6 instead

* Set RF95 pins for T-Beam Supreme

May cause regressions!!

* Remove other-variant changes (BPF-only)

These have been moved to other PRs

* Remove mismatch guarding (we need a more comprehensive approach)

* Add comment back

* Add template for PA curve

* This is a 5-6W radio!! Add TX_GAIN_LORA

* Trunk fmt because NomDeTom hates emdashes

---------

Co-authored-by: Thomas Göttgens <tgoettgens@gmail.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-07-27 13:54:53 +00:00
Benjamin Faershtein
1e982fa78c Sign plaintext packets in licensed mode (#10969)
* feat: sign licensed plaintext packets

Preserve and publish identity keys in licensed mode so existing XEdDSA signatures can authenticate plaintext traffic. Sign licensed broadcasts and direct messages when they fit, while keeping PKI encryption disabled and normal routing behavior unchanged.

* fix(security): persist licensed channel sanitation

* fix(security): close licensed migration lifecycle gaps

* fix(security): address licensed signing review feedback

* test: restore signing globals from Unity teardown

* fix(baseui): allow confirmed ham region selection

* fix(baseui): guard region picker validation

* style: trunk fmt

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
Co-authored-by: Austin <vidplace7@gmail.com>
2026-07-27 02:57:15 +00:00
Benjamin Faershtein
45cb8e7500 feat: preserve normal radio profiles across event firmware (#11110)
* feat: isolate event radio profiles

* fix: preserve identity on degraded config boot

* fix: guard event profile storage

* style: align event profile storage names

* fix: discard event profile on normal boot

* refactor: simplify event profile cleanup

* fix: limit inactive profile migration to event firmware

* fix(event): make event-profile capacity check portable across filesystems

The USERPREFS_EVENT_MODE storage preflight called FSCom.totalBytes() and
FSCom.usedBytes(), which only exist on ESP32's LittleFS wrapper. Every other
backend failed to compile once event mode was enabled:

  error: 'class InternalFileSystem' has no member named 'totalBytes'   (nRF52)
  error: no member named 'totalBytes' in 'fs::FS'                      (Portduino)

This was not caught earlier because the event block is behind
#if USERPREFS_EVENT_MODE, and the existing unit tests only cover the pure
helpers, which compile identically either way.

Add fsTotalBytes()/fsUsedBytes() to FSCommon and implement them per backend:
littlefs v1 traversal for nRF52 (Adafruit_LittleFS) and STM32
(STM32_LittleFS), FSInfo for RP2040, statvfs for Portduino, and the native
methods for ESP32 and nRF54L15. The nRF52/STM32 path reports "full" if the
traversal errors so the capacity check fails safe.

Verified with USERPREFS_EVENT_MODE=1: native-macos test_event_profile_storage
passes 5/5, and heltec-v3, rak4631, rak11310 and rak3172 all build clean.

* fix(event): use std::filesystem for Portduino capacity, bump native-suite-count

Two CI fixes:

- native-windows has no <sys/statvfs.h>, so the Portduino branch of
  fsTotalBytes()/fsUsedBytes() broke the Windows build. Switch to
  std::filesystem::space(), which is already used under ARCH_PORTDUINO in
  HostMetrics.cpp and works on both POSIX and MinGW. Errors report "full"
  so the capacity check still fails safe.

- This PR adds test/test_event_profile_storage, taking test/ from 40 to 41
  suite directories, which trips the native-suite-count reconciliation check.

* fix(event): scope boot-write deferral to the radio profile, address review

Review feedback:

- Copilot: saveProto()'s boot-write deferral applied to every file, so
  loadFromDisk()'s recovery writes (e.g. restoring owner fields into
  devicestate) were silently dropped and never retried, because the ctor CRC
  baselines are computed after loadFromDisk(). Deferral now applies only to
  the radio-profile files, via isRadioProfileFile().

- jp-bennett: eventConfigFromStandard() wrapped a struct copy plus one field
  assignment in a header helper with its own unit test. Inlined at its single
  call site and removed; the behaviour is covered end-to-end by hardware
  validation instead (RAK4631 normal -> event -> normal preserves NodeNum and
  public key while swapping LoRa, and restores the original channel
  byte-identically).

- jp-bennett: the active-backup encrypted-storage migration ran for normal
  builds too, which changed non-event behaviour and contradicted the PR's
  stated event-only scope. Scoped to USERPREFS_EVENT_MODE; the adjacent block
  already covers the inactive standard backup in event builds.

New tests (all verified to fail under mutation, not vacuous):

- test_event_paths_never_collide_with_standard_or_shared_files: the core
  safety property. A path-table slip would make event firmware overwrite the
  user's real config, channels or backup, or capture a shared file like
  devicestate/nodedb. Nothing asserted this before.

- test_only_radio_profile_files_defer_boot_writes: guards the deferral fix
  above so re-widening it fails loudly.

- test_event_reservation_fits_smallest_supported_filesystem: the reservation
  is compile-time but must fit filesystems as small as 14 KiB (STM32WL) and
  28 KiB (nRF52840). Protobuf growth pushing it past those would silently
  stop event profiles persisting - the exact failure mode confirmed by
  fault injection on a RAK4631.

Verified with USERPREFS_EVENT_MODE both on and off: 7/7 tests pass, and
rak4631, heltec-v3, rak11310 and rak3172 all build clean.

* fix: preserve event profile storage recovery

---------

Co-authored-by: Jonathan Bennett <jbennett@incomsystems.biz>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
Co-authored-by: Benjamin Faershtein <benjaminfaershtein@Benjamins-MacBook-Pro-2.local>
2026-07-26 22:58:23 +00:00
Ben Meadors
5c5cb094e6 fix(admin): stop the admin dispatcher from overflowing the ESP32 loopTask stack (#11239)
AdminModule::handleReceivedProtobuf carried a 3008-byte stack frame - 37% of
the 8 KB Arduino loopTask stack. GCC inlines the eight handleGet* helpers into
it, each of which builds a whole meshtastic_AdminMessage (480 B), so the
dispatcher reserved a slot for every one of them at once even though only one
case ever runs. Two more full AdminMessages lived directly in the function.

That left any admin write short of stack for what runs beneath it:
NodeDB::saveToDisk -> saveProto -> pb_encode -> SafeFile -> LittleFS -> flash.
On heltec-v4 the canary fired at 7824 of 8192 bytes while writing
/prefs/config.proto, so no setting ever persisted and the device rebooted.

Mark the response builders NOINLINE and move the module-API and observer
responses into their own functions. Pure refactor; measured on heltec-v4 the
dispatcher frame drops 3008 -> 384 bytes, taking the crashing path from 368
bytes of headroom to 2992.

Fixes #11237
2026-07-26 21:34:38 +00:00
Austin
69b68e2c42 ESP32: Don't run pkg install on build (#11243)
pioarduino runs this anyways. No need to do it twice.
2026-07-26 17:11:58 -04:00
Austin
2c8a2a8cb8 Docker: Only cache qemu/buildx on default branch (#11241)
Docker qemu / buildx are eating up lots of cache for little benefit. Remove unless we're building from the main branch.
2026-07-26 16:47:53 -04:00
Austin
67e12dc8c0 Trunk: Only cache nightly runs, base on develop (#11240)
These trunk caches are eating us for dinner. Remove them for PRs so we aren't storing 20 copies
2026-07-26 14:53:42 -04:00
Jorropo
4800484d41 lint: ignore userPrefs.jsonc in trunk (#11174)
checkov/CKV_SECRET_6 flags the commented-out "large4cats" example, which
is the public default credential for the meshtastic.org MQTT broker rather
than a real secret. Ignore the file in .trunk/trunk.yaml so the example
config stays free of lint-suppression comments.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-26 08:26:27 +00:00