Skip to content

Merge develop into feat/as3935-lightning-sensor - #7

Merged
ndoo merged 318 commits into
meshmy:feat/as3935-lightning-sensorfrom
meshtastic:as3935-merge-develop
Aug 12, 2026
Merged

Merge develop into feat/as3935-lightning-sensor#7
ndoo merged 318 commits into
meshmy:feat/as3935-lightning-sensorfrom
meshtastic:as3935-merge-develop

Conversation

@caveman99

Copy link
Copy Markdown

Merges current develop into feat/as3935-lightning-sensor to clear the conflicts on meshtastic#10931.

Conflict resolutions

  • src/configuration.h: keep the DS248X and HM330X addresses from develop, append the three AS3935 addresses.
  • src/detect/ScanI2C.h: keep DS248X and HM330X in DeviceType, append AS3935.
  • src/modules/Telemetry/EnvironmentTelemetry.cpp: keep the display-source refactor from develop, keep IMMEDIATE_SEND_MAX_STALENESS_MS, drop the duplicate environmentTelemetryModule definition. Develop now defines it as = nullptr above the conflict region.
  • src/modules/Telemetry/EnvironmentTelemetry.h: drop the duplicate extern EnvironmentTelemetryModule *environmentTelemetryModule; at the end of the file. Develop added the same declaration at line 21.
  • protobufs: take develop (84bfb0f). The submodule bump is dropped from the branch until Add AS3935 lightning sensor telemetry fields meshtastic/protobufs#981 merges.

Net diff against develop is unchanged from the original PR: the same 9 files, minus the protobufs bump.

Remaining work on meshtastic#10931

src/mesh/generated/meshtastic/telemetry.pb.h does not contain lightning_strike_count_1h or lightning_distance_km, so AS3935Sensor.cpp will not compile yet. update_protobufs.yml only generates from master or develop, so the headers arrive once meshtastic/protobufs#981 leaves draft and merges.

caveman99 and others added 30 commits July 16, 2026 19:45
decoded.payload.bytes is a 233-byte protobuf field that is not NUL-terminated.
Printing it with a plain "%s" reads until a NUL, which for a full-length payload
with no NUL runs past the field. Use "%.*s" with payload.size, matching the write
form already used a few lines up in SerialModule.

Live sites: RangeTestModule appendFile, SerialModule text output. The same fix is
applied to the commented-out debug lines in RangeTestModule and Router so the
pattern is consistent if they are re-enabled.
* PhoneAPI: gate local admin on the connection, not the wire from

The lockdown admin check in handleToRadioPacket only ran when p.from == 0. from is
a client-supplied wire field, and MeshService::handleToRadio rewrites it to 0 before
AdminModule sees the packet. A client could therefore set from != 0 to skip the
!getAdminAuthorized() drop, then have the packet normalized back to a local-admin
identity and executed - unauthorized admin from an unauthorized connection.

Every packet in handleToRadioPacket already comes from the local connection, so
locality is a property of the connection, not of from. Move the decision into
classifyLocalAdminPacket(), which ignores from and keys only on the admin variant and
the connection's authorization: lockdown_auth is delivered inline, any other admin
from an unauthorized connection is dropped, authorized admin passes through.

The classifier is compiled unconditionally and unit-tested; the guarded caller (built
only in the nRF52 lockdown config) calls it. Test: an unauthorized connection's
ADMIN_APP packet with from != 0 is classified DropUnauthorized.

