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
When we overhear another node rebroadcast one of our own packets, we
generate an implicit-ack ROUTING packet for the local sending process.
That ack is delivered locally to the phone, so pass the overheard
rebroadcast as a relay source and copy its relay_node and the rx_rssi /
rx_snr we heard it at onto the ack. This lets the connected client see
which node relayed our packet and the link quality, instead of only
learning that the packet was repeated.
allocAckNak / sendAckNak gain an optional relaySource parameter. The
ack is sent to ourselves (to == us), so Router::send() is bypassed and
does not overwrite these fields with our own.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
* feat(admin): append the optional ham long_name to the call sign
HamParameters gained a long_name field (meshtastic/protobufs#941) that
handleSetHamMode never read, so a client that sent one still ended up with a node
named after the bare call sign. Join it behind the call sign with the "//"
separator hams already use on the air: call_sign "N0CALL" plus long_name "Attic
Heltec" becomes "N0CALL//Attic Heltec". An absent long_name keeps the previous
call-sign-only name, which is what the on-device region picker still sends.
Being cosmetic, long_name stays out of the whitespace-only rejection that guards
call_sign and short_name: a blank one is dropped rather than costing the operator
the whole licensing request over a stray space, which that path would report only
as a LOG_WARN and so would be invisible from the app. The composed name is
finished with clampLongName() rather than a bare sanitizeUtf8(), matching
handleSetOwner and NodeDB: the proto caps the parts at 7 + 2 + 14 bytes, inside
the 24-byte local budget, and clampLongName is the backstop if either cap moves.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat(admin): enhance handleSetHamMode to return status for request validation
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* Fix trackball dropping short presses and losing the click when tilted
* Do not let a direction counter overwrite an emitted press event
* Accept the first press interrupt when the clock still reads zero
* Classify a press released before the first poll by its latched time
* Implement GeofenceModule for waypoint crossing notifications and integrate with existing modules
* Waypoint Applet Initial Support on InkHUD
* undo tile change
* Update screen when Waypoint shows or dissapears
* Merge branch 'develop' into waypoint-geofence
* Geofence on InkHUD
* Update MapTile.h
* Update WaypointStore.cpp
* Notifications
* remove GF from waypoint screen
* Prevent Focus from closing the notifiaction banner
* Trunk fix
* cleanup
* undo merge conflix mistake
* Waypoint screen on BaseUI
* Focus preserve fix
* UI bugs
* Allow Inkhud to remove waypoint
* Respect Locked Waypoints
* Trunk fix
* Update WaypointStore.cpp
* Use 8-digit hex formatting for waypoint IDs.
0x%x was inconsistent with the repo's own convention (0x%08x for 32-bit IDs, used elsewhere in this file). Fixed here and in two other spots I found with the same issue (WaypointModule.cpp, GeofenceModule.cpp).
* Update ExternalNotificationModule.cpp
* Reject invalid surrogate codepoints in waypoint icon rendering
* Update WaypointModule.cpp
* Update WaypointStore.cpp
* Update WaypointStore.cpp
* Update WaypointStore.cpp
* trunk fix
* fix warnings
* power.h rename to Power.h
* Update Power.h
* Fix executable bit on bin/lint-ifdef-complexity.sh
Lost during a prior merge from develop (Windows checkout doesn't
preserve file mode), causing "execve failed: Permission denied" in
the Trunk Check Runner CI job. develop has this file at 100755;
restoring that here.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* Update README.md
* Clean up waypoint and geofence integration
* Minimize waypoint and geofence implementation
* removed unnecessary gating
* Geofence alert
* trunk fix
* Update test_main.cpp
* Update WaypointStore.cpp
---------
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* fix(radio): put the beacon restore back inside completeSending's if (p)
Reverts the RadioLibInterface and RadioInterface changes from #11573
(ac330e6a6). Hoisting MeshBeaconModule::reconfigureForBeaconTX() out of the
if (p) block changed its meaning from "a send completed" to "the radio went to
standby, for any reason" - and every driver's setStandby() calls
completeSending() unconditionally: on the pre-TX LBT scan, on startReceive(),
and inside reconfigure().
Two shipping faults followed, both confirmed on hardware the next day.
Every beacon transmitted on the wrong preset. isChannelActive() standbys the
radio immediately before each transmit, so the restore ran between the switch
and the key-up. The packet went out carrying the beacon channel hash with home
modem settings - inaudible to listeners on the target preset, an unknown hash
to listeners on the home one. Inert in both directions.
And unbounded recursion: the restore calls iface->reconfigure(), which
standbys, which calls completeSending(), which restores again, each level
running a full applyModemConfig(). It terminated in a HardFault and a silent
reboot (Reset reason 0x4 on nRF52, no panic output). The crash masked the
misdirection - the node died before Started Tx, so the wrong preset was
invisible until the recursion was fixed.
completeSending() clears sendingPacket at the top, so any nested call sees
p == NULL. The if (p) block was an accidental re-entrancy guard, and nothing
named it as such; removing it created both faults at once. Name it now.
This also reverts the beginSending() failure return that motivated the move,
and the startSend() scaffolding built to reach the restore on that path. The
payload bounds check it replaced is reinstated in the next commit, at a point
where refusing a packet is already a supported outcome.
* fix(radio): bound the payload at the radio queue, not mid-transmit
#11573 replaced beginSending()'s assert with a runtime check that logged,
released the packet and returned 0. beginSending() had never returned 0
before, so startSend() gained a failure path it had to unwind - and the
release moved ownership of the packet out of the caller that held it. That
new return value is what made hoisting the beacon restore look necessary.
The check itself is worth keeping. MeshPacket.encrypted has a nanopb maximum
of 256 bytes against a 240-byte radio buffer, and beginSending() is on the
path for relayed frames and phone-sourced packets, neither under our control.
Asserts are commonly compiled out in release builds, so what shipped was an
unchecked 256-into-240 memcpy driven by remote input.
Move it to Router::send(), immediately before iface->send(p) - the single
funnel for every over-the-air transmit. Refusing a packet there is already a
supported outcome: it returns TOO_LARGE, which is what perhapsEncode() already
returns for the same condition on the decoded path, and releases or NAKs
exactly as the duty-cycle limit above it does. Nothing radio-side has happened
at that point, so there is no half-started transmit to tear back down.
perhapsEncode()'s existing check does not cover this case: relayed and
phone-sourced frames arrive already encrypted and never reach it.
beginSending() keeps a last line of defence, but clamps rather than failing,
so it stays a call that always succeeds. Adds MAX_RADIO_PAYLOAD_LEN so both
sites name the same number instead of recomputing it.
Nothing about a beacon can trigger any of this - broadcast_message is
admin-truncated to 100 bytes, the whole MeshBeacon protobuf tops out at 180,
and observed beacons run to 106 - which is why this is separated from the
beacon changes rather than carried with them.
Tests: Router::send() refuses an oversized payload and still sends one that
exactly fills the buffer; beginSending() clamps instead of rejecting, and
leaves ordinary traffic whole.
* fix(beacon): guard the radio switch/restore against re-entry and early restore
Two checks in reconfigureForBeaconTX(), both independent of radio state, so
the switch/restore state machine no longer rests on sendingPacket's lifetime -
which is exactly the implicit coupling that let #11573 through.
A re-entrancy guard. Both branches end in iface->reconfigure(), whose
setStandby() runs completeSending(), which calls straight back in here. While
one call is applying a config, a nested call returns false and leaves it
alone. This covers the switch branch too, which had the same exposure with a
quieter symptom: a second switch before the restore would take the re-entrant
call as a restore and undo the switch still being applied, sending the beacon
on the home channel instead of its target.
A restore gate. The restore now waits for the packet that armed the switch to
actually finish, tracked by id against our own target table rather than by
asking the radio. Every caller that completes or abandons a beacon clears that
packet's target settings first, so a live entry means the TX has not happened
yet. cancelSending() now clears too, which is what keeps a cancelled beacon
from pinning the radio on the beacon config.
Together these make explicit the invariant completeSending()'s if (p) block
was carrying by accident: a future hoist of that call gets a logged no-op
instead of a crash and a misdirected beacon.
Also sets radioSwitched before reconfigure() rather than after, in both
branches, so the flag never describes a radio state that is not yet true.
Diagnostics, because every step of this dance was previously silent about its
own state. Count consecutive switches with no restore between them and log the
depth on both sides, so a change-change-change-restore run reads off the log;
switch #2 onwards prints the held home snapshot, which is the value that has
to survive a second switch. The restore names the config it is restoring to,
so a stale snapshot is visible directly. The re-entrancy guard logs when it
fires - expected exactly twice per beacon, so a burst means something new is
re-entering rather than a silent reboot. And setTargetRadioSettings() now
warns on the slot eviction that previously left a packet to key up on whatever
config was running - no crash, no log, wrong channel.
Reachable only with beacon broadcast enabled (the default flags are
LISTEN_ENABLED | LEGACY_SPLIT, so broadcast is off) and a target differing
from the running config; an identical target takes the early return and never
switches.
Tests: three re-entrancy cases against a RadioInterface whose reconfigure()
re-enters exactly as completeSending() does - bounded, so a regression fails
an assertion instead of overflowing the stack and taking the runner with it -
plus a restore that must defer until the beacon it switched for completes.
* fix(beacon,radio): address review findings on #11596
Payload ceiling was one byte too generous. RadioBuffer::payload is 240 bytes
because the buffer reserves MAX_LORA_PAYLOAD_LEN + 1, but the PHY caps a whole
frame at 255 and beginSending() adds a 16-byte header - so a 240-byte payload
produced a 256-byte frame. Define the ceiling as MAX_LORA_PAYLOAD_LEN -
sizeof(PacketHeader), matching what perhapsEncode() already enforces, with a
static_assert that it still fits the buffer.
Target-table eviction could unblock the restore gate. With every slot live,
setTargetRadioSettings() overwrote slot 0 - and if that slot held the packet the
outstanding switch is gated on, the restore came unblocked and put the home
config back under a beacon that had not keyed up. Skip that entry when choosing
a victim, and refuse the target outright if every slot is in flight. Needs
radioSwitched/switchedForId at file scope so the setter can see them.
Restore on every abandon path, not just the clear. cancelSending() dropped a
queued packet's target without restoring, so a beacon pre-switched by onNotify()
and then cancelled left the radio receiving on the beacon config;
removePendingTXPacket() did neither. Both now route through
abandonBeaconTarget(), as does startSend()'s tx-disabled branch. The restore
gate makes it a no-op when the abandoned packet is not the one we switched for.
No NAK on the oversize drop. p->channel is a wire hash by that point, not an
index, and Channels::getIndexByHash() is declared but never defined. Only
already-encrypted ingress can reach the gate anyway - perhapsEncode() bounds
everything it encodes - and those carry no index to answer on. Release and log.
Tests clear sendingPacket before releasing their packet, and assert against the
payload ceiling rather than the buffer size.
* fix(beacon): route the invalid-target drop through abandonBeaconTarget
onNotify()'s invalid-config drop was the one packet-abandonment path still
clearing the target directly instead of going through abandonBeaconTarget(),
so a packet that armed the radio switch and then failed validation would be
released with the radio left on the beacon config and nothing to restore it.
The helper's restore gate (targetRadioSettingsLive(switchedForId)) makes the
call a no-op for any packet that did not arm the switch, so this closes the
gap without risking a premature restore.
Also trims the switch-state comment to the two-line limit.
* fix(radio): take the abandoned packet as a pointer to const
cppcheck's constParameterPointer failed the check matrix on every board:
abandonBeaconTarget() only forwards the packet to clearTargetRadioSettings(),
which already takes a const pointer, so the parameter should be const too.
* refactor(radio): drive the beacon radio switch through TX hooks
RadioLibInterface named MeshBeaconModule at six call sites behind
MESHTASTIC_EXCLUDE_BEACON guards, so the driver carried per-packet beacon
state: when to switch preset, when a target config was invalid mid-transmit,
and when not to listen on a busy channel. Review on #11596 asked for the
module dependency to come out.
RadioTxHook is what the driver knows instead - beforeTransmit() returning
send/defer/drop, holdsRadio(), packetReleased() - on a self-registering
intrusive list, so nothing is allocated and a build without the beacon module
registers nothing and every call is a no-op. The four abandon paths (cancel,
remove-pending, TX disabled, completeSending) collapse onto one
packetReleased(), and the tri-state means the driver no longer has to know why
a packet wanted a re-delay or a drop.
MeshBeaconTxHook wraps the existing statics; the switch/restore logic, its
re-entrancy guard and its restore gate are untouched. It is created in
Modules.cpp inside the existing exclusion guard, so MESHTASTIC_EXCLUDE_BEACON
now works by nothing registering rather than by #ifdefs in the driver.
Behaviour is unchanged. The invalid-config LOG_DEBUG moves into the module and
the driver logs a generic refusal. Four tests cover the send/defer/drop mapping
and that an empty hook list is a no-op; native:test_mesh_beacon is 59/59.
Also notes in sendBeaconPacket that beacons uplink to MQTT on the primary
slot's uplink_enabled, and that the topic follows the beacon channel under the
crypto-override swap - both intentional.
* fix(beacon): restore the home config for a packet that jumps the queue
The restore gate added in 9cb7b96c9 refused to put the home config back while
the beacon that armed the switch was still live. That is right for a release -
completeSending() runs on every setStandby(), and restoring there would undo
the switch before the beacon had keyed up - but it also caught the case where
the driver is asking about a different packet it is about to transmit.
MeshPacketQueue::enqueue() inserts by priority (std::upper_bound over
CompareMeshPacketFunc), so an ACK or routing packet queued during the beacon's
deferred transmit delay lands ahead of it. beforeTransmit() then saw an
untagged packet, found the beacon still queued, skipped the restore and
returned PRETX_SEND - and the packet transmitted on the beacon's preset, slot
and region. It was encrypted and hashed for the home channel, so no receiver
on either preset could use it.
Apply the gate only to a null p. A non-null untagged packet is the driver
about to key up, which always restores; the restore returns PRETX_DEFER, so
the driver re-runs the delay and the channel scan on the config it will
actually transmit on. beforeTransmit() is the only caller that passes a
non-null untagged packet, so nothing else changes.
Found by CodeRabbit on #11596. native:test_mesh_beacon 60/60, including a
regression test for the queue transition; the four re-entrancy tests still
cover the null-p gate.
---------
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
The 12h floor introduced with traffic management was too aggressive: a
fixed_position or stationary node goes quiet for half a day after its
boot broadcast, so anything that missed that one packet - a node that
joined later, or one that restarted - shows it with no position until
the next refresh.
Drop the floor to 6h, and drop the traffic-management identical-position
dedup window from 11h to 5h with it. The two are a pair: the dedup window
was deliberately sized just under the broadcast floor so a stationary
node's periodic refresh clears its neighbours' window instead of being
dropped as a duplicate. Leaving it at 11h would have made the extra
broadcast pure airtime - aired, then discarded by every receiver - so the
mesh would still have seen a 12h refresh.
Role caps are unchanged and still bind: tracker 1h, lost-and-found 15m.
Both remain shorter than the new 5h default, so those exceptions apply
exactly as before.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>