* First version of DS248X bridge
* Add first iteration of DS248X sensor
* Supports single readings on DS2484
* Supports readings on ch0 for DS2484_800
* Detection of variant for DS248X
* Minor fix on retries for sensor init
* Allow multiple channel detect passes on 8-ch version
* Always read temperature via ROM matching
* Small comment to show how to send all channels
* Minor logging changes
* Prevent one-wire double definitions
* Detect ROMs per round
* Fix comment
* Prevent skipping on DS2482 ALT3 check
* Fix comment (again)
* Fix style checks
* Remove comment for multiple measurements
* Address CodeRabbit review findings on DS248X sensor
Set _variant on every detectVariant path and branch on the member, so a
failed variant probe retries instead of falling into single-channel init.
Search DS2482-800 channels into a scratch buffer so a transient one-wire
failure cannot erase a ROM found on an earlier pass, and count channels
that already hold a ROM.
Check every one-wire return value in readTemperatureROM, validate the
scratchpad CRC, and return DS248X_INVALID_TEMPERATURE on failure so the
existing sentinel checks reject failed reads instead of reporting stale
or uninitialised data.
* Probe IIS2MDCTR WHO_AM_I before the DS248X status check
At HMC5883L_ADDR the DS248X probe reads 0xF0. On the IIS2MDCTR that
sub-address sets auto-increment and targets 0x70, which is reserved and
returns an unspecified value; any of bits 0x02, 0x04 or 0x10 makes the
probe claim the magnetometer as a DS2482.
Reading the WHO_AM_I at 0x4F first is deterministic for both parts. A
DS2482 does not acknowledge 0x4F, an invalid command code, and leaves
its read pointer untouched, so the subsequent read returns Status,
Configuration, Channel Selection or Read Data. None of those can hold
0x40 at scan time, so the DS248X probe still runs and detects it.
This also restores develop's detection order and matches the structure
already used at BMA423_ADDR: specific ID match, then probe, then the
generic fallback.
* Read only the reported channel in getMetrics
getMetrics walked all eight DS2482-800 channels, but only channel 0 is
ever written into the measurement. Each populated channel costs a
blocking 750ms conversion inside readTemperatureROM, so a fully wired
bridge stalled telemetry for roughly 6s per cycle and discarded seven of
the eight readings.
Channels without a sensor were already cheap thanks to the isValidROM
guard, so this only affects boards that actually use more than one
channel, which is the reason to fit a DS2482-800 in the first place.
Multi-channel reporting is handled separately in #10192; a note records
that it should start the conversion on every channel before waiting,
rather than reading each channel end to end.
* Trim DS248X comments to one line each
---------
Co-authored-by: Thomas Göttgens <tgoettgens@gmail.com>
Let's Encrypt Generation Y chains sign a P-256 leaf with the P-384
intermediate YE1 under ISRG Root YE. mqtt.meshtastic.org switched to
this chain on 2026-07-29. With CONFIG_MBEDTLS_ECP_DP_SECP384R1_ENABLED=n
mbedtls cannot parse the peer chain and the TLS handshake aborts with
MBEDTLS_ERR_PK_UNKNOWN_NAMED_CURVE, breaking MQTT over TLS on every
ESP32 target.
Costs about 4 kB of flash.
Fixes#11316
* Minor fix for PMSA003I
* Remove class from State, make state verbose, sleep after init
* Minor changes to gate some ifdefs and state class
* Make decision tree explicit in AQ Telemetry. Also enable on phone
* Minor change in comment
* Fix issue staying on if failed enable. Minor change in log
* Fix up the BiColor boot screen
Fits the color below the divide of the two OLED colors for better boot appearance. Calculates out the same math for any other screens.
* Failed builds because of math, means you change the math
Fixes cppcheck issues
src/motion/QMA6100PSensor.cpp:173: [medium:warning] Member variable 'QMA6100P::rawAccelData' is not initialized in the constructor. Maybe it should be initialized directly in the class QMA6100P? [uninitDerivedMemberVar]
src/motion/QMA6100PSensor.cpp:173: [medium:warning] Member variable 'QMA6100P::_i2cPort' is not initialized in the constructor. Maybe it should be initialized directly in the class QMA6100P? [uninitDerivedMemberVar]
src/motion/QMA6100PSensor.cpp:173: [medium:warning] Member variable 'QMA6100P::_deviceAddress' is not initialized in the constructor. Maybe it should be initialized directly in the class QMA6100P? [uninitDerivedMemberVar]
cppcheck reports noCopyConstructor and noOperatorEq against GxEPD2_Multi
on every e-ink environment (over 80 duplicate pairs on a single
heltec-wireless-paper run, one per template instantiation point):
src/graphics/GxEPD2Multi.h:123: [medium:warning] Class 'GxEPD2_Multi <
GxEPD2_213_FC1 , GxEPD2_213_E0213A367 >' does not have a copy
constructor which is recommended since it has dynamic memory/resource
allocation(s). [noCopyConstructor]
The warning is correct. The constructor news one of two GxEPD2_BW drivers
into a raw pointer member and caches &driver->epd2 in epd2.m_epd2, so the
compiler-generated copy operations would alias that driver: two objects
would drive the same panel, and the second to be destroyed would free a
driver the first still points at.
Nothing copies it - EInkDisplay2 heap-allocates a single instance and
holds a pointer - so declare that intent by deleting the copy operations
rather than adding a suppression.
Also null the unselected driver pointer. Only one of driver0/driver1 is
allocated and the other was left indeterminate; every method branches on
`which` before dereferencing, so this is latent rather than a live bug,
but an indeterminate owning pointer is one refactor away from a wild
dereference.
Behaviour is unchanged: no caller could have copied this type, and the
two added stores only initialize a pointer that is never read.
Verified on heltec-wireless-paper: `./bin/check-all.sh
heltec-wireless-paper` now reports "No defects found" (exit 0), and
`pio run -e heltec-wireless-paper` builds clean. The build matters
separately here because syntaxError is suppressed in suppressions.txt,
so cppcheck alone would stay green on a malformed declaration.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
`pio check` reports at src/main.cpp:878:
[low:style] Variable 'screen_geometry' is reassigned a value before the
old one has been used. [redundantAssignment]
for every variant that pins its panel size with OLED_GEOMETRY_OVERRIDE
(t-impulse-plus -> GEOMETRY_64_32, t-echo-card -> GEOMETRY_72_40). The
diagnostic pairs the override write with the `GEOMETRY_128_128` write in
the SH1107 normalization branch: on those boards that write is a dead
store, clobbered a few lines later.
Skip the geometry writes when the variant pins the panel size. The
screen_model normalization still runs (the driver needs it) and
precedence is unchanged - the override still wins on those boards, and
nothing changes for boards without one. The compile-time USE_SH1107
write is guarded the same way so the defect can't reappear if a future
variant combines the two.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* indicator: RP2040 peripherals for the main firmware
The SenseCAP Indicator RP2040 co-processor serves as a generic
peripheral bridge over a serial protobuf link (interdevice.proto):
- FakeI2C implements TwoWire and tunnels write and read transactions,
so the standard sensor drivers and the I2C scan work unmodified on
the bridged second bus (WIRE1)
- FakeUART forwards GPS NMEA to the regular GPS driver
- SD card access with chunked file transfers, paged directory
listings and card statistics; device-ui loads map tiles and map
styles from the card behind the RP2040
- link at 2M baud with 4KB chunks, message structs kept off task
stacks
Log messages carrying their own bracket tag render it like a thread
name. Replaces the earlier IndicatorSensor/COBS approach.
* indicator: address review
Correlate responses with request ids, serialize the shared TX buffer,
reject oversized frames, fix RX buffer overflow and NMEA truncation,
full-length file paths.
* indicator: assign the GPS FakeUART at runtime
Static initialization order across translation units is undefined,
so createGps() assigns and null-checks the bridged serial instead.
Bound the NMEA length defensively.
* indicator: bump device-ui pin to 27e6c0c
* indicator: ping/pong link probe, non-blocking runOnce, FakeI2C locking
The RP2040 sends nothing unsolicited without a GPS module attached, so
wait_ready now probes with the new ping message instead of listening
passively. runOnce skips its pump while a requester holds link_lock,
keeping the main loop from blocking for a full request timeout. FakeI2C
serializes transactions between the UI task and the main loop with an
owner-tracked lock held from beginTransmission to transaction end.
* indicator: link resync, config-honoring GPS, bridged-bus routing, stats validity
Frame resync scans to the next magic instead of flushing the RX buffer,
and the pump handles all buffered frames per pass. The RX drain reads in
bulk and the protobuf encoder gets the correct buffer bound. GPS honors
the gps_mode setting on the Indicator instead of always running. RTC,
I2C keyboard and motion sensor drivers resolve WIRE1 through
ScanI2CTwoWire::fetchI2CBus so bridged buses reach the right transport.
FakeUART implements flush/availableForWrite/const-write from the Stream
contract and fences its cross-core ring buffer. SdCardInfo.stats_valid
is passed through to device-ui, and the remote FS backend gains the
remove operation used for cleanup of failed tile saves.
* indicator: retry lost link round trips, I2CResult UNSPECIFIED
Remote FS operations retry once on a transport timeout. Correlation ids
drop late responses of the first attempt; a retried append whose first
attempt landed is recognized by the offset conflict carrying the
resulting file size. Definitive failures are not retried, missing-tile
probes stay a single round trip. Regenerated bindings add the
I2CResult.Status UNSPECIFIED zero value so an empty result cannot
decode as success.
* indicator: nack responses, rename bridge classes to I2CProxy/UARTProxy
A request the co-processor cannot decode or handle is nacked, so the
requester fails fast instead of burning its timeout. All requests stage
the shared tx_message under link_lock. FakeI2C and FakeUART are renamed
to I2CProxy and UARTProxy after the pattern they implement, with their
instances following suit. Drops dead code (unused NO_NEWS_PAUSE,
unreachable not-running branches, doubled include guards) and the GPS
pin log line that is meaningless on the tunneled port.
* indicator: refuse a co-processor that speaks another protocol version
The ping/pong handshake now carries InterdeviceVersion. A pong reporting
a version other than ours means the RP2040 runs firmware that does not
match this build, so the bridge stays shut down for the session and the
mismatch is logged with both versions. Requests fail fast instead of
being misinterpreted by the other side.
* indicator: regen protos, interdevice protocol version 2
* indicator: per-task I2C contexts, gated handshake, retryable link failures
The bridged I2C bus is shared between the main loop and the UI task, and
TwoWire has no transaction bracket a lock can span: drivers drain the read
buffer with available()/read() long after requestFrom() returned. Each
calling task therefore gets its own staging and read buffers instead of a
lock that could be left held (or that could not protect the read buffer
anyway). The transaction is staged inside the link, under its lock.
No request is sent before the co-processor has completed the version
handshake, and runOnce keeps probing until it does, so a co-processor that
boots slowly or reboots on its watchdog no longer leaves the bridge dead
for the session. Requests in flight are counted, not flagged: two threads
can be in a request and the first one out must not clear the other's state.
File operations are retried on a lost frame and on a co-processor busy with
card maintenance, but not on a refusal (nack) or a definitive failure, and
they release the SPI lock while they wait so a slow link does not starve
the radio.
* indicator: fail safe on a peer mismatch, wait out card maintenance
FileStatus moved to a fresh tag: reusing the tag of the removed success flag
made every failure status decode as success on a peer that predates it.
A card being mounted (busy) is retried rather than reported as an empty
slot, and a co-processor busy with card maintenance is waited out: mounting
takes seconds and the free space scan of a large card walks its whole FAT,
which is not a reason to report a missing tile. The bridged I2C bus releases
the SPI lock as well, so the keyboard scan on the UI task cannot starve the
radio either. Slot claims in the I2C proxy are atomic, NMEA is not sent to a
peer we refuse to talk to, and the handshake is completed by the unsolicited
ping the co-processor sends when it has booted, which also reports a
reboot.
* indicator: regen protos, FileStatus back on the original tags
* indicator: regen protos, ping/pong carry the InterdeviceVersion enum
* indicator: point the protobufs submodule at the merged interdevice protos
* indicator: pin device-ui to the branch with the remote SD support
* indicator: honor the txOnly flag of flush, report dropped GPS writes
flush() through a Stream pointer discarded the receive buffer: the flag is
txOnly, and HardwareSerial::flush() keeps what has been received. write()
reported bytes as written even when the link refused to send them. The link
probe uses Throttle for its rate limit.
* indicator: decide the log tag on the formatted message, hex request ids
The thread tag was suppressed based on the printf template, which disagrees
with the rendered message it is compared against: a format starting with a
conversion could produce two tags, and one without a trailing bracket-space
lost the tag entirely. vprintf now receives the thread name and picks. Also
shifts only the bytes actually buffered after a frame, throttles with
Throttle and logs request ids as hex.
* indicator: SD mount, eject and format commands over the link
* indicator: bound how long a busy card state blocks the UI task
* indicator: a busy co-processor must not block the UI task for ever
The busy retry re-armed its own budget on every busy answer, so a
co-processor that stayed busy kept the caller in the loop with no way out.
Transport retries and the wait for a busy card are now separate budgets that
only count down.
* indicator: start each request from an aligned receive buffer
A byte run lost mid-response (a UART overflow during a 4KB tile chunk, when
the display starves the RX interrupt) misaligns the assembly buffer. The
buffer was never reset, so the poison outlived the request and cascaded into
the following chunks of the same tile: one glitch dropped a whole multi-chunk
tile, while single-chunk tiles resynced in the idle gap and survived. Each
request now flushes the buffer first, bounding a glitch to the one chunk it
hit. Adds resync/decode/timeout counters, logged rarely, to see the rate.
* indicator: enlarge the LVGL heap for low-zoom map tiles
The heap was 3MB and the image cache reserves 1.5MB of it, so a low-zoom map
tile could not find a large enough contiguous block to decode and rendered
white. 5MB of the 8MB PSRAM fixes it with room to spare.
* indicator: advance the device-ui and protobufs pins to the merged commits
Point the protobufs submodule at the merged SD command protos (protobufs
#986) so it matches the checked in interdevice sources, and bump the
device-ui archive to the current indicator branch tip that carries the SD
button and format UI.
* Update device-ui library dependency URL
* remove cutom sdkconfig
* remove duplicated synchronisation (after PR11278 is in place)
* set commit reference to updated RemoteSDService class
* Add board_level configuration for release
* fix cppcheck errors
---------
Co-authored-by: Manuel <71137295+mverch67@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: CodeRabbit <noreply@coderabbit.ai>
Co-authored-by: mverch67 <manuel.verch@gmx.de>
* Fix W12 battery reading: add ADC_CTRL and correct the divider ratio
The W12 battery config was taken from the vendor's Demo_06_ADC_Read.ino,
which reads GPIO1, multiplies by 2.0 under a literal "Assumption: 2:1
voltage divider" comment, and never touches the ADC enable at all. All
three of those details are wrong.
Schematic W12-MB-V0.2 sheet 1 has the divider behind a P-MOSFET high-side
switch so it only draws from the cell during a reading:
BAT --S[Q6 AO3401A]D-- R50 390K --IO1_ADC_IN-- R51 100K -- GND
|G R49 1K to BAT (gate pull-up: Q6 off by default)
+-- R48 1K -- C[Q7 S8050 NPN]E -- GND, base <- R52 1K <- IO2
So GPIO2 is ADC_CTRL, not "a second (solar/VUSB) divider" as the variant
claimed, and the NPN inverts it, making it active HIGH. Left undriven, Q6
stays off and GPIO1 sits at ground through R51 - a hard 0 raw rather than
the 100-250mV of noise a floating pin gives - so every boot reported
"battery hardware absent (USB-only)" and battery_level 101.
The divider is 390K/100K, so the multiplier is 4.9, not 2.0. That puts a
4.2V cell at only ~857mV on the pin, so drop the attenuation from the
12dB default (0-3100mV) to 2.5dB (0-1250mV) to use the range properly.
This matches the Heltec V3/V4 network, but their ADC_CTRL 37 cannot be
reused here: GPIO33-37 are consumed by this board's octal PSRAM.
Verified on hardware: reports 4067mV / 91%, stable to the mV across
consecutive samples, where it previously read 0mV with a cell attached.
* Trim the battery comment block to house style
Per the repo guideline that code comments stay to one or two lines and
avoid multi-paragraph blocks, drop the ASCII schematic from the header.
The full circuit trace lives in the previous commit message and the PR
description, which is where that rationale belongs.
Comment-only; both define values are unchanged.
- Suppress GenericThreadModule warning when DEBUG_MUTE
- Suppress warning stemming from check_skip_packages
- Cast pointers to uint32_t before subtracting to avoid cppcheck warning
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>
* 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>
* 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>
* 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>
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.
* 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>
* 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>
* 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
* 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
* 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>
* 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
* Strip the default PSK when licensed defaults are installed
* Only record rate-limited portnums from the phone
* Bound payload reads by the received size
* 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.
* 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
* 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