mirror of
https://github.com/alexhopeoconnor/firmware.git
synced 2026-10-04 03:18:10 +10:00
37d35c74f2d89e3c30c34da270e4ebb0fec7236a
12429
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
37d35c74f2 |
Uncomment CALIBRATE_TOUCH in platformio.ini (#11387)
replaces #11349 |
||
|
|
b1132cfec9 |
fix static IP on wired ETH (#11385)
* fix https://github.com/meshtastic/firmware/issues/11370 * address coderabbit review |
||
|
|
80d16234e5 |
fix(lora): skip DIO detach when no ISR is attached (#11386)
* fix(lora): skip DIO detach when no ISR is attached Fixes #11371 * fix(lora): latch the ISR-armed flag instead of tracking attach state The flag is now written once from task context and only read from ISR context. |
||
|
|
512154ff97 |
feat(BaseUI): show 'GPS Time Only' when GNSS has time but no position fix (#11361)
The position frame's drawGpsCoordinates() only distinguished 'No GPS present' / 'No GPS Lock' / coordinates, so a GNSS that had decoded valid time but no fix displayed identically to a cold chip. - GPSStatus: add per-acquisition hasTime flag (5th ctor param, accessor, matches() term, updateStatus() copy) - GPS::runOnce(): publish immediately on the gotTime rising edge so the flag reaches observers on the time-only path, which previously never published; done directly rather than via the end-of-loop block so fixHoldEnds is preserved and hold/power behavior is unchanged. Safe without a location: PositionModule ignores invalid positions. gotTime is already cleared on each GPS_ACTIVE entry, so the state is not sticky across acquisitions. - UIRenderer::drawGpsCoordinates(): the 'No GPS Lock' line becomes 'GPS Time Only' when time is valid. The drawGps() header renderer is intentionally untouched (its branches need separate de-clobbering work). Claude-Session: https://claude.ai/code/session_01CcrasD4QsatunDreANDgCx Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
2e958821f9 |
fix(BaseUI): suppress new-message pop-up while a menu is on screen (#11373)
Menus share the single global banner slot with notification pop-ups (Screen::showOverlayBanner overwrites message, options, and callback unconditionally), so a "New Message" banner arriving mid-menu destroyed the open menu and stole its input. Add NotificationRenderer::isMenuShowing() — true when the active overlay is interactive (a menu with options, or any picker/keyboard/pairing-PIN type) rather than a plain text banner — and skip the new-message banner in handleNewMessage() while such an overlay is up. Screen wake and hasUnreadMessage behavior are unchanged, and a new message can still replace an earlier plain banner. Claude-Session: https://claude.ai/code/session_01PSAouemtAihV5P87AgCadu Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Jason P <applewiz@mac.com> |
||
|
|
e958f7bdef | Add the PMU button handling to BPF (#11379) | ||
|
|
cd716fe384 |
logging: audit log strings for terseness, reclaiming ~6.8 KB of string data (#11374)
* logging: strip redundant punctuation, level prefixes, and 'successfully' from log strings The logger already appends a newline and prints the level tag, so trailing '.', '!', '...', literal \n, and 'Error:'/'Warning:' prefixes inside format strings are wasted flash bytes. Same for 'successfully' (the affirmative form already implies it). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LBiZc9sfPrH1MZ2L3Fxgt1 * logging: tighten verbose log strings in modules, radio, platform, and system code Rewrite wordy log messages to terser equivalents - drop filler words (articles, 'attempting', 'due to', 'please'), use 'Can't X'/'X failed' phrasing, and abbreviate where the codebase already does (config, init, msg, BT). Format specifiers and argument lists are unchanged; distinctive greppable tokens are preserved. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LBiZc9sfPrH1MZ2L3Fxgt1 * logging: tighten verbose log strings in telemetry sensors and GPS Same terseness pass: drop filler, 'Can't X'/'X failed' phrasing, common abbreviations (temp, msg). Specifiers, arguments, and sensor-name prefixes unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LBiZc9sfPrH1MZ2L3Fxgt1 * logging: tighten verbose log strings in mesh core Same terseness pass over NodeDB, Router, MeshService, PhoneAPI, RadioInterface, NextHopRouter, and PacketHistory: 'X failed'/'Can't X' phrasing, imperative verbs, dropped filler. Specifiers and arguments unchanged; duplicate literals kept identical to preserve linker string dedup. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LBiZc9sfPrH1MZ2L3Fxgt1 * logging: 'Unable to/Could not/Cannot' -> "Can't" in log strings Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LBiZc9sfPrH1MZ2L3Fxgt1 * logging: clang-format rewrap after string shortening Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LBiZc9sfPrH1MZ2L3Fxgt1 * logging: restore boot-logo trailing newline and progress-dot strings The terseness pass over-trimmed: the Meshtastic ASCII boot logo kept its blank line via a trailing \n, and three bare "." progress ticks were reduced to empty strings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LBiZc9sfPrH1MZ2L3Fxgt1 * Update src/mesh/wifi/WiFiAPClient.cpp Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * logging: address review feedback on the terseness audit - Node/packet IDs use the repo's 0x%08x convention in NextHopRouter, NodeDB, AdminModule and CannedMessageModule. The sibling log in each if/else pair is converted too, so a pair isn't split across two formats. next_hop stays 0x%x - it's the last-byte relay hint, not a NodeNum. - RTC: the read-path and set-path "not found" warnings were byte-identical, so the linker deduped them and the log couldn't say which one fired. Split into "RTC read:" / "RTC set:". (The four sites live in mutually exclusive #ifdef branches, so the RTC family was never ambiguous.) - SCD4X getAmbientPressure()/setAmbientPressure() logged "altitude", and SCD30 getASC() logged "Can't send command" for a read. Both now name the operation they actually perform. - LOG_ERROR already carries the level: ". Error: %u" -> ", rc=%u" (matching the existing rc=%d house style) and "Error executing X()" -> "X() failed". - Typos and wording: "OTA partiton. (Reason" -> "OTA partition (reason", "CST3530 not response ~" -> "CST3530 no response", "Packet received with to: of 0" -> "to=0", HostMetrics "Error decoding" -> "Can't decode", and the dangling ": " on the NextHopRouter retransmission line. Printf specifier sequences are byte-identical on all 37 touched lines apart from the 6 deliberate %x/%u -> %08x node-ID widenings, all on uint32_t args. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: Ben Meadors <benmmeadors@gmail.com> |
||
|
|
5692518306 |
Update meshtastic/device-ui digest to 4f150e5 (#11376)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> |
||
|
|
5f198c44d7 |
nrf52: reclaim flash on the three targets nearest the warm-store cap (#11363)
* nrf52: drop BSEC2/BME68x from the three targets nearest the flash cap
nrf52_promicro_diy_tcxo has been failing the nrf52_warm_region guard on
develop since 2026-08-05 (image ends at 0xEA918, 2328 B past the 0xEA000
warm-store cap). rak4631 and rak_wismesh_repeater_mini are ~14 KiB clear
while the nrf52 fleet drifts ~2 KiB/day, so both are close behind.
lib_ignore bsec2 + BME68x Sensor library defeats BME680Sensor's
__has_include(<bsec2.h>) gate, recovering ~46 KiB on each:
nrf52_promicro_diy_tcxo 0xEA918 (FAILED) -> 0xDF1A8, 43.6 KiB clear
rak4631 0xE6550 -> 0xDADE8, 60.0 KiB clear
rak_wismesh_repeater_mini 0xE65AC -> 0xDAE64, 59.9 KiB clear
This costs BME680/BME688 support, i.e. RAK1906 on WisBlock. It is a
temporary measure until the environmental sensor roster is opt-in per
board rather than linked into every target.
rak_wismesh_pocket inherits from env:rak4631 but has ~37 KiB clear, so it
is explicitly held at the nrf52 default and keeps BME680. Both lib_ignore
lists extend ${nrf52_base.lib_ignore} so BluetoothOTA and lvgl stay
ignored.
* nrf52: give the RAK boards BME680 back, drop their Ethernet instead
Excluding BSEC2 on rak4631 cost BME680/BME688 support, i.e. the RAK1906
WisBlock module - a common peripheral on the most popular nrf52 board. The
Ethernet stack is the better thing to lose: RAK13800 W5100S users are served
by the dedicated env:rak4631_eth_gw build, which has its own variant.h and
~163 KB clear, so the general-purpose image was carrying a networking stack
for hardware almost none of its users have.
rak_wismesh_pocket already set HAS_ETHERNET 0 for the same reason, so this
follows an existing in-tree precedent rather than inventing one.
rak4631 0xE6578 -> 0xE0D08, 35.7 KB clear (-21.6 KB)
rak_wismesh_repeater_mini -> 0xE0D58, 35.7 KB clear
rak4631_eth_gw -> 0xC1050, 163 KB clear (unchanged)
rak4631_eth_gw_dbg -> 0xC1210, 163 KB clear
That also brings rak4631 back under its declared flash budget in
bin/ram_budgets.json (766,216 vs 786,000 bytes), which develop is currently
failing - size-budget-gate is what has been ejecting PRs from the merge queue.
mqtt/ is deliberately kept: src/mqtt/MQTT.cpp carries both the networked
client (gated on HAS_NETWORKING, now 0) and the phone client-proxy path, so
dropping the directory would have removed MQTT-over-phone as well.
nrf52_promicro_diy_tcxo keeps the BSEC2 exclusion from the previous commit.
It has no Ethernet to reclaim - nrf52_base already filters mesh/eth, mesh/api
and mesh/wifi, and HAS_ETHERNET defaults to 0 - so the sensor library is the
only lever there. It goes 0xEA918 (FAILED) -> 0xDF1C0, 43.6 KB clear.
env:rak4631_eth_gw_dbg now extends env:rak4631_eth_gw rather than env:rak4631:
it already used the gateway env's build_flags and lib_deps, and only that env
still compiles mesh/eth + mesh/api.
* Address CodeRabbit review comments on PR #11363
- Condense the HAS_ETHERNET comment in rak4631/variant.h to one line
- Clarify rak4631/platformio.ini src-filter comment: Ethernet-free,
not networking-free, since MQTT is retained
- Restore custom_meshtastic_* metadata on env:rak4631_eth_gw so
env:rak4631_eth_gw_dbg (which now extends it) doesn't lose the
hw_model/display metadata it previously inherited via env:rak4631
---------
Co-authored-by: Jason P <applewiz@mac.com>
Co-authored-by: Claude <noreply@anthropic.com>
|
||
|
|
de6b23190a |
Test suite rebuild (#11322)
* docs(nodedb): make the native node cap unambiguous The native node cap was stated in four places that disagreed, and the disagreement already caused a wrong diagnosis: a saturated 200-node database looked arithmetically impossible because the cap had been read as 248, computed from a header that does not apply on this platform. The real value is 198. On portduino MAX_NUM_NODES is not a compile-time constant at all - the variant defines it as `portduino_config.MaxNodes`, resolved at runtime, default 200 and settable per host with `General: MaxNodes`. variant.h is reached before mesh-pb-constants.h, so that header's ARCH_PORTDUINO branch never fires and its plausible-looking 250 is dead code. - #error-guard the dead branch rather than leave a wrong number where people grep. The guard found a real defect: seven translation units reach mesh-pb-constants.h without configuration.h (SerialConsole.cpp, StreamAPI.cpp, PacketAPI.cpp, ServerAPI.cpp, PiWebServer.cpp, ServiceEnvelope.cpp, MeshtasticOTA.cpp, and test/TestUtil.cpp), so each was compiling with a different MAX_NUM_NODES - and therefore a different PACKETHISTORY_MAX - than the rest of the build. Each now includes configuration.h first. It cannot be included from mesh-pb-constants.h itself: that reaches SerialConsole.h through DebugConfiguration.h and closes a cycle. - Name the bare 250 in getMaxNodesAllocatedSize() NODEDB_MIGRATION_LOAD_CEILING. It is a decode allowance for files written by larger-cap firmware, not a cap, and it read like one. - Fix docs/node_info_stores.md, which named the wrong source and a "10-250" range that is wrong for native, and the copilot-instructions tunables line that said "portduino 250". * test(harness): give each suite its own scratch HOME and report leftovers Native suites shared one directory. Every suite that constructs a NodeDB loads and saves ~/.portduino/default/prefs/ - nodes.proto, config.proto, channels.proto, module.proto, device.proto, warm.dat, transmit_history.dat - and nothing cleared it, so state leaked suite -> suite within a run and run -> every run after it. A test run could also rewrite a real meshtasticd node database on the same machine. Per-run isolation does not fix this: the leak is generated inside a single run, so the boundary has to be per suite. bin/pio-test-isolate.sh runs each suite in its own scratch $HOME, registered as test_testing_command for env:native and env:coverage so a bare `pio test` and CI get the same boundary, not just bin/run-tests.sh. It runs the binary unchanged and exits with its exit code, so PlatformIO's pass/fail is untouched. Overriding HOME here rather than around `pio` also sidesteps the blocker that a bare HOME= breaks pio's own ~/.platformio/penv/bin/pio lookup. Leftovers are reported as a second axis, PASS/FAIL x CLEAN/DIRTY, because an unintended write has no matching assertion by definition - nobody writes TEST_ASSERT for a save they do not know is happening. The harness asserts it from outside, so it applies to every suite without the author opting in. - Only the *set of changed paths* is asserted, never contents. Hashes answer the boolean "did this change?" and nothing more; content baselines over protobuf bytes would churn on every NodeInfoLite field added, which is how snapshot suites become noise. - Deliberate writes are declared in test/state-manifest.tsv - one central file, suite / flags / mandatory reason. run-tests.sh prints the opt-out count on every run. - Granularity follows the state flag, so the two ship together: per-test by default (TestUtil redefines RUN_TEST to checkpoint after each test, naming the exact test that dirtied things), suite boundary for state=per-suite, where carrying state across test cases is the declared behaviour. - A declared write that does NOT happen is reported as MISSING, not folded into DIRTY. It catches silently broken persistence; a warning for now, since some are conditional. - Graded AMBER, not RED. With isolation in place DIRTY means "undeclared", not "dangerous", and a check that lands red on day one gets switched off. Guard the guard, both halves: state_assert_empty() refuses to run a suite against a sandbox that is not empty (otherwise the after-diff measures against the wrong baseline and reports CLEAN while meaning nothing), and bin/test-state-check.sh drives the real wrapper with fixtures asserting CLEAN / CLEAN / DIRTY / MISSING plus both directions of the empty assertion. A checker that silently matches everything would otherwise pass forever. --write-manifest proposes entries for a human to paste and justify; it never applies them, and neither does CI. * test(harness): stop reporting Unity's exit code as a signal A native suite ends in exit(UNITY_END()), and UNITY_END() returns the failure count. PlatformIO's native runner reads that non-zero exit code as a POSIX signal number, so four failures print "Program received signal SIGILL", five print "SIGTRAP", and the suite is classified [ERRORED] rather than [FAILED]. There is no crash. The signal name tracks the failure count and nothing else - it moved SIGILL -> SIGTRAP when a diagnostic probe added a fifth failure - and it cost hours of hunting a memory bug that did not exist, on an env (native) that carries no sanitizer at all. It also explains the phantom extra test case in the totals: the runner adds a synthetic entry for the signal it thinks it saw. run-tests.sh now says so inline whenever a signal line appears, and the three agent-facing docs say it too. * test(admin): isolate NodeDB and globals per test setUp() did `if (!nodeDB) nodeDB = new NodeDB();` and never deleted it, so 83 of the 85 tests shared one never-reset database and never restored config, owner, devicestate or channelFile. The fixture that does restore them was opt-in and armed by exactly two tests. The setUp comment claiming the rest "set their own config/region state and are unaffected" was not true - the admin handlers under test write all four globals. Route every test through the fixture instead: setUp saves the globals and installs a fresh NodeDB, tearDown restores and deletes it. The two tests that armed it themselves no longer need to. All 85 pass, so nothing was silently relying on the shared state. It costs about 7% of the suite's runtime (a NodeDB construction is a loadFromDisk plus, with a region set, key generation) - worth paying to write the phase 3 tests against a clean fixture rather than 83 tests' residue. Also cap the per-test attribution in the run summary at five entries; the full list stays in the suite's sandbox. * test(fs): cover the bounded file-manifest walk getFiles() runs on every phone sync via STATE_SEND_FILEMANIFEST, and nothing asserted any of its bounding behaviour. It does execute unasserted from test_stream_api's handshakes, but the cap, the depth limit, the wasLimited paths, overlong-path rejection and capacity release were all unguarded. Eight tests, all describing what the code does today: today's code is already correct here, since #10778 landed the by-reference collectFiles(), the 64-entry cap, the strlcpy bounds and the swap-idiom release. They pass on arrival, which is the point - this is the baseline a later change has to leave alone. Two things they do not cover, and cannot: - Moving reserve() outside the __cpp_exceptions guard. Exceptions are on natively, so the #else branch is not compiled. The suite's job there is to prove that change alters nothing observable. - The file.name() null guard. No in-tree backend returns null; the guard is defensive. The manifest-release test pins the swap idiom rather than calling PhoneAPI's releaseFilesManifest(), which is file-local. It asserts capacity() == 0, not just size() == 0 - a size-only check passes on clear(), which is the bug #7924 shipped. Suite count 43 -> 44, recounted against the directories rather than copied. * test(admin): assert node-DB metadata saves skip the radio reload set_favorite_node, set_ignored_node and toggle_muted_node each persist a NodeInfoLite bit and nothing else. MeshService::reloadConfig() gates its region re-derivation and configChanged notification on saveWhat & (SEGMENT_CONFIG | SEGMENT_CHANNELS), so a SEGMENT_NODEDATABASE-only save already skips the live radio reconfigure. Pure characterization - all three pass on develop. Worth pinning because that reconfigure is the path implicated in the WisMesh Tag favourite-node crash, and develop asserts nothing about it: widening the saveWhat mask or reordering the check would currently go unnoticed. Ported from the config-save series along with ConfigChangedCounter (an Observer<void *> counting configChanged notifications, the only externally visible signal that the reload branch was taken) and TEST_NODE_NUM. They join the existing suite, so no suite-count change. * refactor(menu): extract the mute toggle into a named function The node menu's mute action was inline in a banner-callback lambda, and that lambda only ever runs via screen->showOverlayBanner() - which is why nothing in MenuHandler.cpp was reachable from a test. Lift the `selected == Mute` branch into menuHandler::toggleNodeMuted(uint32_t) and call it from the lambda. Behaviour-neutral by construction: same statements, same order, same bare saveToDisk(). The null check moves into the function, so the call site no longer needs its own lookup. Verified by the native build and suite; the byte-identical-image check on a headroom-constrained nRF52 board was not run locally - CI's firmware-size comment covers it. Three tests come with it, all describing today's behaviour: - the bit flips both ways and no configChanged fires (develop never calls reloadConfig on this path); - an unknown node is a no-op rather than a write; - and the segment mask. Flipping one NodeInfoLite bit currently rewrites all five segments via bare saveToDisk(). That is asserted deliberately, with the comment naming it as characterization of a known defect: a pending fix narrows it to SEGMENT_NODEDATABASE, and when it lands this assertion is expected to change, which makes the improvement visible in the diff instead of silent. saveToDisk() is not virtual, so the mask is observed through its effect - remove the five prefs files, toggle, and see which reappear. * docs(test): make every suite count a pointer to the canonical one test/native-suite-count is the registered total and is machine-checked against test/test_* on every full run and by the suite-count-check CI job. Every other statement of the count is a copy that drifts: copilot-instructions said 12, AGENTS.md said 19, and the real number is 44. Replace both literals with a pointer to the file, say explicitly that no document should state the count as a literal, and reframe the two suite listings as descriptions rather than inventories - they carry per-suite information the count does not, so they stay, but nothing should infer completeness from their length. Register the new FS suite in both. * test(harness): randomise suite order, reproducibly Landed last, deliberately. Randomising an order-dependent suite set does not find bugs so much as convert a silent pass into intermittent red, and the first instinct is to revert the randomisation rather than fix the coupling. Phases 1-2 removed the coupling; this keeps it removed. Both runners previously hid order dependence behind a fixed order that happened to differ between them, and neither order was chosen: CI's area rules put admin first, PlatformIO's local discovery is reverse alphabetical and put it last. CI was green by accident. - bin/run-tests.sh --shuffle / --seed <n>. The seed defaults to HEAD's short SHA: one order per commit, so a red is replayable and attributable to the diff instead of flaky, while the project keeps exploring orders. Printed at the start and carried into the RESULT line, so a verdict is replayable from that line alone; the full order is printed on failure, because for an order-dependent failure the order is the diagnostic. - The shuffle is a Fisher-Yates over a MINSTD generator rather than awk's rand(), whose sequence differs between gawk and mawk. A seed that does not reproduce the same order on another machine is not a seed. - Shuffling needs one `pio test -f <suite>` invocation per suite - PlatformIO orders by its own os.walk() over test/ and filters only select - which measures at about 4.7s per suite of extra startup. - CI shuffles its area order, seeded from GITHUB_SHA and printed with the command to replay it locally. Intra-area order stays PlatformIO's; controlling it there would mean per-suite invocations, which is a cost worth deciding separately. Also records the 16 measured entries in test/state-manifest.tsv, each with its reason, taken from a full run's --write-manifest output rather than guessed. * test(default): cover the region-throttle interval overload getConfiguredOrDefaultMsScaled(configured, default, nodes, TrafficType) is the overload every telemetry and position module actually calls, and nothing referenced TrafficType anywhere under test/. All four of its behaviours were unguarded: the no-region guard, the throttle <= 1 short-circuit, the multiply, and the 64-bit overflow clamp. The throttles are real, not hypothetical - EU_866 carries PROFILE_LITE, which sets both positionThrottle and telemetryThrottle to 10, so a change here moves broadcast spacing in that region by an order of magnitude. Each test pins numOnlineNodes at the congestion threshold and uses ROUTER, which never congestion-scales, so the coefficient is 1 and the throttle is the only variable. The overflow case needs a base above INT32_MAX/10, hence three days rather than one. * ci(test): keep pull-request suite order fixed, seed the rest Shuffling the area order on every run - including pull_request - would turn a contributor's PR red for an ordering they did not choose, which is how a randomisation gets reverted instead of the coupling being fixed. That is the exact dynamic the ordering work was sequenced last to avoid, and the previous commit walked straight into it. - pull_request keeps the fixed declared area order. - push and schedule shuffle, seeded from the commit SHA: deterministic per commit, printed, attributable, and never blocking someone else's PR. - A suite_order_seed input on workflow_call and workflow_dispatch overrides both, so a specific failing order can be replayed anywhere, including on a PR. The run log prints which mode it took, the resulting order, and the local command to replay it. * ci(test): satisfy CKV_GHA_7 and yamllint on the seed input The seed is reachable through workflow_call, which callers can pass programmatically. The workflow_dispatch copy tripped checkov's "workflow_dispatch inputs MUST be empty" rule, and suppressing it was not worth it: replaying a specific order is a local operation, and the run log already prints the exact bin/run-tests.sh command to do it. * style(menu): apply the node-ID format convention RadioInterface.cpp documents the rule: 0x%08x in logs, !%08x in user-facing display. MenuHandler held every remaining exception - seven logs printing bare %08X, and two display labels doing the same. Repo-wide there are now no bare %08X node IDs left in log calls. * ci(test): pass workflow inputs through env, not shell interpolation suite_order_seed and github.event_name were spliced into the run: script as ${{ }} text, so a value carrying shell metacharacters would execute as code on the runner rather than being read as data. semgrep (run-shell-injection) and zizmor (template-injection) both flag it. Both now arrive as environment variables and are read as "$VAR". * refactor(test): share the seeded shuffle between the harness and CI bin/run-tests.sh and test_native.yml each carried a byte-identical copy of the MINSTD Fisher-Yates awk. The workflow prints "replay locally: ./bin/run-tests.sh --shuffle --seed $seed" after a shuffled CI run, and that instruction is only true while the two agree - drift would be announced by a replay quietly reproducing a different order than the one that failed. Extract shuffle_suites() to bin/lib/shuffle.sh and source it from both. Permutations verified identical across seeds before and after the move. * fix(test): correct the shared-state MISSING check and summary join Three defects in the new harness: state_classify() matched declarations two different ways - state_path_declared() for "undeclared", a hand-rolled regex for "missing". Interpolating an entry into an ERE also let a metacharacter in a manifest name match a file that is not the declared one. Both directions now go through the one helper. `paste -sd'; '` does not join with "; ": with -s, paste cycles through a multi-character delimiter one character per join, so paths rendered as "a;b c;d e". Replaced with an awk join. test-state-check.sh ran on after a failed cd instead of stopping (SC2164). ./bin/test-state-check.sh: 6/6 fixtures pass, MISSING included. * fix(portduino): bound General.MaxNodes MaxNodes was validated only for <= 0. Any positive value, including a typo'd or pasted-in one, propagates to MAX_NUM_NODES and scales both the node DB and the nodes.proto decode ceiling - failing at boot with no obvious cause. The ceiling is a sanity bound, not a capability limit; raise it if a host genuinely needs more. * docs(nodedb): reconcile the capacity tables The property matrix omitted the ESP32-S3 100-node flash tier that the platform table above it lists, and neither mentioned that the WASM build overrides MaxNodes to 80 in wasm_config_apply(). * fix(nodedb): make mesh-pb-constants.h self-sufficient on portduino The ARCH_PORTDUINO #error assumed it was unreachable in a normal build. It is not: the vendored device-ui sources include this header without configuration.h, which broke both native-tft docker builds. Include configuration.h here instead, ahead of every compile-time default - variant.h overrides MAX_RX_TOPHONE as well as MAX_NUM_NODES, so placing it lower in the file just moves the divergence to a redefinition. The #error stays as a backstop for the case where that include genuinely stops providing the cap. Verified with the native env's own flags: a TU including only this header now compiles, normal-order use of both macros compiles, and NodeDB.cpp compiles. * fix(portduino): raise the MaxNodes ceiling to 16000 Marked artificial: nothing in the node DB fails at 16001. 16000 sits just under the 16384 (128 x 128) population where HopScalingModule saturates its sampling denominator and starts dropping nodes, so a host inside the bound still gets meaningful hop recommendations. * lint(trunk): advise on node IDs logged as bare %08x RadioInterface.cpp documents the convention - 0x%08x in logs, !%08x in display - but nothing enforced it, which is how the MenuHandler cluster drifted. 22 call sites in PacketHistory, NodeInfoModule and PositionModule are still off it. A trunk linter rather than a CI grep job, because trunk checks changed files: new violations get flagged without a 22-site cleanup landing in an unrelated PR. Modelled on the existing too-many-defined definition. Scoped to values it can tell are IDs - an ID-shaped argument (->num, .from, getNodeNum) or message text naming one. A 32-bit hex that is not an ID is out of scope, so the CRC32 logs in ethOTA.cpp are correctly ignored. Emits "note", trunk's only non-blocking level: "warning" and "info" both exit non-zero and would gate CI, which is not what a log-format nit deserves. The pre-existing sites are line-scoped in the allowlist, so a new bad call in those same files is still caught. * lint(trunk): stop exempting the known node-id-format sites The seeded allowlist made the rule green by declaring the backlog acceptable. Empty it instead, so the 22 pre-existing sites are reported and get cleaned up by whoever next edits those files. Costs nothing to do: the rule emits "note", so these are non-blocking either way. The allowlist stays for its real purpose - a value the linter misreads as an ID. * style: log node and packet IDs as 0x%08x Clears the 22 sites the node-id-format linter reports, so the rule starts from zero rather than from a backlog nobody can see - trunk suppresses pre-existing findings by default, so left alone these would not have surfaced on edit the way an empty allowlist implies. Format strings only; no argument or control flow changes. The !%08x user-facing display forms are deliberately untouched - that is the other half of the same convention. * test(harness): build once up front, so suite timings mean something run-tests.sh fused build and run in a single pio invocation, so whichever suite PlatformIO's directory walk reached first absorbed the entire src compile and reported it as its own duration. On a real run that made a 0.03s suite report 13m21s, and hid the build cost from every other number in the summary. Do what .github/workflows/test_native.yml already does: one --without-testing build pass, then run with --without-building. Measured on a full 44-suite run - the build is now a single reported figure and 968 test cases execute in 1.9s, with no suite above 0.084s. Build output goes to its own log rather than $LOG: the outcome regexes match "error:" and "[ERRORED]", so a compiler diagnostic sharing that file would read as a test failure. Both red paths now keep the log they quote from. $LOG and the build log are mktemps the EXIT trap removes, so the three grepped lines were previously all anyone ever saw - and the cause is usually further up than the first [FAILED]. * test(harness): keep the run log on every red path bin/pio-test-isolate.sh already keeps a failing or DIRTY suite's sandbox and log under .pio/test-state/<suite>/. What was missing is the cross-suite view: $LOG is a mktemp the EXIT trap deletes, so run-tests.sh quoted three grepped lines from a file that no longer existed by the time anyone looked. Preserve it as .pio/build/<env>/test-failure.log from both red paths - including "no success summary found", which said "see log" while preserving nothing, and which is exactly the case where the build died before any suite ran and so left no per-suite sandbox either. Cleared at the start of every run, so a green run cannot leave a red one's log lying around looking current. * fix(test): report the real failure count on a shuffled red A shuffled run is one `pio test` invocation per suite, all appending to the same log, so the log carries one PlatformIO "N test cases:" summary per suite. verdict_red() took `tail -1`, which reports whatever the LAST suite did: a failure in suite 3 printed a "0 failed" summary from suite 44 directly under "RED - failures detected:". Sum the summaries instead. A single summary line - every unshuffled run - is passed through verbatim, so the familiar output is byte-identical. The patterns are passed to the awk helper as strings rather than /regex/ literals: awk evaluates a regex literal in argument position as `$0 ~ /re/`, so the callee would receive 0 or 1 and silently sum garbage. * fix(test): do not emit an empty suite name for an empty shuffle `printf '%s\n' "$@"` with no arguments still writes one empty line, and both callers read shuffle_suites through mapfile, so an empty suite list arrived as a single suite named "". Return before the printf when there is nothing to shuffle. * test(harness): state and enforce the Linux host requirement The native harness is a Linux tool: bash 4+ (mapfile), GNU coreutils and GNU find (-printf, md5sum, -executable). Most of that predates this branch - mapfile and both find predicates are already on develop - but none of it was written down, so the requirement was there to be discovered rather than read. Refuse to start on a non-Linux uname instead of degrading. On a BSD userland this would not fail cleanly: it would mis-hash the sandbox and mis-read the suite list, and still print a verdict. A state check that silently measures the wrong thing is worse than one that declines to run. Carrying a per-host fallback was the alternative, and it buys a second code path that nothing in CI exercises. bin/test-native-docker.sh already exists for macOS and non-Linux hosts, and the native-macos PlatformIO env is a build target for meshtasticd, not a test host - the isolation wrapper is registered for env:native and env:coverage only. Documented in the script header, test/README.md, and both agent docs. * fix(test): terminate every suite with exit(UNITY_END()) Two sites across two suites ended on a bare UNITY_END(). That ends the reporting, not the suite: setup() returns, the runtime goes on calling loop(), and the process runs forever. PlatformIO does not notice - it reports a suite from its Unity output, not from process exit - so the suite passes, the run goes green, and the binary stays resident. Thirteen of them had accumulated on one dev box, the oldest 19 hours old. The costs are quiet by construction: - the per-suite sandbox is deleted underneath a live process, so its CLEAN/DIRTY verdict describes what the suite had written when the harness stopped looking, not what it left behind; - .gcda coverage and LeakSanitizer's report both flush from atexit handlers, so a suite that never exits contributes no coverage and gets no leak check; - each survivor pins its own deleted 94 MB binary, which du cannot see. One of the two is the #else of an architecture guard, which is the easiest one to get wrong - it looks like there is nothing to clean up. test_mqtt has a correct exit(UNITY_END()) in its live branch, so a "does this file call exit() anywhere" check passes the file whole. test_serial had two more. develop's serial-config validation rework restructured that suite - the architecture guard is gone and both remaining branches now exit correctly - so this commit no longer has anything to change there; bin/lint-unity-exit.sh, added later on this branch, is what keeps it that way. test/README.md gets a section on it, since the skeleton showing the right shape had not stopped this happening. * test(harness): detect and reap suites that outlive their run A suite that never exits was invisible: PlatformIO reports a suite from its Unity output, so the run stayed green while the binary kept running. Two checks, because they fail differently. Runtime, in bin/pio-test-isolate.sh: the sandbox $HOME is mktemp-unique per suite, so any process still holding it is a survivor of that suite. Matching on the environment rather than a remembered PID identifies one whatever its parentage - a fork, a grandchild, a process already reparented to init - none of which a $! comparison catches. Reaped before the after-fingerprint is taken, so that fingerprint measures a tree nobody is still writing to, and so a run cannot leave processes accumulating on the host. Recorded as a sixth summary column and graded AMBER: the tests did pass, but the CLEAN verdict and the coverage were measured under a false assumption. Author-time, as bin/lint-unity-exit.sh, wired into trunk at "note" like node-id-format: every UNITY_END() must be wrapped in exit(). The rule is per occurrence, and that is the point - a file-level "calls exit() somewhere" check passes test_serial and test_mqtt, which have a correct one in their live branch and a bare one in the #else. Running it over the tree turned up test_mqtt, which the file-level pass had missed. It allows `int rc = UNITY_END(); ...; exit(rc)`, used by test_packet_signing to restore globals between the summary and the exit. That is where the rule gives ground: capturing and never exiting would leak and is not flagged. Flagging a correct idiom would push someone to "fix" working code. bin/test-state-check.sh gains a survivor fixture, asserting the wrapper both reports and reaps - a detector that only reports leaves the host accumulating processes, which is half the harm. 8/8. * fix(lint): make the unity-exit scanner statement-aware The rule judged one physical line at a time, which reports two kinds of correct code as bare: /* a comment that happens to mention UNITY_END() */ <- interior lines were never stripped exit( UNITY_END()); <- exit( and the macro never met On a probe of both, two of three findings were wrong. This is a note-level rule whose whole job is advice, and bin/lint-node-id-format.sh already says why that matters: a false positive costs more than a miss. One that cries wolf gets ignored, and the real finding goes with it. Carry /* ... */ state across lines and accumulate logical statements before testing, with a 12-line cap so one unclosed call cannot swallow the rest of the file - the same structure lint-node-id-format.sh uses, so the two custom linters in bin/ work alike rather than each having its own idea. Verified both directions: the develop-era sources still produce the same four findings, the fixed tree produces none, and a probe covering block-comment interiors, wrapped exit(), line comments, return UNITY_END() and capture-then- exit reports only the genuinely bare calls - including a complete block comment followed by real bare code on the same line, which the state machine has to keep live. Reported by CodeRabbit on #11322. * fix(lint): tokenise instead of pattern-matching, and self-test it Second round of review findings on the same scanner, all confirmed by direct test before changing anything. Six defects, one root cause: layered regexes cannot tokenise C++. False positives (correct code reported): - UNITY_END() inside a string literal read as code False negatives (real leaks missed): - a string containing "/*" opened comment state and swallowed later lines - greedy .* removed everything between two block comments on one line, taking a bare call with it - myexit(UNITY_END()) matched the exit() exemption as a substring - x == UNITY_END() and total += UNITY_END() matched the assignment exemption Replaced with a character-level scan carrying comment state, and token-bounded exemptions: exit must be a whole identifier, and the capture form must be a plain `=`. Raw string literals are still not modelled - there are none under test/, and delimiter tracking for a case that does not occur would be untested code guarding untested code, so it is documented rather than guessed at. Also drops the `return UNITY_END()` exemption. It only terminates from main(), there is no main() under test/, and from a helper it just returns a count. bin/test-lint-unity-exit.sh pins all fifteen cases, every false positive and false negative found in review among them. The rule has been wrong twice in a way that looked fine by inspection; it needed a self-test more than it needed another careful reading. Two further findings in the same review: - bin/run-tests.sh dropped PASSTHRU in shuffled mode, so `--shuffle -vvv` built verbosely and then ran quietly. The shuffled loop now forwards EXTRA_ARGS, which is PASSTHRU minus the -f pair it supplies per suite. - bin/run-tests.sh did not guard `cd "$ROOT_DIR"`. And one that did not reproduce: the survivor fixture's glob does find the pid file (verified with the lookup instrumented - the earlier failure was an artifact of running the script from /tmp, where SCRIPT_DIR cannot resolve). The assertion was still weak, because an empty pid took the "not running" branch and passed vacuously. It now fails if the pid was never recorded, and finds the file by search rather than assuming a directory depth. Reported by CodeRabbit on #11322. * fix(lint): report each UNITY_END occurrence at its own location The self-test only asked "did the linter say anything", so it could not have caught a wrong line, a wrong column, or a missing second finding. Fixtures now assert the exact diagnostics as line:col, and the first run of that assertion found two real problems. The caret pointed at the wrong occurrence. For `exit(UNITY_END()); UNITY_END();` the verdict was right but the column was 17 - the wrapped call - because the scanner stripped terminating forms out of the whole statement and then reported the first occurrence it had seen. Two bare calls on one line reported once. Judged per occurrence now, by looking back through whitespace at what wraps it, so both the count and the caret are right. That also needed a position map from strip_noncode(): removing a comment or collapsing a literal shifts every later column, and counting occurrences in the raw line does not recover it either - TEST_MESSAGE("... UNITY_END() ..."); UNITY_END(); has two occurrences in the raw text and one in the code. Four of the expected columns I wrote by hand were also wrong, off by one. The linter was right in every case; the assertions were not. They are computed from the fixture text now rather than pasted from output, because a baseline accepted from the tool it is testing asserts nothing. 17 fixtures, including the two-on-one-line case from review and its mirror. Reported by CodeRabbit on #11322. |
||
|
|
7d54c13ec0 | fix(crypto): hash full size_t inputs (#11359) | ||
|
|
9cd73fecb9 |
Update meshtastic/device-ui digest to 289daf6 (#11295)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> |
||
|
|
45f0f1ea06 |
fix(rak4631): enable GPS power rail for WisMesh Repeater Mini (#11360)
PIN_GPS_EN was only set for Pocket, so GPS Off left 3V3_S on forRepeater Mini. Gate it on WISMESH_POCKET || WISMESH_REPEATER_MINIso generic rak4631 bases keep sensors powered. |
||
|
|
32c3b62ef3 |
fix(BaseUI): restore node list menu swallowed by module-frame SELECT dispatch (#11358)
* Fix input event handling to correctly identify real module frames in the UI * Refactor input event handling comments for clarity on module frame validation |
||
|
|
08722da3d9 |
fix(lr2021): live LF↔HF reconfigure via full begin() (#11279)
* fix(lr2021): full begin() on live LF/HF reconfigure.Avoid RadioLib -706 / assert when live-switching Sub-GHz ↔ LORA_24. * fix(lr2021): align band-hop begin() with init() robustnessMirror. RF-switch GPIOs, SPI/TCXO retries, and log CRC/RX-gain errors. * chore: trunk fmt LR20x0Interface bandHop line wrap * fix(lr2021): harden live band reconfigure (companion #1) * fix(lr2021): reject invalid freq in band-hop path select Require requestedMHz > 0 in isLr20x0BandHop, expose lr20x0ReconfigurePathfor FullBegin vs incremental selection, and extend native radio tests. --------- Co-authored-by: Thomas Göttgens <tgoettgens@gmail.com> |
||
|
|
74b9f6ff02 |
BaseUI: Select Environmental Data Source (#11209)
* Select Source * Fixes * remove per sensor name --------- Co-authored-by: Thomas Göttgens <tgoettgens@gmail.com> |
||
|
|
b59cd58d75 |
Add HM330x PM Sensor (#11069)
* Add HM330X PM Sensor * Update HM330X library * Bring reclock I2C to HM330x sensor * Fix probeHM330x for variants without AQ telemetry or telemetry in general * Remove old import * Bring back SHT2X from develop * Remove test comment and unused method in HM330X class * Reorder detection method. Add pending TODO for INA219 detection * Rework detection order and add INA219 register check --------- Co-authored-by: Thomas Göttgens <tgoettgens@gmail.com> |
||
|
|
e78b121d9f |
Lr1121 tcxo optional tries xtal first, and get all my yamls in a row (#11215)
* LR11x0: try XTAL before TCXO when oscillator type is uncertain On boards with TCXO_OPTIONAL, a TCXO-first attempt either hangs RadioLib's calibration wait forever on a bare/non-TCXO module (unpatched upstream), or costs a slow failed attempt before falling back even once that's fixed with a timeout. Measured on hardware: XTAL succeeds immediately on a bare module (~350ms) and fails fast and cleanly on a genuine TCXO module (~300ms, RADIOLIB_ERR_SPI_CMD_FAILED), so trying XTAL first is a strict improvement for hang-avoidance regardless of which oscillator is actually present. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * compacted * fix review comment * fix femtofox switches * correct the correction * 13 * 3s timeout * Treat SPI_CMD_TIMEOUT as an LR11x0 init failure The BUSY watchdog breaks RadioLib's wait, so the next bounded transfer returns SPI_CMD_TIMEOUT rather than SPI_CMD_FAILED. Only the latter was checked, so a watchdog-triggered failure fell through to getVersionInfo(), setRfSwitchTable() and startReceive() against an unresponsive chip. Also use Throttle::isWithinTimespanMs() for the watchdog's elapsed-time check instead of raw millis() arithmetic. * Drop the BUSY watchdog and probe XTAL before TCXO The watchdog bounded RadioLib's unbounded BUSY wait in LR11x0::config() by having LockingArduinoHal::digitalRead() report a stuck pin low exactly once. That let a TCXO-first attempt fail cleanly rather than hang, but it meant lying to RadioLib about a GPIO from a HAL shared by every radio driver. Ordering the attempts XTAL-first avoids the hang outright instead: attempt 1 configures no DIO3 Vref, so there is no calibration wait to get stuck in, and the TCXO fallback is only reached on a module that answered and refused XTAL. Attempts are now XTAL, then TCXO, then a settling retry on whichever oscillator was settled on - after a fallback that is a second TCXO attempt. Only TCXO_OPTIONAL builds probe XTAL; a variant that declares a Vref unconditionally still goes straight to it and never probes XTAL at all. SPI_CMD_TIMEOUT stays a failure alongside SPI_CMD_FAILED: a bounded per-command BUSY wait in Module::SPItransferStream() reports it in its own right, independently of the removed watchdog. * Drop a stray tab from the promicro TCXO readme trunk fmt: prettier flags the whitespace-only line inside the <summary> block, which was the only failing check on the PR. --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> Co-authored-by: Thomas Göttgens <tgoettgens@gmail.com> |
||
|
|
03e6b80989 |
Serial config validation (#11339)
* fix(serial): validate serial module config on every platform
AdminModule guarded the serial config validation by architecture but not the
assignment beneath it:
#if ARCH_ESP32 || ARCH_NRF52 || ARCH_RP2040
if (!SerialModule::isValidConfig(...)) return false;
disableBluetooth();
#endif
moduleConfig.serial = c.payload_variant.serial;
So on every other platform an admin "set module config: serial" stored a config
the firmware rejects on ESP32. override_console_serial_port combined with
DEFAULT, SIMPLE, TEXTMSG or PROTO is accepted and persisted today.
Two families are affected, for different reasons:
- portduino/meshtasticd, where the validation did not exist at all:
isValidConfig was a static member of SerialModule, and that class is inside
the same architecture guard, so `nm` finds no such symbol in the native
object.
- STM32WL (rak3172, wio-e5, CDEBYTE_E77-MBL, russell), where it existed and
was never called: the class guard includes ARCH_STM32WL and the AdminModule
call site did not.
Validation is pure config logic with no serial hardware behind it, so it moves
out of the class and out of the guard as a free serialConfigIsValid(). Its only
external references - clientNotificationPool, service, getValidTime - are
already unguarded elsewhere, so it links on every target. AdminModule's include
of SerialModule.h is unguarded for the same reason; the class itself stays
guarded inside the header. Only disableBluetooth() remains architecture-specific.
This changes what meshtasticd and the STM32WL targets accept: a host relying on
the unvalidated path (override_console_serial_port with a mode other than NMEA,
CalTopo or MS_CONFIG) is now rejected, as it already is on ESP32.
test/test_serial has asserted nothing since it was added in
|
||
|
|
40f4a95d72 |
Update meshtastic-esp8266-oled-ssd1306 digest to ace0fcb (#11356)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> |
||
|
|
1dde97f807 |
Fix stack buffer overflow in aes_ccm_encr for partial blocks (#11347)
* Fix out-of-bounds write in aes_ccm_encr for partial blocks aes_ccm_encr() writes a full 16-byte AES block to the output before XOR-ing with the input, so a trailing partial block writes up to 15 bytes past the length the caller asked for. Every caller in the tree passes a buffer with enough slack, so nothing misbehaves today, but the decrypt path clears it by only a few bytes. Encrypt into a temporary block and XOR out of it, matching what aes_ccm_encr_auth() and aes_ccm_decr_auth() already do in this same file. The ciphertext is unchanged. * Add regression test for the CCM partial-block write and drop stale workarounds The guard bytes past the caller's buffer catch the overflow without relying on a sanitizer, so the test is meaningful in the native environment too. encryptCurve25519() no longer needs to write extraNonce before aes_ccm_ae(): the call stays inside numBytes now, so the copy after it is the only one required. The comment warning about the 15-byte overshoot no longer describes the code. |
||
|
|
39e7aa6a6c |
Full T-echo card support + Compact UI (#11342)
* T-echo card * Update NRF52I2SOutput.cpp * Update NRF52I2SOutput.h * cleanup * Update buzz.cpp * use consistent runtime compact-panel check instead of mixing with compile-time macro * Update NodeDB.cpp * Update ExternalNotificationModule.cpp * switched to Throttle::isWithinTimespanMs * Update SharedUIDisplay.h * trunk fix * last cleanup * ClockRenderer.cpp for OLED_COMPACT_UI and setup Unit C6L for new UI. * Fixed regressions in standard OLED and TFT --------- Co-authored-by: Jason P <applewiz@mac.com> |
||
|
|
d89f3eb8fc |
fix(api): bound the stream drain so a full config dump can't starve the watchdog (#11164)
writeStream() drained the whole queue in one call - "send every packet we can". A client asking for the full config gets the node database, then the file manifest, then the packet backlog and the position replay, and none of that returns to loop(). On a full node database (120 entries) the dump runs past eight seconds, so on RP2350, where rp2040Loop() arms an 8s hardware watchdog and is the only thing that calls watchdog_update(), the board resets in the middle of the manifest. Reproducible on every connection; with a small node database the dump finished under the timeout and nothing looked wrong. Measured on a pico2_w5500_e22: last loop iteration at millis=27242, ServerAPI kept logging until uptime 35s, reset at 35.2s = 27.242 + 8.0. Take a slice instead. The PhoneAPI state machine is resumable, so writeStream() stops after STREAM_WRITE_BUDGET_MSEC and reports whether anything is left; runOncePart() then asks to be re-run immediately rather than sleeping out readStream's idle delay, so the dump keeps its throughput while loop() gets to feed the watchdog between slices. Backpressure on a retained frame still returns the normal delay - re-running at once would just spin on a full transport. Verified on hardware: 10/10 full config dumps against a node with 120 entries, no resets, dump still completes in ~8s. |
||
|
|
7302db1672 | Update LoRa firmware on Thinknode M7 (#11337) | ||
|
|
b026bfbd98 |
8mb partition table for ThinkNode-M7 (#11338)
This will break OTA and result in FS wipes. Wouldn't hurt to recommend complete wipes for the install, too. |
||
|
|
98813fe79d |
Shared e-ink hardware layer: foundation for target-by-target migration off GxEPD2 (#11142)
* Add shared e-ink hardware layer (graphics/eink) alongside legacy drivers Foundation for a target-by-target migration off the GxEPD2-based EInkDisplay2/EInkDynamicDisplay/EInkParallelDisplay stack: - src/graphics/eink/: chipset drivers, panel profiles, backlight helper (promoted from the InkHUD driver set, shared by BaseUI and InkHUD) - src/graphics/BaseUIEInkDisplay: OLEDDisplay adapter driving the new layer, with EINK_* compat macros matching EInkDynamicDisplay - [niche] build helper in platformio.ini; graphics/eink/ excluded from arduino_base so unconverted targets are unaffected - Screen/CannedMessageModule dispatch between the two stacks per env - InkHUD-specific touch code in TouchScreenImpl1 guarded with MESHTASTIC_INCLUDE_INKHUD (no-op today, required once BaseUI variants define MESHTASTIC_INCLUDE_NICHE_GRAPHICS without InkHUD) No variant is converted and no legacy file is removed; every existing env builds identical firmware. * Fix clang-format comment alignment in Screen.cpp * Address review findings in the e-ink driver layer - Screen.cpp: exclude InkHUD builds from all NicheGraphics BaseUI guards - BaseUIEInkDisplay: size the OLEDDisplay buffer from its actual indexing - EInkParallel: defer update() while an async refresh is in flight, honor the selected clear mode in the async task, never delete a live task - ED047TC1: clean up on failed initPanel, fix inverted bbepI2CWrite checks - UC8175: drop bogus 0x12 soft reset (0x12 is display refresh on UC8175) - LCMEN2R13EFC1: guard absent reset pin, bound the busy wait - SSD16XX/SSD1682: build the RAM window from the instance, not statics - Doc corrections in driver banners and Drivers/README * SSD16XX/SSD1682: send inclusive Y-end address (height - 1) * LCMEN213EFC1: adopt the shared wait timeout / fail-through pattern wait() now bounds the busy poll via Throttle and sets the EInk failed flag on timeout; sendCommand/sendData fail through like the SSD16XX and UC8175 drivers. EInk::runOnce clears the flag after the failed cycle. |
||
|
|
56da28fd27 |
Update earlephilhower/arduino-pico to 6.0.0 (#11351)
https://github.com/earlephilhower/arduino-pico/releases/tag/6.0.0 |
||
|
|
6eac181c2b | use PSRAM for MBED TLS (#11350) | ||
|
|
ee5f24f24c |
T-Beam BPF: Use the correct SVG (#11352)
Added to the web-flasher repo in https://github.com/meshtastic/web-flasher/commit/1b885865f12be306e312674f68c74fa3eab51317 |
||
|
|
b4969a4a23 | Update earlephilhower/arduino-pico to 5.7.0 (#11333) | ||
|
|
9e60b23334 | Targeted build fixes, especially Heltec T1 (#11345) | ||
|
|
8c0ef44b0b |
Add one wire i2c bridge support (#10078)
* 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> |
||
|
|
58657d484a |
Re-enable mbedtls secp384r1 on ESP32 (#11344)
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 |
||
|
|
ddb7bbf090 |
fix(power): support SGM41562 family on T-Impulse Plus (#11298)
* fix(power): support SGM41562 family on T-Impulse Plus * chore: format SGM41562 code |
||
|
|
5792e8751b |
Fix PMSA003 Enables on RAK4631 (#11331)
* 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 |
||
|
|
41c5c92874 | serial heap status messages (#11340) | ||
|
|
c0ddb209b5 |
Fix up the BiColor boot screen (#11334)
* 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 |
||
|
|
8d3ad2a146 |
Remove board_check (now using normal matrix) (#11312)
This was being misused / misunderstood (most were a no-op already). |
||
|
|
854eae456b |
QMA6100PSingleton constructor fix pio check warnings (#11313)
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] |
||
|
|
88a18663f6 | Actions: Restrict 'check' jobs to only run for pull requests and merge queue events (#11332) | ||
|
|
dc67d3b7e8 | Update stale bot action to version 11 (#11330) | ||
|
|
f481585de4 |
Make GxEPD2_Multi non-copyable to address cppcheck warnings (#11326)
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> |
||
|
|
d9f1622ec1 | Update ci-gate condition to check event types and cancellation status (#11329) | ||
|
|
ad5d877c16 |
Fix cppcheck redundantAssignment for OLED_GEOMETRY_OVERRIDE variants (#11325)
`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> |
||
|
|
4b4e82bd72 |
SenseCAP Indicator: RP2040 peripherals for the main firmware (#6220)
* 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> |
||
|
|
25996e9330 |
Fix W12 battery reading: add ADC_CTRL and correct the divider ratio (#11323)
* 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.
|
||
|
|
1d2fb23a0f |
Consolidate drawGps related code (#11304)
* Update UIRenderer.cpp * Update GPS code path |
||
|
|
d0e9d02cf8 |
Update meshtastic/device-ui digest to 95483d8 (#11318)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> |
||
|
|
478206ea72 |
NodeDB: Change pointer to const for mesh node retrieval in loadFromDisk (#11315)
Addresses cppcheck src/mesh/NodeDB.cpp:2431: [low:style] Variable 'us' can be declared as pointer to const [constVariablePointer] |
||
|
|
85c1bd4836 |
Address STM32 cppcheck warnings (#11314)
- Suppress GenericThreadModule warning when DEBUG_MUTE - Suppress warning stemming from check_skip_packages - Cast pointers to uint32_t before subtracting to avoid cppcheck warning |