* PhoneAPI: wipe the encoded lockdown passphrase, shorten comments

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
…er (#10993)

* stm32wl(rak3172): implement cpuDeepSleep()/shutdown() via STM32LowPower

cpuDeepSleep() was an empty stub, so the SDS deep-sleep PowerFSM state
and low-battery shutdown did nothing but leave the CPU running at full
power. Power::shutdown() also didn't include STM32WL in its arch list,
so an explicit shutdown command just logged a FIXME warning.

Adds STM32LowPower as a lib_dep (rak3172 only, mirroring STM32RTC) and
implements cpuDeepSleep() using it. Standby mode is used for both the
finite-wake (SDS/low battery) and forever (shutdown) paths, via
LowPower.shutdown(), with or without an RTC alarm.

If the LSE-backed hardware RTC never came up (stm32wlRtcAvailable()
false), this is a no-op - safer than sleeping without a confirmed wake
source. LowPower.shutdown() resets the MCU on wake and should never
return; if it somehow does, force a reset via HAL_NVIC_SystemReset()
rather than hanging the device silently forever.

Gated behind the existing HAS_LSE flag, so this is inert on every
variant but rak3172.

Assisted-by: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Andrew Yong <me@ndoo.sg>

* feat(stm32wl): Warn and reset when trying to deepSleep without RTC

This is to prevent leaving the firmware catanonic when firmware has run
its shutdown routine but doesn't actually shutdown.

Signed-off-by: Andrew Yong <me@ndoo.sg>

---------

Signed-off-by: Andrew Yong <me@ndoo.sg>
The SOH/STX control frame carries a client-supplied filename that was passed
straight to FSCom open/remove/exists, so a ".." component could write, read, or
delete outside the filesystem root. On embedded LittleFS this is largely inert
(no parent of the partition root); on the Portduino daemon FSCom is the host
filesystem under a mountpoint, so it is a real arbitrary-path write/read/delete.

Validate the filename before any FS access: reject empty and any ".." path
component, and NAK the transfer. Absolute and subdirectory paths are still
accepted - the file manager transfers them from the manifest and PortduinoFS
confines them to its mountpoint - so only traversal out of the root is blocked.

Reachable only from a local client connection (PhoneAPI: BLE/USB/serial/TCP),
not over the RF mesh; on the daemon the TCP API makes it network-reachable.

native-suite-count goes to 34: +1 for the new test_xmodem suite and +1 correcting
a pre-existing miscount (it read 32 for 33 suite directories).

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
…1028)

On lockdown builds (MESHTASTIC_PHONEAPI_ACCESS_CONTROL, nRF52) the SerialConsole
is a process-lifetime singleton, so the per-connection admin-auth slot keyed by
its inherited PhoneAPI* is reused for every USB/serial client for the whole boot.
An operator's admin unlock stayed latched across serial client swaps: an attacker
plugging into the USB/serial port before the prior session's 15-minute inactivity
timeout inherited admin authorization -- the serial analog of the BLE stale-session
reuse bug closed by resetting state in onConnect()/onDisconnect().

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

Entirely nRF52-lockdown-gated; non-lockdown builds are byte-identical.
* AdminModule: only accept admin responses to requests we sent

An admin *_response short-circuited the auth and session-passkey checks that gate
every other admin message, so any node could deliver one. On a channel the module
listens to unauthenticated, a get_module_config_response drives the remote-hardware
pin handler with attacker-supplied values.

Track the destination of outgoing admin requests (per remote, with the pinned PKC
key when there is one) and accept a response only from a node with a matching
outstanding request, inside the same window as the session passkey. Local (from == 0)
admin is unchanged; PhoneAPI already gates it.

Also fix the response dispatch: get_module_config_response.which_payload_variant is a
ModuleConfig oneof tag, but it was compared against the AdminMessage ModuleConfigType
enum (different numbering), so the handler never ran. Compare against the oneof tag.

* AdminModule: rollover-safe request window, bind response to request type

Two review refinements to the request/response pairing:

Use Throttle::isWithinTimespanMs for the outstanding-request expiry instead of
comparing millis()/1000 sums, which mis-expired across the millis() rollover.

Bind each accepted response to a request type actually sent to that node. Each
outstanding record now carries a bitmask of the response variants its requests
authorize, so a get_owner request no longer admits a get_module_config response.
The mask accumulates per remote, so a client may still pipeline several request
types to one node and have every answer accepted.

* AdminModule: track admin requests per-request, not per-node

Reworks the outstanding-request table so each request is its own entry with its own
expiry window and pinned key, replacing the per-node bitmask that shared one timestamp
and one key across every response variant.

