Commit Graph
1540 Commits
Author SHA1 Message Date
Thomas Göttgens cccefa09a4 fix(rp2xx0): match the real library name when ignoring iLabs_Hearth (#11761)
lib_ignore matches the name from the library manifest, and iLabs_Hearth
declares "iLabs Hearth" with a space. The directory name used in #11757
therefore never matched, and the Pico builds still compile the library and
still fail on its ESP_SERIAL_PORT #error.
2026-09-07 16:02:30 +02:00
Austin fd623f64fd Revert "fix(ci): pin tool-scons to 4.8.1 for ESP targets (#11756)" (#11763)
This reverts commit a8912b1eb5.
2026-09-07 10:02:04 -04:00
Austin 7b8a02bb4c Revert "fix(ci): build with pioarduino core instead of upstream platformio (#…" (#11760)
This reverts commit f631428309.
2026-09-07 09:23:15 -04:00
Thomas Göttgens f631428309 fix(ci): build with pioarduino core instead of upstream platformio (#11759)
* fix(ci): build with pioarduino core instead of upstream platformio

The espressif32 platform is the pioarduino fork, but setup-base installed
upstream platformio and then ran pio upgrade, so every firmware build ran
a core the platform is not built against.

Upstream 6.2.0, released 2026-09-05, moved its tool-scons core dependency
to ~4.41101.0 (SCons 4.11.1). The lazy "import SCons.Tool.FortranCommon"
that smart_link() uses to choose a linker fails there, so every ESP target
died at link-action resolution before compiling a file. Core resolves
tool-scons as a core dependency, so a platform_packages pin cannot help:
core installs its own version and removes the pinned one.

pioarduino core pins SCons 4.8.1 by URL rather than by range, so upstream
releases cannot reach it. Dropping pio upgrade as well, since it re-pulled
the latest upstream core regardless of what pip installed.

Only setup-base changes. The matrix-generation jobs install platformio to
parse the ini files and never build firmware, and the native test suites
pass on upstream core because that platform does not reach smart_link.

* Revert "fix(ci): pin tool-scons to 4.8.1 for ESP targets (#11756)"

This reverts commit a8912b1eb.

The pin never took effect. PlatformIO Core resolves tool-scons as a core
dependency, so it installs its own version and removes the pinned one:

  Installing platformio/tool-scons @ 4.40801.0
  Installing platformio/tool-scons @ ~4.41101.0
  Removing tool-scons @ 4.40801.0

Switching to pioarduino core in the preceding commit fixes this properly,
and leaving a platform_packages entry that fights a core dependency would
only be misleading.
2026-09-07 12:24:12 +02:00
Thomas Göttgens b46aec31f4 fix(rp2xx0): ignore iLabs_Hearth so Pico targets build again (#11757)
arduino-pico gained a bundled iLabs_Hearth library that supplies its own
Preferences.h. NodeDB.cpp includes <Preferences.h> inside an ARCH_ESP32
guard, and the library dependency finder runs in chain mode, which matches
include directives without evaluating the preprocessor. It therefore pulls
the library into every Pico build, and Hearth.cpp refuses to compile on a
board that does not define ESP_SERIAL_PORT:

  iLabs_Hearth/src/Hearth.cpp:121:2: error: #error "iLabs Hearth requires a
  board variant that defines ESP_SERIAL_PORT ..."

The library only reaches the build because the platform resolves
framework-arduinopico from an arduino-pico master commit; the
platform_packages entry here pins the name arduino-pico, which is a
different package and does not override it.

Ignoring the library is enough, since no Pico target uses Matter. The
sibling iLabs_ESP-NOW ships ESP32_NOW.h and ATLink.h, which nothing
includes, so it needs no entry.
2026-09-07 10:45:17 +02:00
Thomas Göttgens a8912b1eb5 fix(ci): pin tool-scons to 4.8.1 for ESP targets (#11756)
PlatformIO Core now pulls tool-scons ~4.41101.0 (SCons 4.11.1), which
overrides the 4.8.1 the pioarduino espressif32 platform asks for. In
4.11.1 the lazy "import SCons.Tool.FortranCommon" that smart_link() uses
to pick a linker raises ModuleNotFoundError, so every ESP environment
fails at link-action resolution, before a single file is compiled:

  *** [.pio/build/<env>/firmware-<env>.elf] ModuleNotFoundError :
      No module named 'SCons.Tool.FortranCommon'

The package is not at fault; FortranCommon.py is present in
tool-scons-4.41101.0 and imports cleanly outside SCons. The failure comes
from the module state SCons's own tool loader leaves behind.

Scoped to esp32_common, which all six ESP architectures extend. nRF52,
STM32 and rp2040 are unaffected and keep the toolchain they have.
2026-09-07 10:44:51 +02:00
renovate[bot] 3520e4fa8c chore(deps): update pschatzmann_arduino-audio-driver to v0.3.1 (#11738)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-06 18:31:10 +00:00
Thomas Göttgens bd19fa8e48 feat(t-connect-pro): add LilyGo T-Connect-Pro variant (#11746)
* 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.
2026-09-06 15:12:58 +00:00
Thomas GöttgensandClaude fb8319efaa fix(t-deck-pro-v1_1): link variant.cpp so the LoRa radio is powered on (#11715)
The T-Deck Pro V1.1 boots into Critical Fault #3 (NoRadio) because
LORA_EN (GPIO 46) is never driven high, so the SX1262 never receives
power and is never detected.

Before #9438 this pin was driven from src/main.cpp under
`#elif defined(T_DECK_PRO)`, which covered both the t-deck-pro and the
t-deck-pro-v1_1 environments, since both build with -D T_DECK_PRO.
#9438 moved that block into variants/esp32s3/t-deck-pro/variant.cpp and
linked it with a build_src_filter added only to the t-deck-pro
environment. t-deck-pro-v1_1 had been added five days earlier and has
neither a variant.cpp nor a build_src_filter, so it silently lost the
pin setup: earlyInitVariant() is a weak symbol with an empty default in
main.cpp, so a missing strong override produces no compile or link
error.

Give t-deck-pro-v1_1 the same variant.cpp as t-deck-pro, plus the
build_src_filter needed to actually link it. This also restores the
LORA_CS, SDCARD_CS and PIN_EINK_CS pre-init lost at the same time; all
three share the SPI bus and need their chip selects deasserted before
the bus is used.

Fixes #11708

Co-authored-by: Claude <noreply@anthropic.com>
2026-09-03 11:29:52 +00:00
rcarterazandThomas Göttgens 3f4b96c24d Fix architecture name for Seeed Wio Tracker L2 (#11713)
* Fix architecture name for Seeed Wio Tracker L2

* Normalize custom_meshtastic_architecture against the board MCU

The declared value reached the manifest unchecked, which is how esp32s3 shipped
here and in the -tft env that extends it. infer_architecture() already derives
the canonical spelling from the board MCU, so prefer it when the two disagree
and print the override.

Scanned all 113 envs declaring an architecture; this variant was the only
mismatch.

* Simplify the architecture override

---------

Co-authored-by: Thomas Göttgens <tgoettgens@gmail.com>
2026-09-03 10:25:22 +00:00
renovate[bot] 51ed88100e chore(deps): update platformio/ststm32 to v20 (#11685)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-03 08:13:48 +00:00
renovate[bot] 077fe4823d chore(deps): update esp32-ch390 to v1.2 (#11712)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-03 08:13:05 +00:00
renovate[bot] fa65b0797e chore(deps): update lovyangfx to v1.2.28 (#11615)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-02 08:58:56 +00:00
Manuel 80c6d2f8da Update device-ui library version in platformio.ini (#11694) 2026-09-02 01:10:54 +00:00
renovate[bot] a2c919d2a2 chore(deps): update platformio/nordicnrf52 to v11 (#11684)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-01 22:25:58 +00:00
Manuelandcoderabbitai[bot] 36c89fa3a7 feat: Support Seeed Wio Tracker L2 (#10909)
* initial commit

* enable power save

* implement mesh LED

* add ADS1115+AW35615 for wio tracker L2

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

* update commit references

* move variant.cpp to extras

* update hw_model

* update lovyanGFX

* point to device-ui commit

* trunk fmt

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

* workaround duplicate defined symbol

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

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

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

* refactored AW35615 into new external library

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

* fix detection of ADS1115

* update device-ui commit reference

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

* add battery curve, fix io expander sync

* add SPILock, simplify macro usage

* update device-ui

* fix wakeup from sleep

* revert because of #11604

* use new AUDIO_AMP_SETTLE_MS

* remove test logs

* enable BaseUI

* use touch screen

* refactor wakekey thread

* fix wake button toggle screen on/off

* fix battery percentage and plugIn state

* Update src/graphics/TFTDisplay.cpp

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

* consider to return I2C errors to make coderabbi happy

* fix warnings

* use Throttle for millis comparison

* fix endTransmission in write

* make the rabbit happy

* spli targets -tft / non-tft

* fix compile

* revert forced use of Throttle

* remove MeshLED

* add HW_MODEL

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-08-28 23:30:27 +00:00
Andrew YongandBen Meadors db84bdf3b4 Reduce ExternalNotificationModule flash usage (RTTTL + InputBroker) (#10989)
* Generalize RTTTL exclusion into MESHTASTIC_EXCLUDE_RTTTL

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

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

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

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

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

* Skip unused InputBroker observer in ExternalNotificationModule

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

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

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

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

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

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

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

---------

Signed-off-by: Andrew Yong <me@ndoo.sg>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2026-08-28 19:58:23 +00:00
Andrew Yong 78219e09cb fix(stm32wl): add TCXO-optional support and fix hardcoded TCXO voltage (#10964)
* stm32wl: consult SX126X_DIO3_TCXO_VOLTAGE instead of hardcoding 1.7V

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

---------

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

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

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

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

Signed-off-by: Andrew Yong <me@ndoo.sg>
Co-authored-by: Tom <116762865+NomDeTom@users.noreply.github.com>
2026-08-28 17:37:32 +00:00
IxitxachitlandManuel 7aa8ad3510 fix(t-watch-ultra): build with the esp32s3 flags, not the classic-ESP32 ones (#11619)
* fix(t-watch-ultra): build with the esp32s3 flags, not the classic-ESP32 ones

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

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

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

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

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

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

* Update platformio.ini

added missing ${device-ui_base.custom_sdkconfig}

---------

Co-authored-by: Manuel <71137295+mverch67@users.noreply.github.com>
2026-08-27 18:27:02 +00:00
Thomas Göttgens 9fbc176e91 Extend userPrefs coverage to the whole channel table and the missing config fields (#11624)
* Extend userPrefs coverage to the whole channel table and the missing config fields

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

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

USERPREFS_CHANNELS_TO_WRITE now parses 0x-prefixed hex, matching the format
userPrefs.jsonc documents, without int(x, 0)'s rejection of a leading-zero
decimal such as "03". A static_assert rejects a USERPREFS_CHANNEL_<n>_PSK
literal wider than psk.bytes, which memcpy would otherwise write over the fields
after it. The USERPREFS_CANNED_MESSAGES copy keeps strncpy's zero-padding and
terminates explicitly, rather than shortening the length, which would have left
the last byte unwritten.
2026-08-27 15:00:59 +00:00
cd6ac90f7e Add waypoint & geofence support with notifications for BaseUI and InkHUD (#10920)
* 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>
2026-08-26 16:17:28 +00:00
Thomas Göttgens 8eda86045b fix(audio): amp settle window, and start melody after codec init (#11604)
* fix(audio): amp settle window, and start melody after codec init (#11597)

* chore(audio): condense the new code comments to two lines
2026-08-25 20:40:39 +00:00
Thomas Göttgens 7b0004806a fix(graphics): drive GPIO backlights from the stored brightness level (#11588)
* fix(graphics): drive GPIO backlights from the stored brightness level

Screen::handleSetOn restored PIN_EINK_EN only when screen_brightness was
exactly 1. The field is 0..255 and defaults to 153, so the frontlight stayed
off after a screen timeout until the next reboot.

InputBroker read screen_brightness as "currently lit" for the touch backlight,
so a stored level made touch-to-light a no-op. The HAPTIC_FEEDBACK_PIN block
then reassigned touchConfig.onPress and onRelease, dropping those handlers on
any variant defining both.

MINI_EPAPER_S3 names its panel power rail PIN_EINK_EN. It was switched off with
the screen and never restored.

graphics::Backlight gains a GPIO backend covering PIN_EINK_EN and
PCA_PIN_EINK_EN, so Screen, MenuHandler and InputBroker call backlightOn,
backlightOff, backlightToggle and backlightIsLit instead of touching pins.
backlightIsLit reports the driven state, separate from the configured level.

Power-up state is declared per variant with GPIO_BACKLIGHT_DEFAULT_ON rather
than hardcoded in the e-ink driver. The backend stores only 0 or 255, so any
other stored level falls back to the variant default and no board changes its
existing behaviour. MINI_EPAPER_S3 is excluded and keeps its rail powered.

Touch handlers are merged so backlight and haptic feedback compose.

Verified on ThinkNode M1: lit at boot, off on timeout, lit on wake, and an
explicit off surviving both wake and reboot.

* chore(thinknode_m1): correct the LED pin comments

P0.13 drives the blue indicator, not a green one. P1.06 is a second drive for
the same red LED as LED_POWER, which is why it stays disabled.

* fix(graphics): clamp GPIO backlight levels at the setter

backlightSet stored whatever level it was given, so a caller passing an
intermediate value left backlightGet and the persisted config holding a level
the rail cannot drive. Clamp to off or on in the setter, which keeps the
invariant at the single write point instead of only at init.
2026-08-25 08:49:37 +00:00
Thomas Göttgens c45b66352b Idle the Wio Tracker L1 buzzer pin at boot (#11601) 2026-08-25 08:31:48 +00:00
48357538ba Meshnology W10: enable the AXP2101 power key as a second button (#11593)
* Meshnology W10: enable the AXP2101 power key as a second button

SW3 is wired to the AXP2101 PWRON pin (via R44 510R, schematic W10-MB-V1.1
pg3), but the key did nothing in firmware.

Power::runOnce() already polls the PMU IRQ status registers over I2C and maps
a PEK short press to INPUT_BROKER_CANCEL when PMU_POWER_BUTTON_IS_CANCEL is
set. However the matching PMU->enableIRQ() lives inside #ifdef PMU_IRQ, while
PMU init runs disableIRQ(ALL) first. Without PMU_IRQ the PKEY_SHORT status bit
is never armed, so the polled read is always false and the define alone is
inert.

AXP_IRQ on this board reaches only expander EXIO5 and is not routed to any
ESP32 GPIO, so define PMU_IRQ as the MCP23017 virtual pin, mirroring how
LORA_DIO1 is handled on this variant. The attachInterrupt() and
gpio_wakeup_enable() uses of PMU_IRQ are inert on a non-GPIO value (both are
unchecked calls, so an invalid pin is ignored rather than fatal); what the
define buys is the enableIRQ() they gate.

Tested on Meshnology W10 hardware: short presses of SW3 now log
"[Power] Input: Corona Button Click", the existing GPIO0 user button continues
to work independently, and the board boots normally. Events surface on the 20s
Power::runOnce() cadence, since with no real interrupt the ISR's
setIntervalFromNow(0) never runs to force an immediate poll.

* style: apply clang-format to meshnology-w10 variant.h

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

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>
2026-08-25 01:44:06 +00:00
IxitxachitlandBen Meadors e8d4573af7 fix(t-watch-ultra): wrap esp_flash_read so NVS survives, keeping BLE bonds (#11583)
* fix(t-watch-ultra): wrap esp_flash_read so NVS survives, keeping BLE bonds

The IDF 5.5 manual-read regression on this board's flash is already worked
around for esp_partition_read, but nvs_flash does not use that API: it reads
the NVS partition through the lower-level esp_flash_read, which still returns
0x00. NVS therefore initialised empty on every boot -- zero entries, zero
namespaces -- even though the data was intact on flash.

Everything stored through NVS was lost each boot, including NimBLE's bond
table. A phone that had already paired was not recognised on reconnect, so
the device ran a fresh pairing and displayed a new passkey every time. The
PIN worked, but the bond never persisted.

Wrap esp_flash_read the same way, using the raw (non-partition) spi_flash_mmap
so it serves callers that never go through the esp_partition_t API. Reads for
any chip other than the default fall back to the real implementation, as do
mmap failures. Gated on T_WATCH_ULTRA; no other board is affected.

* fix(t-watch-ultra): keep the raw-read contract when flash encryption is on

esp_flash_read is specified to return raw, still-encrypted bytes; the flash
cache is what decrypts transparently. Reading through spi_flash_mmap therefore
hands back plaintext where the caller asked for ciphertext.

No target here enables CONFIG_SECURE_FLASH_ENC_ENABLED, so nothing is affected
today, but --wrap is a global interposition and encryption can be burned into
efuse independently of the build config. Check at runtime and leave encrypted
flash to the real implementation.

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2026-08-24 23:02:04 +00:00
Thomas Göttgens ee48094ea8 Fix backwards GPS_RX_PIN/GPS_TX_PIN direction comments (#11585)
GPS.cpp passes GPS_RX_PIN as the MCU's RX pin and GPS_TX_PIN as its TX
pin. Nine variants documented the opposite, which reads as if the pins
were swapped on working hardware (see #11584).

Comment-only change; no pin assignment is touched.
2026-08-24 11:29:54 +02:00
Ben MeadorsandQuency-D bfd1e1a231 Add Heltec RC32, RC52 and RCC6 boards, and LC760CA GNSS support (#11572)
* refactor(graphics): select Arduino_GFX panels with a capability flag

TFTDisplay tested `defined(HACKADAY_COMMUNICATOR)` in a dozen places to mean
"this panel is driven by Arduino_GFX rather than LovyanGFX". Every new
Arduino_GFX board had to be appended to all of them.

Move the decision into the variant as USE_ARDUINO_GFX so the display code
stops naming individual boards. No behaviour change: the Hackaday Communicator
is still the only board that sets it.

* feat(boards): add Heltec RC32, RC52 and RCC6

Three boards around the same 128x220 NV3001B panel: RC32 (ESP32-S3), RCC6
(ESP32-C6) and RC52 (nRF52840). They differ only in how the panel bus is
wired, so they share one branch in TFTDisplay behind TFT_NV3001B.

RC32 and RC52 also carry a rotary encoder on a TCA6408 I2C expander. That
lands as its own input source rather than as board conditionals inside
i2cButton, which is the M5Stack UnitC6L button driver and stays untouched.

On RC52 and RCC6 the panel is an add-on module, so probe it before reporting
a screen. The probe reuses the bit-banged SPI helper that already backs the
T114 ST7789 check.

Arduino_GFX is pinned to the upstream commit that added the NV3001B driver;
it has not shipped in a tagged release yet.

Co-Authored-By: Quency-D <55523105+Quency-D@users.noreply.github.com>

* feat(gps): detect and configure the LC760CA GNSS module

The LC760CA is another Unicore part, so it joins the $PDTINFO probe family
and reuses the CM121 message-rate setup. It answers with CC1161W.

GNSS_MODEL_LC760CA goes immediately before GNSS_MODEL_GENERIC_NMEA: the
sentinel has to stay last because isValidGnssModel() uses it as the exclusive
upper bound on values the probe cache may hold. Placing the new model after
it would leave LC760CA permanently uncacheable.

Co-Authored-By: Quency-D <55523105+Quency-D@users.noreply.github.com>

* fix(graphics): re-init the NV3001B after the panel rail comes back

DISPLAYOFF de-asserts VTFT_CTRL, which cuts power to the panel, so the
controller loses MADCTL, COLMOD and gamma. displayOn() only sends sleep-out
and cannot restore them, leaving the panel dark or in the wrong format after
wake. Re-run begin() once the rail has settled, and repaint in full since the
re-init leaves display RAM undefined.

Also stop the TCA6408 rotary polling from two threads at once. Registering as
an InputPollable meant InputBroker's pollSoon task could call pollOnce() while
runOnce() was mid-transfer on the main thread, with nothing serialising Wire
or the decoder state. Drop InputPollable and have the interrupt wake the
thread instead, the way ButtonThread does, so the bus and the decode stay on
one thread.

* fix(graphics): skip the NV3001B wake when re-init fails

begin() reports whether the bus came up. Ignoring it meant a failed re-init
still lit the backlight and drove a full-screen repaint at a panel that was
never initialised.

* chore(boards): ship the Heltec RC boards at release level

release is the normal level for a variant; the matrix generator still builds
each of these in this PR because they add a new platformio.ini.

---------

Co-authored-by: Quency-D <55523105+Quency-D@users.noreply.github.com>
2026-08-23 11:00:37 +00:00
zelo533andBen Meadors 05f6474108 meshnology-w10: define HAS_SPI_TFT so the TFT screen initializes again (#11042)
#10803 refactored main.cpp to key SPI-TFT Screen creation on HAS_SPI_TFT
instead of the per-controller define list. The W10 variant (#10911) was
written before that refactor and crossed it mid-air, so it never defines
HAS_SPI_TFT and develop builds fall through to the I2C-OLED autodetect
branch: no Screen is ever constructed and the display stays dark, while
everything else (radio, GPS, BLE) works.

Verified on a real W10: with the define, the boot log shows TFTDisplay
creation, backlight power-on and the boot screen, and the ST7789 panel
renders the UI again.

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2026-08-22 19:06:33 -05:00
Ben Meadors f6f116a39d Fill in device registry metadata for recently added hardware (#11567)
Audit of the custom_meshtastic_* manifest on the variants backing the
newest boards, against the protobuf HardwareModel enum, the compiled
HW_VENDOR, the board flash size and the artwork actually published by
the web flasher. No support flag changes here - actively_supported is
left exactly as each variant already had it.

ThinkNode M9 had no HW_VENDOR arm, so every M9 has been reporting
PRIVATE_HW while its manifest advertised 131; add the mapping and
rename the slug to the enum name (THINKNODE_M9) it is meant to mirror.

Seeed SenseCAP Mesh-Tracker X1 moves from the PR matrix to release, and
its images entry now points at seeed_mesh_tracker_x1.svg, which is what
the flasher actually ships - the hyphenated name resolved to nothing.

T-Beam BPF, T-Beam 1W and Heltec Wireless Tracker V2 declared the
architecture as "esp32s3"; the value is copied verbatim into the
manifest, and the flash flow matches on the normalized "esp32-s3".

T-Beam BPF and M5Stack Unit C6L both build default_16MB.csv on 16 MB
flash but declared no partition scheme, which leaves the flasher on the
4 MB fallback offsets for a legacy clean install.

Meshnology W10 and W12 gain the artwork and vendor tag that already
exist for them.
2026-08-22 14:34:49 +00:00
vidplace7 f22ce82f5a fix t-deck-pro: disable BHI260AP support until SensorLib replacement is available
Missed in the previous commit
2026-08-21 12:42:16 -04:00
Austin 5f7077c44e fix t-deck-pro-v1.1: disable BHI260AP support until SensorLib replacement is available (#11562) 2026-08-21 11:25:39 -05:00
Austin 4d9d0f8a16 chore(deps): Correct library dependencies for T-Deck Pro and T-Watch Ultra (#11561) 2026-08-21 10:59:26 -05:00
renovate[bot] 4c640270f2 chore(deps): update lovyangfx to v1.2.27 (#11533)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-21 13:46:14 +00:00
renovate[bot] 1afcdabbe9 chore(deps): update esp8266audio digest to 3430246 (#11557)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-21 10:47:19 +00:00
0b906b4d15 T-Watch Ultra support (#8171)
* feat: T-Watch Ultra support

* fix init touch controller

* add framebuffer

* update to device-ui

* trunk fmt

* update amoled driver reference

* PMU cosmetics

* power off lora

* fix NodeDB defaults

* trySetRTC when fixedPosition

* haptic touch (only BaseUI)

* init lora RF switch

* update LovyanGFX 1.2.19

* earlyInitVariant() adaptations acc. #9438

* update device-ui / touch handling

* Set NFC_CS disabled on boot

* Get t-watch-ultra working better on BaseUI

* Fix compilation

* Fix flash reads on t-watch-ultra

* Get baseui drawing to the screen correctly again on t-watch and add touch IRQ handling

* Add PMU IRQ handling

* Add IMU support

* Change define to avoid collision

* BaseUI changes to support t-watch-s3 rounded screen (#10786)

* BaseUI changes to support t-watch-s3 rounded screen

* Extend margin work to CannedMessages

* Finish merge

* Get audio working on watch-ultra

* trunk fmt

* added custom_meshtastic boilerplate

* T-Echo-Plus: disable BHI260AP while assumingly not implemented

* Drop the duplicate origBold declaration from the merge

* Inset incoming message bubbles on rounded screens

* Fix RTTTL tempo, WiFi screen margins, PMU guard and a duplicate define

* fix compile errror (the 2nd time)

* fix SDcard

* fix/workaround CO5300 pixel flush to SPI

* trunk fmt

---------

Co-authored-by: Jonathan Bennett <jbennett@incomsystems.biz>
Co-authored-by: Thomas Göttgens <tgoettgens@gmail.com>
2026-08-20 12:28:57 +00:00
Thomas Göttgens 80f8611e65 feat(variants): add Seeed Wio Tracker L1 Pro 1W (#11542)
* fix(sx126x): allow boards to opt out of the PA optimization table

Boards driving an external PA can define SX126X_NO_POWER_OPTIMIZATION_TABLE
to use the fixed PA config instead of RadioLib's table, which is tuned for a
bare SX126x.

Default behaviour is unchanged. init() applies the fixed config after begin(),
which programs power through the table.

* feat(variants): add Seeed Wio Tracker L1 Pro 1W

nRF52840 + SX1262 with a 1 W external PA, L76K GNSS, SH1106 OLED.

Uses hw_model 144 (meshtastic/protobufs#1038), opts into
SX126X_NO_POWER_OPTIMIZATION_TABLE and declares SX126X_MAX_POWER explicitly.
The PA gain table is indexed by SX1262 output power in dBm.

Requires protobufs#1038 and a protobuf regen before it builds.

* chore(deps): bump RadioLib to 510e00cf

Carries the current LR11x0 and LR2021 fixes.

* fix(variants): correct L1 Pro 1W QSPI pins and clean up comments

PIN_QSPI_* are logical pin indices. The QSPI flash sits at D19-D24 in
variant.cpp, but the defines carried D21-D26 from seeed_solar_node, where
that block does start at D21. D25 and D26 are trackball pins.

Also replaces mis-encoded characters in the pin comments and drops the
migration note, which referenced a private repo path and a stale PINS_COUNT.

* fix(variants): move L1 Pro 1W out of the per-PR build matrix

board_level = pr is the high-attention tier that builds on every PR. This
board belongs with the mainline set, which uses board_level = release.
2026-08-19 17:05:34 +00:00
Ben Meadors 9fcb289643 fix(thinknode_m9): define SPI_FREQUENCY for the non-MUI build (#11546)
The M9's variant.h defines ST7789_CS, so TFTDisplay.cpp compiles its
ST7789 LGFX branch, which reads SPI_FREQUENCY for the panel write clock
(SPI_READ_FREQUENCY, its pair, is already in variant.h). The flag was only
set in the -tft env, so `build (thinknode_m9, esp32s3)` has failed on
develop since the board landed in #10908:

  src/graphics/TFTDisplay.cpp:504:30: error: 'SPI_FREQUENCY' was not
  declared in this scope; did you mean 'SD_SPI_FREQUENCY'?

Move the flag up into thinknode_m9_base, keeping the 75 MHz the -tft env
already used for the same panel and matching the SD card's 75 MHz on the
bus they share. The -tft env inherits the base flags, so device-ui's
LGFX_GENERIC.h - which falls back to 20 MHz when the macro is absent -
still sees the identical value.
2026-08-18 20:28:01 -05:00
c308d0aca4 feat: Support Elecrow ThinkNode M9 (#10908)
* thinknode-m9 variant

* move lora to SPI1 device

* enable SDcard

* use HSPI

* BaseUI tft -> HSPI

* buzzer, webdav lib

* fix build issues

* M9 default to MUI, no BT, short ringtone

* add keyboard long-press config

* update variant

* add ThingNode-M9 GPS string

* GPS 115200 baud

* Basic BaseUI support

* Fixup power detection

* Compass and KB fixes for M9

* add timed Lock::lock()

* add SD card

* point device-ui to thinknode m9 draft branch

* trunk fmt

* fix FusionCompass

* Fix t-deck-tft linker arg list overflow in CI

* SDcard/lora fix: SPI1 must not be declared twice in arduino 3.x -> reuse SPI1 defined in FSCommon.cpp

* update battery parameters

* reinit SD card when updating; fix PSRAM size

* update lib versions

* fix wakeup on key press (KB_INT)

* fix default nag_timeout for TFT/MUI devices with buzzer

* increase PSRAM and SD freq

* trunk fmt

* update lovyanGFX 1.2.26

* update device-ui commit reference

* fix screen definition

* remove DONE; maybe a keyword or other used identifier

* fixed CI error nag_timeout

* fix prepareSleep initialization

* trunk fmt

* reduce SD SPI frequency

* update device-ui

* fix SDcard issue

* stage

* fix device-ui commit reference

* fix device-ui commit

* update device-ui commit (fixed keyboard lag)

* fix QMI8658

* trunk fmt

* update .ini meta information, align SD freq

* fix device-ui reference to target (ready to merge)

* device-ui for all other targets

* make the rabbit happy

* trunk fmt

* fixed lock screen

* fix compile error

* SPI lock timeout

* apply device-ui fix

* revert bad RadioLib commit hash in platformio.ini

Co-authored-by: mverch67 <71137295+mverch67@users.noreply.github.com>

* fix wrong commit hash change

* fix fix commit fix

* I love changing random numbers in random files

---------

Co-authored-by: Jonathan Bennett <jbennett@incomsystems.biz>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-08-17 17:28:33 +00:00
Tom bca7c0b480 Tom fiddles with the test suite - again (#11517)
* test: make every suite run its own binary, and fail the run when it does not

PlatformIO links every native test program to the one $BUILD_DIR/$PROGNAME path and
attributes Unity output by text alone, never checking that the source file a case came
from belongs to the suite it thinks it ran. Both harnesses had been split into a build
pass (--without-testing) and a run pass (--without-building), and for a non-embedded
platform the run pass never relinks - so all 57 suites executed whichever suite was
linked last, each reporting PASSED under its own name. Introduced for CI in 4906f8a6
and for bin/run-tests.sh in de6b2319; both ran fused, and correctly, before that.

Drop --without-building from both run passes. The --without-testing pass stays as a
warm-up so no single suite absorbs the whole src compile in its reported duration; with
the objects already cached the per-suite step is one test_main.cpp plus a link.

Add bin/check-test-attribution.py, which grades the JUnit reports both harnesses already
produce. It fails on a test case whose source file lies outside the suite that reported
it, and on a suite that was asked to run and produced no cases at all. Wired in three
places: bin/run-tests.sh as a RED verdict ahead of the softer ones, per area in CI so a
mismatch names its area, and once over the merged report so an area that never executed
cannot hide. Suite ownership is matched on whole path segments, so test_mesh does not
claim test_mesh_module, and the -f pattern is resolved against the canonical set rather
than taken as a literal suite name.

* fix(test): pin simradio off for the packet-signing PKI cases

[env:coverage] passes -s to the test binary (74e6723ad, #8251), which sets
portduino_config.force_simradio. wouldEncryptWithPKC() lists !force_simradio among its
preconditions, so perhapsEncode() takes the channel-crypto branch, returns NONE and leaves
pki_encrypted false - failing test_B11_normal_unicast_still_uses_pki and
test_B12_licensed_receiver_does_not_decrypt_pki, both of which assert the production PKI
path. [env:native] passes no such flag, which is the whole of the long-standing
"passes under native, fails under coverage" split; it was never gcov, ASan or a host.

Save and clear the flag in setUp, restore it in tearDown, so the suite asserts the encode
path it is named for under either env's invocation. Same binary, pristine $HOME: 77 tests
0 failures with -s and without, where before -s gave 2 failures.

Whether the unit-test binary should run with -s at all is a separate question - it means CI
exercises the simradio configuration for every suite - and is left alone here.

* fix(router): drive the admin-key fallback budget from the injectable clock

The budget is 8 tokens refilling one per 250ms of wall clock, and
test_admin_key_fallback_is_rate_limited drains it with eight PKI decodes before asserting the
ninth is refused. That gives the drain loop 31ms per iteration, each of which generates a
keypair and does three X25519 operations under gcov and ASan. This box runs them in ~4ms;
a GitHub runner takes ~38ms, so a token refills mid-drain and the packet the test expects to
be blocked decodes. Measured from both runs' own log timestamps, 9.5x apart.

Read the bucket through Time::getMillis() instead of millis(), and have the test set and
advance the virtual clock rather than sleeping. The subtraction was already wrap-correct, so
the deadline guard is unaffected. Restores the clock in tearDown so the rest of the suite is
untouched, and drops ~3s of real sleeping from the run.

* test: declare the event-channel suites' shared state

Both construct a NodeDB, whose constructor persists a default set into an empty prefs
directory, so each writes the five prefs protos. Neither was declared, because until suites
started running their own binaries nothing had ever observed them writing anything.

* test: add a repeat runner for order-independent flakes

A single green run says nothing about a real-time race or a slow-host margin: the rate-limit
budget above passes here with 7x headroom and still fails on a CI runner. Run one suite N
times against a fresh scratch $HOME each time, optionally against CPU contention, and print a
flake rate. Failing runs keep their log and their sandbox; passing runs leave nothing.

Simradio is taken from the env's own test_testing_command, so a stress run reproduces the
real invocation rather than inventing a third one.

* fix(test): keep a native test run off the host's radio

bin/pio-test-isolate.sh sandboxes $HOME, but portduinoSetup() looks for config in
./config.yaml and /etc/meshtasticd/config.yaml - the second absolute, so no $HOME sandbox
can hide it. On a machine running meshtasticd that config selects the real LoRa module and
the run continues into GPIO and SPI setup, so ./bin/run-tests.sh -e native would drive the
developer's own radio without saying so. -e native is also the faster of the two, and the
one reached for when iterating.

[env:coverage] already passes -s, which short-circuits ahead of the config search and returns
before hardware init. Pass it for [env:native] too. That closes the hazard and, incidentally,
makes the two envs invoke the binary identically - they did not, which is the whole of the
long-standing "green locally, red in CI" split.

* test: run every suite with PKC on, and assert it stays that way

force_simradio does two unrelated jobs. It keeps portduinoSetup() off the host's hardware,
which every test run wants, and it makes wouldEncryptWithPKC() return false, which no test
run wants: the encode path under test then falls back to channel crypto and any case
asserting PKI fails, or worse, passes while asserting the wrong thing.

Three suites had each worked this out separately and cleared the flag themselves -
test_admin_session_repro's comment describes the mechanism exactly. Clear it once in
initializeTestEnvironment() instead. By then portduinoSetup() has already skipped the config
search and chosen the simulated radio, and it never reconsults the flag, so clearing it
cannot bring hardware back; the only remaining readers are the PKC gate and an
exit_simulator intercept no test can reach. The per-suite copy added to test_packet_signing
for B11/B12 goes away with it.

Two asserts, because both invariants were true only by inspection:

- No listening sockets. main.cpp's setup()/loop() are compiled out under PIO_UNIT_TESTING, so
  the phone API, MQTT and the web server never start - but nothing checked. A suite that
  pulled in a service binding a port would open one on the developer's machine for the length
  of the run.
- force_simradio still clear, before every test rather than once per suite, since a case that
  restores a struct it snapshotted earlier puts it back and silently disables PKC for
  everything after it. Named per test, so the report points at the case after the culprit.

Both exit rather than TEST_FAIL: they run outside a Unity test frame, and silently repairing
either one would leave the suite that broke it passing. Verified by disabling the clear and
watching the guard fire on the first case instead of reporting two quiet failures.

* test: let the repeat runner vary suite order too

Repeating one binary finds races and slow-host margins; it cannot find state that leaks from
one suite into the next, because only one suite runs. --shuffle drives run-tests.sh --seed
with a fresh seed each iteration and reports which seeds went red, so the shuffle already in
the harness yields a flake rate rather than a single sample. Seeds are printed and replayable.

* fix(test): baseline the environment from whichever runs first

Clearing force_simradio in initializeTestEnvironment() missed the suites that never call it.
test_atak is one, and it also pulls in TestUtil.h, so it got the per-test assert without ever
getting the baseline and aborted on its first case - caught by CI, which is what the assert is
for. test_geocoord_distance, test_meshpacket_serializer and test_utf8 skip the init too, but
include no TestUtil.h at all, so nothing reached them either way.

Move the clear and the socket check into baselineEnvironment(), called from
initializeTestEnvironment() or from the first RUN_TEST, whichever comes first. Suites that
initialise are still asserted from their first case; the rest are baselined at case one and
asserted from case two.

Print the violation on stdout as well as stderr: bin/run-tests.sh filters the program's
stderr, so locally the message vanished and the run reported "exit-time abort (likely
sanitizer)" - the exit code read as a signal number again, with no sign of the real reason.

* test: drop the per-suite simradio exceptions

Three suites had each found that force_simradio disables PKC and cleared it themselves.
initializeTestEnvironment() now clears it once for every suite, so all six sites are dead
code - along with the PortduinoGlue.h include each pulled in for it.

test_event_channel_router's is the one worth removing rather than leaving: it snapshotted the
flag into SavedGlobals and restored it at teardown, which is exactly the shape the per-test
assert exists to catch. Harmless while the snapshot reads false, and a silent PKC-off for
every later case if that ever changed.

The three suites pass unchanged: 54 cases, attribution clean.

* test: tell a deliberate harness abort from a sanitizer fault

A guard in TestUtil.cpp that aborts on purpose - a listening socket, or force_simradio put
back - exits non-zero with no sanitizer report, so it fell through to the exit-time-abort
heuristic and was announced as "RED exit-time abort (tests passed; likely sanitizer)". That
is the same trap as the phantom SIGILL two checks above: a verdict line naming a cause it has
not established, sending the reader after a memory bug that does not exist. It cost hours in
the original investigation and it cost the first read of a test_atak failure today.

Match the FATAL line the guards print on stdout for exactly this purpose, and report the
reason they gave instead of guessing.

* test: say why three suites omit TestUtil.h

They are pure-function - no NodeDB, no router, no sockets, no PKC - so the harness-wide guards
in TestUtil.h would assert conditions they cannot reach, and initializeTestEnvironment()'s RTC
and OSThread setup would pull in portduino globals they otherwise never touch. Suite-level
state cleanliness still applies: bin/pio-test-isolate.sh fingerprints the sandbox from outside
and wraps every suite regardless.

Recorded at the top of each so the omission reads as a decision rather than an oversight - it
looked like the latter when the socket and simradio asserts landed.

* test(traffic): give every case a primary channel

resetTrafficConfig() zeroed channelFile and left channels_count at 0, so the 66 cases that do
not install a channel themselves ran against a device with none. Every router lookup then hit
Channels::getByIndex()'s out-of-range branch and logged, which is 12106 of the suite's 20088
ERROR lines and tests nothing - a real device always has a primary channel, and no case here
asserts channels-unset behaviour.

Install the well-known primary the suite already builds for its precision cases. All 85 pass
unchanged, and the suite's ERROR output drops to 7985, the remainder being decode failures
from test_tm_fuzz_nodenum_blitz's malformed payloads.

* test: budget each suite's LOG_ERROR output

A suite can pass while emitting six figures of ERROR, which buries a real failure and trains
everyone to skim. Count them per suite and grade the count as a second axis, alongside the
CLEAN/DIRTY verdict already computed from the same captured log.

Declared in the same manifest, as a RANGE rather than a ceiling, because for a fuzz suite the
floor is the half that matters: test_fuzz_decode logging ~100k rejections is the suite
working, and the same suite logging none means it stopped feeding malformed input while every
case still passes. Bounds are wide on purpose - they catch a path that has stopped running,
not a drift of a few hundred lines. Undeclared suites get 100, which 50 of 57 already meet.

AMBER, not RED. Three log sites - mesh-pb-constants.cpp:28, Channels.cpp:356, MQTT.cpp:92 -
account for nearly all the remaining volume, and landing this red before they are demoted
would buy exemptions rather than fixes.

* test: canary the attribution check, and run the state self-test in CI

check-test-attribution.py guards against the false green, and nothing guarded the guard. A
checker that has quietly stopped matching looks exactly like a codebase with no problem, which
is how the original went unnoticed for three weeks of green runs.

The canary reproduces the failure deliberately - two suites run with --without-building, so
PlatformIO does not relink and both execute the same leftover binary - and requires the
checker to catch it. It also fails if the reproduction stops reproducing: if PlatformIO ever
relinks per suite under that flag, the reason both harnesses stopped passing it no longer
holds, and the harness should be revisited rather than left on a stale assumption.

bin/test-state-check.sh already existed with fixtures asserting CLEAN/CLEAN/DIRTY/MISSING and
had never run in CI. Wire it in too - the shared-state checker had the same blind spot, and
somebody had already written the test for it.

* fix(ci): run the attribution canary where it cannot clobber the daemon

The canary relinks $BUILD_DIR/$PROGNAME, and in simulator-tests that replaced the daemon
binary with a test suite. The integration test then started it and waited for a listening
socket, which a test binary never opens - by assertion, since initializeTestEnvironment()
now fails a suite that holds one - so the step sat until its 20s timeout and the job exited
124. The canary itself had already passed.

Move it to platformio-tests, where the binary is per-suite already and nothing downstream
needs the daemon, and place it after the coverage capture so its extra runs stay out of the
numbers. The shared-state self-test stays in simulator-tests; it touches no binary.

Fitting failure mode for this branch: one shared program path, two consumers, and the second
one silently getting the first one's build.

* fix(ci): silence the XXE rule on the attribution checker

semgrep blocks xml.etree.ElementTree.parse as XXE-prone. The input here is the JUnit report
PlatformIO wrote moments earlier in the same run, and anything able to plant a hostile report
is already executing its own code in that job, so parsing it defused changes nothing it could
do. defusedxml is in the tree but only under bin/bump_metainfo with its own requirements, and
pulling it onto this path would add an install step to every native test job for no reachable
threat.

Suppressed with a reason at the call site, the same shape as the subprocess-shell-true
suppression in extra_scripts/nrf54l15_linker.py.

* fix(test): address the review findings on the harness guards

Two were real defects rather than style:

- state_count_errors() returned "0\n0" for a log with no ERROR lines, because grep -c prints 0
  and *then* exits 1, so the `|| printf 0` fallback appended a second one. The classifier threw
  a syntax error on it. Dormant only because every suite currently emits at least one ERROR
  line; the planned log-level demotions would have driven most suites to zero and tripped it
  everywhere, looking like the demotions broke the harness.
- check-test-attribution.py returned OK for a report whose cases carry no `file` attribute. It
  cannot prove ownership in that state, so a changed JUnit format would have restored the exact
  false green it exists to catch. Now its own finding, listed and fatal.

The rest: keep the sandbox when an error budget is breached, since that is the one outcome
whose evidence was being deleted; reject a missing or non-numeric option value in
stress-suite.sh instead of running an empty loop and reporting 0/0 as a pass; exit on INT/TERM
rather than cleaning up and carrying on; drive repetitions through pio-test-isolate.sh so a
stress run exercises the real invocation; require the canary to see MISATTRIBUTED rather than
any non-zero exit, so an unreadable report cannot read as a caught mismatch; and check for
listening sockets before every test, since a listener would be opened by the code under test.

resetAdminKeyFallbackBudget() is a new PIO_UNIT_TESTING hook, shaped like the neighbouring
resetRoutingAuthEvaluationCount(). The refill stamp is only meaningful against the clock that
produced it, so a suite switching timebases leaves a stamp from the other one and the next
unsigned subtraction reads as a near-infinite gap - silently refilling the bucket.

Also move the semgrep marker onto its own line: buried mid-sentence in a comment it was
ignored, and the XXE finding stayed blocking.
2026-08-16 11:34:02 +00:00
Thomas Göttgens 34680833b8 fix(test): make the native-windows test suite build and run (#11482)
* fix(test): make the native-windows test suite build and run

pio test -e native-windows failed every suite at the build stage. Five
independent causes, all Windows-only:

- TestUtil.cpp called lstat(), which MinGW-w64 does not provide. The
  state-checkpoint walk added in #11322 is fenced with ARCH_PORTDUINO,
  which native-windows also satisfies, so all 53 suites failed to
  compile. Route it through a stat() shim on _WIN32.

- test_default, test_http_content_handler, test_meshpacket_serializer
  and test_serial define no setUp/tearDown and relied on the weak
  defaults PlatformIO emits in unity_config.c. GCC lowers a weak
  definition on PE-COFF to a weak external, leaving the symbol
  undefined, so it does not satisfy unity.c's reference and the link
  fails. Define them explicitly, as the other 49 suites already do.

- test_mqtt included <arpa/inet.h>, absent on MinGW, for htonl(). Use
  winsock2.h there.

- test_gps_update_scheduling uses TEST_ASSERT_DOUBLE_WITHIN. Unity
  omits double support unless UNITY_INCLUDE_DOUBLE is defined, so the
  assertion compiled to an unconditional failure. Define it for the
  env.

- test_getfiles_rejects_overlong_path is excluded on _WIN32. Overrunning
  the 228-byte file_name needs at least 229 bytes below the portduino
  root, and that root is already ~34 bytes, so every qualifying path
  passes the 260-byte MAX_PATH: the nested mkdir() fails, the file is
  never created, and getFiles() has nothing to drop. No component
  layout satisfies both limits.

Each of the seven suites that failed on Windows was verified
individually after the change. test_fscommon_getfiles still fails in a
full run, for a cause outside this change: rmDir() does not remove
directories on Windows, so empty dirs left by an earlier run survive
setUp() and make getFiles() report a depth truncation. That is a
pre-existing FSCommon bug, reported separately.

No Linux or macOS behaviour changes: every guard is _WIN32-only except
UNITY_INCLUDE_DOUBLE, which is scoped to env:native-windows.

* fix(test): define UNITY_INCLUDE_DOUBLE for every native env

The flag was scoped to env:native-windows, but the gap is not
Windows-specific. Verified on Debian with gcc against the Linux env's
own Unity 2.6.1 and PlatformIO's generated native unity_config:

  UNITY_INCLUDE_DOUBLE : NOT defined
  UNITY_EXCLUDE_DOUBLE : defined
  test_double_within:FAIL: Unity Double Precision Disabled

UNITY_INCLUDE_DOUBLE appears nowhere in the repo, the ini files, the
workflow, or PlatformIO's unity runner, which adds only
UNITY_INCLUDE_CONFIG_H. So TEST_ASSERT_DOUBLE_* is an always-failing
stub on Linux and macOS too, not only on Windows.

Moved to portduino_base.build_flags_common, which every native env
resolves: native, native-tft, native-fb, native-tft-debug, coverage,
coverage-event-policy, native-macos, native-windows and native-wasm.

This does change Linux and macOS: TEST_ASSERT_DOUBLE_* becomes a real
comparison instead of a stub. test_gps_update_scheduling is the only
suite using those macros and its arithmetic is integer-based and
bit-identical across platforms, so it should pass wherever it runs.
Note it currently reports PASSED on CI in 0.03s while emitting no Unity
output at all, so those assertions appear never to execute there; that
is tracked separately and is not addressed here.
2026-08-14 00:51:40 +00:00
Austin 30e6e0ec8c Do not build nucleo_wl55jc upon PR (#11489)
Honestly don't build it at all, it's a devkit.
2026-08-13 13:21:53 -04:00
Ben Meadors b565a07a83 Remove proprietary Bosch BSEC blob; open in-tree IAQ estimator for BME680 (#11381)
* Remove proprietary Bosch BSEC blob; open in-tree IAQ estimator for BME680

BSEC2 cost ~37-39 KB flash and ~4-5 KB static RAM on ~190 of ~240 build
targets, linked whether or not a BME680 was attached, and was a no-source
proprietary archive inside GPLv3 release binaries. The firmware consumed
exactly one BSEC-exclusive output: the IAQ value.

- New BME680IaqEstimator: clean-room log-domain baseline tracker
  (humidity-compensated gas resistance vs a rise-fast/decay-slow ceiling,
  0-500 scale matching the existing UI bands), pure math, unit-tested on
  native (test_bme680_iaq, 15 tests incl. a deep-sleep reboot simulation).
  Warm-up/burn-in progress persists to /prefs/bme680.dat via SafeFile so
  one-sample-per-wake SENSOR nodes converge across reboots; stale
  /prefs/bsec.dat is removed once.
- BME680Sensor: single-path rewrite on Adafruit_BME680 with async
  once-per-minute sampling (~20x lower heater duty than BSEC LP mode),
  a hard 2-minute publish-freshness bound (a dead sensor stops reporting
  instead of freezing its last reading on the wire), and suppression of
  bogus gas_resistance=0 points from heater-unstable cycles.
- platformio.ini: environmental_extra_common/_extra/_no_bsec collapsed
  into one section; Bosch BSEC2 + BME68x deps deleted; per-variant BSEC
  link-path hacks and the TEMPORARY promicro lib_ignore removed.
  nrf52_promicro_diy_tcxo regains BME680 support at 36 KB clear of the
  warm-store cap; rak4631 lands at 75 KB clear.
- EnvironmentTelemetry: iaq rendering gates on has_iaq (a genuine IAQ of
  0 now displays); stale BSEC comments rewritten.
- rak4631 size budgets tightened (113000->108000 RAM, 786000->746000
  flash) to lock in the reclaimed headroom.
- bin/bme680_iaq_replay.cpp: host-side replay harness for tuning the
  estimator against captured BSEC traces (mean abs error + band
  agreement), no reflashing needed.

Measured (develop -> this branch): rak4631 -38.8 KB flash / -4.9 KB RAM;
heltec-v3 -36.4 KB / -4.0 KB; tlora-v2-1-1_6 +1.3 KB (its IAQ
approximation had been dead code since #9663 due to an inverted isfinite
check and now actually runs).

Note: gas_resistance stays kOhm on the wire for fleet compatibility; the
proto comment claiming MOhm gets a separate meshtastic/protobufs docs PR.

* Address CodeRabbit review feedback

- Use Throttle::isWithinTimespanMs for all elapsed-time predicates in
  BME680Sensor per coding guidelines (deadline math for the async reading
  completion stays raw, as it targets an absolute timestamp)
- Make the state file name members static constexpr
- Replay tool: cast uint16_t before %u (default argument promotion), report
  malformed input lines instead of silently skipping, and fail non-zero on
  stream read errors

* Address CodeRabbit nitpicks

- Replace the local clampf helper with std::clamp (meshUtils.h's clamp drags
  in Arduino.h, which would break the estimator's standalone host build that
  the replay harness depends on)
- Trim the replay tool's file header to a two-line summary; the full build,
  capture, and tuning workflow moves to docs/bme680_iaq_replay.md
2026-08-13 13:21:16 -04:00
Andrew Yong 210014e81f feat(stm32wl): add ST Nucleo-WL55JC variant (#11384)
STM32 Nucleo-64 development board with STM32WL55JC MCU, SMPS,
supports Arduino and ST morpho connectivity.

https://www.st.com/en/evaluation-tools/nucleo-wl55jc.html

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

Signed-off-by: Andrew Yong <me@ndoo.sg>
2026-08-13 11:06:37 +00:00
Andrew YongandJonathan Bennett b4c2eb0b78 refactor(led): generalize LED_LORA init from ThinkNode-M7 (#11437)
Migrate LED_LORA init to match existing LED init patterns.

Signed-off-by: Andrew Yong <me@ndoo.sg>
Co-authored-by: Jonathan Bennett <jbennett@incomsystems.biz>
2026-08-13 08:31:39 +02:00
renovate[bot] b68de08c6b chore(deps): update esp32-ch390 to v1.1.1 (#11398)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-12 06:34:44 +00:00
Jonathan BennettandClaude Fable 5 5baad2e2a8 logging: compile out LOG_TRACE by default, demote chatty DEBUG lines, drop redundant logs (#11391)
* logging: gate LOG_TRACE behind MESHTASTIC_TRACE_LOGGING, drop redundant reclock logs

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

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

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

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

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

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

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

* logging: address CodeRabbit review on trace-gate PR

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

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

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

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

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

* logging: clang-format rewrap after specifier widening

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

* Even fewer bytes!

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

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

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

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

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

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

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


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

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

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

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

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-08-12 00:05:51 +00:00
Carlos ValdesandJonathan Bennett 204f88ddfe fix(nrf54l15): restore the nrf54l15dk build (#11410)
* fix(nrf54l15): restore the nrf54l15dk build

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

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

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

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

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

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

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

---------

Co-authored-by: Jonathan Bennett <jbennett@incomsystems.biz>
2026-08-11 20:15:05 +00:00
546b9d9e40 Block coordinate traffic on configured event channels (#11045)
* Block coordinate traffic on configured event channels

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

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

* Suppress event coordinates in reliable relay paths

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

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

* Reject blocked phone coordinates before rate limiting

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

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

* Prevent event coordinates from reaching MQTT

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

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

* Add event coordinate policy preference

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

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

* Test event coordinate policy in native CI

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

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

* Make event policy test tolerate a full NodeDB

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

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

* Test Router event coordinate enforcement

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

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

* Test PhoneAPI event coordinate retry handling

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

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

* Test reliable event coordinate suppression

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

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

* Test MQTT event coordinate suppression

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

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

* Run event policy behavioral suites in native CI

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

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

* tests: address CodeRabbit review feedback

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

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

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

---------

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 12:34:08 +00:00