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>
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>
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.
* 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.
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
- 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>
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>
* 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>
* 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.
* 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.
* 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.
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.
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
* 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
#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>
* 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>
* 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.
* 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>
* 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.
* 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>
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>
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().
* 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>
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.
* 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.
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().
#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.
* 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
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.
* 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.
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.
* 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