That sharing let a later request to the same node extend an earlier one's window and,
worse, clear its PKC pin: an unpinned request cleared keyValid, so a plaintext response
to an earlier PKC-pinned request was then accepted. Per-request entries keep each pin
intact. Identical requests are de-duplicated (a client may fetch several config subtypes,
all answered by one response variant) and eviction compares elapsed time, which is
rollover-safe.

Test: a pinned request's response still requires its key after an unpinned request to the
same node.

* AdminModule: match module-config subtype and consume answered requests

Two refinements to the request/response pairing:

Only remote_hardware get_module_config_response mutates state (the pin table), so it
must answer a request for that exact ModuleConfigType, not just any module-config
request. Each entry records the requested subtype and the gate checks it.

A matched request is now consumed on accept, so a node cannot replay a state-mutating
response within the window. Because one request yields one response, request de-dup is
dropped (a client's N indexed get_channel requests are N entries, each consumed once).

Tests: a non-remote-hardware request does not admit a remote_hardware response, and a
second copy of an answered response is rejected.
decryptForHash accepted chIndex == getNumChannels() before reading
getHash(chIndex), which indexes one past hashes[MAX_NUM_CHANNELS].
Use >= so an out-of-range index is rejected before the array read.
…city (#11048)

* TrafficManagement: gate role/NodeInfo cache writes on signer authenticity

The tier-3 role cache and the PSRAM NodeInfo response cache were updated
from any received NodeInfo with no authenticity check, so a spoofed
NodeInfo could set a node's cached role (granting dedup exceptions) or
poison the cached user served in direct responses. Skip both cache
writes when a known signer's NodeInfo arrives unsigned, matching the
identity-update gate on the direct-response path.

* TrafficManagement: hoist shared NodeInfo signer lookup

Compute the sender node lookup and unauthenticated-signer check once per
NodeInfo packet and reuse it for both the cache-refresh gate and the
direct-response identity gate, avoiding a second O(N) getMeshNode scan.

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
* Add telemetry update interval to userprefs

* Add telemetry screen configs to userprefs

* Removed duped code from cherry-pick
…#11053)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
* Use hardware RNG for session passkey and PKC extra nonce

The admin session passkey and the Curve25519 extra nonce were drawn
from Arduino random(), which is not a CSPRNG. Source them from
HardwareRNG::fill, mirroring the signing path, and fall back to the
seeded CSPRNG (CryptRNG) only when no hardware source is available.

* AdminModule: make session passkey expiry rollover-safe

session_time was compared as millis()/1000 seconds with additive
thresholds, which breaks across the millis() wrap and could keep a stale
admin session key valid. Store session_time in millis() and use
Throttle::isWithinTimespanMs for the 150s refresh and 300s validity
windows.

* AdminModule: track session passkey validity with an explicit flag

session_time == 0 was used as the uninitialized sentinel, but millis()
is legitimately 0 in the first millisecond of uptime, so a passkey
issued then would be treated as no session. Use a dedicated
session_passkey_valid flag instead.

* AdminModule: camelCase the session passkey validity flag

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
…11049)

* Add RAK WisMesh Pod variant and fix Tag LPCOMP wake on user shutdown

* chore: trunk fmt - replace Unicode em-dash in Pod comment

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
…runtime (#11025)

* Stop accelerometer thread when double-tap/wake-on-motion disabled at runtime

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

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

* Keep accelerometer thread running when its sensor drives the compass

* Guard accelerometer thread config toggles against a null thread pointer

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

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

---------

Co-authored-by: Thomas Göttgens <tgoettgens@gmail.com>
* Add retries for SCD4X data reading

* Adds a retry loop with 3 tries by default to get sensor data from the SCD4X

* Fix log and happy path
native-suite-count drifted from the actual test/test_*/ directory count: #10669
added two suites (test_admin_session_repro, test_pki_admin_fallback) but bumped
the count by only one, and #11037 added test_xmodem without bumping it at all.
The file reads 35 against 37 real suites, which bin/run-tests.sh reports as AMBER
on every full run. Correct it to 37.
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Fix renovate comments for:
- Crypto
- Adafruit_nRF52_Arduino
- adafruit/Adafruit-MLX90614-Library
- meshtastic/Fusion
* advertise a superset of EU regional presets to client devices.

* trunk
* Honor Node Ignore messages

* Screenless node fix
Removed:
- MessageStore::addFromPacket and addFromString, superseded by
  tryAddFromPacket
- GeoCoord rangeRadiansToMeters, distanceTo, bearingTo
- Router::rawSend, declared virtual with no override and no caller
- ContentHandler handleHotspot, handleFs, handleAdminSettings,
  handleAdminSettingsApply, handleDeleteFsContent and their commented
  route registrations, plus the now unreachable htmlDeleteDir and the
  handleUpdateFs declaration that had no definition
- ContentHelper replaceAll
- OnScreenKeyboardModule popup chain: showPopup, clearPopup, drawPopup,
  drawPopupOverlay and their state, unreachable since the frame based UI
  was replaced by baseUI
- DebugRenderer drawDebugInfoTrampoline, drawDebugInfoSettingsTrampoline
  and the orphaned drawFrameSettings
- NodeListRenderer calculateMaxScroll, drawColumns and a stale extern
  haveGlyphs declaration with no definition
- UIRenderer::haveGlyphs, Screen::blink,
  NotificationRenderer::showKeyboardMessagePopupWithTitle,
  VirtualKeyboard::getInputText
- InkHUD touchNavLeft, touchNavRight, Applet::getActiveNodeCount,
  ThreadedMessageApplet::saveMessagesToFlash
- TwoButton::setHandlerUp, TwoButtonExtended setHandlerUp,
  setJoystickDownHandlers, setJoystickUpHandlers
- CannedMessageModule LaunchRepeatDestination, isCharInputAllowed,
  hasMessages
- TrafficManagementModule resetStats, recordRouterHopPreserved,
  saturatingIncrement
- UnitConversions::MetersPerSecondToMilesPerHour
- EncryptedStorage getSessionRemainingSeconds
- BMI270Sensor::writeRegisters, GPS::hasFlow, FSCommon copyFile,
  SerialConsole consolePrintf, buzz playLongPressLeadUp,
  memGet displayPercentHeapFree
renovate Bot and others added 29 commits August 9, 2026 09:37
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
…g 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>
…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>
… 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>
* 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.
* fix #11370

* address coderabbit review
…#11388)

* fix(bin): correct filename check and esptool v5 subcommands in .bat installers

device-install.bat rejected every valid firmware-*.factory.bin name. The
substring-strip comparison was negated, so it errored when the suffix was
present instead of when it was absent.

Both scripts hardcoded esptool subcommand spellings. device-install.bat used
the v4 underscore forms only. device-update.bat used the v5 write-flash with
the v4 read_flash_status, so it worked fully on neither version. Probe the
help output once and select the spelling, mirroring bin/device-install.sh.

The probe uses %ESPTOOL_CMD% rather than !ESPTOOL_CMD! because cmd does not
split a delayed-expanded command token carrying a path into program and
arguments.

Fixes #8156

* fix(bin): make the -P interpreter option work in the .bat installers

Both scripts invoked ESPTOOL_CMD through delayed expansion. cmd does not split
a delayed-expanded command token that carries a path into program and
arguments, so "-P C:\path\python.exe" exited 9009 and the scripts reported
"esptool not found". Use %ESPTOOL_CMD% at the two command positions per file.

device-update.bat additionally wrapped the interpreter in doubled quotes, which
made python treat python.exe as a source file. Quote the path once, as
device-install.bat does, so interpreter paths containing spaces also work.

* fix(bin): anchor the .factory.bin suffix check and harden esptool detection

The filename check matched .factory.bin anywhere in the name, so
firmware-x.factory.bin.bak passed and the script then derived
firmware-x.bak.mt.json for metadata. Compare the last 12 characters instead,
matching the anchored glob in bin/device-install.sh.

A quoted interpreter path that does not exist returns 3 rather than 9009, so
the missing-esptool check skipped it and the script died at the probe with no
message. Treat 3 as missing as well.

device-update.bat read %ERRORLEVEL% after a CALL that overwrote it, so the
missing-esptool check never fired. Capture the exit code before logging it.
…ext (#11392)

The nRF52 BLE pairing banner sends "Bluetooth\nPIN\n[M]<pin>" where [M] is
a medium-font marker for the PIN line. The renderer only honored font tags
for lines covered by the parsed-line cache, so any path that draws a banner
line from the raw message - a stored-without-reparse banner, or a draw
racing the parse from the BLE task - showed a literal "[M]" ahead of the
pairing PIN.

Extract the per-line text/font decision into resolveBannerLine(): parsed
(tag-stripped) line when the cache covers it, otherwise strip a leading
font tag from the raw line on the fly and honor the font it names. Picker
content (e.g. node names) is deliberately exempt - it can be user data and
must never be tag-interpreted.

Also measure line widths on the text actually rendered (they were measured
on the raw, un-stripped string, skewing box width and centering), and check
p[1] for NUL before reading p[2] in the tag probe, which could read one
byte past the end of a line that ends in '['.

Add a native test suite (test_banner_font_tags) covering tag parsing, the
BLE pairing message, the cache-miss fallback, and the picker exemption.


Claude-Session: https://claude.ai/code/session_01CFMdkE6d8fY28Ax3hFVDMU

Co-authored-by: Claude <noreply@anthropic.com>
* fix(Power): survive a BQ27220 fuel gauge that fails to init

Keep the BQ25896 as the battery source when only the gauge fails, so Power
stays enabled instead of falling through to an ADC that these variants do not
have. Null-guard the gauge in getBattVoltage() and isCharging().

Retry the gauge from the power thread (3 attempts, 60s apart, address probe
first) since it is soldered on, and reset the I2C master after a failed init
so the bus scan does not run against a stale transaction.

Fixes #11372

* fix(Power): address review feedback on the BQ27220 retry

Derive "no attempt yet" from gaugeAttemptsLeft instead of a millis() zero
sentinel, drop the zero-padding on the logged I2C address, and condense the
new comment blocks.
* Block coordinate traffic on configured event channels

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* Suppress event coordinates in reliable relay paths

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* Reject blocked phone coordinates before rate limiting

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* Prevent event coordinates from reaching MQTT

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* Add event coordinate policy preference

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* Test event coordinate policy in native CI

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* Make event policy test tolerate a full NodeDB

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* Test Router event coordinate enforcement

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* Test PhoneAPI event coordinate retry handling

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* Test reliable event coordinate suppression

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* Test MQTT event coordinate suppression

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* Run event policy behavioral suites in native CI

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* tests: address CodeRabbit review feedback

- test_event_channel_phone_api: complete the setUp/tearDown save-restore
  pair. GlobalState now carries cryptLock and myNodeInfo; setUp() nulls
  cryptLock before constructing MockRouter (Router's ctor asserts it is
  unset), and tearDown() restores both so the suite leaves no global
  mutated. Not reachable today - the globals start null in this binary -
  but the pair was asymmetric.

- Replace the strcpy calls this branch added on Channel.settings.name
  (char[12]) with the bounded form the rest of the test tree already uses,
  strncpy(dst, src, sizeof(dst) - 1). Covers the flagged site in
  test_nexthop_routing plus the six equivalents in
  test_event_channel_phone_api, test_mqtt and test_position_precision,
  which trip the same ast-grep dangerous-buffer-functions-cpp rule.

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

---------

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: caveman99 <25002+caveman99@users.noreply.github.com>
* 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

* Add multi-sensor measurements for one-wire sensors using wildcard message

* Move to unpacked measurements in one-wire

* Add admin command to set main temperature in 8-channel one-wire bridge.

* Fix merge ref

* Trunk fmt

* Remove unused variable

---------

Co-authored-by: Thomas Göttgens <tgoettgens@gmail.com>
* Correct for awake time in AQ telemetry

* Minor typo on debug log

* Avoid updating start of cycle twice

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Fix log type

* Only update ahead of time if successful transmit

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
* Fix the all-zero MAC address for own node

* Increment native suite count from 46 to 47

* Fix condition for copying MAC address in PhoneAPI
* Derive the native suite count on the fly instead of registering it in a file

test/native-suite-count was a manually-maintained register of the test_*
directory count, reconciled against the actual directories by
bin/run-tests.sh (as an AMBER verdict) and by a dedicated suite-count-check
CI job. The reconciliation only ever guarded the file itself: the check
that matters - suites that actually ran vs. the test_* directories on
disk - already derives its expected count from a directory walk, so the
file added a bookkeeping step to every suite addition/removal without
adding signal.

Remove the file and everything that existed to keep it honest:

- bin/run-tests.sh: drop the canonical-count file read, the count-mismatch
  AMBER verdict, and the [canonical: x/y] suffix; the verdict lines already
  carry ran/expected from the directory walk. The shuffle seed suffix stays.
- test_native.yml: delete the suite-count-check job and its needs: edges.
- Docs (copilot-instructions.md, AGENTS.md, test/README.md) and the
  test-script comments now describe the count as derived from test/test_*
  at run time.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01APCEfNjd1X7ErDHEzT6Dqd

* Add suite-shrinkage-check: fail a PR that silently loses a test_* suite

With test/native-suite-count gone, nothing in CI noticed the suite set
shrinking: platformio test discovers and runs whatever test_* directories
exist, and bin/run-tests.sh derives its expected count from the same walk,
so a suite directory lost in a bad rebase or an overzealous cleanup just
means fewer suites run - every remaining check stays green.

Restore that tripwire git-aware instead of file-based: on pull_request
runs, compare the test_* directory list at the PR's merge base against the
PR result. A vanished suite fails the job unless its name appears in the
PR title, PR body, or a commit message in the PR's range - a deliberate
removal satisfies that by stating what it removes; an accidental loss
cannot. Other events skip: they have no natural base, and PRs are where
accidents arrive. No job depends on this one (a skipped job would skip
its dependents).

Incidentally: test/ currently holds 47 test_* directories while the
deleted count file said 46 - the manual register had already drifted,
which is exactly the bookkeeping failure mode this replaces.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01APCEfNjd1X7ErDHEzT6Dqd

* Re-pad the verdict table after shortening the AMBER row

Shrinking the AMBER cell left the table's column padding inconsistent,
which trunk (prettier + markdownlint MD060) rejects.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01APCEfNjd1X7ErDHEzT6Dqd

---------

Co-authored-by: Claude <noreply@anthropic.com>
* fix(nrf54l15): restore the nrf54l15dk build

Three unrelated faults stacked up, so the env has not built from a clean
cache for some time. All three were diagnosed in July but never committed.

Pin framework-zephyr to 3.40201.251021 (Zephyr 4.2.1). Seeed's platform
script only maps their own seeed-xiao-* board ids to a package; any other
board -- ours included -- falls back to whatever platform.json declares as
the default, which is now Zephyr 4.4.0. Its west manifest pulls a CMSIS_6
whose cmsis_gcc.h calls the ACLE builtins __sxtb16/__sxtab16, and none of
the GCC ARM toolchains PlatformIO ships (8.2.1/9.2.1/9.3.1) declare them in
arm_acle.h. In C that is only an implicit-declaration warning; in C++ it is
a hard error. So a fresh cache silently breaks the build even though
nothing in the tree changed.

Guard the MMC5983MA case in MagnetometerThread with __has_include. The
switch arm constructs MMC5983MASensor unconditionally, so any env whose
libdeps lack SparkFun_MMC5983MA_Arduino_Library fails with "expected
type-specifier before 'MMC5983MASensor'".

Add Print::availableForWrite() to the nrf54l15 Arduino shim. The shim
declares flush() but not availableForWrite(), which StreamFrameWriter
calls -- so it went unnoticed until that code landed.

Verified: clean build of nrf54l15dk from an empty package cache, SUCCESS in
16:01, FLASH 39.04% (570804 B of 1428 KB), RAM 65.65%. The three had never
been exercised together -- a previous run with only the pin applied got
17:30 in before hitting the other two.

* review: collapse the pin rationale to one repo-local comment

The block was pasted twice, and both copies pointed at a note that does not
exist in this repository. Kept one, and only the part a reader here can act
on: why the fallback happens, and why it is a C++ error rather than the
warning the pure-C Zephyr core gets away with.

---------

Co-authored-by: Jonathan Bennett <jbennett@incomsystems.biz>
…11415)

* fix(nrf52): LTO was dropping the board variant's weak hook overrides

Whole-image LTO (enabled arch-wide for nrf52840 in #10655) inlines the empty
weak body of earlyInitVariant()/lateInitVariant()/variant_shutdown()/
variant_nrf52LoopHook()/variantDefault*Config() at the call site, because the
weak default and the call site live in the SAME translation unit. The strong
override in variants/<arch>/<board>/variant.cpp is then never linked, and the
board's hardware setup silently does not run.

nrf52_lto.py's -fno-lto variant recompile does not help here: the caller is the
problem, not the variant object.

Needs both ingredients, so this only affects 2.8: the earlyInitVariant()
indirection landed in #9438 and is present in v2.7.26 too, but v2.7.26 has no
-flto, so the override linked normally.

Found on the muzi R1 Neo, whose earlyInitVariant() drives DCDC_EN_HOLD (P0.13,
the DC-DC hold after the user button) and NRF_ON (P0.29, "tells IO controller
device is on"). Both were dropped from the image, so the companion MCU never saw
the nRF application come up and stayed in its DFU indication (purple LED).
Verified in the ELF: pre-fix setup() runs straight from waitUntilPowerLevelSafe()
to the LED_NOTIFICATION block with no earlyInitVariant symbol in the binary and
no pinMode/digitalWrite on P0.13 or P0.29 anywhere; post-fix it calls the real
override. HW-confirmed on an R1 Neo.

Also affected on nrf52840: earlyInitVariant() on 10 variants (incl. t-echo-card,
which sequences its RT9080 3V3 rail there), variant_shutdown() on 18 variants
(t114, t-echo, ThinkNode M1-M8, meshlink, wio-tracker-L1 ... - sleep pin parking,
so deep-sleep leakage), variant_nrf52LoopHook() on 3 RAK variants. Confirmed
dropped on heltec-mesh-node-t114 by build, not just by inspection.

Fix is __attribute__((noinline)) on both the weak declaration and definition -
the same guard already carried by loopCanSleep(), preFSBegin(), PowerHAL and
variant_enableBatteryLpcompWake(), whose comment in main-nrf52.cpp already
documents this exact failure mode.

Also extend _VARIANT_OVERRIDES in extra_scripts/nrf52_lto.py from just
_Z11initVariantv to all eight hooks. That post-link guard already had the right
logic and would have caught this on every PR - it simply was not listing
Meshtastic's own weak variant hooks, only the core's. With the list extended it
goes red on both r1-neo and heltec-mesh-node-t114 when the noinline is reverted,
and green with it. Its failure message now names both possible causes.

* review: trim the noinline rationale comments to two lines

Per AGENTS.md ("keep code comments minimal - one or two lines, max"), the
incident detail and extended background belong in the PR description, not the
source. Keeps the LTO/noinline rationale and the pointer to the guard.

---------

Co-authored-by: Jonathan Bennett <jbennett@incomsystems.biz>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
… drop redundant logs (#11391)

* logging: gate LOG_TRACE behind MESHTASTIC_TRACE_LOGGING, drop redundant reclock logs

LOG_TRACE now compiles out by default so trace-level diagnostics cost no
flash; enable with -DMESHTASTIC_TRACE_LOGGING. Portduino keeps it on for
the traceFilename packet-trace feature.

Remove the 66 caller-side I2C reclock/restore log lines in the telemetry
sensors: ReClockI2C::setClock/restoreClock already log both frequencies
internally (now at trace level, since they fire every sensor read).

Also unify near-duplicate literals (colon/case/punctuation variants) so
linker string dedup applies, and drop an information-free bare 'done'.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LBiZc9sfPrH1MZ2L3Fxgt1

* logging: demote chatty per-packet/per-poll DEBUG lines to trace level

With LOG_TRACE compiled out by default, per-iteration chatter (packet
bookkeeping, sensor poll values, e-ink refresh reasons, GPS pin states,
UI runState traces) now costs no flash on device builds while remaining
one -DMESHTASTIC_TRACE_LOGGING away. 108 lines demoted, 4 information-
free lines removed; failure paths, drop reasons, and one-time init logs
all stay at debug level.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LBiZc9sfPrH1MZ2L3Fxgt1

* logging: address CodeRabbit review on trace-gate PR

- GPS: pass serial-derived buffers as %s args, never as format strings
  (untrusted bytes could contain % directives)
- 0x%08x for packet id / NodeNum per convention (Router, CannedMessage,
  NeighborInfo); unsigned casts for size_t args; %u for uint32_t delta
- EInk: async full-refresh begin/complete back to DEBUG (rare state
  transitions); per-frame SKIPPED lines stay trace

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LBiZc9sfPrH1MZ2L3Fxgt1

* logging: gate trace on the flag's value, not its presence

-DMESHTASTIC_TRACE_LOGGING=0 previously *enabled* trace logging because
the gate tested definedness. The flag now defaults per-platform
(portduino 1, else 0) and both backends test the value, so =0 disables,
=1 or a bare -D enables. Also cast tx_after-millis() to uint32_t for %u
(millis() is unsigned long on native).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LBiZc9sfPrH1MZ2L3Fxgt1

* logging: clang-format rewrap after specifier widening

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LBiZc9sfPrH1MZ2L3Fxgt1

* Even fewer bytes!

* logging: keep compile-gated debug lines at debug level; fix native-suite-count

Lines already inside default-off #ifdef blocks (GPS_DEBUG,
DEBUG_LOOP_TIMING) cost no flash and should stay visible at debug level
when their gate is enabled, rather than also requiring
MESHTASTIC_TRACE_LOGGING.

test/native-suite-count lags the two test_event_channel_* suites added
by #11045 (develop's Native Suite Count check has the same mismatch);
bump 46 -> 47.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LBiZc9sfPrH1MZ2L3Fxgt1

* gps: route GPS_DEBUG diagnostics through a LOG_DEBUG_GPS() macro (#11414)

Replaces 27 log-only #ifdef GPS_DEBUG blocks across GPS.cpp,
PositionModule, MeshService, and GPSStatus.h with a single-line
LOG_DEBUG_GPS() call (src/gps/GPSLog.h, modeled on LOG_MIGRATION:
value-gated, ((void)0) when off). Blocks containing declarations,
control flow, hexDump, or nested conditionals keep an explicit
'#if GPS_DEBUG' guard. RTC.cpp's per-reading raw time dumps and
per-candidate rejection chatter fold under the same gate; quality
transitions and boot-time seeding stay at debug.

Also fixes the '// define GPS_DEBUG' missing-# typo in two variant
headers and updates all seven commented examples to the value form
('#define GPS_DEBUG 1') required by the value-based gate.


Claude-Session: https://claude.ai/code/session_01LBiZc9sfPrH1MZ2L3Fxgt1

Co-authored-by: Claude <noreply@anthropic.com>

* gps: declare RTC gmtime result as pointer to const (cppcheck)

With the setTime debug dump gated behind GPS_DEBUG, all remaining uses
of t are reads; cppcheck (constVariablePointer) now flags it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LBiZc9sfPrH1MZ2L3Fxgt1

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Conflict resolutions:
- configuration.h: keep the DS248X and HM330X addresses, append the AS3935 addresses
- ScanI2C.h: keep DS248X and HM330X in DeviceType, append AS3935
- EnvironmentTelemetry.cpp: keep the display-source refactor from develop, keep IMMEDIATE_SEND_MAX_STALENESS_MS, drop the duplicate environmentTelemetryModule definition
- EnvironmentTelemetry.h: drop the duplicate extern environmentTelemetryModule declaration
- protobufs: take develop (84bfb0f); meshtastic/protobufs#981 supplies the AS3935 fields

Note: protobufs#981 as it stands assigns lightning_strike_count_1h = 24,
lightning_distance_km = 25 and AS3935 = 55, which collide with
adc_voltage_ch0, adc_voltage_ch1 and HM330X on protobufs master. It needs
renumbering before merge.
@ndoo
ndoo merged commit 7415f1b into meshmy:feat/as3935-lightning-sensor Aug 12, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.