cppcheck reports returnByReference on XModemAdapter::getForPhone():
`meshtastic_XModem` carries a 128-byte payload buffer plus header fields,
so returning it by value copied the whole struct on every call.
Return `const meshtastic_XModem &` instead, and mark the method const -
it is a pure read of xmodemStore, with resetForPhone() being what drains
it. Every caller either copies into a value or reads a single field, so
no call site changes.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Tom <116762865+NomDeTom@users.noreply.github.com>
limitPower() converts the member `power` in place (regulatory clamp, then the
TX_GAIN_LORA/FEM subtraction) and relied on applyModemConfig() having just
re-seeded it. Since #10025 every driver calls it from both reinitChip() and
programModemParams(), and the recovery paths added in #11676/#11678 run the two
back-to-back, so each recovery re-converts an already-converted value. On a
RAK13302 (22-entry gain table) one recovery walks a 30 dBm request
30 -> 22 -> 13 dBm and a second one down toward the -9 dBm floor, while
config.lora.tx_power still reads 30. Seed `power` from config.lora.tx_power at
the top of limitPower(); applyModemConfig() always writes the resolved value
back there, so a single call is unchanged.
Co-authored-by: Tom <116762865+NomDeTom@users.noreply.github.com>
cppcheck (iterateByValue) flagged five range-for loops in the NodeDatabase
pb_callbacks that copy a whole protobuf entry off the vector only to pass
its address to pb_encode_submessage(), which takes a const void *. Bind by
const reference instead.
Drops one meshtastic_NodePositionEntry, NodeTelemetryEntry, NodeStatusEntry,
NodeEnvironmentEntry and NodeInfoLite_Legacy copy per node per encode pass;
these run on every nodes.proto save.
The nodes_tag loop in NodeDB.cpp is intentionally left as a by-value copy:
it mutates item.snr_q4/item.snr to the on-disk quantized form before
encoding, so it is a working copy rather than a redundant one. cppcheck
does not flag it.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
* fix(phoneapi): resend my_info when the node num moves mid-session
The first region set mints the PKI key and moves my_node_num to
crc32(public_key) live. my_info only went out during the want_config_id
handshake, so an already-connected client kept addressing the old number and
its admin packets NAKed PKI_SEND_FAIL_PUBLIC_KEY until it reconnected.
PhoneAPI tracks the number it last reported and re-sends my_info from
STATE_SEND_PACKETS when it no longer matches. createNewIdentity() nudges
fromNum so clients poll.
Fixes#11718
* fix(phoneapi): key the MyInfo re-announce off a one-shot state
Review follow-up. The per-connection reportedNodeNum field is gone: adding
per-instance members to PhoneAPI is documented as breaking USB-CDC enumeration
on the nRF52 Adafruit framework, and the baseline was never set for
SPECIAL_NONCE_ONLY_NODES, which skips STATE_SEND_MY_INFO and so emitted an
unexpected my_info after config_complete_id.
MeshService::identityMoved is set with the nudge and cleared once the notify
pass has reached every observer, so PhoneAPI::onNotify arms STATE_RESEND_MY_INFO
on each connected client in that single pass and stores nothing per connection.
The test now drives NodeDB::createNewIdentity() and MeshService::loop() instead
of writing my_node_num directly, and asserts the transport wake-up. Nodes-only
sync asserts no trailing my_info. drainToIdle() honours its read cap.
* fix(phoneapi): restart the dump when the node num moves mid-sync
Review follow-up. A client still in its config dump has already been sent the
old my_info and has no steady state for the one-shot to fall back from, so the
notify pass cleared identityMoved without covering it and the client finished
syncing on the obsolete number.
PhoneAPI::onNotify now restarts such a client's dump, which is the existing
re-handshake path. Skipped for a client that has not reached my_info yet and for
SPECIAL_NONCE_ONLY_NODES, which never sends one.
test_node_num_change_mid_dump_restarts_sync renumbers mid-dump and asserts the
restart, the new number, and that no part of the config is lost. Verified to
fail without the fix.
* fix(phoneapi): make the identity-move signal survive a concurrent notify pass
Review follow-up. The identity move can run off the loop task: a local admin
set_config reaches AdminModule through Router::sendLocal() on whichever task
delivered it. A bool cleared by MeshService::loop() could therefore be set and
cleared without any client being armed, losing the re-announce.
A generation counter replaces the bool. loop() snapshots it with fromNum before
notifying and only advances the seen counter afterwards, so anything bumped
during the pass is still pending. The same snapshot fixes a notify for a
fromNum bump that arrived mid-pass being marked delivered.
test_node_num_change_mid_dump_restarts_sync now asserts the whole restarted
dump: header order, channels, both config sections, our node record, nonce.
Also trims the MyInfo redaction comment to the two-line cap.
* fix(nodedb): keep self at index 0 after a live renumber, restart nodes-only syncs
Review follow-up. createNewIdentity() removed our old row and appended the new
one, leaving index 0 pointing at some other node. PhoneAPI's own-nodeinfo read
and the demote/evict scans that skip index 0 to protect us both rely on that
slot being self, so a renumbered node handed every client a stranger's record as
its own. Pinned the way nodeDBSelfCare() does it.
onNotify no longer exempts SPECIAL_NONCE_ONLY_NODES from the mid-sync restart.
That dump carries no my_info, but it does carry the self record, which the move
invalidates the same way. Such a client also gets the re-announce once its sync
lands in STATE_SEND_PACKETS, which it previously never did.
The generation counters are atomic. Every interleaving was already safe, since
observers read the live counter and the seen counter only advances to a pre-pass
snapshot, but the concurrent plain accesses were a data race on paper.
* fix(meshservice): make fromNum atomic
Review follow-up. The counter is bumped from whichever task queued the packet
and read by loop(). It is private to MeshService, so the type change covers
every access.
* Hide Navigation Bar on EInk Shutdown
* Revert "Hide Navigation Bar on EInk Shutdown"
This reverts commit 744812555b.
* Hide Navigation Bar on EInk Shutdown
* It's Hide, not Drop
Every PR targeting develop has been red since 2026-09-07 on the seven ESP32
check jobs, while the nRF52, RP2040 and STM32 jobs pass on identical source.
gh-action-firmware#61 moved the ESP32 container images onto the pioarduino
core. Its esp32 platform ships its own tool-cppcheck 2.20.1 and reinstalls it
over anything the repo pins, so ESP32 now analyses with cppcheck 2.20 while
every other platform still resolves platformio/tool-cppcheck 1.21100.230717,
i.e. 2.11. 2.20 parses far more of this tree than 2.11 ever managed, so checks
that were always enabled fired for the first time: 388 defects, ESP32 only.
#11776 cleared the two unknownMacro errors. Of the 386 left, two are worth
acting on and are fixed rather than suppressed:
* SerialModule dereferenced a null Position in NMEA/CALTOPO mode. `decoded`
stays NULL when pb_decode_from_bytes() fails, but printWPL() was called
with *decoded regardless, so a malformed position payload on our portnum
crashed the node. Emit the waypoint only on a successful decode.
* InkHUD's 12-hour clock passed a signed 12 to a %u conversion.
The rest are style and performance suggestions - functionStatic and the
const-correctness family account for 351 of them. Suppress those check ids so
the gate means the same thing on every platform again, scoping the one-off
ones to their file so a new occurrence elsewhere still fails. Burning them
down is worth doing deliberately, not under a CI outage.
Verified in the CI container images: all seven previously failing ESP32
environments pass, and rak4631, tracker-t1000-e and t-echo-plus still pass
under cppcheck 2.11.
pioarduino's cppcheck 2.20 reports functionStatic for all 20 methods of the
no-op graphics::Screen defined under !HAS_SCREEN: none of them touch a member,
so it offers to make them static. The advice is wrong here - the stub exists
only to mirror the real Screen's instance API so call sites like
screen->setFrames(...) compile on screenless boards, so the methods have to
stay non-static member functions.
The header is included by 85 translation units, so this fired 1700 times on
the two screenless esp32 boards in the check matrix (heltec-ht62-esp32c3-sx1262
and tlora-c6) - the entire src/graphics low-severity count for those boards.
bin/check-all.sh passes --fail-on-defect=low, so it was failing those jobs.
Wrap the stub in an inline cppcheck-suppress-begin/end block, matching the
inline-suppression style already used elsewhere in the tree.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* feat(t-connect-pro): add LilyGo T-Connect-Pro variant
ESP32-S3R8, 16MB flash, 8MB octal PSRAM. SX1262 LoRa, 480x222 ST7796 LCD
with CST226SE touch, W5500 ethernet and a 10A relay on EXT_NOTIFY_OUT.
LoRa, display and ethernet share one SPI bus (SCK 12 / MISO 13 / MOSI 11),
so every peripheral stays on SPI2_HOST.
board_level is extra and HW_VENDOR falls through to PRIVATE_HW until a
HardwareModel enum value is allocated.
* fix(w5500): serialize shared-bus SPI access with spiLock
Arduino's ETHClass reaches SPI through SPIClass, whose mutex is invisible to
LovyanGFX. On a board where both share a bus the MAC reads glitched frame
headers, and the resulting ESP_LOGE flood blocks the W5500 RX task on the
console UART until the task watchdog reboots the device.
SharedBusEthernet installs esp_eth directly so its custom_spi_driver
callbacks can take spiLock, the mutex the radio, display, SD and sensors
already share. It derives from NetworkInterface, so localIP(), connected(),
config() and the GOT_IP events are unchanged.
Selected by ETH_SHARED_SPI; boards without it keep the stock ETHClass path.
Measured on T-Connect-Pro under a 150 x 1472 byte flood with the display
active: 34948 truncated frames, 3 reboots and 20% packet loss before,
none after.
* fix(cst226se): honour reset pin, screen rotation and skip wrong-model probes
Drive TOUCH_RST when the variant defines one, and stop passing I2C pins to
begin() so SensorLib does not re-init a bus the scan already owns.
Derive touch geometry from SCREEN_ROTATE the way TFTDisplay does, so a
rotated panel maps to the landscape UI rather than the raw panel size.
Use TouchDrvCST226 rather than the TouchDrvCSTXXX wrapper. Pinning the model
does not stop the wrapper walking CST816 and CST92xx, whose retries cost
about 3.5s of boot. T-Beam behaviour is unchanged.
* style: trim comments to the two-line limit
Follows the comment rule in .github/copilot-instructions.md, which the
original commits missed.
* feat(t-connect-pro): use the T_CONNECT_PRO hardware model
Depends on meshtastic/protobufs#1062. Does not build until that merges and
the generated headers are synced, since meshtastic_HardwareModel_T_CONNECT_PRO
does not exist yet.
Drops -D PRIVATE_HW, which becomes a no-op once HW_VENDOR resolves, and
promotes board_level to release.
* chore(t-connect-pro): mark as community supported
Support level 3, matching the other unlicensed LilyGo boards.
* fix(w5500): roll back partial init when begin() fails
begin() returns early when ethHandle is set, so a failure after
esp_eth_driver_install() left the handle populated and every later call
returned true with no working driver.
teardown() releases the event handler, netif glue, netif, driver, PHY and MAC
in reverse creation order, and every failure path now uses it.
* refactor(w5500): drop config the custom SPI driver never reads
spi_devcfg and spi_host_id are only read by w5500_spi_init, which esp_eth
skips when custom_spi_driver is set, so the device config fields were dead.
Also drops the handle() accessor, its only caller is the class's own event
handler, the eventRegistered flag, since unregistering an unregistered
handler is safe, the redundant _esp_netif guard around destroyNetif(), and
the TFT_CS indirection, which this panel path does not read.
* fix(w5500): fail begin() when the event handler cannot register
The return value was ignored, so a failed registration still started Ethernet
and returned true while onEthEvent never fired. WiFiAPClient would then miss
ETH_CONNECTED, GOT_IP and DISCONNECTED, leaving the link up with the firmware
believing it was down.
* fix(position): stamp the broadcast cadence only when a position packet was actually sent
* fix(position): honor the router verdict and always fall back to nodeinfo when no position goes out
* fix(position): snapshot Time::getMillis() for the throttle stamps and trim the added comments
* fix(position): consume the radio-generation change only on a position send that went out
* refactor(position): share one smart-broadcast path and drop the duplicated fresh-position guard
* fix(position): persist smart broadcasts to the transmit history
* fix(telemetry): don't broadcast the noise floor default as a real reading
LocalStats.noise_floor has no has_/presence bit, so RadioLibInterface's
NOISE_FLOOR_DEFAULT (-120 dBm) placeholder was indistinguishable on the
wire from a genuine -120 dBm reading whenever no valid RSSI sample had
ever been collected (radio not idle when sampled, or every reading
falling outside the plausible bounds).
Gate the assignment on hasNoiseFloorSamples() and log a warning instead
of shipping the placeholder. The field stays at its zero-init default
in that case.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Address review: trim the noise_floor comment and demote the log to debug
An empty sample buffer is expected during early boot, so LOG_WARN overstated it.
* Restore the periodic noise floor feeder lost in merge e55947595
updateNoiseFloor() shipped in #9347 with three call sites. Review removed the
onNotify() one because sampling on the ISR path can overflow the 256-byte radio
FIFO; the maintainer's guidance was to keep a periodic call from thread context
instead. The remaining completeSending() and startReceive() calls then vanished
in merge commit e55947595 "Merge upstream develop into noise-floor", which took
develop's rewrite of both functions wholesale. No commit since has called it.
That left DeviceTelemetry::getLocalStatsTelemetry() as the only caller, so the
20-sample window advanced at most once per local stats send: one sample every 15
minutes, five hours to fill, and the internal 5s throttle never reached.
Sample from the existing AGC maintenance tick, before periodicRadioMaintenance()
because resetAGC() recalibrates the frontend and biases an RSSI read taken right
after it. NOISE_FLOOR_UPDATE_INTERVAL_MS stays at 5s as an inner floor; the 60s
call site sets the real rate. The window now fills in about 20 minutes.
* Trim the noise floor change
Drop the updateNoiseFloor() call on the telemetry path: the 60s tick feeds the
window now, so it contributed about one sample in fifteen and cost an SPI-gated
read on the local stats path.
The else-branch existed only to log; the "Sending local stats" LOG_INFO in the
same function already reports noise_floor=0 when no sample exists.
---------
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Thomas Göttgens <tgoettgens@gmail.com>
* fix(metadata): report all compiled-out module configs
Add MQTT_CONFIG, NEIGHBORINFO_CONFIG, STOREFORWARD_CONFIG and
TELEMETRY_CONFIG bits to getDeviceMetadata().excluded_modules,
guarded by the same macros that gate the modules in
src/modules/Modules.cpp (MESHTASTIC_EXCLUDE_MQTT,
MESHTASTIC_EXCLUDE_NEIGHBORINFO, MESHTASTIC_EXCLUDE_STOREFORWARD,
HAS_TELEMETRY). Widen three existing conditions: PAXCOUNTER_CONFIG
now also reports when an ESP32 build sets MESHTASTIC_EXCLUDE_PAXCOUNTER;
BLUETOOTH_CONFIG now also reports when HAS_BLUETOOTH is 0 on nRF52/ESP32;
NETWORK_CONFIG collapses the per-arch nRF52/RP2040 arms into a single
!HAS_NETWORKING check.
Clients read excluded_modules to decide which module config screens to
show. Four bits were never set when the module was compiled out, and
three were set only for a subset of the affected builds, so clients
offered config screens for modules absent from the firmware: MQTT on
every STM32WL target, TELEMETRY on nrf54l15 and minimize builds,
NEIGHBORINFO on russell and several nRF52 RAK boards.
The change is confined to getDeviceMetadata(). The bitmask is advisory:
clients use it to hide menu entries and it does not touch the module
config wire protocol. No PhoneAPI, NodeDB or AdminModule changes.
Assisted-by: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Andrew Yong <me@ndoo.sg>
* fix(metadata): exclude Store & Forward on unsupported architectures
Define MESHTASTIC_EXCLUDE_STOREFORWARD for any build that is neither
ARCH_ESP32 nor ARCH_PORTDUINO.
StoreForwardModule registers only on those two architectures, but the
macro was previously set only by minimize builds and a few variant
flags, so getDeviceMetadata() still advertised STOREFORWARD_CONFIG on
nRF52, RP2040 and STM32WL. Every other use of the macro is already
nested in an ARCH_ESP32/ARCH_PORTDUINO block, so ESP32 and Portduino
builds are unaffected.
Assisted-by: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Andrew Yong <me@ndoo.sg>
---------
Signed-off-by: Andrew Yong <me@ndoo.sg>
* fix(pki): reject a restored pre-2.8 low-entropy key at set time, explain the swap
Restoring/setting a private key is a private-key change: the public key is
*generated* from it. The low-entropy blacklist check in generateCryptoKeyPair
runs against the stored public_key at entry, which is empty on a bare key
restore — so a known pre-2.8 weak key derived from the provided private key was
never caught at set time. It was only detected on the next boot (once the weak
public key had been persisted and re-checked), which looks to the user like
their saved key silently "did not stick", and their node number
(== crc32(public_key)) had quietly changed too.
- NodeDB::generateCryptoKeyPair: in the provided-private-key branch, re-check
the *derived* public key against LOW_ENTROPY_HASHES. If it matches, replace it
with a fresh secure keypair and set keyIsLowEntropy so the reason is surfaced.
- AdminModule set-config(security): when the restore path regenerated a rejected
low-entropy key, send a client warning at set time explaining the key can't be
restored and the node number changed. Scoped to that branch so a stale flag
from a boot-time regeneration can't fire on unrelated security sets.
No protobuf changes; reuses the existing ClientNotification warning path.
Signed-off-by: Garth Vander Houwen <garthvh@yahoo.com>
* fix(pki): gate low-entropy restore warning on successful keygen
generateCryptoKeyPair returns false on an unset LoRa region before
resetting keyIsLowEntropy, so the set-time warning could fire on a stale
flag. Capture the return value and require both.
Shorten the rationale comments to two lines each.
* fix(pki): clear key sizes when a restored private key derives nothing
The provided-private-key branch sets private_key.size and public_key.size
to 32 before regeneratePublicKey() runs. On failure it returned false with
both sizes still set, and AdminModule persisted that pair; every later
keygen then re-derived from the same dead key. Clear both on the failure
path so the next keygen mints a fresh identity.
Add test_admin_radio coverage for the set-time restore path: a derived
low-entropy key warns and rotates, a stale keyIsLowEntropy flag with
keygen blocked does not warn, and a failed derivation clears both sizes.
* fix(pki): validate a restored public key that is itself blacklisted
A restore supplying both private_key and public_key reached neither keygen
branch, so a whole pre-2.8 low-entropy pair was accepted and persisted at
set time and only caught on the next boot. Re-derive when the supplied
public key is blacklisted, which routes it through the same rejection and
warning as the bare-private-key restore. A non-blacklisted keypair import
is unaffected.
Install the test crypto stub through a helper and drop it in
restoreAdminRadioGlobals(), so a failed assertion's longjmp cannot leak a
freed engine into later tests.
* fix(pki): only warn about a swapped key when one was actually swapped
keyIsLowEntropy is set from the stored public key at function entry, so a
restore whose supplied public key is blacklisted set it even when keygen
merely re-derived the public key from a private key that was kept. The
warning then claimed a new key had been generated and the node number
changed, which was only half true. Gate it on the private key actually
being replaced.
* fix(pki): re-check a freshly minted keypair against the blacklist
Both mint sites called crypto->generateKeyPair() once and trusted the
result, so an entropy source still producing known-weak keys could persist
another blacklisted identity. Route both through a helper that re-checks
and retries a bounded number of times, then logs if it cannot do better.
Pass the caller's own copy of the private key to generateCryptoKeyPair()
instead of config.security.private_key.bytes, which aliased the memcpy
destination inside it.
* fix(pki): fail keygen when every replacement stays blacklisted
generateBlacklistCheckedKeyPair() logged an error after exhausting its
retries but left the compromised keypair in place and its callers marked
the keygen successful, persisting exactly the identity the check exists to
reject. Return a flag, clear both key sizes on exhaustion, and abort both
callers so the next keygen starts clean.
Match the declaration guard to the definition's, and derive the expected
mint count in the retry test from the configured one.
* refactor(pki): drop the keygen retry loop, fail on the first weak mint
Retrying cannot help: an entropy source that lands on one of the twelve
blacklisted keys is broken, and a second call to it produces the same
result. With real entropy the odds are ~2^-250, so the loop never runs
twice in practice either. Check once and fail, which is the same guarantee
in a third of the code.
* fix(pki): check the derived key on the stored-private-key path too
factory_reset_config keeps the private key and clears the public one, so
the entry check sees no stored key, reports "not low entropy" and takes the
regenerate branch, which adopted whatever it derived. A preserved pre-2.8
key was therefore accepted for a whole boot cycle before the next boot
caught it - the same silent revert this PR exists to remove.
Hoist the post-derive blacklist check into a helper and use it on both
derive paths.
* fix(pki): clear key sizes when stored-private derivation fails too
The stored-private-key path set public_key.size to 32 up front and left it
there when regeneratePublicKey() failed, so config claimed a pair the node
never got - the same defect already fixed on the provided-key path.
Both paths now derive through one helper that clears on failure and vets
the derived key, replacing the separate blacklist-replace helper.
---------
Signed-off-by: Garth Vander Houwen <garthvh@yahoo.com>
Co-authored-by: Thomas Göttgens <tgoettgens@gmail.com>
* fix(stm32wl): advertise canShutdown if HAS_LSE
Define HAS_CPU_SHUTDOWN on HAS_LSE STM32WL builds and, on that path,
report canShutdown in getDeviceMetadata() from the runtime
stm32wlRtcAvailable() check.
canShutdown was always false on STM32WL: HAS_CPU_SHUTDOWN was never set
for the architecture and pmu_found is never set there, so apps hid the
shutdown control even though deep-sleep shutdown works on HAS_LSE builds
via cpuDeepSleep() -> STM32LowPower::shutdown(). Reading the runtime
check keeps the report accurate when the LSE crystal fails to lock,
where cpuDeepSleep() resets instead of sleeping.
The #else branch is untouched, so non-STM32WL and non-HAS_LSE builds
report canShutdown exactly as before.
Assisted-by: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Andrew Yong <me@ndoo.sg>
* fix(stm32wl): reject HAS_CPU_SHUTDOWN without HAS_LSE
Add an #error in architecture.h when an STM32WL build has
HAS_CPU_SHUTDOWN set but HAS_LSE unset.
getDeviceMetadata() takes the stm32wlRtcAvailable() branch under
HAS_CPU_SHUTDOWN, but that function is compiled only under HAS_LSE, so a
build forcing HAS_CPU_SHUTDOWN=1 with HAS_LSE=0 would reference it with
no declaration or definition. No current variant does this, and
architecture.h derives HAS_CPU_SHUTDOWN from HAS_LSE in the same block,
so ordinary builds are unaffected.
Assisted-by: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Andrew Yong <me@ndoo.sg>
---------
Signed-off-by: Andrew Yong <me@ndoo.sg>
* feat: log heap watermark, largest free block and subsystem breakdown
The periodic heap line reported only free/total, which cannot distinguish a
leak from fragmentation, and the MemAudit per-subsystem breakdown was only
ever printed at boot.
Add ESP.getMinFreeHeap()/getMaxAllocHeap() wrappers to MemGet (0 on platforms
that cannot report them) and include both in the 5-minute line, then log the
MemAudit breakdown on the same tick. A falling watermark is a leak; a steady
watermark with a shrinking largest block is fragmentation, and the breakdown
names the tagged subsystem that moved.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E9ZacpGtsA6DWavqr5Ty2i
* docs: correct the watermark interpretation in logHeapUsage comment
A single step down in the minimum-free watermark is a transient allocation,
not proof of a leak; it takes repeated new lows across samples.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E9ZacpGtsA6DWavqr5Ty2i
---------
Co-authored-by: Claude <noreply@anthropic.com>
- 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().