Commit Graph
12608 Commits
Author SHA1 Message Date
Garth Vander HouwenandClaude Fable 5.1 32cd2604d6 docs(security): credit the serial setting for withholding the BLE pairing PIN
The first draft called the setting an operational convenience. That undersold
it: the Bluetooth pairing PIN is logged (NRF52Bluetooth.cpp:310 and :420,
NimbleBluetooth.cpp:657), and RedirectablePrint gates all serial output on
serial_enabled, so disabling the console genuinely withholds the PIN from
anyone watching the port.

Reframe the section around that: state the disclosure it closes and when that
matters, then keep the limits it does not cover.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-13 11:25:20 -07:00
Garth Vander HouwenandClaude Fable 5.1 5f3f7dff39 docs(security): state that the serial setting is not a boundary, document lockdown
security.serial_enabled = false gates only the serial console's API frames
(SerialConsole.cpp) and log output (RedirectablePrint.cpp). Bluetooth, WiFi
and Ethernet API access never consult it, so a node with serial disabled and
Bluetooth on still offers full local API access. Operators have read it as a
hardening control for physically exposed nodes, which it is not.

Document what it does and does not do, and point operators at lockdown as the
mechanism that actually restricts local access: per-connection auth with
config redaction, encrypted storage at rest, and APPROTECT. Note honestly that
lockdown is nRF52 only, is opt-in at build time, and ships in no released
variant, and record the resulting platform gap under known limitations.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-13 11:23:00 -07:00
Austin d5873ead0a R2: Cache harder! (#11710)
Increase the cache-control age limits for R2 uploads.

Releases: Cache for 1 day in browser, 1 month on CDN

Nightly: Cache for 1 hour in browser, 1 day on CDN

Add metadata to R2 uploads so we can track which commit and which GitHub Actions run produced the upload.
2026-09-02 21:02:31 -04:00
Thomas Göttgens 9850b76351 ci(test): shard the native test suite across a matrix (#11706)
* ci(test): shard the native test suite across a matrix

Replace the single sequential runner with a matrix populated by
bin/test-shards.py from the test/ tree: areas over --max-suites are
split, smaller ones packed, and a --max-shards budget bounds the
fan-out. A collector job merges the per-shard JUnit reports, checks the
union against the canonical suite set, and states the verdict. Native
PlatformIO Tests remains as the single required check over the matrix.

Drop the --without-testing warm build. PlatformIO links every native
test program to the same $BUILD_DIR/$PROGNAME, so the area run relinked
each suite regardless. ccache carries the shared src objects between
shards instead; one shard is flagged cache_writer so a single entry is
saved.

The coverage-event-policy and coverage-channel-table envs and the
attribution canary move into their own matrix rows and job.

Harden the new paths: bound the matrix row count so a branch cannot size
the fan-out, reject multi-line or empty $GITHUB_OUTPUT values, fail the
whole-run attribution gate on an empty expected set, upload exact report
and tracefile names instead of globs, and pass the repo path to
bin/lib/shuffle.sh as an argument rather than into bash -c source text.

12 shards, largest 9 suites.

* ci(test): minimal test toolchain, cap shard runtime, fix pack overflow

Add .github/actions/setup-native-test, used by the shard and canary jobs
in place of setup-native. It drops the redundant second checkout, both
submodules (src/mesh/generated is tracked, meshtestic is the hardware
harness), cppcheck, and the adafruit-nrfutil, poetry and meshtastic pip
installs, and folds in ccache and lcov. setup-base and setup-native are
unchanged, so the firmware matrix and every other consumer keep theirs.

Cap the shard job at 30 minutes. A lost runner held one for 48 of the
360 GitHub allows by default, and there are twelve of them.

pack() could exceed --max-suites: ceil(total / cap) is a lower bound and
whole areas do not divide, so three areas of 6 at cap 10 put 12 in one
of two bins. Grow the bin count until every bin fits.

Validate the fixed-env test_filter tokens against SUITE_RE. PlatformIO
accepts globs there, and those tokens reach the same word-split and the
same attribution gate as discovered names. Split with read -ra so a
token cannot glob against the workspace either.

Report the suite count rather than the length of the -f argument array,
which counted every name twice.

Trim comments to the one or two lines AGENTS.md asks for.

* ci(test): quote the $GITHUB_OUTPUT redirects

Applied to all five, including the three that predate this branch, so the
file is consistent rather than half-converted.
2026-09-02 21:05:03 +00:00
Ben Meadors a6ef5d1ce6 fix(esp32s3): drop the SenseCAP Indicator's 120 MHz boot clock (#11705)
boards/seeed-sensecap-indicator.json was the only board file carrying
"f_boot": "120000000L". Under platformio/espressif32 6.x that key only
selected a prebuilt bootloader image. Under pioarduino HybridCompile,
which this board uses since it moved to the 3.3.11-based core (#11238),
f_boot becomes the compile-time clock for both flash and PSRAM, so every
2.8 build of the Indicator is compiled with CONFIG_ESPTOOLPY_FLASHFREQ_120M,
CONFIG_SPI_FLASH_HPM_ON and CONFIG_SPIRAM_SPEED_120M (octal PSRAM at
120 MHz is an experimental ESP-IDF feature). The device hangs in early
flash/PSRAM init before the boot watchdog is disarmed and reset-loops
with RTCWDT_RTC_RST and no bootloader output, also after a full erase
and install.

Without the key the build falls back to f_flash (80 MHz) like every
other ESP32-S3 board, reports "80MHz for both Flash and PSRAM", and
produces a bootloader byte-identical to the T-Deck's 2.8 bootloader.

Fixes #11691
2026-09-02 12:48:03 +00:00
Andrew Yong 3c04a79031 fix(stm32wl): improve reboot-to-DFU reliability (#11698)
- In enter_dfu, arm enterDfuAtMsec = millis() + 5s and return instead of
  resetting inline; the want_response ACK then goes out the normal path
  and Power::powerCommandsCheck() calls enterDfuMode() at the deadline.
  Nudge the deadline off 0 in the rare case the addition wraps to it,
  since powerCommandsCheck() reads 0 as unarmed. The delay is the
  client's detach window - and the margin a WebSerial web flasher needs
  (meshtastic/web-flasher#426).
- In enterDfuMode(), stop the GPS and drain/end every configured UART
  before the reset. The ROM bootloader autobauds off the first byte on
  USART1 (PB6/PB7) or USART2 (PA2/PA3), and on every WL variant a
  console UART or the GPS stream sits on those pins. Factor the drain
  into quiesceSerial() and reuse it in cpuDeepSleep().
- Move earlyBootCheck from constructor(101) to .preinit_array, ahead of
  the core's premain()/SystemClock_Config() whatever the link order, and
  reset RCC before jumping to system memory.

The handler used to reset the MCU inline, before the ACK was sent and
while the client still held the console UART. The STM32WL ROM bootloader
autobauds off the first byte received; a stray byte during the handoff
(a trailing protobuf frame, a port-close DTR/RTS glitch) desynced it and
left the device unreachable at any baud until a hard reset.

STM32WL only: every hunk is behind #if defined(ARCH_STM32) or lives in
main-stm32wl.cpp. nrf52, rp2040 and the rest are unchanged.

Known limitation: gps->disable() only issues a UBX sleep command, so a
non-u-blox or otherwise free-running GPS with no hardware enable/standby
pin keeps transmitting on its UART past this point. If that UART is
USART1 (PB6/PB7) or USART2 (PA2/PA3), the ROM bootloader can still
autobaud onto the GPS stream instead of the host. New STM32WL hardware
designs should keep GPS UARTs off those two bootloader-autobaud pins, or
provide a way to power down or hold the GPS in reset before DFU.

Assisted-by: Claude Sonnet 5 <noreply@anthropic.com>

Signed-off-by: Andrew Yong <me@ndoo.sg>
2026-09-02 10:47:23 +00:00
renovate[bot] fa65b0797e chore(deps): update lovyangfx to v1.2.28 (#11615)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-02 08:58:56 +00:00
Lynxie 4cb912ff55 fix(gps): remember valid fixes across search cycle (#11697) 2026-09-02 08:18:51 +00:00
Thomas Göttgens 97d916ede8 Update supported versions in SECURITY.md 2026-09-02 09:02:30 +02:00
renovate[bot] 8c4b69c3bc chore(deps): update meshtastic/device-ui digest to 5870d3a (#11700)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-02 01:44:50 +00:00
Manuel 80c6d2f8da Update device-ui library version in platformio.ini (#11694) 2026-09-02 01:10:54 +00:00
AustinandClaude Opus 5 1ed10f4883 fix(raspihttp): build against OpenSSL 4.0's const X509 name getters (#11523)
Ubuntu 26.10 ships OpenSSL 4.0, which const-qualified the return of
X509_get_subject_name() and X509_get_issuer_name():

  3.5/3.6:  X509_NAME *X509_get_subject_name(const X509 *a);
  4.0:      const X509_NAME *X509_get_subject_name(const X509 *a);

generate_self_signed_x509() grabbed the certificate's own subject name
and mutated it in place, so the assignment to a non-const X509_NAME *
now fails to compile. Unlike notBefore/notAfter there is no X509_getm_
mutable variant to fall back on.

Build the X509_NAME standalone instead and hand it to X509_set_subject_
name()/X509_set_issuer_name(), which take a const name and copy it on
every OpenSSL from 1.1.0 through 4.0. The setters dup the name, so ours
is freed on both the success and failure paths. This also lets the
X509_NAME_add_entry_by_txt() calls be error-checked, which they were
not before; the caller already X509_free()s the partially built cert
when we return -1.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 23:41:52 +00:00
renovate[bot] a2c919d2a2 chore(deps): update platformio/nordicnrf52 to v11 (#11684)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-01 22:25:58 +00:00
Austin fca4fa88c5 Bump version (2.8.1) (#11695)
Bump version to 2.8.1
Fix broken meshtasticd version bumps while we're in here
2026-09-01 18:00:06 -04:00
Austin 39383b9fcf Actions: Also upload release / nightly builds to R2 (#11689) 2026-09-01 14:49:31 -04:00
Ben MeadorsandTom df34ef1081 fix(radio): recover from chip state loss in the RX/TX hot paths too (#11678)
* fix(radio): recover from chip state loss in the RX/TX hot paths too

* fix(radio): address CodeRabbit findings on the hot-path recovery PR (#11680)

* fix(radio): address CodeRabbit findings on the hot-path recovery PR

SX128x: startReceive() still called the old asserting setStandby() before
the new trySetStandby(). The assert fired first, so the recovery path added
below it could never run - the exact chip-state-loss crash this PR exists to
fix was still live on SX128x. Remove the stale call.

LR11x0: resolvedTcxoVoltage was set once after the primary begin() attempts,
but two later paths - firmware recovery and the one-shot firmware update -
call begin() again with tcxoVoltage and never updated it. On a TCXO_OPTIONAL
board that only came up via one of those paths, reinitChip() would recover
with the wrong oscillator setting. Update resolvedTcxoVoltage after each of
those begin() calls too.

LR20x0: reconfigure() discarded RadioLibInterface::reconfigure()'s result -
the band-hop path always returned true regardless, and the same-band path
reused the same flag for chip-programming errors, so a base-class failure
could both mask itself as success and wrongly trigger a full re-init. Track
the base-class result (reconfigureSuccess) separately from the chip result
(standbySuccess), and return the former.

Also shortens the recovery-rationale comments in RadioLibInterface.h and
SX126xInterface.cpp to 1-2 lines per the repo's comment convention, the
rationale now covered once in the base class.

* fix(radio): finish the recovery ladder and stop recovery from rebooting

Follow-up to the CodeRabbit findings, plus two gaps found auditing the
branch against its own intent (never reboot on chip state loss; recover in
place).

RX left off was unrecoverable on an idle node. Every startReceive() call
site is event-driven - RX/TX ISR, the CAD-busy branch, startSend()'s failure
path, init(), reconfigure() - and a radio with RX off cannot raise an RX
interrupt, so nothing re-arms it unless the node happens to transmit or the
user changes config. A listen-only or quiet node stayed deaf for good, which
is worse than the reboot this replaced. main.cpp's existing 60 s AGC tick now
calls periodicRadioMaintenance(), which re-arms RX when rxOffline is set and
otherwise does the AGC reset as before.

In-place repair now gives up rather than retrying forever. After
MAX_CHIP_RECOVERY_FAILURES consecutive failures - a throttle window apart, so
minutes of a provably dead chip - schedule rebootAtMsec, the same deliberate
reboot Portduino already uses for LoRa_in_error. A reboot re-runs init(),
which redoes the power-enable GPIOs, settle delays and TCXO probing that
begin() alone skips. Both counters reset in RadioLibInterface::startReceive(),
the one point every driver reaches only once the chip accepts the RX start.

SX128x: reconfigure()'s recovery reached reinitChip()'s region-mismatch
branch, which rewrites config.lora.region, saves, and calls ESP.restart() /
NVIC_SystemReset(). A runtime recovery must never reboot - that is the crash
this path exists to prevent, and it would fire with a config save pending.
Gated to the boot-time call via a fromInit parameter.

LR20x0: a rejected setRxBoostedGainMode cleared the success flag and so
forced a full fullBegin() chip reset. It is a warn-level cosmetic setting,
treated as warn-only in LR11x0's equivalent, and not a lost-state signature.

Also logs suppressed recovery attempts at debug level; previously a chip that
stayed dead recorded one critical error and then went completely silent.

* fix(radio): count RX re-arms, not re-inits, in the recovery ladder

LR20x0's recoverChipStateLoss() is fullBegin(), which re-arms RX itself but
reports success on begin() alone. A re-init that came back with RX still dead
therefore reset chipRecoveryFailures, so a chip that could be re-inited forever
while never receiving again held the ladder at zero and never reached the
reboot. The other drivers had the same hole from the other side: the caller's
retry startReceive() runs after the reset, so a retry that failed again left the
count cleared.

RadioLibInterface::startReceive() is now the only place the ladder clears, and
it only runs once the chip actually accepted RX. The threshold is judged at the
top of the next attempt - a throttle window later, after that attempt's retry
(the caller's, or fullBegin's own) has had its chance to clear it. That also
drops the old false positive where the reboot was armed before the retry that
would have succeeded.

RF95Interface::startReceive() set isReceiving directly instead of calling the
base, so on RF95 nothing ever cleared rxOffline or the ladder: the first failed
RX start left periodicRadioMaintenance() re-initing forever, and with the count
now advancing it would have rebooted a working radio.

---------

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

---------

Co-authored-by: Tom <116762865+NomDeTom@users.noreply.github.com>
2026-09-01 16:08:33 +00:00
Thomas Göttgens 14eaa5587d Honor mute when waking the screen for a received message (#11688)
* fix(ui): honor mute when waking the screen for a received message

TextMessageModule fired powerFSM.trigger(EVENT_RECEIVED_MSG) for every text
packet, gated only by shouldWakeOnReceivedMessage(), which checks external
notification, device role and battery level but never the mute flags. A muted
channel therefore suppressed the banner and still lit the screen.

MessageRenderer::handleNewMessage() only computed mute for MessageType::BROADCAST,
so a DM from a muted node produced a banner and a wake.

Add isMutedForPacket() in Channels: a DM addressed to us reads the sender's
NodeInfoLite mute bit, every other packet reads the mute bit of the channel it
arrived on. This is the predicate ExternalNotificationModule already applied to
the buzzer, vibra and LED outputs, hoisted so all three call sites share it.

Bell and alert messages still break through mute on both paths, unchanged.

No protobuf or config change: ChannelSettings.module_settings.is_muted and the
NodeInfoLite mute bit already exist and are already settable from the device menu
and via AdminMessage.toggle_muted_node.

Closes #11674

* fix(ui): let an alert break through mute on the screen wake path

In COLOR display mode TextMessageModule skips handleNewMessage(), so
powerFSM.trigger(EVENT_RECEIVED_MSG) is the only wake an alert gets. Gating it
on mute alone dropped that wake for a bell on a muted channel.

Add MeshService::isAlertPayload(): an ASCII BEL in the payload while at least one
alert_bell_* output is enabled. The wake gate is now "not muted, or an alert".
MessageRenderer uses the same predicate instead of its own inline bell scan,
which also lifts that scan's arbitrary 100 byte cap.

Rename three test cases. Their names carried exactly 35 characters after the
test_ prefix, which matches the Lob API key format and tripped trufflehog in the
trunk check gate.
2026-09-01 11:52:40 +00:00
Thomas Göttgens 427ed0f1a0 Load optional modules dropped into src/modules/optional/ (#11673)
* Load optional modules dropped into src/modules/optional/

bin/optional-modules.py scans src/modules/optional/ for a directory <Name>/ holding <Name>.h and generates $BUILD_DIR/OptionalModules.h with an include and a setup<Name>() call for each, which Modules.cpp picks up through __has_include. The directory does not exist in a stock checkout, so a stock build generates a header that defines nothing, OPTIONAL_MODULES_SETUP compiles away, and nothing is registered. Sources under the directory are already covered by the default recursive build_src_filter, so dropping a module in needs no platformio.ini edit.

* Address review: skip a module directory that is not a usable identifier

The directory name becomes a setup<Name>() call, so foo-bar/ would have generated setupfoo-bar() and failed to compile with the error pointing at generated code rather than at the directory. Names that cannot form an identifier are now skipped with a message that names the directory.
2026-09-01 11:13:30 +00:00
Thomas Göttgens b823c8d7fe Let a module hold the screen (#11631)
* Let a module hold the screen

Screen::setModalModule() marks one module as owning the screen, honoured at the three places that would otherwise take it away: the carousel advance in runOnce(), the new-message banner in handleNewMessage(), and Cmd::STOP_ALERT_FRAME, which any caller can currently fire to cancel any alert frame regardless of who started it. Only the owning pointer can release it, so a module with a modal state no longer has to patch Screen.cpp to keep an alert from vanishing when a chat message arrives. The default is nullptr and no in-tree caller sets it, so every existing build behaves exactly as before.

* Address review: clear pauseBanner even while a module holds the screen

START_ALERT_FRAME sets NotificationRenderer::pauseBanner and STOP_ALERT_FRAME is
the only thing that clears it, so swallowing the whole command left banners
suppressed for good once a module took the screen. Only the setFrames() teardown
is now gated on the modal owner.

* Take the modal owner as a pointer to const

Screen never dereferences it; the pointer is only stored and compared, so const
is what the parameter and the member both mean. Fixes the cppcheck
constParameterPointer defect on clearModalModule().

* Add isShowingModuleFrame() so a module can claim keys on its own frame

Input observers registered by modules run before Screen's, so a module that handles UP/DOWN has to know whether its own frame is the one being looked at, or it takes the key away from the frame that is. moduleFrames is already index-aligned with the frame list for drawModuleFrame(), so the check is a lookup against the current frame.

* Address review: match drawModuleFrame's frame selection, trim the comment

Mid-transition drawModuleFrame() renders transitionFrameTarget, so comparing only currentFrame reported false while the module's frame was actually on screen and its input observer would have ignored keys. The header comment is back inside the two-line limit.
2026-09-01 08:50:39 +00:00
HarukiToreda 3683566f62 Don't show new message banner on message screen (#11671)
* message banner

* Update MessageRenderer.cpp

* Fix message banner suppression race on Portduino
2026-09-01 00:45:42 +00:00
Ben Meadors 47db0e3020 fix(admin): don't disable BLE on config paths that never reboot (#11651)
Three places took BLE down and left nothing to bring it back. The nRF52
auto-re-advertise bug masked them by restoring advertising ~1s later; with
that fixed (#11650) the outage is real, lasting until the next PowerFSM
transition - up to screen_on_secs, 10 minutes on a default client.

- restore_preferences passed 1000 to reboot(), which takes seconds, arming
  the reset ~16.7 minutes out instead of the intended ~1s. With BLE disabled
  for a pending reboot the node was unreachable for that whole window. Use
  DEFAULT_REBOOT_SECONDS and disable before arming, matching the factory and
  nodedb reset paths.
- mesh_beacon sets shouldReboot=false but was not in the list that spares a
  variant from the blanket disable, unlike statusmessage. Add it.
- MQTT and Serial disable BLE inside their own case, bypassing the
  transaction check above them. Inside an edit transaction saveChanges()
  defers the reboot, so BLE went down with no restore - reachable today by
  importing a device profile containing either module config.

Build: heltec-mesh-node-t096. Tests: test_module_config 3/3.
v2.8.0.47db0e3
2026-08-31 19:38:35 +00:00
Ben Meadors b8faaaf54b fix(fs): size the files manifest with a malloc probe, not a heap walk (#11667)
heap_caps_get_largest_free_block() walks every TLSF block of every
matching heap while holding the allocator lock. On ESP32-S3 boards with
PSRAM in the malloc pool, that walk runs long enough during the config
handshake that WiFi RX on the other core blocks in wifi_malloc() and the
interrupt watchdog reboots the node.

Use the bounded malloc() probe (already the non-ESP32 path) on every
target instead: TLSF malloc is O(1), so the allocator lock is only held
momentarily. Touch the probe through a volatile pointer so LTO cannot
elide the malloc()/free() pair.

Fixes #11666
2026-08-31 18:56:05 +00:00
Ben Meadors 7dffd66c59 fix(radio): recover a chip that lost its state instead of assert-crashing in reconfigure() (#11676)
* fix(lr11x0): recover a chip that lost its state instead of assert-crashing in reconfigure()

* fix(radio): extend chip-state-loss recovery to SX126x, SX128x, RF95, and LR20x0
2026-08-31 16:47:04 +00:00
Manuel 52d521426b Wio Tracker L2: try-fix battery percentage (#11668)
* try-fix battery percentage

* initialize cached_mv

* use AnalogBatteryLevel class to calculate percentage level
2026-08-30 22:34:12 +00:00
IxitxachitlandBen Meadors 7239fe886a fix(BaseUI): let a module frame with no menu fall through the SELECT dispatch (#11659)
#11209 added a module-frame branch to the SELECT chain that claims the press
for any non-null moduleFrames entry, but its body acts only on the
environmental telemetry frame. Every other module frame lands there and the
press dies: the branches below it - waypoint among them - are unreachable.

#11358 already patched one casualty by excluding the nullptr padding, which
restored the node list. Real module frames stayed swallowed, so the waypoint
menu #10920 appended to the end of the chain has never opened on BaseUI.

Enter the branch only when a module frame actually has a menu, so anything
without one falls through to the frames matched after it.

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
v2.8.0.7239fe8
2026-08-29 11:31:55 -05:00
Copilotandthebentern 9625c6bebf nrf52840: prevent duplicate I2C switch case for LP5562/MMC5983MA (#11658)
* Initial plan

* fix: avoid duplicate I2C switch case for LP5562/MMC5983MA

Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>
2026-08-29 08:47:28 -05:00
renovate[bot] 2afe097be6 chore(deps): update meshtastic/device-ui digest to 9d9b9df (#11653)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-29 09:50:11 +00:00
Manuelandcoderabbitai[bot] 36c89fa3a7 feat: Support Seeed Wio Tracker L2 (#10909)
* initial commit

* enable power save

* implement mesh LED

* add ADS1115+AW35615 for wio tracker L2

* add ES8311, GT911, AW35615, LP5814 to I2C scanner

* update commit references

* move variant.cpp to extras

* update hw_model

* update lovyanGFX

* point to device-ui commit

* trunk fmt

* fix IO expander (have to take from SensorLib for now as long as AudioThread has the limitation to only support SensorLib and the previous IO expander clashes with duplicate names in arduino-audio-driver)

* workaround duplicate defined symbol

* remove SensorLib; add lightweight Pca9555 class and use unified USE_PCA95X5; add wake button detection

* keep TP_INT disabled(OUTPUT) as we use wake button for wakeup

* PA off by default, enabled when playing sound; add some delay because typical class-D amps (NS4150 family) spec 20–50ms for the output stage to reach full swing after power-on

* refactored AW35615 into new external library

* local revert of PR10571 as this PR completely breaks the alert sound

* fix detection of ADS1115

* update device-ui commit reference

* add synchronisation to IO expander and call toggleDisplay() on wake button press

* add battery curve, fix io expander sync

* add SPILock, simplify macro usage

* update device-ui

* fix wakeup from sleep

* revert because of #11604

* use new AUDIO_AMP_SETTLE_MS

* remove test logs

* enable BaseUI

* use touch screen

* refactor wakekey thread

* fix wake button toggle screen on/off

* fix battery percentage and plugIn state

* Update src/graphics/TFTDisplay.cpp

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

* consider to return I2C errors to make coderabbi happy

* fix warnings

* use Throttle for millis comparison

* fix endTransmission in write

* make the rabbit happy

* spli targets -tft / non-tft

* fix compile

* revert forced use of Throttle

* remove MeshLED

* add HW_MODEL

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-08-28 23:30:27 +00:00
Ben Meadors f8a8d12477 fix(ble): stop BLE from coming back up during the pre-reboot window (#11650)
* fix(ble): stop BLE from coming back up during the pre-reboot window

Saving a reboot-requiring config over BLE (e.g. screen timeout) made the
node disconnect, re-advertise, let the phone reconnect, and then drop it
again at the reset. Two causes:

nRF52: admin messages from the phone run synchronously on Bluefruit's BLE
event task, so the BLE_GAP_EVT_DISCONNECTED caused by shutdown() is only
processed after we return - and that handler restarts advertising because
restartOnDisconnect(true) was never cleared. Stopping advertising first is
a no-op while a connection is live (the SoftDevice isn't advertising), so
the deferred event brought it straight back. Clear the restart flag and
stop advertising before dropping the link, mirroring nRF54L15's ble_enabled
gate. This also closes a main-thread race on the shutdown path where
Advertising.stop() could land between connection teardown and Bluefruit's
auto-restart within the same event dispatch.

PowerFSM (all platforms): darkEnter/onEnter/powerEnter/powerExit/serialExit
unconditionally re-enable BLE, so any state transition inside the reboot
window - a button press while the banner is up, the screen timeout, USB
plug/unplug - turned BLE back on after AdminModule had deliberately torn it
down. Route them through a helper that skips the re-enable while
rebootAtMsec/shutdownAtMsec is armed; every writer of those deadlines is an
imminent restart.

* style: trim rationale comments to house 1-2 line limit

The full mechanism is in the original commit message and PR description.
2026-08-28 22:32:01 +00:00
Andrew YongandBen Meadors db84bdf3b4 Reduce ExternalNotificationModule flash usage (RTTTL + InputBroker) (#10989)
* Generalize RTTTL exclusion into MESHTASTIC_EXCLUDE_RTTTL

ExternalNotificationModule already stubbed out RTTTL playback for
STM32WL/portduino/ESP32C6 via a raw ARCH/CONFIG_IDF check, but the
ringtone config plumbing around it (protobuf message, encode/decode
tables, /prefs/ringtone.proto persistence, admin get/set-ringtone
handlers) still compiled in even though it can never do anything on
those platforms.

Introduce MESHTASTIC_EXCLUDE_RTTTL and gate the dead ringtone plumbing
behind it too. The flag is set in each architecture's *_base build_flags
(stm32_base, esp32c6_base, portduino_base) rather than in the module
itself - this matches how every other MESHTASTIC_EXCLUDE_* flag in the
tree is set (e.g. stm32_base already sets ten of them directly, and
esp32c6_base already excludes PAXCOUNTER for an analogous
platform-can't-support-this reason), rather than introducing a new
per-architecture C header pattern. Behavior is unchanged on all three
platforms; overridable via -D like every other MESHTASTIC_EXCLUDE_*
flag.

Also guard the two HAS_I2S ringtone-playback call sites with
!MESHTASTIC_EXCLUDE_RTTTL alongside HAS_I2S, since rtttlConfig itself
is now only declared when RTTTL is not excluded. No current platform
defines both HAS_I2S and MESHTASTIC_EXCLUDE_RTTTL simultaneously, so
this has no effect today, but prevents a future HAS_I2S platform that
also excludes RTTTL from failing to compile.

Saves 368 bytes flash / 236 bytes RAM on wio-e5 with no loss to the
GPIO on/off notification toggle itself, which does not depend on
RTTTL.

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

* Skip unused InputBroker observer in ExternalNotificationModule

The inputObserver CallbackObserver member was declared unconditionally,
even though its only use site was already gated behind
MESHTASTIC_EXCLUDE_INPUTBROKER (set for all of stm32 in stm32.ini).
Because it's a non-trivial member, the compiler still generated its
constructor/destructor as part of ExternalNotificationModule's own
lifecycle even when InputBroker is compiled out entirely.

Gate the member and its only consumer, handleInputEvent(), behind the
same flag as their use site, and match the codebase's dominant
!MESHTASTIC_EXCLUDE_X style (used ~330 times) rather than
!defined(MESHTASTIC_EXCLUDE_X) (used ~20 times) while touching this
flag's other call site. Saves an additional 288 bytes flash on wio-e5,
no RAM change, no functional impact since InputBroker was already
unused on this platform.

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

* fix(native-wasm): exclude RTTTL to unbreak build

The browser node builds its own build_flags from arduino_base rather
than inheriting portduino_base, so it did not pick up the
MESHTASTIC_EXCLUDE_RTTTL flag added to portduino_base. With the inline
ARCH_PORTDUINO stub in ExternalNotificationModule.h now replaced by that
flag, native-wasm tried to include the unavailable NonBlockingRtttl.h.

Set MESHTASTIC_EXCLUDE_RTTTL=1 directly in the native-wasm env
alongside its other exclusion flags.

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

---------

Signed-off-by: Andrew Yong <me@ndoo.sg>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2026-08-28 19:58:23 +00:00
Ben Meadors 7afd270f39 Gut beacon send-as-node and consolidate TX onto broadcast_targets (#11646)
* Gut beacon send-as-node and consolidate TX onto broadcast_targets

Two MeshBeaconConfig changes, both against fields that never reached a tagged
release, so there is no migration for existing nodes.

broadcast_send_as_node let a client name a node ID to send beacons AS, rewriting
the packet's `from`. Firmware never applied it - the assignment was commented
out, so `from` was always the local node and the field was a settable, persisted
no-op. It was also unsound as designed: rewriting `from` forges no signature, it
only makes isFromUs() false, so perhapsEncode() skips XEdDSA signing and
receivers get an unsigned packet attributed to another node.

broadcast_on_channel / broadcast_on_region / broadcast_on_preset were a second
way to name a beacon destination alongside broadcast_targets, chosen silently on
whether broadcast_targets was empty. The comments claimed the two were
equivalent; they were not. An inline ChannelSettings carries name and PSK, so
broadcast_on_channel could transmit on a channel absent from the node's channel
table, which channel_index cannot express. That is dropped deliberately - the
channel must exist on the node.

Empty broadcast_targets now synthesises one target on the running preset and
region over the primary channel, matching what the scalar path produced when
left unset, so an otherwise unconfigured node still beacons.

The USERPREFS_MESH_BEACON_ON_* keys go with the fields. A preconfigured build
that still defines one now fails at compile time with a pointer to the
USERPREFS_MESH_BEACON_TARGET_0_* equivalents, rather than silently losing its
beacon channel. The replacement names a channel-table slot, so such a build must
also provision that channel.

MeshBeaconConfig shrinks 324 -> 240 bytes and ModuleConfig 328 -> 244, against
the 512-byte MAX_TO_FROM_RADIO_SIZE ceiling that FromRadio sits 2 bytes under.

The protobufs submodule points at a branch carrying both proto changes; it needs
re-pointing to master once meshtastic/protobufs#1047 and #1048 merge.

* Point protobufs submodule at master now that the beacon protos are merged

meshtastic/protobufs#1047 and #1048 are in master, so drop the temporary
beacon-proto-integration pin. MeshBeaconConfig stays 240 bytes and ModuleConfig
244, unchanged from the integration branch.

The bump also picks up master's unrelated additions: the MESHNOLOGY_W12 and
MESHPAGER_X2 hardware models, and a ground-speed unit correction in Position.
2026-08-28 19:51:43 +00:00
Andrew Yong 78219e09cb fix(stm32wl): add TCXO-optional support and fix hardcoded TCXO voltage (#10964)
* stm32wl: consult SX126X_DIO3_TCXO_VOLTAGE instead of hardcoding 1.7V

Every STM32WL variant except rak3172 got setTCXOVoltage(1.7) unconditionally,
regardless of what the board's hardware actually needs, and rak3172 got no
TCXO configuration at all - so a real RAK3172-T (populated TCXO) failed
radio init outright.

Read SX126X_DIO3_TCXO_VOLTAGE per variant instead. When TCXO_OPTIONAL is
also defined, retry once on XTAL if the TCXO attempt fails, mirroring the
existing pattern in LR11x0Interface.cpp, LR20x0Interface.cpp, and the
SX1262/SX1268 paths in RadioInterface.cpp.

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

* stm32wl(rak3172): support both non-T and -T hardware via TCXO-optional

RAK3172 is XTAL-only; RAK3172-T has a populated 3.0V TCXO, matching RAK's
own reference radio_conf.h. One PlatformIO environment now serves both:
tries the TCXO first, falls back to XTAL if not populated.

Hardware-verified on a TCXO-equipped board electrically equivalent to
RAK3172-T. Genuine non-T hardware not available to re-verify the fallback
path; reasoned from RadioLib source instead (see PR description).

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

* stm32wl(wio-e5): declare the module's 1.7V TCXO explicitly

Matches Seeed's own reference radio driver. wio-e5 previously relied on
the hardcoded 1.7V fallback being removed by the preceding commit, which
would have broken it - declare the voltage explicitly instead.

Hardware-verified via SWD: without this define, the radio interface fails
to come up at all (sendtext NAKs with NO_INTERFACE, meaning rIf is null).
With it, NO_INTERFACE goes away and the device sends/receives normally.

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

* stm32wl(CDEBYTE_E77-MBL): mark TCXO voltage optional, hardware varies by unit

EByte changed the E77-MBL hardware in early 2024: units with serial number
>= 3202995 have a TCXO, older units have a ceramic crystal oscillator
instead. Both ship under the same module name, so probe for the TCXO and
fall back to XTAL rather than assuming either.

https://github.com/olliw42/mLRS-docu/blob/main/docs/EBYTE_E77_MBL.md

Not hardware-tested - no E77-MBL board available this session.

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

* stm32wl: trim TCXO comment blocks to repo's 1-2 line guideline

Per review feedback on PR #10964 (CodeRabbit nitpicks) - the rak3172 and
CDEBYTE_E77-MBL variant.h comments were 4-line blocks, exceeding the
repo's comment-length convention. Condensed to one line each, same
information and links retained.

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

---------

Signed-off-by: Andrew Yong <me@ndoo.sg>
2026-08-28 18:21:58 +00:00
Andrew YongandTom 57d17cfd44 fix(stm32wl): recover from littlefs internal corruption instead of hanging (#11230)
LFS_ASSERT (src/platform/stm32wl/littlefs/lfs_util.h) was a plain assert(),
which on STM32WL hangs forever with no diagnostic (__wrap___assert_func is
while(true);, see main-stm32wl.cpp). STM32_LittleFS::begin() is already
designed to treat corruption as recoverable - format and retry, see
fsFormat()/NodeDB::saveToDisk() - but that only works if lfs_mount() cleanly
returns an error. An internal littlefs consistency check failing (metadata
pair/CRC/block-allocator invariants) never returns at all, so a bad flash
sector or power loss mid-write could permanently brick a device that would
otherwise have recovered via the existing reformat path.

nRF52 already hit this and fixed it (LFS_NO_ASSERT + a custom lfs_assert()
that reboots into a reformat, see meshtastic/firmware#3818). Port the same
approach to STM32WL: LFS_NO_ASSERT routes LFS_ASSERT through a custom
lfs_assert() instead of disabling the check outright, and lfs_assert()
requests a reformat-on-next-boot via a .noinit SRAM magic value (the same
mechanism already used for the DFU bootloader redirect in this file, chosen
specifically because backup/TAMP registers don't reliably survive a soft
reset in this toolchain) and reboots, rather than trying to reformat
littlefs from inside its own possibly-mid-operation callback.

Unlike nRF52 (a third-party Adafruit library patched via a -include
override so as not to fork it), STM32WL's littlefs copy is already a
project-owned vendored file, so lfs_util.h is edited directly.

Assisted-by: Claude Sonnet 5 <noreply@anthropic.com>

Signed-off-by: Andrew Yong <me@ndoo.sg>
Co-authored-by: Tom <116762865+NomDeTom@users.noreply.github.com>
2026-08-28 17:37:32 +00:00
Jason P 467dc44cfa Update applyLoraRegion to enable TX on set (#11643)
* Update applyLoraRegion to enable TX on set

* Don't enable TX if in HamMode. User must set callsign first

* Don't use isHam, use owner.is_licensed
2026-08-28 17:20:02 +00:00
Ben Meadors 7e9525ad83 feat(baseui): default US to LongTurbo on first region selection (#11637)
Selecting US in the BaseUI region chooser now installs LongTurbo instead of
LongFast, but only for out-of-box setup: the outgoing region must be UNSET, so
a later switch to US leaves whatever preset the node is running alone.

Scoped to the menu on purpose. The US entry in regions[] keeps LongFast as its
default preset, so preset repair, admin/phone writes and every other route onto
US are unchanged. A build pinning USERPREFS_LORACONFIG_MODEM_PRESET, a preset
already moved off the install default, or use_preset=false all outrank it.

The decision is lifted into menuHandler::presetForRegionSelection() so it is
reachable without a Screen, following toggleNodeMuted().
2026-08-28 11:55:20 +00:00
IxitxachitlandManuel 7aa8ad3510 fix(t-watch-ultra): build with the esp32s3 flags, not the classic-ESP32 ones (#11619)
* fix(t-watch-ultra): build with the esp32s3 flags, not the classic-ESP32 ones

The env was the only esp32s3 variant extending ${esp32_base.build_flags} (since
#8171). That base adds -D ESP32_FORCE_IRAM_MEMSET -Wl,--wrap=memset
-Wl,--wrap=memcpy, and the wrappers in IramMemcpy.c/IramMemset.c decide whether
the cache is on by reading 0x3FF00040 - DPORT_PRO_CACHE_CTRL_REG on the classic
ESP32, an address the S3 does not map at all (soc.h: DRAM 0x3FC88000-0x3FD00000,
DROM 0x3C000000-0x3E000000, IRAM 0x40370000-0x403E0000, peripherals 0x60000000).

--wrap is link-wide, so every memcpy/memset in the image - including inside the
precompiled WiFi, lwIP and flash driver libraries - branched on that undefined
read. Two long-standing board-specific bugs came from it, both dating to #8171,
which introduced the wrong base and the first workaround in the same commit:

* WPA2 networks associated and completed the 4-way handshake, then never got a
  DHCP lease, while open networks worked normally (#11513).
* Direct flash reads returned 0x00 for data that was correct on flash, so NVS
  came up empty every boot and dropped BLE bonds (#11530).

Switching the env to esp32s3_base fixes both on hardware: WPA2 gets a lease, and
NVS survives a reboot with the bond intact. The read workaround that #11530
needed - -Wl,--wrap=esp_partition_read, -Wl,--wrap=esp_flash_read and
esp_partition_read_mmap_wrap.c - is therefore removed as well.

The module excludes the env inherited from esp32_base go with it, so the board
now matches every other esp32s3 variant: web server and paxcounter are built
(paxcounter still only runs when enabled in config), and MESHTASTIC_EXCLUDE_AUDIO
was already inert here because AudioModule additionally requires USE_SX1280.
-UMESHTASTIC_EXCLUDE_ACCELEROMETER goes too, having only existed to undo an
inherited -D.

Also guards ESP32_FORCE_IRAM_MEMSET behind CONFIG_IDF_TARGET_ESP32, so a variant
cannot enable the classic-ESP32 probe on another target again.

* Update platformio.ini

added missing ${device-ui_base.custom_sdkconfig}

---------

Co-authored-by: Manuel <71137295+mverch67@users.noreply.github.com>
2026-08-27 18:27:02 +00:00
Ben Meadors 63f0f1edd0 fix(nodedb): clear the whole LocalModuleConfig when installing defaults (#11627)
installDefaultModuleConfig() memset sizeof(meshtastic_ModuleConfig) - the
368-byte union-backed wire oneof - over `moduleConfig`, which is a
meshtastic_LocalModuleConfig: 1092 bytes with every submessage inlined. The
function assigns only the fields it cares about and relies on that memset to
zero the rest, so every byte past offset 368 that it never assigns kept its
previous value across what is supposed to be a full reset.
installDefaultConfig() directly above already used the correct
sizeof(meshtastic_LocalConfig); only the module variant was wrong.

statusmessage is the field this shows up on. It sits at offset 609 and is
never assigned by the defaults installer, so it survives both routes into
installDefaultModuleConfig():

  - moduleConfig.version < DEVICESTATE_MIN_VER -> "old, discard". The decode
    succeeded, so the complete old config is in RAM and its statusmessage
    survives the discard verbatim.
  - loadProto() failure -> whatever a partial decode wrote there survives
    (loadProto itself clears correctly, using the caller's objSize).

node_status is char[80]. When the surviving bytes carry no NUL, nanopb
refuses the field ("unterminated string"), pb_encode_to_bytes() returns 0 and
PhoneAPI::getFromRadio() returns 0. config_state has already advanced, so the
frame is never retried - and 0 is the client's end-of-data sentinel, so the
rest of the config dump goes with it and the client never receives
StatusMessageConfig.

traffic_management is not affected: installDefaultModuleConfig() calls
installTrafficManagementDefaults(), which reassigns the whole submessage and
its has_ flag regardless of the memset size.

Also add has_traffic_management to the has_* list in saveToDiskNoRetry() for
consistency - it was the only module config missing from it.
2026-08-27 17:21:14 +00:00
Austin 335d778fae Flatpak: rename Meshtastic -> MeshtasticD (#11629)
resubmitted against `develop`
2026-08-27 15:48:04 +00:00
Bob Reese 260cf903e8 Check for ambientLightingThread non-null before use (#11590) 2026-08-27 15:23:23 +00:00
Thomas Göttgens 9fbc176e91 Extend userPrefs coverage to the whole channel table and the missing config fields (#11624)
* Extend userPrefs coverage to the whole channel table and the missing config fields

initDefaultChannel() handled only indices 0-2, so USERPREFS_CHANNELS_TO_WRITE above 3 produced live secondary channels carrying the public default PSK; it now covers all eight slots, with bin/platformio-custom.py completing every field of a configured index so indices 0-2 stay byte-identical. Adds USERPREFS_CHANNEL_<n>_IS_MUTED, USERPREFS_CONFIG_DEVICE_REBROADCAST_MODE, USERPREFS_CONFIG_DEVICE_NODE_INFO_BROADCAST_SECS, USERPREFS_CONFIG_LORA_CONFIG_OK_TO_MQTT, USERPREFS_CONFIG_SECURITY_IS_MANAGED and USERPREFS_CANNED_MESSAGES, applied after installRoleDefaults() and validated the way AdminModule validates a set-config. Adds test_userprefs_channels, covering the configured table under coverage-channel-table and the stock defaults under every other env.

* Address review: hex channel count, PSK width assert, canned-message termination

USERPREFS_CHANNELS_TO_WRITE now parses 0x-prefixed hex, matching the format
userPrefs.jsonc documents, without int(x, 0)'s rejection of a leading-zero
decimal such as "03". A static_assert rejects a USERPREFS_CHANNEL_<n>_PSK
literal wider than psk.bytes, which memcpy would otherwise write over the fields
after it. The USERPREFS_CANNED_MESSAGES copy keeps strncpy's zero-padding and
terminates explicitly, rather than shortening the length, which would have left
the last byte unwritten.
2026-08-27 15:00:59 +00:00
Ben Meadors 122ec0e9f4 Revert "feat(baseui): default US to LongTurbo on first region selection"
This reverts commit dbba2b3f6c.
2026-08-27 11:43:20 -05:00
Ben Meadors dbba2b3f6c feat(baseui): default US to LongTurbo on first region selection
Selecting US in the BaseUI region chooser now installs LongTurbo instead of
LongFast, but only for out-of-box setup: the outgoing region must be UNSET, so
a later switch to US leaves whatever preset the node is running alone.

Scoped to the menu on purpose. The US entry in regions[] keeps LongFast as its
default preset, so preset repair, admin/phone writes and every other route onto
US are unchanged. A build pinning USERPREFS_LORACONFIG_MODEM_PRESET, a preset
already moved off the install default, or use_preset=false all outrank it.

The decision is lifted into menuHandler::presetForRegionSelection() so it is
reachable without a Screen, following toggleNodeMuted().
2026-08-27 11:23:02 -05:00
renovate[bot] 3666ec9c6c chore(deps): update nanopb to v0.4.92 (#11595)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-27 09:49:53 +00:00
renovate[bot] a4e22a46b5 chore(deps): update meshtastic/device-ui digest to 6813f38 (#11614)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-27 09:49:22 +00:00
Ben Meadors 9a59e9088d fix(test): restore the sendAckNak overrides broken by #10767 (#11626)
#10767 added a relaySource parameter to the RoutingModule::sendAckNak
virtual, but the five test mocks that derive from RoutingModule still
declared the six-parameter signature with `override`. Nothing overrides
the new virtual, so all five suites fail to compile and the native test
job has been red on develop since the merge:

  test/test_reliable_ack_matrix/test_main.cpp:167:10: error: 'void
  MockRoutingModule::sendAckNak(meshtastic_Routing_Error, NodeNum,
  PacketId, ChannelIndex, uint8_t, bool)' marked 'override', but does
  not override

Widen the five mocks to the new signature.

Also carry has_rx_rssi with rx_rssi in allocAckNak(). rx_rssi has
explicit presence, so copying only the value left has_rx_rssi false and
nanopb dropped the field at encode time - the phone never saw the
relayer's RSSI that #10767 set out to deliver.

Cover both: test_reliable_ack_matrix asserts the overheard rebroadcast
is handed through as the relay source on the decodable path and the
opaque #11502 ingress path, and that no other ACK/NAK claims a relayer;
test_mesh_module drives a real RoutingModule and asserts the relay
fields, has_rx_rssi included, survive all the way to the phone.
2026-08-27 06:32:06 -05:00
Thomas Göttgens a8934a16d4 Route waypoint expiry through waypointIsActive instead of a raw getTime compare (#11621)
* Route waypoint expiry through waypointIsActive instead of a raw getTime compare

* Let isExpired own the zero-clock policy for purgeExpired too

* Resolve the clock in isExpired when a packet carries no valid rx_time
2026-08-27 09:46:34 +00:00
Thomas Göttgens eb6df1c649 ci: resolve the PR diff base without a shallow refetch (#11623)
The setup job checks out with fetch-depth: 0, then refetched the base branch with
--depth=1 before calling merge-base. A depth-limited fetch into a complete clone
writes .git/shallow and grafts the fetched tip as parentless, so merge-base finds
no common commit once the base branch has moved past the pull request merge
commit. The step then failed under set -e with a bare exit 1 and no message.

Use the origin/<base> ref the checkout already provides.
2026-08-27 09:31:47 +00:00
Tom e3aa86b3e7 fix(power): let a configured INA outrank the board's charge-status pin (#11510)
* fix(power): let a configured INA outrank the board's charge-status pin

AnalogBatteryLevel::isCharging() chose its source with a preprocessor
chain that put EXT_CHRG_DETECT / BATTERY_CHARGING_INV ahead of the INA
current check, so on any board defining a charge-status pin the INA arm
was compiled out entirely. Setting device_battery_ina_address changed
only the reported voltage, never the charging state, even though
getBattVoltage() has always let a configured INA outrank BATTERY_PIN.

The INA path has sat inside that #else since it was introduced in #5271,
but it only started biting the Seeed Xiao nRF52840 Kit when 070deb290
(#6930, v2.6.11) gave the variant EXT_CHRG_DETECT for the onboard
BQ25101 ~CHG line. Charging through an external charger leaves that pin
idle, so the node reports "not charging" forever and the UI never shows
the charging icon.

Move the INA check ahead of the pin arms, keeping the SGM41562 and
RAK9154 checks first since those report real charger state. hasINA() is
false unless the user set a non-zero device_battery_ina_address matching
a detected INA, so stock boards are unaffected, and
DISABLE_INA_CHARGING_DETECTION remains the per-board opt-out. The no-pin
and telemetry-disabled arms keep their previous behaviour exactly.

Fixes #11485

* fix(power): report INA readiness from the sensor, not runOnce()'s delay

hasINA() read runOnce()'s return as a success flag, but it is a poll
interval: initI2CSensor() hands back the same
DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS whether or not the device
answered, and it has done so since the helper was written in #1498, a
year before hasINA() arrived in #2536.

So a detected-but-unusable INA reported as present. With a real sensor
that failed begin() this self-corrected on the next call, because
initI2CSensor() clears the nodeTelemetrySensorsMap entry on failure. With
a NullSensor - what these globals resolve to when no driver is compiled
in - it never did: NullSensor::runOnce() returns INT32_MAX and never sets
status or initialized, so hasINA() stayed true, getINAVoltage() returned
0 mV, and getBatteryPercent() sat at -1 with the battery reported absent.

Open the sensor if needed, then return isRunning(), which is the status
the sensor actually tracks. Collapsing the four arms onto that helper
lets the repeated config lookup become a local.

* fix(power): route INA260 current into charging detection

INA260Sensor exposed bus voltage but not current, and getINACurrent()
had no INA260 arm, so a configured INA260 read 0 mA and isCharging()
always answered false. Now that a configured INA outranks the board's
charge-status pin, that became a regression on boards defining one.

Implement CurrentSensor on INA260Sensor, where the reading was already
being taken for telemetry via readCurrent(), and add the matching arm to
getINACurrent(). This also fixes the pre-existing always-false result on
boards with no charge-status pin at all.
2026-08-27 09:30:43 +00:00
Thomas Göttgens 5b787fc636 Remove the docs directory (#11622)
The firmware design docs were published to meshtastic/meshtastic in #11488 and
the directory was deleted. bme680_iaq_replay.md re-added it.

The replay harness build command moves into the header comment of
bin/bme680_iaq_replay.cpp, the only file that referenced the document.
2026-08-27 08:56:23 +00:00
Thomas Göttgens a89f1920e1 fix(traffic): don't re-stamp dropped duplicate positions, which slid the dedup window indefinitely (#11620)
* fix(traffic): don't re-stamp dropped duplicate positions, which slid the dedup window indefinitely

* test(traffic): trim the regression test comment and derive its counts
2026-08-27 07:37:20 +00:00