* fix(fs): bound getFiles() result size and harden traversal
The recursive filesystem traversal in getFiles() could grow its result
vector without limit, contributing to heap pressure on memory-constrained
devices during client config starts. The implementation also relied on
unbounded strcpy and per-entry String() allocations for dot-suffix
filtering, and leaked the directory handle when the opened path was not
a directory.
Rework the traversal to address all of those at once:
- Add optional maxCount (default 64) and wasLimited out-param. The walk
stops once maxCount entries have been collected and wasLimited is set,
so callers can distinguish a complete listing from a truncated one.
Default arguments preserve backward compatibility for existing callers.
- Extract the recursive walk into a collectFiles() helper in an
anonymous namespace, accumulating into a single caller-owned vector
instead of building and inserting temporary vectors at every recursion
level.
- Replace strcpy with strlcpy so file_name writes are bounded by the
destination buffer.
- Replace String(name).endsWith(".") with a stack-based
pathEndsWithDot() helper to avoid a heap allocation per entry.
- Close the directory handle when the opened path is not a directory,
and close the file in both branches of recursion instead of only the
leaf branch (the previous code leaked the handle when depth was
exhausted).
- Compile-gate the std::bad_alloc reserve probe-down behind
__cpp_exceptions / __EXCEPTIONS so the fallback only compiles where
bad_alloc is actually catchable. No-exception targets (nRF52, RP2040,
STM32, ESP32-C6) skip the probe and rely on the bounded maxCount
alone, which is safe because the default cap is small enough to fit
comfortably in available RAM on every supported platform.
* fix(phone-api): skip manifest scan for node-info-only config requests
A client can request a node-info-only config response by sending
want_config_id with the SPECIAL_NONCE_ONLY_NODES nonce. The manifest
walk in handleStartConfig() ran unconditionally even though
STATE_SEND_FILEMANIFEST short-circuits past the manifest on that path,
so the SPI lock and recursive filesystem traversal were wasted work.
On memory-constrained devices this extra heap pressure was enough to
trigger allocation aborts and spontaneous reboots before the node-info
response could complete.
Gate the manifest call behind config_nonce != SPECIAL_NONCE_ONLY_NODES
so the node-info-only path no longer touches the filesystem at all, and
release any stale manifest storage left over from a previous run.
For normal full-config requests, also tighten the scan limits from
depth 10 / unbounded to depth 3 / max 64 entries via the new getFiles()
parameters. A 10-level recursive listing was never useful for a client
file picker and amplified heap pressure for no benefit. Log a warning
when the listing was truncated so operators can distinguish a bounded
result from a genuinely empty filesystem.
A few small cleanups fall out of the above:
- Replace raw spiLock->lock()/unlock() with a LockGuard so the lock is
released even if getFiles() throws on exception-enabled targets.
- Use a releaseFilesManifest() helper (swap-with-empty idiom) in
close() and the STATE_SEND_FILEMANIFEST drain path instead of the
previous clear()+shrink_to_fit() pair. Equivalent behavior, but the
swap idiom is the canonical vector-freeing pattern and centralizing
it avoids divergence between call sites.
- Use %zu for the size_t log arguments.
* fix(fs): harden bounded directory traversal
Close each child directory entry before recursive descent so bounded manifest scans do not stack file handles on constrained filesystems.
Treat null or overlong paths as truncated listings, handle reserve length failures like allocation failures, and keep manifest constants aligned with repository naming.
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
---------
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* fix(esp32): skip RTC timer wake on user shutdown
Do not arm esp_sleep_enable_timer_wakeup when msecToWake is portMAX_DELAY (UI shutdown), matching nRF52 system_off semantics.
fix(rak_wismesh_tap_v2): Tag OCV curve and 16MB partition
Add OCV_ARRAY matching WisMesh Tag for accurate SOC. Use 16MB flash partition scheme for TAP V2 hardware.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Trunkt
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
* Wipe message Store on factory reset
* Check for destination 0 in a new message, and convert to broadcast
* Make sure CHARGE_LED_state gets turned off, to avoid stuck LEDs
* Take the spiLock in MessageStore when clearing messages
* Trunk
* Add thinknode M5 voltage curve
* Fix the oops
* Use standard GPS enable pin, for smarter power control on M3
* Enable GPS pin in variant.cpp initialization
---------
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
13302 and 13300 had missing module config for slot 2, also the rf switch and voltage info was missing.
Without that the auto setting in the config.yaml will try to use the default lora-hat-rak-6421-pi-hat.yaml and things do not work correctly.
* Initial plan
* Fix SensorLib isBitSet macro conflict with SparkFun MMC5983MA library
SensorLib 0.3.4 defines isBitSet as a C preprocessor macro in SensorLib.h,
which conflicts with SparkFun_MMC5983MA_IO.h's class method of the same name.
When both libraries are included in the same translation unit (e.g., via
configuration.h → SensorRtcHelper.hpp → SensorLib chain, alongside the
SparkFun MMC5983MA library in lib_deps), the macro expansion causes compile
errors like 'expected unqualified-id before const'.
Fix: undefine the isBitSet macro right after including SensorRtcHelper.hpp
in configuration.h, so it doesn't interfere with SparkFun's class method.
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
* Infinite calibration loop fix
* Save calibration
* Screen refresh
* reduce repeated code
* reduce repeated code to reduce flash
* fix Waypoint compass size and no fix no heading labels
* Don't show compass unless we have a heading and location
* If no calculated heading from moving, we should have no heading
* Slow walking calculated heading and auto stale heading when not moving
* Triming flash space
* cleanup
* show "?" when no location or heading for distance and heading screen
* cleanup
* Stale heading logic
* final trim
* Compass Calibration screen redesign
* Trunk Fix
* Compile fix
* patch
* Update src/motion/MotionSensor.cpp
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* Update WaypointModule.cpp
---------
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Adjust keyboard cell height calculation for better layout consistency across different screen sizes.
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
Current release tags are actually based upon the latest state of `develop` currently...
Specify target_commitish to always use the commit that triggered the build
* Enabled SX_LNA_EN by default
* Update I2C configuration for IO direction and pull settings
---------
Co-authored-by: Thomas Göttgens <tgoettgens@gmail.com>
* Fix position precision for direct sends
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* Clarify zero position precision logging
* Use const channel reference for position precision
* Use C linkage for position precision test entrypoints
---------
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Move WiFi.onEvent(WiFiEvent) registration before createSSLCert() to
prevent a race where the ESP32 auto-reconnects during cert generation
and fires GOT_IP before the handler is attached, causing
onNetworkConnected() to never run and the TCP/HTTP API services to
never initialize when booting without USB serial.
Also call onNetworkConnected() from reconnectWiFi() on all platforms
(not just RP2040) as a safety net; it is already guarded by
APStartupComplete so it only runs once.
Start reccomending the pioarduino VS Code extension instead of the PlatformIO extension.
pioarduino-based builds cannot complete correctly using the platformio extension. Normal platformio builds (nrf52, stm32) are unaffected//still work correctly.
Devs may need to delete their ~.platformio and .pio directories once after install in order to build properly.
* ThinkNode G3, ETH support WIP
* ThinkNode G3, ETH support WIP
* ThinkNode G3, ETH support WIP
* ThinkNode G3, ETH support WIP
* ThinkNode G3, ETH support WIP
* ThinkNode G3, ETH support WIP
* ThinkNode G3, ETH support WIP
* rename variant and add guard macros
* older G3 operational. M7 next.
* Split out G3 and M7 to different variants. Completely new PCB design. The G3 stays on 'PRIVATE_HW'
* Define button behaviour and use all of the device flash
---------
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: caveman99 <25002+caveman99@users.noreply.github.com>
Co-authored-by: Jonathan Bennett <jbennett@incomsystems.biz>
* Enhance GPS search failure handling backoff logic
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* Remove stray submodule gitlink for .claude worktree
A 160000 (gitlink) entry for .claude/worktrees/naughty-payne-60fdb7
pointing at f2923590bc was accidentally committed in 9db15780f. The
path isn't a real submodule — it's a Claude Code agent worktree that
shouldn't be tracked.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Refactor memory management in Syslog and StoreForwardModule
* Implement destructor for Lock
* Refactor RotaryEncoder and PacketHistory to use smart pointers for better memory management
* CH341 should use unique_ptr for improved memory management
* Fix checks in PH
* Improve Syslog::vlogf to handle variable argument lists more safely
* Fix initOk method to use nullptr for null pointer check
Use distro provided Python at build time (instead of the `python` images from dockerhub) and install `grpcio-tools` using the distro provided packages.
This should speed up build times, ESPECIALLY on riscv64 (where prebuilt `grpcio-tools` wheels are not provided on pip).
Co-authored-by: Copilot <copilot@github.com>
* Fix MAC_from_string to use input parameter instead of global config for MAC address parsing
* Enhance MAC_from_string validation and error handling
* Add missing include for <cctype> in PortduinoGlue.cpp
* Add TCP support for Meshtastic MCP interface / tests and update docs
* Address TCP endpoint validation and error handling in connection
* TCP connection handling and device listing logic
* Fix docstring formatting in normalize_tcp_endpoint function
Upstream support has been added in Debian and Alpine.
Only build as part of `docker_manifest` (Beta/Alpha/Daily) releases, because these will take a **while** thanks to qemu.
Co-authored-by: Copilot <copilot@github.com>
* Add clamping logic for milliseconds conversion and unit tests
* Simplify comments in secondsToMsClamped function
Removed detailed comments about seconds to milliseconds conversion.
* macOS: enable CH341 LoRa-hardware path — fix serial truncation, document setup
Verified on Apple Silicon with a CH341A USB-SPI bridge (VID 0x1A86,
PID 0x5512) wired to an SX1262 (Meshstick variant) that the existing
`pine64/libch341-spi-userspace` lib_dep works on macOS as-is — Apple's
bundled CH34x driver only matches the CH340 *UART* variant
(PID 0x7523), so the CH341A's interface 0 is left unclaimed and
libusb opens / configures / claims it directly via IOUSBHostInterface.
End-to-end test: meshtasticd boots, libusb claim succeeds, SX1262 init
returns 0, TCP API serves the meshtastic CLI's --info / --sendtext flow.
Two changes:
1. **`PortduinoGlue.cpp:497`**: pass `sizeof(serial)` (= 9) instead of
the literal `8` to `Ch341Hal::getSerialString()`. The function in
`USBHal.h:61-68` treats `len` as buffer size and reserves one slot
for the null terminator (`bytesCopied = (len - 1) < 8 ? (len - 1) : 8`),
so passing 8 produced a 7-char serial — which then broke the
`strlen(serial) == 8` check at line 502, skipping the auto-MAC
derivation from serial + product string. On Linux this was masked
by the BlueZ HCI MAC fallback in `getMacAddr()` at lines 139-157,
but on macOS that fallback is `__linux__`-guarded so the serial path
is mandatory and the truncation left `mac_address` empty, causing
the daemon to exit with `*** Blank MAC Address not allowed!`.
2. **`variants/native/portduino/platformio.ini`**: expand the
`[env:native-macos]` comment block with a "Real LoRa hardware on
macOS" section. Documents:
- Why no upstream library change is needed (Apple kext targets
CH340/UART, not CH341A/SPI; libusb's `#ifdef __linux__` skip is
correct for macOS in this case).
- How to point `meshtasticd` at an existing platform-agnostic
`bin/config.d/lora-*.yaml` for CH341 hardware.
- The auto-MAC-derivation contract (now working with this fix).
- `ioreg` and `LIBUSB_DEBUG=4` diagnostic recipes for the failure
mode where a third-party WCH `CH34xVCPDriver` *would* claim
interface 0 (`kmutil unload -b <bundleID>` workaround).
No upstream library forks, no PR chain, no additional lib_deps —
the existing `pine64/libch341-spi-userspace` + libusb-1.0 stack does
the right thing on macOS already.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Apply suggestion from @Copilot
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Router::handleReceived stores its allocCopy of the encrypted packet in the
class member p_encrypted. callModules() invokes module replies that re-enter
the router via MeshService::sendToMesh -> Router::sendLocal, which on a
broadcast reply recursively calls handleReceived. The inner call overwrites
the outer's p_encrypted without releasing it; on the way out it nulls the
member, the outer release(p_encrypted) now releases nullptr, and the original
allocation is permanently leaked. ~381 B per recursion.
Promote p_encrypted to a function-local so each invocation owns its own copy
for its full lifetime. The MQTT-publish null check at the call site (added by
PR #9136 as a workaround for this bug) stays in place because allocCopy can
still legitimately return nullptr on packetPool exhaustion.
Copilot's review of PR #8999 (the original introduction) flagged this exact
pattern at merge time:
"Storing p_encrypted as a class member can cause issues with recursive or
concurrent calls to handleReceived() since each call would overwrite the
previous packet pointer."
The historical reason for the member (S&F needing to retain the encrypted
copy across calls) was satisfied differently by PR #9799 (ServerAPI converted
to std::unique_ptr + cleanup on connection close), so the member is no longer
load-bearing.
Reproduces issues #9632 / #10101 / #8729 (heap leak when MeshMonitor
connected; TCP drops on Station G2 / LILYGO ServerAPI dump abort).
Hardware A/B on Station G2 under sustained TCP-API retry storm (open :4403,
request config, disconnect mid-stream, repeat at ~0.6/s) - 9 min run:
| Build | heapFree drift | rebootCount delta |
| this patch | -1.5 KB (noise)| 0 |
| stock 2.7.13 | -73 KB (8.1KB/min) | +1 (OOM crash) |
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
The T-Deck-Pro V1.1 PR (#9378) had a backwards merge (e7b66281) that pulled
236 commits of master INTO a 1-commit fork branch, then a second merge
(8fd0a7f2) duplicated the entire T_DECK/T_DECK_PRO/T_LORA_PAGER/HACKADAY
peripheral init block in setup(). Also removes a stray
digitalWrite(BLE_LED, LED_STATE_OFF) in the HACKADAY section that was an
artifact of the evil merge at e7b66281.
* niche: add InkHUD port for LilyGo T5-E-Paper-S3-Pro (ED047TC1)
Add a NicheGraphics EInk driver adapter for the 4.7" ED047TC1 parallel
e-paper display used on the T5-E-Paper-S3-Pro (H752-01). The driver
wraps FastEPD and handles the polarity difference between InkHUD's
buffer format (0xFF = white) and FastEPD's (0x00 = white).
Rewrite variants/esp32s3/t5s3_epaper/nicheGraphics.h which was an
incomplete copy of the Heltec VM-E290 setup referencing undefined SPI
pin macros and a non-existent BUTTON_PIN_SECONDARY. The board uses a
parallel display, not the small SPI DEPG0290BNS800 that was referenced.
* fix: guard inputBroker null dereference in TouchScreenImpl1::init()
When MESHTASTIC_EXCLUDE_INPUTBROKER is defined (e.g. InkHUD builds),
inputBroker is nullptr. Calling inputBroker->registerSource() in that
state caused a LoadProhibited panic on any board that has both
HAS_TOUCHSCREEN=1 and the InputBroker excluded.
Add a null check before registerSource() to prevent the crash.
* niche: fix display rotation for T5-E-Paper-S3-Pro InkHUD port
Set rotation=3 (270° CW) in nicheGraphics.h to correct for FastEPD
scanning the ED047TC1 panel in portrait orientation, resulting in
correct landscape display output.
* fix: update buffer format descriptions and remove polarity inversion for InkHUD and FastEPD
* fix: update ED047TC1 driver to handle inactive pixel borders and adjust safe-area dimensions
* fix: comment out ruler diagnostic for E-Ink driver
* feat: implement TouchInkHUDBridge for direct touch event handling in InkHUD
* niche: add FreeSans 18pt/24pt Win1253 (Greek) fonts for larger InkHUD displays
Add Win1253-encoded FreeSans 18pt and 24pt font headers to support Greek
script on larger InkHUD screens (e.g., the 4.7" ED047TC1 at ~234 DPI).
Register FREESANS_24PT_WIN1253 and FREESANS_18PT_WIN1253 macros in AppletFont.h.
Set fontLarge=24pt, fontMedium=18pt, fontSmall=12pt in nicheGraphics.h for the
T5-E-Paper-S3-Pro.
* feat(ed047tc1): use true partial update for FAST refresh
Replace fullUpdate(CLEAR_FAST) with partialUpdate() for FAST display
updates. FastEPD's partialUpdate() diffs pCurrent against pPrevious
and only applies the update waveform to rows that have changed, leaving
unchanged rows with a neutral signal.
This reduces visible flicker on routine updates (new messages, position
changes) — only the affected region of the screen refreshes. Full-screen
CLEAR_SLOW updates are preserved for periodic ghosting cleanup, driven
by InkHUD's setDisplayResilience() ratio.
* feat(t5s3-epaper): enable frontlight via LatchingBacklight
Wire up BOARD_BL_EN (GPIO11) to InkHUD's LatchingBacklight driver.
Enable the backlight menu item so users can toggle "Keep Backlight On"
via Settings. The backlight turns on automatically when the menu opens
and off when it closes.
* Fix RTC chip (PCF8563 not PCF85063) and GT911 I2C address collision
- variant.h used PCF85063_RTC but the board has a PCF8563. The difference
is the RAM register: PCF85063 has 1 byte of RAM; PCF8563 does not. The
PCF85063 driver was trying to write this register on init, failing every
time, and setDateTime writes were silently discarded — RTC time was
never persisted across reboots. Switch to PCF8563_RTC/PCF8563_INT.
Before:
[E][SensorPCF85063.hpp:375] initImpl(): Failed to write to RAM memory
register. Maybe this chip is pcf8563.
Read RTC time from PCF85063 getDateTime as 2026-04-05 00:00:23
PCF85063 setDateTime 2026-04-05 18:40:59
Read RTC time from PCF85063 getDateTime as 2026-04-05 00:00:19 ← lost
After:
PCF8563 found at address 0x51
Read RTC time from PCF8563 getDateTime as 2026-04-05 18:58:37 ← persisted
PCF8563 setDateTime 2026-04-05 18:58:44
Read RTC time from PCF8563 getDateTime as 2026-04-05 18:58:44 ← round-trips
- GT911 touch was initialized with GT911_SLAVE_ADDRESS_L (0x5D), which
collides with the SFA30 air quality sensor also at 0x5D on the same
I2C bus. Switch to GT911_SLAVE_ADDRESS_H (0x14): the library drives
INT high during reset to program the GT911 to address 0x14,
eliminating the address conflict.
Before:
SFA30 found at address 0x5d
[I][TouchDrvGT911.hpp:568] initImpl(): Try using 0x5D as the device address
After:
SFA30 found at address 0x5d
[I][TouchDrvGT911.hpp:544] initImpl(): Try using 0x14 as the device address
* t5s3_epaper: fix GT911 ghost-SFA30 via early I2C address latch
Investigation findings
----------------------
Boot logs showed "SFA30 found at address 0x5d" on every cold power-on,
and AirQualityTelemetry was registering an SFA30 sensor. However, every
readMeasuredValues() call returned error 268 (0x010C = Sensirion
WriteError | I2cAddressNack), meaning the I2C write to 0x5D was being
NACK'd — inconsistent with a real SFA30.
Root cause: the GT911 touch controller latches its I2C address from the
INT pin level at reset time (GT911 datasheet §4.3). GPIO3 (INT) defaults
LOW on ESP32-S3 cold boot → GT911 always powers up at 0x5D
(SLAVE_ADDRESS_L). The I2C scanner runs before lateInitVariant() had a
chance to reprogram the chip.
The scanner's SFA30 detection (ScanI2CTwoWire.cpp) sends the 2-byte
command 0xD060 to 0x5D and requests 48 bytes back. GT911 ACKs the
write (treating it as a register address) and returns 48 bytes of
register data, passing the length check — a false-positive SFA30
detection.
Confirmed via second cold-boot log: after the previous commit moved GT911
to 0x14 in lateInitVariant(), address 0x5D *still* appeared in the scan
because the scanner runs first. The board has no physical SFA30 fitted.
Fix
---
Add the GT911 address-latch reset sequence to earlyInitVariant(), before
Wire is initialised and before the I2C scan runs. Per the datasheet:
drive RST LOW, drive INT HIGH (selects address 0x14 / SLAVE_ADDRESS_H),
hold >100 µs, release RST, wait >5 ms startup. GPIO-only, no Wire
dependency. lateInitVariant() then repeats this sequence internally via
touch.begin(); the double-reset is harmless.
Verified in boot log:
Before: "SFA30 found at address 0x5d", 5 I2C devices, NACK errors
After: no SFA30 entry, 4 I2C devices (TCA9535/PCF8563/BQ27220/BQ25896),
GT911 found at 0x14 and touch initialised successfully,
AirQualityTelemetry registers no sensors (correct — no SFA30 present)
* t5s3_epaper: add variant_shutdown() for touch sleep and backlight off
Put GT911 into low-power standby (command 0x05) and drive BOARD_BL_EN
LOW before deep sleep to avoid unnecessary current draw.
* t5s3_epaper: fix touch gesture routing and coordinate mapping
readTouch() now transforms raw GT911 axes to visual-frame coordinates
based on the current display rotation (rotation=3 is the hardware
identity). This ensures TouchScreenBase detects swipe direction
correctly regardless of which rotation the user has selected.
TouchInkHUDBridge dynamically sets joystick.alignment = (4-rotation)%4
on each touch event so that (rotation+alignment)%4==0 always, keeping
nav calls pass-through without remapping.
nicheGraphics.h now calls loadSettings() first so that rotation is
persisted across reboots. rotation=3 and other first-boot defaults are
only applied when tips.firstBoot is set. alignment is recomputed from
the loaded rotation on every boot.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* t5s3_epaper: fix GT911 sleep timing via notifyDeepSleep observer
touch.sleep() was called from variant_shutdown(), which runs inside
cpuDeepSleep() — after Wire.end() had already torn down the I2C bus in
doDeepSleep(). This caused Wire NULL TX buffer errors and left the GT911
awake during deep sleep.
Register a CallbackObserver on notifyDeepSleep, which fires before
Wire.end(), so the I2C command reaches the chip while the bus is live.
Pattern matches LatchingBacklight and other NicheGraphics components.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* t5s3_epaper: fix touch nav and applet defaults in nicheGraphics
Enable joystick mode post-begin so menu scroll and swipe-up/down
gestures are not silently dropped by the joystick.enabled gate in
Events.cpp. Activate DMs and Channel 0/1 applets with correct
autoshow defaults matching the mini-epaper-s3 reference pattern.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Update nicheGraphics.h
* t5s3_epaper: fix ED047TC1 driver docs and remove spurious beginPolling
Addressing PR review comments:
Remove beginPolling(1, 0) after the blocking FastEPD update — it
incorrectly set updateRunning=true for one loop cycle after the
hardware was already done, causing busy() to briefly return true.
Since isUpdateDone() always returns true, no polling is needed.
Also fix stale comments: safe-area buffer size was 944×532, now
944×523; V_OFFSET_ROWS didn't exist, replaced with the actual
V_OFFSET_TOP=9 / V_OFFSET_BOTTOM=8 constant names.
* t5s3_epaper: clean up applet addition formatting in setupNicheGraphics
* t5s3_epaper: guard ED047TC1.cpp against non-T5S3 InkHUD builds
The InkHUD base config pulls in all of src/graphics/niche/ so every
InkHUD device compiled ED047TC1.cpp, triggering the #error on line 48
for boards that define neither T5_S3_EPAPER_PRO_V1 nor V2.
Wrap the file body with #ifdef T5_S3_EPAPER_PRO so it is only compiled
for T5S3 targets. The #error is preserved inside the guard to catch
future hardware revisions that forget to update the driver.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com>
* Add authoring guide for native unit tests in README.md
* Enhance documentation for agent tooling and native unit tests in README and related files
---------
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
* Add ESP32 Power Management lessons learned document
Documents our experimentation with ESP-IDF DFS and why it doesn't
work well for Meshtastic (RTOS locks, BLE locks, USB issues).
Proposes simpler alternative: manual setCpuFrequencyMhz() control
with explicit triggers for when to go fast vs slow.
* docs(prompts): fix markdown fence language tags
* docs: remove ESP32 power management notes
Add a meshtasticd config for the Luckfox Pico Max with the Waveshare Pico LoRa SX1262 TCXO HAT.
Tested on hardware with successful SX1262 init, broadcast, and direct messaging.
The CALIBRATE_ALL (0x7F) command inside resetAGC() clears bit 0 of the
undocumented 0x8B5 register. That bit is set once in init() by #9571 and
#9777 to improve SX1262 RX sensitivity, and the AGC-reset path was not
re-applying it. Result: every SX1262 node silently loses the RX
sensitivity patch ~60s after boot and never recovers until reboot.
Empirically confirmed on Heltec Mesh Node T114 (nRF52840 + SX1262):
- Post-calibration read of 0x8B5 = 0x04 (bit 0 cleared)
- After re-apply: 0x05 (bit 0 set)
Reproducible every AGC_RESET_INTERVAL_MS tick.
Fix re-applies the register bit alongside the existing post-calibration
re-applies (setDio2AsRfSwitch, setRxBoostedGainMode).
* Fix heap blowout on TBeams
* Update src/graphics/draw/MessageRenderer.cpp
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* Set MESSAGE_HISTORY_LIMIT to 10 for original ESP32 to optimize RAM usage
* Optimize message frame allocation to prevent excessive memory usage
* Refine message history limits for resource-constrained builds and cap cached lines to prevent heap overflow
* Update src/graphics/draw/MessageRenderer.cpp
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* fix(native): implement BinarySemaphorePosix with proper pthread synchronization
The BinarySemaphorePosix class (used on all Linux/portduino/native builds)
had stub implementations: give() was a no-op and take() just called
delay(msec) and returned false. This broke the cooperative thread scheduler
on native platforms — threads could not wake the main loop, radio RX
interrupts were missed, and telemetry never transmitted over the mesh.
Replace the stubs with a proper binary semaphore using pthread_mutex_t +
pthread_cond_t + bool signaled:
- take(msec): pthread_cond_timedwait with CLOCK_REALTIME timeout, consumes
signal atomically (binary semaphore semantics)
- give(): sets signaled=true, signals condition variable
- giveFromISR(): delegates to give(), sets pxHigherPriorityTaskWoken
Tested on Raspberry Pi 3 Model B (ARM64, Debian Bookworm) with Adafruit
LoRa Radio Bonnet (SX1276). Before fix: no radio TX/RX, no telemetry on
mesh. After fix: bidirectional LoRa, MQTT gateway, telemetry all working.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* ARCH_PORTDUINO
* Refactor BinarySemaphorePosix header for ARCH_PORTDUINO
* Change preprocessor directive from ifndef to ifdef
* Gate new Semaphore code to Portduino and fix STM compilation
* Binary Semaphore Posix better error handling
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
Co-authored-by: Jonathan Bennett <jbennett@incomsystems.biz>
* fix MESHTASTIC_EXCLUDE_SCREEN
* mesh-tab map constraints (2 MB PSRAM)
* point MUI commit to the related PR
---------
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
* Fix TAG low‑voltage reboot hang after App configuration
* nRF52: Move low-VDD System OFF logic to variant hook
* Addressed review
* serialize SAADC access with shared mutex for VDD and battery reads
* raise LPCOMP wake threshold to ensure rising-edge wake
* Trunk fmt
---------
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
* fix(routing): prevent licensed users from rebroadcasting packets from unlicensed or unknown users
* fix(routing): prevent licensed users from rebroadcasting packets to or from unlicensed users
---------
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
The W5100S Ethernet chip has only 4 hardware sockets. On RAK4631
Ethernet gateways with syslog and NTP enabled, all 4 sockets were
permanently consumed (NTP UDP + Syslog UDP + TCP API listener + TCP
API client), leaving none for MQTT, DHCP lease renewal, or additional
TCP connections.
- NTP: Remove permanent timeClient.begin() at startup; NTPClient::update()
auto-initializes when needed. Add timeClient.end() after each query to
release the UDP socket immediately.
- Syslog: Remove socket allocation from Syslog::enable(). Open and close
the UDP socket on-demand in _sendLog() around each message send.
- MQTT: Fix socket leak in isValidConfig() where a successful test
connection was never closed (PubSubClient destructor does not call
disconnect). Add explicit pubSub->disconnect() before returning.
Made-with: Cursor
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
PlatformIO registry is (probably) rate limiting us. Mitigate by switching to GitHub source zips (based upon tags).
This change does not include any updates, simply swapping to a new download location.
* Fix TransmitHistory to improve epoch handling
* Enable epoch handling in unit tests
* Improve comments and test handling for epoch persistence in TransmitHistory
* Add boot-relative timestamp handling and unit tests for TransmitHistory
* loadFromDisk should handle legacy entries and clean up old v1 files after migration
* Revert "loadFromDisk should handle legacy entries and clean up old v1 files after migration"
This reverts commit eb7e5c7acf.
* Add NodeInfoModule integration for RTC quality changes and trigger immediate checks
* Update test conditions for RTC quality checks
We cache and dedupe our dependencies, referring to them with multiple methods/urls is just noise.
```
lewisxhe/XPowersLib@0.3.3
lewisxhe/SensorLib@0.3.4
```
This does *not* include any updates, just a cleanup.
The touch-to-backlight feature was gated behind TTGO_T_ECHO_PLUS, but
the regular T-Echo has the same backlight pin (PIN_EINK_EN, P1.11).
This changes the guard to use PIN_EINK_EN only, so any device with an
e-ink backlight pin gets the feature.
Fixes#7630
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
* Fix issue 9792, decode packet for TR test
* Fix 9792: Assure packet id decoded for TR test
* Potential fix for pull request finding
Log improvement for failure to decode packet.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* trunk fmt
---------
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Tom Fifield <tom@tomfifield.net>
* fix: MQTT settings silently fail to persist when broker is unreachable
isValidConfig() was testing broker connectivity via connectPubSub() as
part of config validation. When the broker was unreachable (network not
ready, DNS failure, server down), the function returned false, causing
AdminModule to skip saving settings entirely — silently.
This removes the connectivity test from isValidConfig(), which now only
validates configuration correctness (TLS support, default server port).
Connectivity is handled by the MQTT module's existing reconnect loop.
Fixes#9107
* Add client warning notification when MQTT broker is unreachable
Per maintainer feedback: instead of silently saving when the broker
can't be reached, send a WARNING notification to the client saying
"MQTT settings saved, but could not reach the MQTT server."
Settings still always persist regardless of connectivity — the core
fix from the previous commit is preserved. The notification is purely
advisory so users know to double-check their server address and
credentials if the connection test fails.
When the network is not available at all, the connectivity check is
skipped entirely with a log message.
* Address Copilot review feedback
- Fix warning message wording: "Settings will be saved" instead of
"Settings saved" (notification fires before AdminModule persists)
- Add null check on clientNotificationPool.allocZeroed() to prevent
crash if pool is exhausted (matches AdminModule::sendWarning pattern)
- Fix test comments to accurately describe conditional connectivity
check behavior and IS_RUNNING_TESTS compile-out
* Remove connectivity check from isValidConfig entirely
Reverts the advisory connectivity check added in the previous commit.
While the intent was to warn users about unreachable brokers,
connectPubSub() mutates the isConnected state of the running MQTT
module and performs synchronous network operations that can block
the config-save path.
The cleanest approach: isValidConfig() validates config correctness
only (TLS support, default server port). The MQTT reconnect loop
handles connectivity after settings are persisted and the device
reboots. If the broker is unreachable, the user will see it in the
MQTT connection status — no special notification needed.
This returns to the simpler design from the first commit, which was
tested on hardware and confirmed working.
* Use lightweight TCP check instead of connectPubSub for validation
Per maintainer feedback: users need connectivity feedback, but
connectPubSub() mutates the module's isConnected state.
This uses a standalone MQTTClient TCP connection test that:
- Checks if the server IP/port is reachable
- Sends a WARNING notification if unreachable
- Does NOT establish an MQTT session or mutate any module state
- Does NOT block saving — isValidConfig always returns true
The TCP test client is created locally, used, and destroyed within
the function scope. No side effects on the running MQTT module.
---------
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
* Fix for preserving pki_encrypted and public_key when relaying UDP multicast packets to radio.
PKI DMs sent over UDP multicast had their pki_encrypted flag and public_key fields explicitly cleared before being forwarded to the LoRa radio. This caused the receiving node to treat the packet as a channel-encrypted message it couldn't decrypt, silently dropping it.
The MQTT ingress path correctly preserves these fields. The UDP multicast ingress path should behave the same way.
* Zeroize MeshPacket before decoding
Zeroize MeshPacket before decoding to prevent data leakage.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
* Fix RAK4631 Ethernet gateway API connection loss after W5100S brownout
PoE power instability can brownout the W5100S while the nRF52 MCU keeps
running, causing all chip registers (MAC, IP, sockets) to revert to
defaults. The firmware had no mechanism to detect or recover from this.
Changes:
- Detect W5100S chip reset by periodically verifying MAC address register
in reconnectETH(); on mismatch, perform full hardware reset and
re-initialize Ethernet interface and services
- Add deInitApiServer() for clean API server teardown during recovery
- Add ~APIServerPort destructor to prevent memory leaks
- Switch nRF52 from EthernetServer::available() to accept() to prevent
the same connected client from being repeatedly re-reported
- Add proactive dead-connection cleanup in APIServerPort::runOnce()
- Add 15-minute TCP idle timeout to close half-open connections that
consume limited W5100S hardware sockets
Fixesmeshtastic/firmware#6970
Made-with: Cursor
* Log actual elapsed idle time instead of constant timeout value
Address Copilot review comment: log millis() - lastContactMsec to show
the real time since last client activity, rather than always logging the
TCP_IDLE_TIMEOUT_MS constant.
Made-with: Cursor
* Update src/mesh/api/ServerAPI.h
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* Stop UDP multicast handler during W5100S brownout recovery
After a W5100S chip brownout, the udpHandler isRunning flag stays
true while the underlying socket is dead. Without calling stop(),
the subsequent start() no-ops and multicast is silently broken
after recovery.
Made-with: Cursor
* Address Copilot review: recovery flags and timeout constant
Move ethStartupComplete and ntp_renew reset to immediately after
service teardown, before Ethernet.begin(). Previously, if DHCP
failed the early return left ethStartupComplete=true, preventing
service re-initialization on subsequent retries.
Replace #define TCP_IDLE_TIMEOUT_MS with static constexpr uint32_t
for type safety and better C++ practice.
Made-with: Cursor
---------
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* Fix NodeInfo suppression logic to ensure suppression only applies to external requests
* Ensure NodeInfo reply suppression logic to only apply for external requests which are actually nodeinfo packets
Problem:
- Inserting a µSD card causes RadioLib to hit a critical error and reboot
- Device enters a boot loop as the SD card remains inserted
Reproduction:
- Insert a µSD card and power on
- RadioLib reports a critical error on boot
- Device reboots, repeating indefinitely
Root cause:
- On T-Lora Pager, SX1262 and the µSD slot share the same physical SPI bus
(same SCK/MOSI/MISO pins, differentiated only by CS)
- SDCARD_USE_SPI1 is intended for boards where SD is on a separate SPI bus;
it initializes a second ESP32 SPI peripheral (SPI3) for SD
- SPI2 is already driving those same pins for LoRa, so both controllers
simultaneously drive the same GPIO lines, causing bus contention
Fix:
- Remove SDCARD_USE_SPI1 so both devices share a single SPI peripheral (SPI2),
with CS pins providing device selection as intended
- Tested on a custom fork of device-ui; LoRa and SD card map tiles both work
correctly with an SD card inserted
Signed-off-by: Andrew Yong <me@ndoo.sg>
* Enable pre-hop drop handling by default
* Remove early break if BME/DPS sensors are not detected at the BME address
* revert sneaky change
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
The Seeed Xiao S3 Kit's default GPS is an L76K which operates at 9600 baud, so when this variant was defined that baud rate was specified.
However, this is a development board and it is expected that users can attach their own devices. This includes GPS, which may operate at a different baud rate. The current fixed baud rate prevents this, so this patch removes that setting.
This will revert to the regular automatic probe method. This will successfully detect the L76K as before (the same speed as before since 9600 baud is the first baud rate checked), but also allow other GPSes at other baud rates to be detected.
Thanks to @ScarpMarc for the report
Fixes https://github.com/meshtastic/firmware/issues/9373#issuecomment-3774802763
Fixes this build error:
<command-line>: error: expected unqualified-id before '-' token
/home/<snip>/.platformio/packages/framework-arduinoespressif32/variants/esp32s3/pins_arduino.h:15:22: note: in expansion of macro 'LED_BUILTIN'
15 | static const uint8_t LED_BUILTIN = SOC_GPIO_PIN_COUNT + PIN_RGB_LED;
| ^~~~~~~~~~~
More info: https://github.com/meshtastic/firmware/pull/9122#issuecomment-4028263894
The fix is consistent with variants/esp32s3/heltec_v3/platformio.ini
This commit is intentionally on the develop branch, because it's harmless to
develop branch, and makes us more ready for pioarduino when the time comes.
Heltec v4 introduced in: https://github.com/meshtastic/firmware/pull/7845
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
* Add C++17 support
* Add C++17 runtime probes and update build configurations for STM32 and Portduino
* Remove unflags
* Update C++ standard flags across the platforms
* Convert a couple of instances to structured bindings
* NRF52 platform.txt to add C++17 support
* Still need the unflags apparently
* Remove C++17 runtime probe tests from test_main.cpp
* Reconfigured doesnt need a nodiscard
* Remove nodiscard attribute from init() method in RadioInterface
* Remove mbedtls/error.h from build flags
Removed include directive for mbedtls/error.h from build flags.
* Fix IRAM overflow
* Fix IRAM overflow
* Add build flag to exclude MQTT from rak11200
* Update C++ standard from gnu17 to gnu++17
* Update ESP8266Audio dependency to Meshtastic fork for compatibility
* Update ESP8266Audio dependency to Meshtastic fork for compatibility across multiple platformio.ini files
Addendum to PR #9791
The sourcedebs cannot currently be built offline >30 days after release. Extend the cache-expiry-mangling hack to sourcedeb packaging.
socketSendUDP() calls yield() while waiting for SEND_OK, allowing the
cooperative scheduler to run AsyncUDP::runOnce(). When runOnce() calls
parsePacket() on the same socket during an active send, the multicast
loopback packet arriving from the switch produces a garbled 8-byte RX
header (wrong source IP, wrong payload length), leading to protobuf
decode errors in UdpMulticastHandler.
Add a volatile isSending flag that writeTo() sets around endPacket()
(the only yield point). runOnce() checks this flag and skips
parsePacket() while a send is in progress. Loopback packets stay
buffered in the W5100S RX buffer and are read cleanly on the next
poll cycle.
Also propagate writeTo() return value in UdpMulticastHandler::onSend()
instead of unconditionally returning true.
Made-with: Cursor
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
* Nodelist cleanup
* Trunk fix
* getTextWidth cleanup
Updated the lambda to cache width in textW and reuse it instead of repeatedly calling getTextWidth(text) for unchanged text.
* Putting back hopAway ==0 condition as mentioned relayed signal is missleading
---------
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
* Update Heltec Tracker v2 to version KCT8103L.
* Fixed the issue of inaccurate comments.
---------
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
P0.04 is a digital power-enable pin for the NTC/LUX sensors, not an ADC
input. The old code was calling analogRead() on a floating GPIO that
happened to read ~mid-rail, coincidentally producing reasonable
temperature values.
- Rename T1000X_VCC_PIN to T1000X_SENSOR_EN_PIN and drive it HIGH in
initVariant() for both T1000-E and T1000-S variants
- Read BATTERY_PIN (with ADC_MULTIPLIER) instead, clamped to the 3.0V
LDO output (NTC_REF_VCC) for the NTC resistance calculation
* Cardputer Kit
BMI270 WIP
* BMI270 support
* verify that the number of bytes read matches the requested length
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* trunk'd
* remove excessive logging
* Kick the screen when unsleeping
* Update the st7789 library, and enable displayon and displayoff
* Battery detection
* Default to arrow keys and enter, while in menus.
* Enable Backlight control
* Update src/detect/ScanI2CTwoWire.cpp
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* updateState method now accepts shouldRequestFocus parameter for better maintainability
---------
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Jonathan Bennett <jbennett@incomsystems.biz>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
* Add Portduino Enable pins
* Add hat plus custom fields
* Punt on the GPIO device detection for now
* Simplify TX_GAIN_LORA for RAK13302
---------
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
This fixes builds in very clean environment with default build target (e.g.
-t buildfs *not* explicitly specified).
Tested: pio run -e heltec-v3
Fixes#9035
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
Adds ROUTER_LATE and CLIENT_BASE to preferred rebroadcaster check
(skip unsolicited NodeInfo) and prevents ROUTER_LATE from setting
rebroadcast mode to NONE, which would silently break relaying.
ROUTER_LATE is an infrastructure relay that should use the same power
assumptions, interval defaults, and LoRa wake behavior as ROUTER.
Without this, ROUTER_LATE uses client-class defaults and will not
wake on LoRa activity from deep sleep, making it a broken relay.
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
ROUTER_LATE already has high base intervals and should not be further
scaled by congestion. TAK_TRACKER is a tracker variant that should
skip congestion scaling like TRACKER does.
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
ROUTER and ROUTER_LATE should not accumulate favorites by sending DMs.
Also replaces magic number 12 with meshtastic_Config_DeviceConfig_Role_CLIENT_BASE.
ROUTER_LATE should be treated as an impolite telemetry role like
ROUTER, responding to multi-hop broadcast requests and using
aggressive send timing without channel utilization checks.
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
ROUTER_LATE now preserves node_info_broadcast_secs during factory
reset and auto-enables Store & Forward server mode, matching ROUTER
infrastructure behavior.
* refactor: update throttling factor calculation and add unit tests for scaling behavior
* refactor: adjust throttling factor calculation for improved accuracy in different configurations
* Update src/mesh/Default.h
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* Update src/mesh/Default.h
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* refactor: enhance throttling factor calculation and introduce pow_of_2 utility function
* refactor: improve expected ms calculation in unit tests for Default::getConfiguredOrDefaultMsScaled
* refactor: improve scaling logic for routers and sensors in computeExpectedMs function
---------
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* Improve resource cleanup on connection close
* Copilot had some good feedback. Let's just make the api a unique pointer
* Update src/mesh/api/ServerAPI.cpp
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* Copilot stupidly suggesting we call protected methods
* Gotta do it in the superclasses as well
* Fix moar
* Refactor MQTT unit test to ensure proper subscription handling and clear side effects
---------
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* Add ESP32 Power Management lessons learned document
Documents our experimentation with ESP-IDF DFS and why it doesn't
work well for Meshtastic (RTOS locks, BLE locks, USB issues).
Proposes simpler alternative: manual setCpuFrequencyMhz() control
with explicit triggers for when to go fast vs slow.
* Added a lambda function to clear startup output in the MQTT unit test to ensure a clean state before and after the MQTT subscription process.
getByIndex allocates memory and returns dummy channel whenever
chIndex validation fails. Comment implies this may happen
when malformed packet is received. The fix changes implementation
so static dummyChannel is returned in such case.
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
* enhance tx queue priority management
In busy environments, especially for ROUTER_LATE role, tx queue
fills very quickly. Delayed packets became late but new packets
to be retransmitted won't be put into the tx queue as old ones
stay there for a very long time (even a minute or more). This change
makes meshtastic prioritize new packets over late packets from tx queue
and allows to remove late packet from back of tx queue when there
is no space for a new one.
* apply copilot recommendation for cast
---------
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
* Don't launch canned message when waking screen or silencing notification
* Add screen ifdefs
* Get the #if right
---------
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
* fix(MQTT): First MapReport does not get sent
Throttle::isWithinTimespanMs(last_report_to_map, map_publish_interval_msecs) is
used to maintain the map reporting interval, but because last_report_to_map has
an initial value of 0, the map report routine does not start until the system
millis() time has passed map_publish_interval_msecs.
Fix this by adding a check that last_report_to_map is not 0.
Signed-off-by: Andrew Yong <me@ndoo.sg>
* feat(MQTT): Send MapReporting immediately upon location fix
Do not update last_report_to_map when Map Report is attempted without a valid
location, as this results in waiting up to an hour (or configured Map Report
interval).
That usually happens because most nodes do not keep GPS warm, so GPS usually
locks after the first attempt at Map Report.
This change also results in the log WARNing message getting spammed until a
location is obtained, so remove the message for now.
Signed-off-by: Andrew Yong <me@ndoo.sg>
* feat(MQTT): Throttled warning when position is not available for MapReport
Signed-off-by: Andrew Yong <me@ndoo.sg>
---------
Signed-off-by: Andrew Yong <me@ndoo.sg>
* Add transmit history for throttling that persists between reboots
* Fix RAK long press detection to prevent phantom shutdowns from floating pins
* Update test/test_transmit_history/test_main.cpp
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* Test fixes and placeholder for content handler tests
---------
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Change file name from BLEDfuScure.cpp to BLEDfuSecure.cpp. Fix filenames
in documentation.
Signed-off-by: Koko <github@kokosoftware.pl>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
* Add GPIO_DETECT_PA portduino config, and support 13302 detection with it.
* Tweak PA detect gpio to use pinMapping
* minor yaml output fixes
---------
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
* Align telemetry broadcast want_response behavior with traceroute
* Fixes
* Reduce side-effects by making the telemetry modules handle the ignorerequest
* Remove unnecessary ignoreRequest flag
* Try inheriting from MeshModule
* Add exclusion for sensor/router roles and add base telem module
* Implement 'agc' reset for SX126x chip family
There's no actual agc on SX126x chips but you can reset the analog
registers by doing a warm sleep & running calibration.
* Address PR comments & implement for LR11x0 too
* calibrate for configured frequency band
* Gate LR11X0_AGC_RESET
* Apply SX1262 register 0x8B5 patch for improved GC1109 RX sensitivity
Sets the LSB of undocumented SX1262 register 0x8B5 on Heltec V4 and
Wireless Tracker V2 boards with the GC1109 FEM. This patch was
recommended by Heltec/Semtech and tested in MeshCore PR #1398, where
it significantly reduced packet loss on the Heltec V4.
* Use higher level function
* Add .venv/ to .gitignore
* Reduce maximum concurrent HTTPS connections to save memory
* Add heap check and limit connections to prevent HTTPS connection related crashes
* Use Throttle::isWithinTimespanMs for overflow-safe heap warning throttle
* Update src/mesh/http/WebServer.cpp
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* Fix double-free heap corruption and parser leak in web server handlers
- Remove double-free in handleFsBrowseStatic, handleNodes, handleScanNetworks:
JSONValue(const JSONArray&) shallow-copies pointers, so delete value already
recursively frees all elements. The explicit cleanup loops were deleting the
same pointers a second time, corrupting the ESP32 heap allocator metadata.
This is the likely root cause of #8827 (SSL setup failures after uptime).
- Fix memory leak in handleFormUpload: two early-return paths were missing
delete parser.
- Remove unused global HTTPClient httpClient and its includes.
---------
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* Hold GC1109 PA_POWER and PA_EN during deep sleep for LNA RX wake
Use rtc_gpio_hold_en to latch PA_POWER (LDO) and PA_EN (CSD) HIGH
during deep sleep so the GC1109 LNA remains powered for wake-on-packet
RX. Previously these pins used weak pull-ups which could lose state.
On deep sleep wake, skip these pins in the blanket RTC hold release and
instead release them in SX126xInterface::init() after GPIO registers are
set HIGH first, avoiding a power glitch on the GC1109.
Trade-off: ~6.5mA additional deep sleep current for significantly
improved wake-on-packet RX sensitivity (~17dB).
Reference: https://github.com/meshcore-dev/MeshCore/pull/1600
* Add LDO startup delay before GC1109 chip enable
TLV75733P LDO has ~550us startup time (datasheet tSTR). On cold boot,
wait 1ms for VBAT to stabilise before driving CSD/CPS, per GC1109
power-on sequence requirement. On deep sleep wake the LDO is held on
via RTC latch so no delay is needed.
* onNavUp() sets the menu cursor to the last item if not shown
* skip headers when showing the menu cursor in onNavUp() and onNavDown()
* skip headers when showing the menu cursor in onButtonShortPress()
* brace cursor incrementing
---------
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
Co-authored-by: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com>
* Don't ever define PIN_LED or BLE_LED_INVERTED (#9494)
* Don't ever define PIN_LED
* Deprecate BLE_LED_INVERTED
* Add StatusMessage module and config overrides (#9351)
* Add StatusMessage module and config overrides
* Trunk
* Don't reboot node simply for a StatusMessage config update
* Missed in reviews - fixing send bubble (#9505)
* Prefer EXT_PWR_DETECT pin over chargingVolt to detect power unplugged (#9511)
* Make sure we always return a value in NodeDB::restorePreferences() (#9516)
In case FScom is not defined there is no return statement. This
moves the return outside of the ifdef to make sure a defined
value is returned.
* Inkhud battery icon improvements. (#9513)
* Inkhud battery icon improvements.
Fixes the battery icon draining from the flat side towards the bump, which is backwards from general design language seen on most devices
By request of kr0n05_ on discord, adds the ability to mirror the battery icon which fixes that issue in another way, and is also a common design seen on other devices.
* Remove option for icon mirroring
* Add border + dither to battery to prevent font overlap
* Fix trunk format
* Code cleanup, courtesy of Xaositek.
* Add reply bot module with DM-only responses and rate limiting (#9456)
* Implement Meshtastic reply bot module with ping and status features
Adds a reply bot module that listens for /ping, /hello, and /test commands received via direct messages or broadcasts on the primary channel. The module always replies via direct message to the sender only, reporting hop count, RSSI, and SNR. Per-sender cooldowns are enforced to reduce network spam, and the module can be excluded at build time via a compile flag. Updates include the new module source files and required build configuration changes.
* Update ReplyBotModule.cpp
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* Update src/modules/ReplyBotModule.h
Match the existing MESHTASTIC_EXCLUDE_* guard pattern so the module is excluded by default.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* Update src/modules/ReplyBotModule.cpp
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* Tidying up
---------
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
* HotFix for ReplyBot - Modules.cpp included and moved configuration.h (#9532)
* Undefine LED_BUILTIN (#9531)
Keep variant in sync with
https://github.com/meshtastic/firmware/commit/df40085
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
* Add agc reset attempt (#8163)
* Add agc reset attempt
* Add radioLibInterface include
* Trunk
* AGC reset don't crash, don't naively call
* Update src/main.cpp
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* Use Throttle function
---------
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* Remove unused hmx variable (#9529)
The variable is not used at all in the function, remove it to
silence the compiler warning.
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
* Rename LED_PIN to LED_POWER, move handling out of main to dedicated module (#9512)
* Rename LED_PIN to LED_POWER, move handling out of main to dedicated module
* Misc
* Remove errant endif
* Fix hop_limit upgrade detection (#9550)
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
* meshtasticd: Fix install on Fedora 43 (#9556)
* RPM: Include meshtasticd-start.sh (#9561)
* Add Slash Key to VirtualKeyboard (#9563)
Addition of ? and / to the virtual Keyboard via short and long press
* Add support for CW2015 LiPo battery fuel gauge (#9564)
* Add support for CW2015 LiPo battery fuel gauge
* Address Copilot's concerns, minor fixups
* Make LED_POWER blip even in critical battery (#9545)
* Enable FORTIFY and SP for native builds (#9537)
* Enable FORITFY and NX for native builds
meshtasticd does have an executable stack and is not built with fortify, which makes exploitation of memory corruption bugs easier than it has to be. This enables fortify and a non-executable stack.
This gives the following improvements on Debian Trixie:
$ checksec --file=./.pio/build/native/meshtasticd
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Partial RELRO No canary found NX enabled PIE enabled No RPATH No RUNPATH 13516 Symbols No 0 17 ./.pio/build/native/meshtasticd
$ checksec --file=./.pio/build/native/meshtasticd
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Partial RELRO Canary found NX enabled PIE enabled No RPATH No RUNPATH 13519 Symbols Yes 12 20 ./.pio/build/native/meshtasticd
Tested with --sim mode I do not get any crashes or similar.
* Enable FORTIFY and NX for native builds
meshtasticd does have an executable stack and is not built with fortify, which makes exploitation of memory corruption bugs easier than it has to be. This enables fortify and a non-executable stack.
This gives the following improvements on Debian Trixie:
$ checksec --file=./.pio/build/native/meshtasticd
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Partial RELRO No canary found NX enabled PIE enabled No RPATH No RUNPATH 13516 Symbols No 0 17 ./.pio/build/native/meshtasticd
$ checksec --file=./.pio/build/native/meshtasticd
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Partial RELRO Canary found NX enabled PIE enabled No RPATH No RUNPATH 13519 Symbols Yes 12 20 ./.pio/build/native/meshtasticd
Tested with --sim mode I do not get any crashes or similar.
* Enable FORTIFY and SP for native builds
meshtasticd does have a stack canaries and is not built with fortify, which makes exploitation of memory corruption bugs easier than it has to be. This enables fortify and stack canaries.
This gives the following improvements on Debian Trixie:
$ checksec --file=./.pio/build/native/meshtasticd
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Partial RELRO No canary found NX enabled PIE enabled No RPATH No RUNPATH 13516 Symbols No 0 17 ./.pio/build/native/meshtasticd
$ checksec --file=./.pio/build/native/meshtasticd
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Partial RELRO Canary found NX enabled PIE enabled No RPATH No RUNPATH 13519 Symbols Yes 12 20 ./.pio/build/native/meshtasticd
Tested with --sim mode I do not get any crashes or similar.
---------
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
* Update built-in documentation for current method of implementation (#9592)
* Refactor logging in ProtobufModule to ensure message details are logged after successful decoding (#9536)
* Automated version bumps (#9604)
Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>
* Add missing openocd_target to custom nrf52 boards (#9603)
This stops platformio complaining about `Missing target configuration for me25ls01-4y10td` etc when trying to flash with nrfutil.
* Add sdl libs for native builds (#9595)
* Add sdl libs for native builds
* Alpine try again
* fix some random compiler warnings (#9596)
* Modify the dependency library of v4-tft (#9507)
* BaseUI: Favorite Screen Signal Quality improvement (#9566)
* Favorite Signal Quality improvement
* Show Voltage if node shares it.
* Trunk Fix
* Change Favorite tittle to encase name with Asterisks
* Add Pluggin In condition for Battery Line
* Adjust getUptimeStr Prefixes
* Create isAPIConnected for SharedCommon usage
* Correct leftSideSpacing to account for isAPIConnected
---------
Co-authored-by: Jason P <applewiz@mac.com>
* ExternalNotification and StatusLED now call AmbientLighting to update… (#9554)
* ExternalNotification and StatusLED now call AmbientLighting to update RGB LEDs. Add optional heartbeat
* Don't overwrite RGB state if heartbeat is disabled.
* Use the right define
* Remove another .h and make rgb static
* move rgb objects into AmbientLighting class
* Straighten out AmbientLighting Thread object
* Use %f for floats
* Fixes on SCD4X admin comands (#9607)
* Fixes on SCD4X admin comands
* Minor fix in logs for SEN5X
---------
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
* feat/add sfa30 (#9372)
* Move PMSA003I to separate class and update AQ telemetry
* AirQualityTelemetry module not depend on PM sensor presence
* Remove commented line
* Fixes on PMS class
* Add missing warmup period to wakeUp function
* Fixes on compilation for different variants
* Add functions to check for I2C bus speed and set it
* Initial implementation for SFA30Sensor
* Move PMSA003I to separate class and update AQ telemetry
* AirQualityTelemetry module not depend on PM sensor presence
* Remove commented line
* Fixes on PMS class
* Add missing warmup period to wakeUp function
* Fixes on compilation for different variants
* Add functions to check for I2C bus speed and set it
* Add ScreenFonts.h
Co-authored-by: Hannes Fuchs <hannes.fuchs+git@0xef.de>
* PMSA003I 1st round test
* Fix I2C scan speed
* Fix minor issues and bring back I2C SPEED def
* Remove PMSA003I library as its no longer needed
* Add functional SCD4X
* Fix screen frame for CO2
* Add admin commands to SCD4X class
* Add further admin commands and fixes.
* Remove unused I2C speed functions and cleanup
* Cleanup of SEN5X specific code added from switching branches
* Remove SCAN_I2C_CLOCK_SPEED block as its not needed
* Remove associated functions for setting I2C speed
* Unify build epoch to add flag in platformio-custom.py (#7917)
* Unify build_epoch replacement logic in platformio-custom
* Missed one
* Fix build error in rak_wismesh_tap_v2 (#7905)
In the logs was:
"No screen resolution defined in build_flags. Please define DISPLAY_SIZE."
set according to similar devices.
* Put guards in place around debug heap operations (#7955)
* Put guards in place around debug heap operations
* Add macros to clean up code
* Add pointer as well
* Cleanup
* Fix memory leak in NextHopRouter: always free packet copy when removing from pending
* Formatting
* Only queue 2 client notification
* Merge pull request #7965 from compumike/compumike/fix-nrf52-bluetooth-memory-leak
Fix memory leak in `NRF52Bluetooth`: allocate `BluetoothStatus` on stack, not heap
* Merge pull request #7964 from compumike/compumike/fix-nimble-bluetooth-memory-leak
Fix memory leak in `NimbleBluetooth`: allocate `BluetoothStatus` on stack, not heap
* Update protobufs (#7973)
Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>
* T-Lora Pager: Support LR1121 and SX1280 models (#7956)
* T-Lora Pager: Support LR1121 and SX1280 models
* Remove ifdefs
* Trunk
* Trunk
* Static memory pool allocation (#7966)
* Static memory pool
* Initializer
* T-Lora Pager: Support LR1121 and SX1280 models (#7956)
* T-Lora Pager: Support LR1121 and SX1280 models
* Remove ifdefs
---------
Co-authored-by: WillyJL <me@willyjl.dev>
* Portduino dynamic alloc
* Missed
* Drop the limit
* Update meshtastic-esp8266-oled-ssd1306 digest to 0cbc26b (#7977)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
* Fix json report crashes on esp32 (#7978)
* Tweak maximums
* Fix DRAM overflow on old esp32 targets
* Guard bad time warning logs using GPS_DEBUG (#7897)
In 2.7.7 / 2.7.8 we introduced some new checks for time accuracy.
In combination, these result in a spamming of the logs when a bad time is found
When the GPS is active, we're calling the GPS thread every 0.2secs.
So this log could be printed 4,500 times in a no-lock scenario :)
Reserve this experience for developers using GPS_DEBUG.
Fixes https://github.com/meshtastic/firmware/issues/7896
* Scale probe buffer size based on current baud rate (#7975)
* Scale probe buffer size based on current baud rate
* Throttle bad time validation logging and fix time comparison logic
* Remove comment
* Missed the other instances
* Copy pasta
* Fix GPS gm_mktime memory leak (#7981)
* Fix overflow of time value (#7984)
* Fix overflow of time value
* Revert "Fix overflow of time value"
This reverts commit 0847969201.
* That got boogered up
* Remove PMSA003 include from modules
* Add flag to exclude air quality module
* Rework PMSA003I to align with new I2C scanner
* Reworks AQ telemetry to match new dynamic allocation method
* Adds VBLE_I2C_CLOCK_SPEED build flag for sensors with different I2C speed requirements
* Reworks PMSA003I
* Move add sensor template to separate file
* Split telemetry on screen options
* Add variable I2C clock compile flag
* Added to Seeed Xiao S3 as demo
* Fix drawFrame in AQ module
* Module settings override to i2cScan module function
* Move to CAN_RECLOCK_I2C per architecture
* Add reclock function in TelemetrySensor.cpp
* Add flag in ESP32 common
* Minor fix
* Move I2C reclock function to src/detect
* Fix uninitMemberVar errors and compile issue
* Make sleep, wakeUp functions generic
* Fix STM32 builds
* Add exclude AQ sensor to builds that have environmental sensor excludes
* Add includes to AddI2CSensorTemplate.h
* SEN5X first pass
* WIP Sen5X functions
* Further (non-working) progress in SEN5X
* WIP Sen5X functions
* Changes on SEN5X library - removing pm_env as well
* Small cleanup of SEN5X sensors
* Minor change for SEN5X detection
* Remove dup code
* Enable PM sensor before sending telemetry.
This enables the PM sensor for a predefined period to allow for warmup.
Once telemetry is sent, the sensor shuts down again.
* Small cleanups in SEN5X sensor
* Add dynamic measurement interval for SEN5X
* Only disable SEN5X if enough time after reading.
* Idle for SEN5X on communication error
* Cleanup of logs and remove unnecessary delays
* Small TODO
* Settle on uint16_t for SEN5X PM data
* Make AQTelemetry sensors non-exclusive
* Implementation of cleaning in FS prefs and cleanup
* Remove unnecessary LOGS
* Add cleaning date storage in FS
* Report non-cumulative PN
* Bring back detection code for SEN5X after branch rebase
* Add placeholder for admin message
* Add VOC measurements and persistence (WIP)
* Adds VOC measurements and state
* Still not working on VOC Index persistence
* Should it stay in continuous mode?
* Add one-shot mode config flag to SEN5X
* Add nan checks on sensor data from SEN5X
* Working implementation on VOCState
* Adds initial timer for SEN55 to not sleep if VOCstate is not stable (1h)
* Adds conditions for stability and sensor state
* Fixes on VOC state and mode swtiching
* Adds a new RHT/Gas only mode, with 3600s stabilization time
* Fixes the VOCState buffer mismatch
* Fixes SEN50/54/55 model mistake
* Adapt SEN5X to new sensor list structure. Improve reclock.
* Improve reClockI2C conditions for different variants
* Add sleep, wakeUp, pendingForReady, hasSleep functions to PM sensors to save battery
* Add SEN5X
* Fix merge errors
* Small reordering in PMS class for consistency
* If one sensor fails, AQ telemetry still reports data
* Small formatting fix
* Add SEN5X to AQI in ScanI2C
* SCD4X now part of AQ module with template list
* Fixes difference between idle and sleep
* In LowPower, sleep is disabled
* Requires testing for I2C clock comms for commands
* Remove unnecessary import
* Add co2 to serialized AQ metrics
* Add SFA30 with new sensor template in AQ module
* Update library dependencies in platformio.ini
* Fix unitialized variables in SEN5X constructor
* Fix missing import
* Fix uninitMemberVars
* Fix import error for SCD4X
* Fix I2CClock logic
* Fix not reclocking back to 700000Hz
* Fix multiple sensors being read simultaneously
* The logic in AQ module is different to the one in EnvironmentTelemetryModule. In Env module, if any sensor fails to getMetrics, no valid flag for the module. This prevents other sensors to report data.
* Fix pending clock change in PMSA003
* Cleanup of SEN5X class
* Exclude AQ sensor from wio-e5 due to flash limitations
* Fix I2C clock change logic
* Make sure clock is always set to needed value
* Fix returns
* Fix trunk
* Fix on condition in reclock
* Fix trunk
* Final SFA30 class implementation
* Add HCHO to screen and improve logs
* Add metrics to mesh packet serializer
* Minor fixes in logs
* OCD tidy up of logs
* Fix sleep function
* Remove old I2C_CLOCK_SPEED code
---------
Co-authored-by: nikl <nikl174@mailbox.org>
Co-authored-by: Hannes Fuchs <hannes.fuchs+git@0xef.de>
Co-authored-by: Nashui-Yan <yannashui10@gmail.com>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
Co-authored-by: Tom Fifield <tom@tomfifield.net>
Co-authored-by: Mike Robbins <mrobbins@alum.mit.edu>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>
Co-authored-by: WillyJL <me@willyjl.dev>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
* Log rxBad PacketHeaders with more info (id, relay_node) like printPacket, so we can try to match RX errors to other packets in the logs. (#9614)
* Exclude status message module
* fix: zero entire public key array instead of only first byte (#9619)
* Update protobufs (#9621)
* Xiao NRF - define suitable i2c pins for the sub-variants (#8866)
Co-authored-by: Christian Walther <cwalther@gmx.ch>
* Update src/detect/ScanI2C.cpp
Co-authored-by: Wessel <wessel@weebl.me>
* Update src/modules/Telemetry/Sensor/SFA30Sensor.cpp
Co-authored-by: Wessel <wessel@weebl.me>
* Update src/modules/Telemetry/Sensor/SFA30Sensor.cpp
Co-authored-by: Wessel <wessel@weebl.me>
* Update src/modules/Telemetry/Sensor/SFA30Sensor.cpp
Co-authored-by: Wessel <wessel@weebl.me>
* Update src/mesh/NodeDB.cpp
Co-authored-by: Wessel <wessel@weebl.me>
* convert GPS global and some new in gps.cpp to unique_ptr (#9628)
Trying this to see if anything bad happen if I were to replace most raw pointers with unique_ptr.
I didn't used std::make_unique since it is only supported in C++14 and onwards but until we update esp32 to arduino 3.x the ESP32 xtensa chips use a C++11 std.
* replace delete in RedirectablePrint.cpp with std::unique_ptr (#9642)
Is part of the unique_ptr modernization effort.
* replace delete in EInkDynamicDisplay.{cpp,h} with std::unique_ptr (#9643)
Is part of the unique_ptr modernization effort.
* Undefine LED_BUILTIN for Heltec v2 variant (#9647)
* Undefine LED_BUILTIN for Heltec v2 variant
* Undefine LED_BUILTIN for Heltec v2.1 variant
---------
Co-authored-by: Jorropo <jorropo.pgm@gmail.com>
* replace delete in RadioInterface.cpp with std::unique_ptr (#9645)
Is part of the unique_ptr modernization effort.
* fix typo in PIN_GPS_SWITCH (#9648)
Wasn't caught by CI.
* replace delete in CryptoEngine.{cpp,h} with std::unique_ptr (#9649)
Is part of the unique_ptr modernization effort.
* workaround NCP5623 and LP5562 I2C builds (#9652)
Theses two appear to be buggy on r1-neo and nomadstar meteor pro, they rely on Wire.h being included previously to their import.
Idk why other platforms using the same smart LEDs are working while theses ones don't.
This should make CI green on the dev branch.
* replace delete in AudioThread.h with std::unique_ptr (#9651)
Is part of the unique_ptr modernization effort.
* Add USB_MODE=1 for Station G2 (#9660)
* InkHUD: Favorite Map Applet (#9654)
* fix a lot of low level cppcheck warnings (#9623)
* simplify the observer pattern, since all the called functions are const getters.
* use arduino macro over std: for numerical values and refactor local variables in drawScrollbar()
* oh, so Cppcheck actually complained about const pointers not being const.
* slowly getting out of ifdef hell
* fix inkHUD warnings as well
* last 2 check warnings
* git checks should fail on low defects from now on
* Feat/add scd30 (#9609)
* Merge develop into SCD30
* Add SCD30 class
* Fix logging and admin commands
* Minor cleanup and logging improvements
* Minor formatting issue
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* Improvements on setTemperature
* Fix casting float-uint16_t
* Pass 100 for resetting temperature offset
* Fix issues pointed out by copilot
* Add quick reboot to set interval quicker on scd30
* Change saveState to only happen after boot and minor log changes
* Fix missing semicolon in one shot mode log
---------
Co-authored-by: Thomas Göttgens <tgoettgens@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
* fix: respect DontMqttMeBro flag regardless of channel PSK (#9626)
The previous PSK check was broken from its introduction in #4643 —
memcmp was used in boolean context without comparing to 0, inverting
the condition. Since no one noticed for over a year, the PSK-based
filtering provided no practical value. Simplifying to always respect
the sender's preference is both more correct and easier to reason about.
* our firmware action is too clever
Update pio_target and add pio_opts for checks.
* fix detection of SCD30 by checking if the size of the return from a 2 byte register read is correct (#9664)
* fix detection of SCD30 by checking if thee size of the return from a 2 byte register read is correct
fix signedness warning in PMSA003 sensor code.
* Add alternate path for LPS22HB
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* check EndTransmission for errors and compare returned length to expected value
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* #9623 resolved a local shadow of next_key by converting it to int. (#9665)
* zip a few gitrefs down (#9672)
* InkHUD: Allow non-system applets to subscribe to input events (#9514)
* Allow inkhud user applets to consume inputs with opt-in system
Adds a way for applets to subscribe to input events while keeping it off
by default to preserve compatibility and expected behaviours. Adds
example for use as well.
* Add check for nullptr on getActiveApplet uses
* Remove redundant includes
* Move subscribedInputs to protected
* More consistent naming scheme
* Fake IAQ values on Non-BSEC2 platforms like Platformio and the original ESP32 (#9663)
* BSEC2 Replacement
- add approximation for IAQ to non-BSEC2 platforms.
- Re-add this sensor to ESP32 targets, and refactor env_extra includes.
- Fix C++ 11 compatibility
* Check for gas resistance 0
* ULED_BUILTIN for 9m2ibr_aprs_lora_tracker (#9685)
---------
Co-authored-by: Jonathan Bennett <jbennett@incomsystems.biz>
Co-authored-by: Jason P <applewiz@mac.com>
Co-authored-by: Eric Sesterhenn <eric.sesterhenn@x41-dsec.de>
Co-authored-by: Vortetty <33466216+Vortetty@users.noreply.github.com>
Co-authored-by: Mattatat25 <108779801+mattatat25@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Max <rekin.m@gmail.com>
Co-authored-by: Colby Dillion <colby.dillion@pacshealth.com>
Co-authored-by: Austin <vidplace7@gmail.com>
Co-authored-by: Tom <116762865+NomDeTom@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Chloe Bethel <chloe@9net.org>
Co-authored-by: Thomas Göttgens <tgoettgens@gmail.com>
Co-authored-by: Quency-D <55523105+Quency-D@users.noreply.github.com>
Co-authored-by: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com>
Co-authored-by: oscgonfer <oscgonfer@users.noreply.github.com>
Co-authored-by: nikl <nikl174@mailbox.org>
Co-authored-by: Hannes Fuchs <hannes.fuchs+git@0xef.de>
Co-authored-by: Nashui-Yan <yannashui10@gmail.com>
Co-authored-by: Tom Fifield <tom@tomfifield.net>
Co-authored-by: Mike Robbins <mrobbins@alum.mit.edu>
Co-authored-by: WillyJL <me@willyjl.dev>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Wessel <wessel@weebl.me>
Co-authored-by: Christian Walther <cwalther@gmx.ch>
Co-authored-by: Jorropo <jorropo.pgm@gmail.com>
Co-authored-by: Eric Barch <ericb@ericbarch.com>
Co-authored-by: Clive Blackledge <clive@ansible.org>
* BSEC2 Replacement
- add approximation for IAQ to non-BSEC2 platforms.
- Re-add this sensor to ESP32 targets, and refactor env_extra includes.
- Fix C++ 11 compatibility
* Check for gas resistance 0
* Allow inkhud user applets to consume inputs with opt-in system
Adds a way for applets to subscribe to input events while keeping it off
by default to preserve compatibility and expected behaviours. Adds
example for use as well.
* Add check for nullptr on getActiveApplet uses
* Remove redundant includes
* Move subscribedInputs to protected
* More consistent naming scheme
The previous PSK check was broken from its introduction in #4643 —
memcmp was used in boolean context without comparing to 0, inverting
the condition. Since no one noticed for over a year, the PSK-based
filtering provided no practical value. Simplifying to always respect
the sender's preference is both more correct and easier to reason about.
* Merge develop into SCD30
* Add SCD30 class
* Fix logging and admin commands
* Minor cleanup and logging improvements
* Minor formatting issue
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* Improvements on setTemperature
* Fix casting float-uint16_t
* Pass 100 for resetting temperature offset
* Fix issues pointed out by copilot
* Add quick reboot to set interval quicker on scd30
* Change saveState to only happen after boot and minor log changes
* Fix missing semicolon in one shot mode log
---------
Co-authored-by: Thomas Göttgens <tgoettgens@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
* simplify the observer pattern, since all the called functions are const getters.
* use arduino macro over std: for numerical values and refactor local variables in drawScrollbar()
* oh, so Cppcheck actually complained about const pointers not being const.
* slowly getting out of ifdef hell
* fix inkHUD warnings as well
* last 2 check warnings
* git checks should fail on low defects from now on
Theses two appear to be buggy on r1-neo and nomadstar meteor pro, they rely on Wire.h being included previously to their import.
Idk why other platforms using the same smart LEDs are working while theses ones don't.
This should make CI green on the dev branch.
Trying this to see if anything bad happen if I were to replace most raw pointers with unique_ptr.
I didn't used std::make_unique since it is only supported in C++14 and onwards but until we update esp32 to arduino 3.x the ESP32 xtensa chips use a C++11 std.
* Move PMSA003I to separate class and update AQ telemetry
* AirQualityTelemetry module not depend on PM sensor presence
* Remove commented line
* Fixes on PMS class
* Add missing warmup period to wakeUp function
* Fixes on compilation for different variants
* Add functions to check for I2C bus speed and set it
* Initial implementation for SFA30Sensor
* Move PMSA003I to separate class and update AQ telemetry
* AirQualityTelemetry module not depend on PM sensor presence
* Remove commented line
* Fixes on PMS class
* Add missing warmup period to wakeUp function
* Fixes on compilation for different variants
* Add functions to check for I2C bus speed and set it
* Add ScreenFonts.h
Co-authored-by: Hannes Fuchs <hannes.fuchs+git@0xef.de>
* PMSA003I 1st round test
* Fix I2C scan speed
* Fix minor issues and bring back I2C SPEED def
* Remove PMSA003I library as its no longer needed
* Add functional SCD4X
* Fix screen frame for CO2
* Add admin commands to SCD4X class
* Add further admin commands and fixes.
* Remove unused I2C speed functions and cleanup
* Cleanup of SEN5X specific code added from switching branches
* Remove SCAN_I2C_CLOCK_SPEED block as its not needed
* Remove associated functions for setting I2C speed
* Unify build epoch to add flag in platformio-custom.py (#7917)
* Unify build_epoch replacement logic in platformio-custom
* Missed one
* Fix build error in rak_wismesh_tap_v2 (#7905)
In the logs was:
"No screen resolution defined in build_flags. Please define DISPLAY_SIZE."
set according to similar devices.
* Put guards in place around debug heap operations (#7955)
* Put guards in place around debug heap operations
* Add macros to clean up code
* Add pointer as well
* Cleanup
* Fix memory leak in NextHopRouter: always free packet copy when removing from pending
* Formatting
* Only queue 2 client notification
* Merge pull request #7965 from compumike/compumike/fix-nrf52-bluetooth-memory-leak
Fix memory leak in `NRF52Bluetooth`: allocate `BluetoothStatus` on stack, not heap
* Merge pull request #7964 from compumike/compumike/fix-nimble-bluetooth-memory-leak
Fix memory leak in `NimbleBluetooth`: allocate `BluetoothStatus` on stack, not heap
* Update protobufs (#7973)
Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>
* T-Lora Pager: Support LR1121 and SX1280 models (#7956)
* T-Lora Pager: Support LR1121 and SX1280 models
* Remove ifdefs
* Trunk
* Trunk
* Static memory pool allocation (#7966)
* Static memory pool
* Initializer
* T-Lora Pager: Support LR1121 and SX1280 models (#7956)
* T-Lora Pager: Support LR1121 and SX1280 models
* Remove ifdefs
---------
Co-authored-by: WillyJL <me@willyjl.dev>
* Portduino dynamic alloc
* Missed
* Drop the limit
* Update meshtastic-esp8266-oled-ssd1306 digest to 0cbc26b (#7977)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
* Fix json report crashes on esp32 (#7978)
* Tweak maximums
* Fix DRAM overflow on old esp32 targets
* Guard bad time warning logs using GPS_DEBUG (#7897)
In 2.7.7 / 2.7.8 we introduced some new checks for time accuracy.
In combination, these result in a spamming of the logs when a bad time is found
When the GPS is active, we're calling the GPS thread every 0.2secs.
So this log could be printed 4,500 times in a no-lock scenario :)
Reserve this experience for developers using GPS_DEBUG.
Fixes https://github.com/meshtastic/firmware/issues/7896
* Scale probe buffer size based on current baud rate (#7975)
* Scale probe buffer size based on current baud rate
* Throttle bad time validation logging and fix time comparison logic
* Remove comment
* Missed the other instances
* Copy pasta
* Fix GPS gm_mktime memory leak (#7981)
* Fix overflow of time value (#7984)
* Fix overflow of time value
* Revert "Fix overflow of time value"
This reverts commit 0847969201.
* That got boogered up
* Remove PMSA003 include from modules
* Add flag to exclude air quality module
* Rework PMSA003I to align with new I2C scanner
* Reworks AQ telemetry to match new dynamic allocation method
* Adds VBLE_I2C_CLOCK_SPEED build flag for sensors with different I2C speed requirements
* Reworks PMSA003I
* Move add sensor template to separate file
* Split telemetry on screen options
* Add variable I2C clock compile flag
* Added to Seeed Xiao S3 as demo
* Fix drawFrame in AQ module
* Module settings override to i2cScan module function
* Move to CAN_RECLOCK_I2C per architecture
* Add reclock function in TelemetrySensor.cpp
* Add flag in ESP32 common
* Minor fix
* Move I2C reclock function to src/detect
* Fix uninitMemberVar errors and compile issue
* Make sleep, wakeUp functions generic
* Fix STM32 builds
* Add exclude AQ sensor to builds that have environmental sensor excludes
* Add includes to AddI2CSensorTemplate.h
* SEN5X first pass
* WIP Sen5X functions
* Further (non-working) progress in SEN5X
* WIP Sen5X functions
* Changes on SEN5X library - removing pm_env as well
* Small cleanup of SEN5X sensors
* Minor change for SEN5X detection
* Remove dup code
* Enable PM sensor before sending telemetry.
This enables the PM sensor for a predefined period to allow for warmup.
Once telemetry is sent, the sensor shuts down again.
* Small cleanups in SEN5X sensor
* Add dynamic measurement interval for SEN5X
* Only disable SEN5X if enough time after reading.
* Idle for SEN5X on communication error
* Cleanup of logs and remove unnecessary delays
* Small TODO
* Settle on uint16_t for SEN5X PM data
* Make AQTelemetry sensors non-exclusive
* Implementation of cleaning in FS prefs and cleanup
* Remove unnecessary LOGS
* Add cleaning date storage in FS
* Report non-cumulative PN
* Bring back detection code for SEN5X after branch rebase
* Add placeholder for admin message
* Add VOC measurements and persistence (WIP)
* Adds VOC measurements and state
* Still not working on VOC Index persistence
* Should it stay in continuous mode?
* Add one-shot mode config flag to SEN5X
* Add nan checks on sensor data from SEN5X
* Working implementation on VOCState
* Adds initial timer for SEN55 to not sleep if VOCstate is not stable (1h)
* Adds conditions for stability and sensor state
* Fixes on VOC state and mode swtiching
* Adds a new RHT/Gas only mode, with 3600s stabilization time
* Fixes the VOCState buffer mismatch
* Fixes SEN50/54/55 model mistake
* Adapt SEN5X to new sensor list structure. Improve reclock.
* Improve reClockI2C conditions for different variants
* Add sleep, wakeUp, pendingForReady, hasSleep functions to PM sensors to save battery
* Add SEN5X
* Fix merge errors
* Small reordering in PMS class for consistency
* If one sensor fails, AQ telemetry still reports data
* Small formatting fix
* Add SEN5X to AQI in ScanI2C
* SCD4X now part of AQ module with template list
* Fixes difference between idle and sleep
* In LowPower, sleep is disabled
* Requires testing for I2C clock comms for commands
* Remove unnecessary import
* Add co2 to serialized AQ metrics
* Add SFA30 with new sensor template in AQ module
* Update library dependencies in platformio.ini
* Fix unitialized variables in SEN5X constructor
* Fix missing import
* Fix uninitMemberVars
* Fix import error for SCD4X
* Fix I2CClock logic
* Fix not reclocking back to 700000Hz
* Fix multiple sensors being read simultaneously
* The logic in AQ module is different to the one in EnvironmentTelemetryModule. In Env module, if any sensor fails to getMetrics, no valid flag for the module. This prevents other sensors to report data.
* Fix pending clock change in PMSA003
* Cleanup of SEN5X class
* Exclude AQ sensor from wio-e5 due to flash limitations
* Fix I2C clock change logic
* Make sure clock is always set to needed value
* Fix returns
* Fix trunk
* Fix on condition in reclock
* Fix trunk
* Final SFA30 class implementation
* Add HCHO to screen and improve logs
* Add metrics to mesh packet serializer
* Minor fixes in logs
* OCD tidy up of logs
* Fix sleep function
* Remove old I2C_CLOCK_SPEED code
---------
Co-authored-by: nikl <nikl174@mailbox.org>
Co-authored-by: Hannes Fuchs <hannes.fuchs+git@0xef.de>
Co-authored-by: Nashui-Yan <yannashui10@gmail.com>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
Co-authored-by: Tom Fifield <tom@tomfifield.net>
Co-authored-by: Mike Robbins <mrobbins@alum.mit.edu>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>
Co-authored-by: WillyJL <me@willyjl.dev>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
* ExternalNotification and StatusLED now call AmbientLighting to update RGB LEDs. Add optional heartbeat
* Don't overwrite RGB state if heartbeat is disabled.
* Use the right define
* Remove another .h and make rgb static
* move rgb objects into AmbientLighting class
* Straighten out AmbientLighting Thread object
* Use %f for floats
* Favorite Signal Quality improvement
* Show Voltage if node shares it.
* Trunk Fix
* Change Favorite tittle to encase name with Asterisks
* Add Pluggin In condition for Battery Line
* Adjust getUptimeStr Prefixes
* Create isAPIConnected for SharedCommon usage
* Correct leftSideSpacing to account for isAPIConnected
---------
Co-authored-by: Jason P <applewiz@mac.com>
* Enable FORITFY and NX for native builds
meshtasticd does have an executable stack and is not built with fortify, which makes exploitation of memory corruption bugs easier than it has to be. This enables fortify and a non-executable stack.
This gives the following improvements on Debian Trixie:
$ checksec --file=./.pio/build/native/meshtasticd
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Partial RELRO No canary found NX enabled PIE enabled No RPATH No RUNPATH 13516 Symbols No 0 17 ./.pio/build/native/meshtasticd
$ checksec --file=./.pio/build/native/meshtasticd
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Partial RELRO Canary found NX enabled PIE enabled No RPATH No RUNPATH 13519 Symbols Yes 12 20 ./.pio/build/native/meshtasticd
Tested with --sim mode I do not get any crashes or similar.
* Enable FORTIFY and NX for native builds
meshtasticd does have an executable stack and is not built with fortify, which makes exploitation of memory corruption bugs easier than it has to be. This enables fortify and a non-executable stack.
This gives the following improvements on Debian Trixie:
$ checksec --file=./.pio/build/native/meshtasticd
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Partial RELRO No canary found NX enabled PIE enabled No RPATH No RUNPATH 13516 Symbols No 0 17 ./.pio/build/native/meshtasticd
$ checksec --file=./.pio/build/native/meshtasticd
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Partial RELRO Canary found NX enabled PIE enabled No RPATH No RUNPATH 13519 Symbols Yes 12 20 ./.pio/build/native/meshtasticd
Tested with --sim mode I do not get any crashes or similar.
* Enable FORTIFY and SP for native builds
meshtasticd does have a stack canaries and is not built with fortify, which makes exploitation of memory corruption bugs easier than it has to be. This enables fortify and stack canaries.
This gives the following improvements on Debian Trixie:
$ checksec --file=./.pio/build/native/meshtasticd
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Partial RELRO No canary found NX enabled PIE enabled No RPATH No RUNPATH 13516 Symbols No 0 17 ./.pio/build/native/meshtasticd
$ checksec --file=./.pio/build/native/meshtasticd
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Partial RELRO Canary found NX enabled PIE enabled No RPATH No RUNPATH 13519 Symbols Yes 12 20 ./.pio/build/native/meshtasticd
Tested with --sim mode I do not get any crashes or similar.
---------
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
* Implement Meshtastic reply bot module with ping and status features
Adds a reply bot module that listens for /ping, /hello, and /test commands received via direct messages or broadcasts on the primary channel. The module always replies via direct message to the sender only, reporting hop count, RSSI, and SNR. Per-sender cooldowns are enforced to reduce network spam, and the module can be excluded at build time via a compile flag. Updates include the new module source files and required build configuration changes.
* Update ReplyBotModule.cpp
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* Update src/modules/ReplyBotModule.h
Match the existing MESHTASTIC_EXCLUDE_* guard pattern so the module is excluded by default.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* Update src/modules/ReplyBotModule.cpp
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* Tidying up
---------
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
* Inkhud battery icon improvements.
Fixes the battery icon draining from the flat side towards the bump, which is backwards from general design language seen on most devices
By request of kr0n05_ on discord, adds the ability to mirror the battery icon which fixes that issue in another way, and is also a common design seen on other devices.
* Remove option for icon mirroring
* Add border + dither to battery to prevent font overlap
* Fix trunk format
* Code cleanup, courtesy of Xaositek.
# Claude Code slash commands for the mcp-server test suite
Three AI-assisted workflows wrapping `mcp-server/run-tests.sh` and the meshtastic MCP tools. Each one has a twin in `.github/prompts/` for Copilot users.
| Slash command | What it does | Copilot equivalent |
| `/test [args]` | Runs the test suite (auto-detects hardware) and interprets failures | `.github/prompts/mcp-test.prompt.md` |
| `/diagnose [role]` | Read-only device health report via the meshtastic MCP tools | `.github/prompts/mcp-diagnose.prompt.md` |
| `/repro <test> [n=5]` | Re-runs one test N times, diffs firmware logs between passes and failures | `.github/prompts/mcp-repro.prompt.md` |
## Why two surfaces
The Claude Code commands and Copilot prompts cover the same three workflows but each speaks its host's idiom:
- **Claude Code** (`/test`) uses `$ARGUMENTS` for pass-through, has direct access to Bash + all MCP tools registered in the user's settings, and runs in the terminal context.
- **Copilot** (`/mcp-test`) runs in VS Code's agent mode; it has terminal + MCP access too but typically asks the operator to confirm inputs interactively.
A contributor using either IDE gets equivalent assistance. Keep the two in sync when behavior changes — the diff of intent should be minimal.
## House rules
- **No destructive writes without explicit operator approval.** Skills that could reflash, factory-reset, or reboot a device must describe the action and stop — the operator authorizes.
- **Interpret failures, don't just echo them.** The skill body should pull firmware log lines from `mcp-server/tests/report.html` (the `Meshtastic debug` section, attached by `tests/conftest.py::pytest_runtest_makereport`) and classify the failure.
- **Keep MCP tool calls sequential per port.** SerialInterface holds an exclusive port lock; two parallel tool calls on the same port deadlock.
- **Never speculate about root cause.** If the evidence doesn't support a classification, say "unknown" and list what you'd need to disambiguate.
## Adding a new command
1. Write the Claude Code version at `.claude/commands/<name>.md` with YAML frontmatter:
```yaml
---
description: one-line purpose (used for auto-invocation by the model)
argument-hint: [optional-hint]
---
```
2. Write the Copilot equivalent at `.github/prompts/mcp-<name>.prompt.md` with:
```yaml
---
mode: agent
description: ...
---
```
3. Add the row to the table above. Cross-link in both bodies.
4. Smoke-test on Claude Code first (`/<name>` should appear in autocomplete), then in VS Code Copilot (`/mcp-<name>` in Chat).
description:Produce a device health report using the meshtastic MCP tools (device_info, list_nodes, get_config, short serial log capture)
argument-hint:[role=all|nrf52|esp32s3|<port>]
---
# `/diagnose` — device health report
Call the meshtastic MCP tool bundle and format a structured health report for one or all detected devices. Zero guesswork for the operator.
## What to do
1.**Enumerate hardware.** Call `mcp__meshtastic__list_devices(include_unknown=True)`. For each entry where `likely_meshtastic=True`, capture `port`, `vid`, `pid`, `description`.
2.**Filter by `$ARGUMENTS`**:
- No args, `all` → every likely-meshtastic device.
-`nrf52` → only devices with `vid == 0x239a`.
-`esp32s3` → only devices with `vid == 0x303a` or `vid == 0x10c4`.
- A `/dev/cu.*` path → only that one port.
- Anything else → treat as a substring match against the `port` string.
3.**For each selected device, in sequence (NOT parallel — SerialInterface holds an exclusive port lock):**
- Optionally, if the device seems unhappy (fails to connect, `num_nodes==1` when ≥2 are plugged in, missing firmware*version), open a short firmware log window: `mcp__meshtastic__serial_open(port=<p>, env=<inferred-env>)`, wait 3s, `serial_read(session_id=<s>, max_lines=100)`, `serial_close(session_id=<s>)`. The env should be inferred from the VID map in `mcp-server/run-tests.sh` (nrf52 → rak4631, esp32s3 → heltec-v3) unless `MESHTASTIC_MCP_ENV*<ROLE>` is set.
4.**Hub health** (call once, not per-device): `mcp__meshtastic__uhubctl_list()` — enumerates every USB hub the host can see. Note which hubs advertise `ppps=true` and which hub hosts each Meshtastic device (cross-reference by VID). Flag it in the report if:
- No hub advertises PPPS → `tests/recovery/` can't run on this setup; hard-recovery via `uhubctl_cycle` isn't available.
- A Meshtastic device is on a non-PPPS hub → note it; operator may want to move the device to a PPPS hub to unlock auto-recovery.
-`uhubctl_list` raises `ConfigError: uhubctl not found` → just say `uhubctl not installed` in the report; don't treat as a fault.
firmware : no panics in last 3s; NodeInfoModule emitted 2 broadcasts
```
Keep it scannable. If a field is missing or abnormal (no pubkey for a known peer, region=UNSET, num_nodes inconsistent with the hub, device on non-PPPS hub), flag it inline with a short `⚠︎ <one-line reason>`.
6. **Cross-device correlation** (only when >1 device is inspected):
- Do both sides see each other in `nodesByNum`? If one does and the other doesn't, that's asymmetric NodeInfo — flag it.
- Do the LoRa configs match? (region, channel_num, modem_preset should all agree; mismatch = no mesh)
- Do the primary channel NAMES match? Mismatch = different PSK = no decode.
7. **Recorder slice (cheap, always available).** The mcp-server runs an autouse log recorder that's been collecting from every connected device. Pull two short slices to surface anything weird that's already happened:
- `mcp__meshtastic__logs_window(start="-2m", level="WARN|ERROR|CRIT", max_lines=20)` — recent firmware errors. If empty, say "no recent errors"; don't manufacture concern.
- `mcp__meshtastic__telemetry_timeline(window="1h", field="free_heap", max_points=60)` — heap trend. If `slope_per_min < -50`, flag it and recommend `/leakhunt window=6h` for a deeper read; otherwise just note the current free heap.
- If `recorder_status` shows `running:false` or `files.telemetry.last_ts` is null, note "recorder has no telemetry yet — enable `set_debug_log_api(True)` to populate" and skip this step gracefully.
8. **Suggest next actions only for specific, recognisable failure modes**:
- Stale PKI pubkey one-way → "run `/test tests/mesh/test_direct_with_ack.py` — the retry + nodeinfo-ping heals this in the test path."
- Region mismatch → "re-bake one side via `./mcp-server/run-tests.sh --force-bake`."
- Device unreachable, reachable via DFU → `touch_1200bps(port=...)` + `pio_flash`. If not even DFU responds AND the device is on a PPPS hub, escalate to `uhubctl_cycle(role=..., confirm=True)`.
- CP2102-wedged-driver on macOS → see the note in `run-tests.sh`.
- Heap slope strongly negative → "run `/leakhunt window=6h` for a full timeline + classification."
## What NOT to do
- No writes. No `set_config`, no `reboot`, no `factory_reset`. This is a read-only diagnostic skill — if the operator wants to change state, they'll ask explicitly.
- No `flash` / `erase_and_flash`. Those are separate escalations.
- No holding SerialInterface across tool calls — open, query, close; next device. The port lock is exclusive.
# `/leakhunt` — read the recorder, classify a memory leak
Use the always-on recorder (`mcp-server/.mtlog/`) to read a heap timeline plus the matching log slice and produce a one-page verdict: **steady / slow leak / fragmentation / OOM-imminent**. No firmware changes, no special build flags — the LocalStats telemetry packet that the firmware already broadcasts every ~60 s carries `heap_free_bytes` and `heap_total_bytes`.
| LocalStats packet | (default) | ~60 s | No | Free — always on |
| `[heap N]` log prefix | `-DDEBUG_HEAP=1` | every log line | Yes (Thread X leaked) | Bigger flash + log volume |
Both feed the same `telemetry_timeline(field="free_heap")` query — when DEBUG_HEAP is on, the recorder synthesizes telemetry rows from log prefixes (tagged `source: debug_heap`), so a single timeline call gets whichever signal is available. **For a slow leak diagnosis, the default path is plenty** (60 s cadence over 6 h = 360 points; linear regression over that nails sub-100-byte/min slopes). **DEBUG_HEAP is for attribution** — when the slope is real and you need to know which thread is leaking.
2.**Verify the recorder is alive** — call `mcp__meshtastic__recorder_status`. Check:
-`running == True`
-`files.telemetry.lines > 0` (at least one telemetry packet recorded — if zero, the device hasn't broadcast LocalStats yet OR `set_debug_log_api` has never been on; tell the operator to run `mcp__meshtastic__set_debug_log_api(enabled=True)` and wait one device-update interval)
-`files.telemetry.last_ts` within the last 5 minutes (if older, the device is silent — log that, not "leak detected")
3.**Detect whether DEBUG_HEAP is active** — `mcp__meshtastic__logs_window(start="-2m", grep=r"\\[heap \\d+\\]", max_lines=3)`. If any line matches, the firmware has the prefix → DEBUG_HEAP is on, expect higher-cadence data and `heap_event` rows. If zero matches over the last 2 minutes, you're on the LocalStats-only path.
4.**Pull the timeline** — `mcp__meshtastic__telemetry_timeline(window=$window, variant=$variant, field=$field, max_points=200)`. Read:
-`samples` — how many raw points contributed
-`min`, `max` — total swing
-`slope_per_min` — units per minute (linear regression over the whole window)
5.**Pull the log context for the same window** — `mcp__meshtastic__logs_window(start="-${window}", grep="Heap status|leaked heap|freed heap|out of memory|Alloc an err|panic|abort", max_lines=200)`. These are the strings the firmware emits when something memory-related happens (`DEBUG_HEAP` builds emit `"Heap status:"` and `"leaked heap"` lines; production builds emit `"Alloc an err"` on failure and `"out of memory"` on OOM).
6.**Pull marker events** so we know if the operator labeled phases — `mcp__meshtastic__events_window(start="-${window}", kind="mark|connection_lost|connection_established")`. If a `connection_lost` overlaps a sharp drop, that's not a leak; that's a reboot.
6a. **(DEBUG_HEAP only) Per-thread attribution** — `mcp__meshtastic__logs_window(start="-${window}", grep="leaked heap", max_lines=200)`. Each row has a structured `heap_event` field with `{kind, thread, before, after, delta}`. Aggregate by thread: sum the `delta` over the window per thread name. The thread with the largest cumulative negative delta is your suspect. Note the count too — a thread with 50× small leaks is different from 1× big leak.
7.**Classify** based on what the data says, NOT on what you wish it said. Use these rules in order:
- **Insufficient data** (< 5 samples): say so. Suggest a longer window or longer wait. Stop.
- **Reboot mid-window**: if any `connection_lost` event is present AND `free_heap` jumped UP at that timestamp, the device rebooted. Note it; pre-reboot trend may be a leak but you only have part of the curve.
- **OOM-imminent**: any `Alloc an err=` or `out of memory` line in the log slice. This trumps everything; flag urgently.
- **Slow leak**: `slope_per_min < -50` AND `max - min > 1000` AND no reboot. The heap is monotonically (or near-monotonically) declining. Estimate time-to-zero: `min / -slope_per_min` minutes. Surface it.
- **Fragmentation suspect**: `slope_per_min` close to zero (|x| < 50) BUT min trends down across the window AND the log slice shows `Alloc an err` warnings WITHOUT total OOM. Means free total is OK but largest contiguous block is shrinking. Recommend a `DEBUG_HEAP` build to confirm.
- **Steady**: |slope_per_min| < 50, no error lines. Heap is fine.
- **Recovery curve**: slope is POSITIVE — heap recovered. Either a workload completed or GC fired. Note it; not a leak.
After flash, set debug_log_api back on and wait one window; re-run `/leakhunt`.
- SLOW LEAK, **DEBUG_HEAP on** → cite the top-leaking thread name from step 6a. Point at the corresponding source file (`grep -rn "ThreadName(\"<name>\")" src/`); the operator decides what to fix.
- FRAGMENTATION SUSPECT → propose pre-allocating any per-packet buffers; or rebuilding with `CONFIG_HEAP_TASK_TRACKING=y` on ESP32 to see who's holding the largest blocks.
- OOM-IMMINENT → flag for immediate attention; don't wait for the next telemetry interval.
- STEADY → say so; stop. Don't invent problems.
## What NOT to do
- Don't assume a leak from a single dip. LocalStats fires every ~60 s and the firmware naturally allocates+frees on each broadcast cycle; one packet sees the trough. Look at the slope, not the deltas.
- Don't recommend code changes. This skill diagnoses; the operator decides what to fix.
- Don't enable `set_debug_log_api` automatically — if it's off, telemetry isn't reaching pubsub anyway, and the recorder will be empty. Tell the operator to flip it on and wait, then re-run.
- Don't run heavy workloads to "trigger the leak." The recorder is passive; we read what's there.
## Companion: `mark_event` for stress runs
If the operator wants to test under stimulus (e.g. blast 50 broadcasts and see what the heap does), they can frame the experiment with markers:
```text
mark_event("burst-start")
… run the workload …
mark_event("burst-end")
/leakhunt window=15m
```
The markers land in both `events.jsonl` and `logs.jsonl`, so the report can show "free_heap dipped 8 KB during the burst window, recovered to baseline within 2 LocalStats cycles" → not a leak.
description:Re-run a specific test N times in isolation to triage flakes, diff firmware logs between passes and failures
argument-hint:<test-node-id> [count=5]
---
<!-- markdownlint-disable MD029 -->
# `/repro` — flakiness triage for one test
Re-run a single pytest node ID N times in isolation, track pass rate, and surface what's _different_ in the firmware logs between the passing attempts and the failing ones. Turns "it's flaky, I guess" into "it fails when X, passes when Y."
## What to do
1.**Parse `$ARGUMENTS`**: first token is the pytest node id (e.g. `tests/mesh/test_direct_with_ack.py::test_direct_with_ack_roundtrip[nrf52->esp32s3]`); second token is an integer count (default `5`, cap at `20`). If the first token doesn't look like a test path (no `::` and no `tests/` prefix), treat the whole `$ARGUMENTS` as a `-k` filter instead.
2.**Sanity-check the hub first** (so we're not measuring "nothing plugged in" N times): call `mcp__meshtastic__list_devices`. If the test name contains `nrf52` or `esp32s3` and the matching VID isn't present, stop and report — re-running won't help.
Capture: exit code, duration, and (on failure) the `Meshtastic debug` firmware log section from `mcp-server/tests/report.html`. `-p no:cacheprovider` suppresses pytest's `.pytest_cache` writes so iterations don't influence each other.
5. **On mixed outcomes**: diff the firmware log tails between a representative passing attempt and a representative failing attempt. Focus on:
- Error-level lines only present in failures (`PKI_UNKNOWN_PUBKEY`, `Alloc an err=`, `Skip send`, `No suitable channel`)
- Timing around the assertion event — did a broadcast go out, was there an ACK, did NAK fire?
- Device state fields that changed (nodesByNum entries, region/preset, channel_num)
Surface the top 3 differences as a "passes when / fails when" table. Don't dump full logs — pull specific lines with uptime timestamps.
5a. **Archive recorder slices per attempt** (no extra device interaction; the recorder runs autouse). Right after each attempt finishes, capture its `(start_ts, end_ts)` and call `mcp__meshtastic__recorder_export(start=<start>, end=<end>, dest_dir="mcp-server/tests/repro_artifacts/<safe-test-id>/attempt_<n>/")`. This drops a `logs.jsonl`, `telemetry.jsonl`, `packets.jsonl`, and `events.jsonl` snapshot scoped to the attempt window. Use these for cross-attempt diffs in step 5: `jq '.line' logs.jsonl` is faster than re-running the test, and the telemetry slice lets you compare heap behavior across attempts.
6. **Classify the flake** into one of:
- **LoRa airtime collision** → pass rate improves with fewer concurrent transmitters; propose a `time.sleep` gap or retry bump in the test body.
- **PKI key staleness** → fails on first attempt, passes after self-heal; existing retry loop in `test_direct_with_ack.py` handles this.
- **NodeInfo cooldown** → `Skip send NodeInfo since we sent it <600s ago` in fail-only logs; needs `broadcast_nodeinfo_ping()` warmup.
- **Hardware-specific** (one direction fails, other passes; one device's firmware is older; driver wedged) → specific recovery pointer. For a device that's wedged past `touch_1200bps`, the next escalation is `uhubctl_cycle(role=..., confirm=True)` to hard-power-cycle its hub port (requires `uhubctl` installed).
- **Device went dark mid-run** → fails from some attempt onward, never recovers, firmware log stops arriving. Almost always hardware: a Guru crash + frozen CDC. Hard-power-cycle via `uhubctl_cycle(role=..., confirm=True)` before the next iteration; if that also fails, escalate to replug.
- **Genuinely unknown** → say so; don't invent a root cause.
7. **Report back** with:
- Pass rate and mean duration.
- Classification + evidence (the specific log lines that support it).
- A suggested next step (re-run with specific args, open `/diagnose`, edit a specific test file, nothing).
- `/repro broadcast_delivers` — no `::`, no `tests/`, so interpreted as `-k broadcast_delivers`; runs every matching test the default 5 times.
- `/repro tests/telemetry/test_device_telemetry_broadcast.py 3` — shorter run for a slow test.
## Constraints
- Don't exceed `count=20` per invocation — airtime and USB wear add up. If the user asks for 50, negotiate down.
- Don't rebuild firmware as part of triage; flakes that only reproduce under different firmware belong in a separate session.
- If the FIRST attempt fails AND the rest all pass, that's a classic "state leak from a prior test" → say so and suggest running with `--force-bake` or starting from a clean state rather than chasing the first failure.
description:Run the mcp-server test suite (auto-detects devices) and interpret the results
argument-hint:[pytest-args]
---
# `/test` — mcp-server test runner with interpretation
Run `mcp-server/run-tests.sh` and make sense of the output so the operator doesn't have to.
## What to do
1.**Invoke the wrapper.** From the firmware repo root, run:
```bash
./mcp-server/run-tests.sh $ARGUMENTS
```
The wrapper auto-detects connected Meshtastic devices, maps each to its PlatformIO env, exports the required `MESHTASTIC_MCP_ENV_*` env vars, and invokes pytest. If the user passed no arguments, the wrapper supplies a sensible default set (`tests/ --html=tests/report.html --self-contained-html --junitxml=tests/junit.xml -v --tb=short`). A `--report-log=tests/reportlog.jsonl` arg is always appended (unless the operator passed their own). `--assume-baked` is deliberately NOT in the defaults — `test_00_bake.py` has its own skip-if-already-baked check and runs the ~8 s verification by default. Operators can opt into the fast path with `--assume-baked`, or force a reflash with `--force-bake`.
2. **Read the pre-flight header.** First ~6 lines print the detected hub (role → port → env). If that line reads `detected hub : (none)`, the wrapper will narrow to `tests/unit` only — say so explicitly in your summary so the operator knows hardware tiers were skipped.
3. **On pass**: one-line summary of the form `N passed, M skipped in <duration>`. Don't enumerate the test names — the user can read those. Do mention any SKIPPED tests and name the cause:
- `"role not present on hub"` → device unplugged; operator knows to reconnect.
- `"firmware not baked with USERPREFS_UI_TEST_LOG"` → tests/ui skipped because the macro isn't in firmware yet; suggest `--force-bake`.
- `"no PPPS-capable hubs detected"` → tests/recovery skipped because the hub doesn't support per-port power; the tier will never run on that setup.
- `"opencv-python-headless is not installed"` → tests/ui auto-deselected by run-tests.sh; suggest `pip install -e 'mcp-server/.[ui]'`.
4. **On failure**: for every FAILED test, open `mcp-server/tests/report.html` and extract the `Meshtastic debug` section for that test. pytest-html embeds the firmware log stream + device state dump there; the 200-line firmware log tail is usually enough to explain the failure. Summarise: which test, one-line assertion message, the firmware log lines that matter (things like `PKI_UNKNOWN_PUBKEY`, `Skip send NodeInfo`, `Error=`, `Guru Meditation`, `assertion failed`). For UI-tier failures also glance at `mcp-server/tests/ui_captures/<session>/<test>/transcript.md` — it records each step's frame + OCR.
- **Environmental**: device unreachable, port busy, CP2102 driver wedged. Suggest the specific recovery in escalation order: (a) replug USB, (b) `touch_1200bps(port=...)` + `pio_flash` for nRF52 DFU, (c) `uhubctl_cycle(role="nrf52", confirm=True)` when a device is fully wedged past DFU (needs `uhubctl` installed — `baked_single`'s auto-recovery hook does this once automatically). Also check `git status userPrefs.jsonc`.
- **Regression**: same assertion fails repeatedly, firmware log shows a new/unusual error. Surface the diff between expected and observed, identify the module likely responsible.
6. **Never run destructive recovery automatically.** If a failure looks like it needs a reflash, factory*reset, `uhubctl_cycle`, or USB replug, \_describe what to do* — don't execute. The operator decides.
## Arguments handling
- No args → wrapper's defaults (full suite).
- `$ARGUMENTS` passed verbatim to the wrapper, which passes them to pytest.
- The session fixture snapshots `userPrefs.jsonc` at session start and restores at teardown (plus on `atexit`). After a clean run, `git status userPrefs.jsonc` should be empty. If the wrapper's pre-flight printed a warning about a stale sidecar, call that out — means a prior session crashed.
- `mcp-server/tests/report.html` and `junit.xml` are regenerated on every run; the HTML is self-contained (shareable).
@@ -4,15 +4,16 @@ This document provides context and guidelines for AI assistants working with the
## Project Overview
Meshtastic is an open-source LoRa mesh networking project for long-range, low-power communication without relying on internet or cellular infrastructure. The firmware enables text messaging, location sharing, and telemetry over a decentralized mesh network.
Meshtastic is an open-source LoRa mesh networking project for long-range, low-power communication without relying on internet or cellular infrastructure. The firmware enables text messaging, location sharing, and telemetry over a decentralized mesh network. The project uses **C++17** as its language standard across all platforms.
### Supported Hardware Platforms
- **ESP32** (ESP32, ESP32-S3, ESP32-C3) - Most common platform
- **ESP32** (ESP32, ESP32-S3, ESP32-C3, ESP32-C6) - Most common platform
- **nRF52** (nRF52840, nRF52833) - Low power Nordic chips
- **RP2040/RP2350** - Raspberry Pi Pico variants
- **STM32WL** - STM32 with integrated LoRa
- **Linux/Portduino** - Native Linux builds (Raspberry Pi, etc.)
- **macOS native** - Headless `meshtasticd` on Apple Silicon / x86_64; see `variants/native/portduino/platformio.ini` for Homebrew prereqs + CH341 LoRa setup
### Supported Radio Chips
@@ -70,6 +71,70 @@ PKI (Public Key Infrastructure) messages have special handling:
- Accepted on a special "PKI" channel
- Allow encrypted DMs between nodes that discovered each other on downlink-enabled channels
## Encryption & Key Management
Meshtastic packets on the air are typically encrypted one of two ways: the **per-channel symmetric** layer (AES-CTR with a shared PSK) for broadcasts and channel traffic, and the **per-peer PKI** layer (X25519 ECDH → AES-256-CCM) for direct messages and remote admin. A channel with a 0-byte PSK (or Ham mode, which wipes PSKs) transmits cleartext — see the size table below. Both are implemented in `src/mesh/CryptoEngine.cpp`; the send/receive dispatch lives in `src/mesh/Router.cpp`; admin authorization lives in `src/modules/AdminModule.cpp`.
### High-level model
- **Channels** are symmetric rooms: anyone with the PSK can read any message on the channel. Channel 0 is the "primary" channel and ships with the short-form default PSK on factory devices, forming the public mesh most users join. (The LoRa modem preset `LONG_FAST` lives on `config.lora.modem_preset` and is an independent field — don't conflate "channel 0 default PSK" with the modem preset name.)
- **DMs** addressed to a single node require PKI so that other holders of the channel PSK can't read them. Outside Ham mode, Meshtastic does not fall back to channel-symmetric encryption when the destination public key is unknown.
- **Remote admin** is a DM carrying an `AdminMessage`. The receiver only acts on it if the sender's public key is on its allowlist (`config.security.admin_key[0..2]`).
- **Ham mode** (`owner.is_licensed=true`, where `owner` is the local `meshtastic_User` record) disables PKI entirely and sends cleartext — FCC Part 97 prohibits encryption on amateur bands.
- **No ratchet, no session.** Every packet is encrypted from scratch — a stateless design that matches the high-loss, store-and-forward nature of LoRa.
### Symmetric channel encryption (AES-CTR)
`CryptoEngine::encryptPacket` / `decrypt` / `encryptAESCtr` in `src/mesh/CryptoEngine.cpp`.
- **Cipher**: AES-CTR, AES-128 or AES-256 depending on key length. Same routine in both directions (CTR is a stream cipher, so encrypt == decrypt).
- **0 bytes** → no encryption, cleartext on the air
- **1 byte** → short-form index into the well-known `defaultpsk[]` in `src/mesh/Channels.h`. Index 0 = cleartext; 1 = defaultpsk unchanged; 2..255 = defaultpsk with its last byte incremented by (index − 1). This is what the CLI's `--ch-set psk default` produces.
- **16 bytes** → raw AES-128 key
- **32 bytes** → raw AES-256 key
- **2..15 bytes** → zero-padded to 16 and used as AES-128 (with a warn log); **17..31 bytes** → zero-padded to 32 and used as AES-256 (with a warn log). Defensive fallback for malformed PSK input, not something to rely on.
- **Nonce (128 bit)**: `packet_id` (u64 LE) ‖ `from_node` (u32 LE) ‖ `block_counter` (u32, starts at 0). Built in `CryptoEngine::initNonce`.
- **No AEAD**: channel packets carry no MAC, so the channel-hash byte is not an integrity or authenticity check. `Channels::getHash` is a 1-byte XOR-derived hint over the channel name bytes and PSK bytes that helps receivers pick a candidate channel/PSK for decryption. Because it is only a small hint and collisions are easy to find, it should be described purely as a PSK-selection aid, not as a security filter an attacker cannot bypass.
- **Channel 0 is special in one way only**: it's the channel the Router attempts PKI decryption on before falling through to AES-CTR. Non-zero channels always go straight to AES-CTR.
### PKI encryption for DMs (X25519 ECDH + AES-256-CCM)
`CryptoEngine::encryptCurve25519` / `decryptCurve25519` in `src/mesh/CryptoEngine.cpp`.
- **Keypair**: Curve25519 (aka X25519), 32-byte public + 32-byte private. Stored in `config.security.public_key` / `private_key`; the public half is mirrored into `owner.public_key` so it rides along in NodeInfo broadcasts and propagates through the mesh like any other identity field.
- **Key generation** (`generateKeyPair`): stirs `HardwareRNG::fill()` (64 B from platform TRNG when available), the 16-byte `myNodeInfo.device_id`, and a call to `random()` into the rweather/Crypto library's software RNG, then `Curve25519::dh1`. `regeneratePublicKey` recomputes the public half from a known private (used when restoring from backup).
- **Keygen entry points**: at boot, `NodeDB` calls `generateKeyPair` (or `regeneratePublicKey` when a stored private key is present and passes a low-entropy check) **directly** when `!owner.is_licensed` and `config.lora.region != UNSET`. `ensurePkiKeys` wraps the same logic for runtime/admin flows — it's the path `AdminModule::handleSetConfig` runs when first assigning a valid region or when security config is written; **do not assume it's the universal boot-time gate**, because the NodeDB path bypasses it.
- **Handshake**: `Curve25519::dh2(local_private, remote_public) → 32-byte shared secret → SHA-256 → 32-byte AES-256 key`. Recomputed per packet. The SHA-256 step is effectively a KDF over the raw ECDH output.
- **Cipher**: AES-256-CCM via `aes_ccm_ae` / `aes_ccm_ad` (`src/mesh/aes-ccm.cpp`). MAC length (the `M` parameter) is **8 bytes**. No AAD — the MAC covers ciphertext only.
- **Nonce (13 bytes / 104 bit)**: `aes_ccm_ae`/`aes_ccm_ad` use a 13-byte CCM nonce (`L = 2` is hardcoded in `src/mesh/aes-ccm.cpp`), not a 16-byte nonce. For PKI packets, `CryptoEngine::initNonce(fromNode, packetNum, extraNonce)` starts from the usual packet-derived nonce material, then overwrites nonce bytes `4..7` with a fresh 32-bit `extraNonce = random()`. The effective nonce bytes are therefore: bytes `0..3` = `packet_id`, bytes `4..7` = transmitted `extraNonce`, bytes `8..11` = `from_node`, byte `12` = `0x00`. The receiver reconstructs the same 13-byte nonce from the packet metadata plus the appended `extraNonce`.
- **Wire overhead**: 12 bytes appended to the ciphertext = 8-byte MAC ‖ 4-byte extraNonce. Defined as `MESHTASTIC_PKC_OVERHEAD = 12` in `src/mesh/RadioInterface.h`. Only the 4-byte `extraNonce` is sent; the rest of the 13-byte CCM nonce is reconstructed from packet fields as described above. The Router's send path checks this overhead against `MAX_LORA_PAYLOAD_LEN` before committing to PKI.
- **Send selection** (`Router::send`): the sender enters the PKI path when **all** hold — we're the originator AND not Ham mode AND not Portduino simradio AND not on the `serial`/`gpio` channels (unless the packet is already marked `pki_encrypted`) AND `config.security.private_key.size == 32` AND destination is a single node (not broadcast) AND the portnum isn't infrastructure. `TRACEROUTE_APP`, `NODEINFO_APP`, `ROUTING_APP`, and `POSITION_APP` are routed through channel encryption even when DMed (these need to be readable by relaying peers). Once on the PKI path, if the destination's public key isn't in our NodeDB the send **fails** with `PKI_SEND_FAIL_PUBLIC_KEY` — it does not silently fall back to channel encryption. If the client explicitly set `pki_encrypted=true` and any condition blocks PKI, the send fails with `PKI_FAILED`.
- **Receive selection** (`Router::perhapsDecode`): try PKI decrypt first when `channel == 0` AND `isToUs(p)` AND not broadcast AND both peers have public keys in NodeDB AND `rawSize > MESHTASTIC_PKC_OVERHEAD`. On success the packet gets `pki_encrypted=true` stamped and the sender's public key copied into `p->public_key` for downstream authorization.
### Remote admin authorization
Implemented in `src/modules/AdminModule.cpp` → `handleReceivedProtobuf`. The authorization check runs in this order:
1.**Response messages** — if `messageIsResponse(r)` is true (the payload is a response to one of our earlier admin requests), it's accepted without any further check. The in-file comment flags this as a known-untightened gap: a stricter implementation would remember which `public_key` we last queried and reject responses that don't match.
2.**Local admin** — `mp.from == 0` (phone app over BLE, serial CLI, internal module); never travels over the air. **Rejected** if `config.security.is_managed` is true, because managed devices expect admin to arrive over the air through an authorized remote path.
3.**Legacy admin channel (deprecated)** — the packet arrived on a channel named literally `"admin"`. Gated by `config.security.admin_channel_enabled`; returns `NOT_AUTHORIZED` if the flag is false. Kept for backward compatibility; new deployments should use PKI admin.
4.**PKI admin (preferred for remote)** — `mp.pki_encrypted == true` AND `mp.public_key` matches one of `config.security.admin_key[0..2]` (up to three authorized 32-byte Curve25519 public keys, typically copied from the admin node's own `user.public_key`).
5.**Fallthrough** → `NOT_AUTHORIZED`.
On top of authorization, any remote admin message that **mutates** state (not a request, not a response) also has to pass a session-key check (`checkPassKey`): the client must first pull a fresh 8-byte `session_passkey` via `get_admin_session_key_request`, then echo that passkey back in the mutating message. The device rotates the passkey after 150 s and rejects values older than 300 s — a narrow anti-replay window on top of the PKI layer.
`config.security.is_managed = true` disables **local** admin writes (`mp.from == 0` is rejected). It does not by itself force every admin action through PKI — the legacy `"admin"` channel still authorizes remote admin when `config.security.admin_channel_enabled == true`. The AdminModule refuses to persist `is_managed=true` unless at least one `admin_key` is populated — a deliberate guard against operators locking themselves out.
### Key-rotation hazards (actions that invalidate peers)
- **`factory_reset_device`** (the "full" variant, calls `NodeDB::factoryReset(eraseBleBonds=true)`) → **wipes** the X25519 private key; a fresh keypair is generated on the next region-set. Every existing peer holds the old public key, so DMs to this node silently fail PKI decrypt until every peer re-exchanges NodeInfo.
- **`factory_reset_config`** (the "partial" variant, calls `NodeDB::factoryReset()` with `eraseBleBonds=false`) → **preserves** the X25519 private key in `installDefaultConfig(preserveKey=true)`; the public key is zeroed and gets rebuilt from the preserved private key on the next boot via the NodeDB path's `regeneratePublicKey` call. Identity is preserved and the mesh does not need to re-exchange keys.
- **`region=UNSET → valid region`** → `ensurePkiKeys` runs inside the same `handleSetConfig` path; missing keys get generated at that moment.
- **Ham mode transitions** — entering Ham mode (`user.is_licensed=true`) runs `Channels::ensureLicensedOperation`, which **wipes every channel PSK** (all traffic becomes cleartext) and disables the legacy admin channel. The X25519 private key is preserved on the device but not used because `Router::send` skips PKI when `owner.is_licensed` is true. Leaving Ham mode re-enables PKI with the preserved keypair but does not restore the wiped channel PSKs — the operator has to re-set them.
- **Channel 0 PSK change** → every peer must re-learn the channel hash; cached NodeInfo becomes temporarily unreachable until the next broadcast.
- **`security.private_key` blanked via admin** → regenerates both halves (unless in Ham mode) and propagates the new public key via NodeInfo.
- Follow existing code style - run `trunk fmt` before commits
- Prefer `LOG_DEBUG`, `LOG_INFO`, `LOG_WARN`, `LOG_ERROR` for logging
- Use `assert()` for invariants that should never fail
- C++17 features are available (`std::optional`, structured bindings, `if constexpr`, etc.)
- **Keep code comments minimal — one or two lines, max.** Comment only when the _why_ isn't obvious from the code; never restate what the next line does. No multi-paragraph block comments explaining straightforward changes. The diff and commit message carry the rationale; the code carries the behavior.
- **Use `Throttle` for time-based rate limiting, not raw `millis()` math.** `src/mesh/Throttle.h` provides `Throttle::isWithinTimespanMs(lastMs, intervalMs)` (returns true while inside the cooldown) and `Throttle::execute(&lastMs, intervalMs, func)` (function-pointer form that updates the timestamp on fire). Use these for any "did N ms pass since X" check — raw `millis() > lastMs + N` is rollover-unsafe (breaks after ~49.7 days) and inconsistent with the rest of the codebase. The helpers compute `now - lastMs` with unsigned subtraction, which wraps correctly.
### Naming Conventions
@@ -118,70 +211,151 @@ firmware/
#### Module System
Modules inherit from `MeshModule` or `ProtobufModule<T>` and implement:
Modules use a three-tier class hierarchy:
-`handleReceivedProtobuf()` - Process incoming packets
-`allocReply()` - Generate response packets
-`runOnce()` - Periodic task execution (returns next run interval in ms)
1.**`MeshModule`** - Base class. Implement `wantPacket()` and `handleReceived()`. Returns `ProcessMessage::STOP` or `ProcessMessage::CONTINUE`.
2.**`SinglePortModule`** - Handles a single portnum. Simplified `wantPacket()` that checks `decoded.portnum`.
3.**`ProtobufModule<T>`** - Template for protobuf-based modules. Handles encoding/decoding automatically.
Most modules also inherit from **`OSThread`** for periodic tasks (the "mixin" pattern):
-`Default::getConfiguredOrDefaultMsScaled(configured, default, numNodes)` - Scales based on network size
#### Thread Safety
- Use `concurrency::Lock` for mutex protection
- Use `concurrency::Lock` and `concurrency::LockGuard` for mutex protection
- Radio SPI access uses `SPILock`
- Prefer `OSThread` for background tasks
### Hardware Detection
`src/detect/ScanI2C` automatically enumerates 80+ I2C device types at boot including displays, sensors, RTCs, keyboards, PMUs, and touch controllers. This drives automatic initialization of the correct drivers.
### Graphics/UI System
Multiple display driver families in `src/graphics/`:
- Read-only, static display optimized for minimal refreshes and low power
- Configured per-variant via `nicheGraphics.h`
- Separate PlatformIO config: `src/graphics/niche/InkHUD/PlatformioConfig.ini`
### Input System
`src/input/InputBroker` is the centralized input event dispatcher. Supports multiple input sources: buttons, keyboards (BBQ10, Cardputer, TCA8418), touch screens, rotary encoders, and matrix keyboards.
### Power Management
`src/PowerFSM.*` implements a finite state machine with states: `stateON`, `statePOWER`, `stateSERIAL`, `stateDARK`. Key events: `EVENT_PRESS`, `EVENT_WAKE_TIMER`, `EVENT_LOW_BATTERY`, `EVENT_RECEIVED_MSG`, `EVENT_SHUTDOWN`. Conditionally excluded with `MESHTASTIC_EXCLUDE_POWER_FSM` (falls back to `FakeFsm`).
### Motion Sensors
`src/motion/AccelerometerThread` provides background motion monitoring with automatic screen wake and double-tap button press detection. Supports 10+ accelerometer/gyroscope chips (BMA423, BMI270, MPU6050, LIS3DH, LSM6DS3, STK8XXX, QMA6100P, ICM20948, BMX160).
### Telemetry Sensor Library
`src/modules/Telemetry/Sensor/` contains 50+ I2C sensor drivers organized by category:
`src/mesh/api/` provides a template-based `ServerAPI` for client communication over WiFi (`WiFiServerAPI`) and Ethernet (`ethServerAPI`). Default port: **4403**. HTTP server in `src/mesh/http/`. JSON serialization in `src/serialization/MeshPacketSerializer`.
### Hardware Variants
Each hardware variant has:
-`variant.h` - Pin definitions and hardware capabilities
Separate pytest suite that exercises real USB-connected Meshtastic devices. See the **MCP Server & Hardware Test Harness** section below for invocation, tier layout, and agent usage rules.
## MCP Server & Hardware Test Harness
The `mcp-server/` directory houses a firmware-aware [MCP](https://modelcontextprotocol.io/) server plus a pytest-based integration suite. AI agents that speak MCP get a well-defined tool surface for flashing, configuring, and inspecting physical Meshtastic devices — use it instead of hand-rolling `pio` or `meshtastic --port` calls where possible. `mcp-server/README.md` is the operator-facing setup doc; this section is the agent-facing usage contract.
The repo registers the server via `.mcp.json` at the repo root — Claude Code picks it up automatically once `mcp-server/.venv/` is built (`cd mcp-server && python3 -m venv .venv && .venv/bin/pip install -e '.[test]'`).
| Run the regression suite | `./mcp-server/run-tests.sh` (or `/test` slash command) |
| Diagnose a specific device | `/diagnose [role]` slash command (read-only) |
| Triage a flaky test | `/repro <node-id> [count]` slash command |
**One MCP call per port at a time.**`SerialInterface` holds an exclusive OS-level lock on the serial port for its lifetime. If a `serial_*` session is open on `/dev/cu.usbmodem101`, calling `device_info` on the same port will fail fast pointing at the active session. Sequence calls: open → read/mutate → close, then next device. Never parallelize tool calls on the same port.
### MCP tool surface (43 tools)
Grouped by purpose. Full argument shapes in `mcp-server/README.md`; a few high-value signatures are called out here.
- **USB power control** (via `uhubctl`, per-port PPPS toggle): `uhubctl_list` (read-only), `uhubctl_power(action='on'|'off', confirm=True)`, `uhubctl_cycle(delay_s, confirm=True)`. Target by raw `(location, port)` or by `role` (`"nrf52"`, `"esp32s3"`); role lookup checks `MESHTASTIC_UHUBCTL_LOCATION_<ROLE>` + `_PORT_<ROLE>` env vars first, falls back to VID auto-detection.
- **Observability** (UI tier + operator ad-hoc): `capture_screen(role, ocr=True)` — grabs a USB-webcam frame of the device OLED and optionally OCRs it. Requires `mcp-server[ui]` extras (`opencv-python-headless`, `easyocr`) and `MESHTASTIC_UI_CAMERA_DEVICE_<ROLE>` env var; falls through to a 1×1 black PNG `NullBackend` when unconfigured.
`confirm=True` is a tool-level gate on top of whatever permission prompt your MCP host shows. **Don't bypass it** by asking the host to auto-approve — it exists specifically because MCP hosts sometimes remember "always allow this tool" and that's dangerous for `factory_reset`, `erase_and_flash`, `uhubctl_power(action='off')`, and `uhubctl_cycle`.
**TCP / native-host nodes.** Setting `MESHTASTIC_MCP_TCP_HOST=<host[:port]>` makes `list_devices` surface a `meshtasticd` daemon (e.g. the `native-macos` build) as a synthetic `tcp://host:port` entry, and `connect()` routes through `meshtastic.tcp_interface.TCPInterface` instead of `SerialInterface`. Every read/write/admin tool that flows through `connect()` works against the daemon transparently. USB-only tools (`pio_flash`, `erase_and_flash`, `update_flash`, `touch_1200bps`, `serial_open`, `esptool_*`, `nrfutil_*`, `picotool_*`) raise a clear `ConnectionError` when handed a `tcp://` port; `pio_flash` against a `native*` env raises a `FlashError` (no upload step — use `build` and run the binary directly). The pytest harness still assumes USB-attached devices per role; TCP-aware fixtures are deferred. See `mcp-server/README.md` § "TCP / native-host nodes".
### Hardware test suite (`mcp-server/run-tests.sh`)
The wrapper auto-detects connected devices (VID → role map: `0x239A` → `nrf52`, `0x303A`/`0x10C4` → `esp32s3`), maps each role to a PlatformIO env (`nrf52` → `rak4631`, `esp32s3` → `heltec-v3`, overridable via `MESHTASTIC_MCP_ENV_<ROLE>`), then invokes pytest. Zero pre-flight config needed from the operator.
Suite tiers (collected + run in this order via `pytest_collection_modifyitems`):
1.`tests/unit/` — pure Python (boards parse, pio wrapper, userPrefs parse, testing profile, uhubctl parser). No hardware.
2.`tests/test_00_bake.py` — flashes each detected device with current `userPrefs.jsonc` merged with the session's test profile. Has its own skip-if-already-baked check comparing region + primary channel to the session profile; skips cheaply on warm devices.
3.`tests/mesh/` — multi-device mesh: bidirectional send, broadcast delivery, direct-with-ACK, mesh formation within 60s. Parametrized `[nrf52->esp32s3]` and `[esp32s3->nrf52]`. Includes `test_peer_offline_recovery` which uses uhubctl to physically power off one peer mid-conversation (requires uhubctl; skips without).
6.`tests/recovery/` — `uhubctl` power-cycle round-trip + NVS persistence across hard reset. Requires `uhubctl` installed and a PPPS-capable hub; entire tier auto-skips otherwise.
7.`tests/ui/` — input-broker-driven screen navigation with camera + OCR evidence.
8.`tests/fleet/` — PSK seed session isolation.
9.`tests/admin/` — channel URL roundtrip, owner persistence across reboot.
10.`tests/provisioning/` — region + modem + slot bake, admin key presence, `UNSET` region blocks TX, userPrefs survive factory reset.
Invocation patterns:
```bash
./mcp-server/run-tests.sh # full suite (auto-bake-if-needed)
./mcp-server/run-tests.sh --force-bake # reflash before testing
./mcp-server/run-tests.sh --assume-baked # skip bake (caller vouches for device state)
./mcp-server/run-tests.sh tests/mesh # one tier
./mcp-server/run-tests.sh tests/mesh/test_direct_with_ack.py # one file
./mcp-server/run-tests.sh -k telemetry # name filter
```
**No hardware detected?** The wrapper auto-narrows to `tests/unit/` only and prints `detected hub : (none)` in the pre-flight header. Agents interpreting the output should call this out explicitly — a 52-test green run without hardware is qualitatively different from a 12-unit-test green run.
**Artifacts every run produces:**
-`mcp-server/tests/report.html` — self-contained pytest-html. Each test gets a `Meshtastic debug` section with the tail of firmware log + device state dump. **Open this first** on failures; it's the canonical evidence source.
-`mcp-server/tests/junit.xml` — CI-parseable.
-`mcp-server/tests/reportlog.jsonl` — pytest-reportlog stream (`$report_type` keyed JSONL). Consumed by the live TUI.
-`mcp-server/tests/fwlog.jsonl` — firmware log mirror from the `meshtastic.log.line` pubsub topic. Populated by the `_firmware_log_stream` autouse session fixture.
### Live TUI (`meshtastic-mcp-test-tui`)
A Textual-based live view that wraps `run-tests.sh`. Tails reportlog for per-test state, streams firmware logs, polls device state at startup + post-run (gated out of the active run because `hub_devices` holds exclusive port locks). Key bindings:
.venv/bin/meshtastic-mcp-test-tui tests/mesh # args pass through to pytest
```
The plain CLI stays primary; the TUI is for operators who want a live dashboard. Both consume the same `run-tests.sh`.
### Slash commands (Claude Code + Copilot)
Three AI-assisted workflows wrap the test harness. Claude Code operators get `/test`, `/diagnose`, `/repro`; Copilot operators get `/mcp-test`, `/mcp-diagnose`, `/mcp-repro`. Bodies:
- **Interpret failures, don't just echo them.** Pull firmware log tails from `report.html` and classify each failure as transient / environmental / regression. Use the exact format in `.claude/commands/test.md`.
- **No destructive writes without operator approval.** Any skill that could reflash, factory-reset, or reboot a device must describe the action and stop. The operator authorizes.
- **Sequential MCP calls per port.** See above.
- **"Unknown" is a valid classification.** If evidence doesn't support a root cause, say so and list what would disambiguate. Do not invent.
- **`_session_userprefs`** (autouse session) — snapshots `userPrefs.jsonc` at session start, merges the session test profile via `userprefs.merge_active(test_profile)`, restores at teardown. Four layers of safety: pytest teardown + `atexit` + sidecar file (`userPrefs.jsonc.mcp-session-bak`) + startup self-heal in `run-tests.sh`. **Do not edit `userPrefs.jsonc` from inside a test.**
- **`_firmware_log_stream`** (autouse session) — subscribes to `meshtastic.log.line` pubsub on every connected `SerialInterface` and mirrors lines to `tests/fwlog.jsonl`. Drives the TUI firmware-log pane.
- **`_debug_log_buffer`** (autouse per-test) — captures last 200 firmware log lines + device state for attachment to the pytest-html `Meshtastic debug` section on failure.
- **`hub_devices`** (session) — `dict[role, SerialInterface]` with session-long exclusive port locks. Reason the TUI's device poller is gated to startup + post-run only.
- **`baked_mesh`** — parametrized mesh-pair fixture; depends on `test_00_bake`. `pytest_generate_tests` in `conftest.py` auto-generates `[nrf52->esp32s3]` and `[esp32s3->nrf52]` variants.
- **`test_profile`** — session-scoped dict: region, primary channel, admin key, PSK seed. Derived from `MESHTASTIC_MCP_SEED` (defaults to `mcp-<user>-<host>`).
### Firmware integration points tied to the test harness
Two firmware changes exist specifically so the test harness works reliably. **Keep these in mind when touching related code.**
- **`src/mesh/StreamAPI.cpp` + `StreamAPI.h`** — `emitLogRecord` uses a dedicated `fromRadioScratchLog` + `txBufLog` pair and a `concurrency::Lock streamLock`. Before this fix, `debug_log_api_enabled=true` would tear `FromRadio` protobufs on the serial transport because `emitTxBuffer` and `emitLogRecord` shared a single scratch buffer. The conftest enables the log stream session-wide; without this fix the device would corrupt its own FromRadio replies mid-session.
- **`src/mesh/PhoneAPI.cpp`** — `ToRadio``Heartbeat(nonce=1)` triggers `nodeInfoModule->sendOurNodeInfo(NODENUM_BROADCAST, true, 0, true)` for serial clients, mirroring the pre-existing behavior for TCP/UDP clients in `PacketAPI.cpp`. The mesh tests rely on this to force a NodeInfo broadcast right after connect so the peer discovers them before the test's first assertion.
If you're modifying `StreamAPI`, `PhoneAPI`, `NodeInfoModule`, or `userPrefs` flow, run `./mcp-server/run-tests.sh` at minimum before asking for review.
| `userPrefs.jsonc` dirty after test run | `git status --porcelain userPrefs.jsonc` | If non-empty, re-run `./mcp-server/run-tests.sh` once — the pre-flight self-heal restores from sidecar. If still dirty, `git checkout userPrefs.jsonc`. |
| Port busy / wedged CP2102 on macOS | `lsof /dev/cu.usbserial-0001` | Kill the holder. USB replug if the kernel still reports busy. Often a stale `pio device monitor` or zombie `meshtastic_mcp` process. |
| nRF52 appears unresponsive | `list_devices` shows VID `0x239A` but `device_info` times out | `touch_1200bps(port=...)` drops it into the DFU bootloader → `pio_flash` re-installs. |
| Device fully wedged (Guru Meditation, frozen CDC, no DFU) | `list_devices` shows the VID but every admin call times out | `uhubctl_cycle(role="nrf52", confirm=True)` hard-power-cycles the port via USB hub PPPS. `baked_single`'s auto-recovery hook does this once automatically if uhubctl is installed. Falls back to physical replug if no PPPS hub. |
| Multiple MCP server processes | `ps aux \| grep meshtastic_mcp` shows >1 | Kill all but the one your MCP host spawned. Zombies hold ports and break tests. |
| Mesh formation fails, one side sees peer but other doesn't | `/diagnose` (or `list_nodes` on both sides) | Asymmetric NodeInfo. `test_direct_with_ack` has a heal path; `/repro` it a few times. If persistent, both devices' clocks may be out of sync with their NodeInfo cooldown. |
| "role not present on hub" in skip reasons | `list_devices` | Expected if a device is unplugged. Reconnect before re-running the tier. |
| Entire `tests/recovery/` tier skipped | `command -v uhubctl` | Expected if `uhubctl` isn't on PATH. Install via `brew install uhubctl` (macOS) or `apt install uhubctl` (Debian/Ubuntu). Also skips if no hub advertises PPPS. |
| Entire `tests/ui/` tier skipped ("firmware not baked with USERPREFS_UI_TEST_LOG") | reportlog.jsonl for the skip reason | Re-run with `--force-bake` so the UI-log macro gets compiled into the fresh firmware. First run after the Round-3 landing always re-bakes. |
| `tests/ui/` runs but captures are all 1×1 black PNGs | `MESHTASTIC_UI_CAMERA_DEVICE_ESP32S3` | Env var not set → `NullBackend`. Point a USB webcam at the heltec-v3 OLED and set the device index; `.venv/bin/python -c "import cv2; [print(i, cv2.VideoCapture(i).read()[0]) for i in range(5)]"` discovers it. |
| Tests fail only on first attempt then pass on rerun | — | State leak from a prior session. Run with `--force-bake` to reset to a known state. |
### Never do these without asking
-`factory_reset` — wipes node identity; regenerates PKI keypair. Mesh peers will reject old DMs until re-exchange. Legitimate only when the operator explicitly wants it.
-`erase_and_flash` — full chip erase; destroys all on-device state.
description:Device health report via the meshtastic MCP tools (Copilot equivalent of the Claude Code /diagnose slash command)
---
# `/mcp-diagnose` — device health report
Equivalent of `.claude/commands/diagnose.md`. Use when the operator asks to "check the devices", "what's the mesh looking like", "is nrf52 alive", etc.
This prompt assumes the meshtastic MCP server is registered with your VS Code Copilot agent. If it isn't, fall back to running `./mcp-server/run-tests.sh tests/unit` plus a short `device_info` script via the terminal.
## What to do
1.**Enumerate hardware** via the `list_devices` MCP tool (with `include_unknown=True`). For each entry where `likely_meshtastic=True`, capture `port`, `vid`, `pid`, `description`.
2.**Apply the operator's filter** (if any):
- No filter → every likely-meshtastic device.
-`nrf52` → `vid == 0x239a`
-`esp32s3` → `vid == 0x303a` or `vid == 0x10c4`
- A `/dev/cu.*` path → only that port.
- Anything else → substring match on port.
3.**For each selected device, in sequence (don't parallelize — SerialInterface holds an exclusive port lock):**
- If anything looks off (can't connect, `num_nodes` wrong, missing `firmware_version`), open a short firmware-log window: `serial_open(port=<p>, env=<inferred>)`, wait 3 seconds, `serial_read(session_id, max_lines=100)`, `serial_close(session_id)`. Infer env from VID (0x239a → `rak4631`, 0x303a/0x10c4 → `heltec-v3`) unless an `MESHTASTIC_MCP_ENV_<ROLE>` env var overrides it.
4.**Hub health** (call once, not per-device): `uhubctl_list()` — enumerates every USB hub the host sees. Cross-reference each Meshtastic device's VID to find which hub + port it's on. Flag in the report if:
- No hub advertises `ppps=true` → `tests/recovery/` can't run; hard-recovery via `uhubctl_cycle` isn't available.
- A Meshtastic device is on a non-PPPS hub → note it; moving to a PPPS hub unlocks auto-recovery.
-`uhubctl_list` raises `ConfigError: uhubctl not found` → report as "uhubctl not installed"; don't treat as a device fault.
5.**Render per-device report** as a compact block:
Flag abnormalities inline with `⚠︎ <short reason>` — missing pubkey on a known peer, region UNSET, mismatched channel name, device on non-PPPS hub, etc.
- Do `region`, `channel_num`, `modem_preset` match across devices?
- Do the primary channel names match? (Different name → different PSK → no decode.)
7. **Suggest next steps only for recognizable failure modes**, never speculatively:
- Stale PKI one-way → "`/mcp-test tests/mesh/test_direct_with_ack.py` — the test's retry+nodeinfo-ping heals this."
- Region mismatch → "re-bake one side via `./mcp-server/run-tests.sh --force-bake`."
- Device unreachable, DFU reachable → `touch_1200bps(port=...)` + `pio_flash`. If not even DFU responds and the device is on a PPPS hub, escalate to `uhubctl_cycle(role=..., confirm=True)`.
- CP2102-wedged-driver on macOS → see `run-tests.sh` notes.
## Hard constraints
- **Read-only.** No `set_config`, no `reboot`, no `factory_reset`, no `flash`. If the operator wants mutation, they'll escalate explicitly.
- **Open/query/close per device.** Never hold multiple SerialInterfaces to the same port. The port lock is exclusive.
- **Don't infer env beyond the VID map** — if the operator has an unusual board, ask them which env to use rather than guessing.
description:Re-run a specific test N times to triage flakes; diff firmware logs between passes and failures (Copilot equivalent of the Claude Code /repro slash command)
---
# `/mcp-repro` — flakiness triage for one test
Equivalent of `.claude/commands/repro.md`. Use when the operator says "that one test is flaky — dig in", "repro the direct_with_ack failure", "why does X sometimes fail?".
## What to do
1.**Parse the operator's input** into two pieces:
- **Test identifier** — either a pytest node id (has `::` or starts with `tests/`) or a `-k`-style filter (plain substring like `direct_with_ack`).
- **Count** — integer, default `5`, cap at `20`. If the operator asks for 50, negotiate down and explain (airtime + USB wear).
2.**Sanity-check the hub** via the `list_devices` MCP tool. If the test name references `nrf52` or `esp32s3` and the matching VID isn't present, stop and report — re-running won't help.
`-p no:cacheprovider` keeps pytest from caching anything between iterations. Capture: exit code, duration, and (on failure) the `Meshtastic debug` firmware-log section from `mcp-server/tests/report.html`.
5. **On mixed outcomes, diff the firmware logs** between one representative pass and one representative fail. Focus on:
- Error-level lines present only in failures (`PKI_UNKNOWN_PUBKEY`, `Alloc an err=`, `Skip send`, `No suitable channel`, `NAK`)
- Timing around the assertion point (broadcast sent? ACK received? retry fired?)
- Device-state fields that changed between attempts
Surface the top 3 differences as a compact "passes when / fails when" table with uptime timestamps. Don't dump full logs.
6. **Classify** the flake into one of:
- **LoRa airtime collision** — pass rate improves with fewer concurrent transmitters. Suggest a `time.sleep` gap or retry bump in the test body.
- **PKI key staleness** — first attempt fails, subsequent ones pass; existing retry-loop pattern in `test_direct_with_ack.py` is the fix.
- **NodeInfo cooldown** — `Skip send NodeInfo since we sent it <600s ago` in fail-only logs; needs a `broadcast_nodeinfo_ping()` warmup.
- **Hardware-specific** — one direction consistently fails, firmware versions differ, CP2102 driver wedged, etc. For a device wedged past `touch_1200bps`, recommend `uhubctl_cycle(role=..., confirm=True)` to hard-power-cycle its hub port (requires `uhubctl` installed).
- **Device went dark mid-run** — fails from some iteration onward and never recovers; firmware log stops arriving. Almost always a Guru crash with frozen CDC. Recommend `uhubctl_cycle` before the next iteration; escalate to replug if that also fails.
- **Unknown** — say so. Don't invent a root cause.
7. **Report back** with:
- Pass rate + mean duration.
- Classification + the specific log evidence for it.
- A concrete next step (tighter assertion, more retries, open `/mcp-diagnose`, file a bug, nothing).
## Examples
- `tests/mesh/test_direct_with_ack.py::test_direct_with_ack_roundtrip[esp32s3->nrf52] 10` — 10 runs of that parametrized case.
- `broadcast_delivers` — no `::`, no `tests/`; treat as `-k broadcast_delivers`; runs every match 5 times.
- `tests/telemetry/test_device_telemetry_broadcast.py 3` — shorter count for a slow test.
## Notes
- If the FIRST attempt fails and the rest pass, that's a state-leak signature — suggest starting from `--force-bake` or a clean device state rather than chasing the first-failure firmware logs.
- If ALL N fail, this isn't a flake — it's a regression. Say so, stop iterating, escalate to `/mcp-test` for full-suite context.
- Don't rebuild firmware during triage. Flakes that only reproduce under different firmware belong in a separate session with a plan.
description:Run the mcp-server test suite and interpret results (Copilot equivalent of the Claude Code /test slash command)
---
# `/mcp-test` — mcp-server test runner with interpretation
Equivalent of the Claude Code `/test` slash command in `.claude/commands/test.md`. Use this when the operator asks you to "run the tests", "check the mcp test suite", "run the mesh tests", etc.
## What to do
1.**Invoke the wrapper** from the firmware repo root:
```bash
./mcp-server/run-tests.sh [pytest-args]
```
If the operator specified a subset (e.g. "just the mesh tests"), pass it through as `tests/mesh` or a pytest `-k filter`. If they said nothing, use the wrapper's defaults (full suite with pytest-html report).
The wrapper auto-detects connected Meshtastic devices, maps each to its PlatformIO env, exports the required env vars, and invokes pytest. Zero pre-flight config needed from the operator.
2. **Read the pre-flight header** (first few lines of wrapper output). The `detected hub :` line lists role → port → env mappings. If it reads `(none)`, the wrapper narrowed to `tests/unit` only — call that out explicitly so the operator knows hardware tiers were skipped.
3. **On pass**: one-line summary like `N passed, M skipped in <duration>`. Don't enumerate test names. DO mention any non-placeholder SKIPs and name the cause:
- `"role not present on hub"` → device unplugged; operator should reconnect.
- `"firmware not baked with USERPREFS_UI_TEST_LOG"` → tests/ui skipped; the UI-log compile macro isn't in the baked firmware. Suggest `--force-bake`.
- `"no PPPS-capable hubs detected"` → tests/recovery skipped because the attached hub doesn't support per-port power switching; won't run on that setup.
- `"opencv-python-headless is not installed"` → tests/ui auto-deselected by `run-tests.sh`. Suggest `pip install -e 'mcp-server/.[ui]'`.
4. **On failure**: open `mcp-server/tests/report.html` (pytest-html output, self-contained) and extract the `Meshtastic debug` section for each failed test. That section includes a firmware log stream (last 200 lines) and device state dump. For each failure, summarise:
- test name
- one-line assertion message
- the specific firmware log lines that explain why (look for `PKI_UNKNOWN_PUBKEY`, `Skip send NodeInfo`, `Error=`, `Guru Meditation`, `assertion failed`, `No suitable channel`)
- for UI-tier failures also check `mcp-server/tests/ui_captures/<session>/<test>/transcript.md` (per-step frame + OCR)
5. **Classify each failure** as one of:
- **Transient flake** — LoRa collision, first-attempt NAK with self-heal pattern, timing-sensitive assertion. Suggest `/mcp-repro <test-id>` to confirm.
- **Environmental** — device unreachable, port busy, CP2102 driver wedged on macOS. Suggest recovery in escalation order: (a) replug USB, (b) `touch_1200bps` + `pio_flash` for nRF52 DFU, (c) `uhubctl_cycle(role=..., confirm=True)` for a device wedged past DFU (needs `uhubctl` installed; `baked_single` does this once automatically when available). Also check `git status userPrefs.jsonc`.
- **Regression** — same assertion fails repeatedly on re-runs, firmware log shows novel errors. Identify the firmware module likely responsible.
6. **Do NOT run destructive recovery automatically**. If a failure looks like it needs a reflash, factory*reset, `uhubctl_cycle`, or replug — \_describe the steps* and let the operator decide. Never burn airtime or flash cycles without approval.
## Arguments convention
Operators generally invoke this prompt either with no arguments (full suite) or with a specific subset. Examples:
- `tests/mesh` — one tier
- `tests/mesh/test_direct_with_ack.py::test_direct_with_ack_roundtrip` — one test
- `--force-bake` — reflash devices first
- `-k telemetry` — name-filter
## Side-effects to confirm in your summary
- `userPrefs.jsonc` should be clean after a successful run. The session fixture in `mcp-server/tests/conftest.py` (`_session_userprefs`) snapshots and restores. Check `git status --porcelain userPrefs.jsonc` and report if it's non-empty.
- `mcp-server/tests/report.html` and `junit.xml` regenerate on every run.
- The wrapper prints a warning if a `.mcp-session-bak` sidecar was left over from a crashed prior session and auto-restores from it — mention that if it happened.
Guide for developing a new Meshtastic firmware module.
## Module Hierarchy
Choose the appropriate base class:
1.**`MeshModule`** — Raw base class. Override `wantPacket()` and `handleReceived()`. Returns `ProcessMessage::STOP` or `ProcessMessage::CONTINUE`.
2.**`SinglePortModule`** — Handles a single `meshtastic_PortNum`. Constructor takes `(name, portNum)`. Simplified `wantPacket()` checking `decoded.portnum`. Use `allocDataPacket()` to create outgoing packets.
3.**`ProtobufModule<T>`** — Template for protobuf-encoded modules. Constructor takes `(name, portNum, fields)`. Override `handleReceivedProtobuf()`. Use `allocDataProtobuf(payload)` to create outgoing packets.
Most modules also mix in `concurrency::OSThread` for periodic background tasks.
Add a `MESHTASTIC_EXCLUDE_MYMODULE` guard. This allows the module to be excluded from constrained builds. The flag name must follow the pattern: `MESHTASTIC_EXCLUDE_` + uppercase module name.
## Protobuf Messages (if needed)
1. Define messages in `protobufs/meshtastic/` (e.g., `mymodule.proto`)
2. Add a `.options` file for nanopb field size constraints
3. Regenerate with `bin/regen-protos.sh`
4. Generated code appears in `src/mesh/generated/meshtastic/`
5. Assign a `meshtastic_PortNum` if the module uses a new port number
## Timing and Defaults
Use `Default` class helpers for configurable intervals:
Guide for adding a new I2C telemetry sensor driver to Meshtastic firmware.
## Overview
Telemetry sensors live in `src/modules/Telemetry/Sensor/`. There are 50+ existing drivers organized by measurement type. Each sensor integrates with one of the telemetry modules:
If the sensor needs an external library, add it to the `lib_deps` in the relevant base platformio.ini configs:
```ini
lib_deps=
${env.lib_deps}
mysensorlibrary@^1.0.0
```
Or use a conditional dependency if it's platform-specific.
## Unit Conversions
If the sensor reports values in non-standard units, use `src/modules/Telemetry/UnitConversions.h` for conversion helpers (e.g., Celsius ↔ Fahrenheit, hPa ↔ inHg).
## Checklist
- [ ] Create `src/modules/Telemetry/Sensor/MySensor.h` and `.cpp`
- [ ] Inherit from `TelemetrySensor` base class
- [ ] Implement `setup()` and `getMetrics()` methods
- [ ] Add `meshtastic_TelemetrySensorType` enum entry in `telemetry.proto`
- [ ] Add I2C address to `src/detect/ScanI2C` for auto-detection
- [ ] Add protobuf fields in `telemetry.proto` if new data types needed
- [ ] Regenerate protos: `bin/regen-protos.sh`
- [ ] Wire into the appropriate telemetry module (Environment/AirQuality/Power/Health)
- [ ] Add library dependency if external library required
- Architecture names are normalized (e.g., `esp32s3` → `esp32-s3`)
## InkHUD E-Ink Variants
For e-ink display variants using the InkHUD framework, add `nicheGraphics.h`:
```cpp
// nicheGraphics.h — InkHUD configuration for this variant
#define INKHUD // Enable InkHUD
// Configure display, applets, and refresh behavior per device
```
InkHUD has its own PlatformIO config: `src/graphics/niche/InkHUD/PlatformioConfig.ini`
## I2C Device Detection
If the variant has I2C devices, ensure `src/detect/ScanI2C` will detect them. The auto-detection system handles 80+ device types including displays, sensors, RTCs, keyboards, PMUs, and touch controllers at boot.
## Custom Board Definitions
If the PlatformIO board doesn't exist, create a custom board JSON in `boards/`:
```json
{
"build":{
"arduino":{"ldscript":"esp32s3_out.ld"},
"core":"esp32",
"f_cpu":"240000000L",
"f_flash":"80000000L",
"flash_mode":"qio",
"mcu":"esp32s3",
"variant":"esp32s3"
},
"connectivity":["wifi","bluetooth"],
"frameworks":["arduino","espidf"],
"name":"My Custom Board",
"upload":{
"flash_size":"8MB",
"maximum_ram_size":327680,
"maximum_size":8388608
},
"url":"https://example.com",
"vendor":"MyVendor"
}
```
## Checklist
- [ ] Create `variants/<arch>/<name>/variant.h` with pin definitions
- [ ] Create `variants/<arch>/<name>/platformio.ini` extending correct base
- [ ] Set `custom_meshtastic_support_level` (1 or 2)
- Before starting on some new big chunk of code, it it is optional but highly recommended to open an issue first
to say "Hey, I think this idea X should be implemented and I'm starting work on it. My general plan is Y, any feedback
is appreciated." This will allow other devs to potentially save you time by not accidentially duplicating work etc...
is appreciated." This will allow other devs to potentially save you time by not accidentally duplicating work etc...
- Please do not check in files that don't have real changes
- Please do not reformat lines that you didn't have to change the code on
- We recommend using the [Visual Studio Code](https://platformio.org/install/ide?install=vscode) editor along with the ['Trunk Check' extension](https://marketplace.visualstudio.com/items?itemName=trunk.io) (In beta for windows, WSL2 for the linux version),
- We recommend using the [Visual Studio Code](https://platformio.org/install/ide?install=vscode) editor along with the ['Trunk Check' extension](https://marketplace.visualstudio.com/items?itemName=trunk.io) (In beta for windows, WSL2 for the Linux version),
because it automatically follows our indentation rules and its auto reformatting will not cause spurious changes to lines.
- If your PR fixes a bug, mention "fixes #bugnum" somewhere in your pull request description.
- If your other co-developers have comments on your PR please tweak as needed.
- Please also enable "Allow edits by maintainers".
- Please do not submit untested code.
- If you do not have the affected hardware to test your code changes adequately against regressions, please indicate this, so that contributors and commnunity members can help test your changes.
- If you do not have the affected hardware to test your code changes adequately against regressions, please indicate this, so that contributors and community members can help test your changes.
- If your PR gets accepted you can request a "Contributor" role in the Meshtastic Discord
stale-issue-message:This issue has not had any comment or update in the last month. If it is still relevant, please post update comments. If no comments are made, this issue will be closed automagically in 7 days.
This repository is the [Meshtastic](https://meshtastic.org) firmware — a C++17 embedded codebase targeting ESP32 / nRF52 / RP2040 / STM32WL / Linux-Portduino LoRa mesh radios — plus a Python MCP server in `mcp-server/` that AI agents use to flash, configure, and test connected devices.
## Primary instruction file
**Read `.github/copilot-instructions.md` first.** That file is the canonical agent-facing document for this repo. It covers project layout, coding conventions (naming, module framework, Observer pattern, thread safety), the build system, CI/CD, the native C++ test suite, and — most importantly for automation work — the **MCP Server & Hardware Test Harness** section. Read it top-to-bottom before starting any non-trivial change.
This file (`AGENTS.md`) is a short pointer + quick reference for agents that don't read `.github/copilot-instructions.md` by default.
| Generate CI matrix | `./bin/generate_ci_matrix.py all [--level pr]` |
## MCP server (device + test automation)
The `mcp-server/` package exposes ~32 MCP tools for device discovery, building, flashing, serial monitoring, and live-node administration. Tools are grouped as:
Setup: `cd mcp-server && python3 -m venv .venv && .venv/bin/pip install -e '.[test]'`. The repo registers the server via `.mcp.json` — Claude Code picks it up automatically.
See `mcp-server/README.md` for argument shapes and the **MCP Server & Hardware Test Harness** section of `.github/copilot-instructions.md` for agent usage rules (tool surface, fixture contract, firmware integration points, recovery playbooks).
## Slash commands (AI-assisted workflows)
Three test-and-diagnose workflows exist as slash commands:
- **`/test` (Claude Code) / `/mcp-test` (Copilot)** — run the hardware test suite and interpret failures
- **`/diagnose` / `/mcp-diagnose`** — read-only device health report
- **`/repro` / `/mcp-repro`** — flakiness triage: re-run one test N times, diff firmware logs between passes and failures
Bodies live in `.claude/commands/` and `.github/prompts/` respectively. `.claude/commands/README.md` is the index.
## Encryption at a glance
Two layers, both in `src/mesh/CryptoEngine.cpp`:
- **Channel (symmetric)** — **AES-CTR** with a channel-wide PSK (AES-128 or AES-256). Nonce = packet_id ‖ from_node ‖ block_counter. No AEAD; integrity is soft (channel-hash filter). The well-known default PSK lives in `src/mesh/Channels.h`; a 1-byte PSK is a short-form index into it.
- **Per-peer PKI** — **X25519 ECDH** (Curve25519, 32-byte keys) → SHA-256 → **AES-256-CCM** with an 8-byte MAC. Fresh 32-bit `extraNonce` per packet, sent in the clear alongside the MAC. 12-byte wire overhead (`MESHTASTIC_PKC_OVERHEAD`). Used for DMs. Also used for remote admin (`src/modules/AdminModule.cpp`), where AdminMessage authorization is gated by `config.security.admin_key[0..2]`. Disabled entirely in Ham mode (`user.is_licensed=true`).
Key rotation to never trigger casually: only the **full** factory reset (`factory_reset_device`, `eraseBleBonds=true`) wipes `security.private_key` and regenerates the keypair — every peer holds the old public key, so DMs silently fail PKI decrypt until NodeInfo re-exchanges. The **partial** config reset (`factory_reset_config`) preserves the private key and doesn't invalidate peer relationships. Explicitly blanking `security.private_key` via admin also triggers regen. See the **Encryption & Key Management** section of `.github/copilot-instructions.md` for the full spec (nonce layout, send/receive selection logic including infrastructure-portnum exceptions, admin-key + session-passkey authorization, `is_managed` scope, key-rotation hazards).
## House rules
- **No destructive device operations without operator approval.** `factory_reset`, `erase_and_flash`, `reboot`, `shutdown`, history-rewriting git ops — describe the action and stop. Operator authorizes.
- **One MCP call per serial port at a time.** The port lock is exclusive; concurrent calls deadlock. Sequence: open → read/mutate → close, then next device.
- **`userPrefs.jsonc` is session state during tests.** The `_session_userprefs` fixture snapshots + restores it; never edit it from inside a test.
- **Don't speculate about firmware root causes.** When evidence doesn't support a classification, say "unknown" and list what would disambiguate.
- **Run `trunk fmt` before proposing a commit.** The `trunk_check` CI gate will reject unformatted code.
- **`confirm=True` on destructive MCP tools is a real gate, not a formality.** Don't bypass it via auto-approve settings.
- **Keep code comments minimal — one or two lines, max.** Comment only when the _why_ isn't obvious from the code; never restate what the next line does. No multi-paragraph block comments explaining straightforward changes. The diff and commit message carry the rationale; the code carries the behavior.
- **Use `Throttle` for time-based rate limiting, not raw `millis()` math.** `src/mesh/Throttle.h` provides `Throttle::isWithinTimespanMs(lastMs, intervalMs)` (returns true while inside the cooldown) and `Throttle::execute(&lastMs, intervalMs, func)` (function-pointer form that updates the timestamp on fire). Use these for any "did N ms pass since X" check — raw `millis() > lastMs + N` is rollover-unsafe (breaks after ~49.7 days) and inconsistent with the rest of the codebase. The helpers compute `now - lastMs` with unsigned subtraction, which wraps correctly.
## Typical agent workflows
### Flashing a device
1.`list_devices` → find the port + likely VID
2.`list_boards` → confirm the env, or use the known default for the hardware
3.`pio_flash(env=..., port=..., confirm=True)` for any arch, or `erase_and_flash(env=..., port=..., confirm=True)` for an ESP32 factory install
2.`list_nodes(port=...)` — full peer table (SNR, RSSI, pubkey presence, last_heard)
3.`get_config(section="lora", port=...)` — LoRa settings for cross-device comparison
Sequence these; don't parallelize on the same port.
### Testing a firmware change
1. Build locally: `pio run -e <env>`
2. Flash the test device: `pio_flash(env=..., port=..., confirm=True)`
3. Run the suite: `./mcp-server/run-tests.sh tests/<tier>` or `/test tests/<tier>`
4. On failure, open `mcp-server/tests/report.html` → `Meshtastic debug` section for the firmware log tail + device state dump
5. Iterate
### Debugging a flaky test
1.`/repro <test-node-id> [count]` — re-runs the test N times, diffs firmware logs between passes and failures
2. If the first attempt always fails and the rest pass, that's a state-leak pattern → suggest `--force-bake` or a clean device state, don't chase the first failure
3. If all N fail, this isn't a flake — it's a regression. Stop iterating and escalate to `/test` for full-suite context.
| `.mcp.json` | MCP server registration for Claude Code |
## Recovery one-liners
- **`userPrefs.jsonc` dirty after a test run?** Re-run `./mcp-server/run-tests.sh` once (pre-flight self-heals from the sidecar). If still dirty: `git checkout userPrefs.jsonc`.
- **nRF52 not responding?** `mcp__meshtastic__touch_1200bps(port=...)` drops it into the DFU bootloader, then `pio_flash` re-installs.
- **Device fully wedged (no DFU)?** `mcp__meshtastic__uhubctl_cycle(role="nrf52", confirm=True)` hard-power-cycles it via USB hub PPPS. Needs `uhubctl` installed (`brew install uhubctl` / `apt install uhubctl`); on Linux without udev rules, permission errors fail fast, so use `sudo uhubctl` yourself or configure udev access.
- **Port busy?** `lsof <port>` to find the holder. Usually a stale `pio device monitor` or zombie `meshtastic_mcp` process. Kill it.
- **Multiple MCP servers running?** `ps aux | grep meshtastic_mcp` — zombies hold ports. Kill all but the one your host spawned.
- **macOS: `LIBUSB_ERROR_BUSY` on a CH341 LoRa adapter?** A third-party WCH `CH34xVCPDriver` is claiming interface 0. Find the bundle ID with `ioreg -p IOUSB -l -w 0 | grep -B2 -A30 0x5512`, then `sudo kmutil unload -b <bundleID>`. Apple's bundled CH34x kext targets the CH340 UART (PID 0x7523), not the SPI bridge — it's never the culprit.
| `MESHTASTIC_MCP_ENV_<ROLE>` | Override PlatformIO env for a role (e.g. `MESHTASTIC_MCP_ENV_NRF52=rak4631-dap`). Default map: `nrf52→rak4631`, `esp32s3→heltec-v3`. |
| `MESHTASTIC_MCP_SEED` | PSK seed for the session test profile. Defaults to `mcp-<user>-<host>`. |
| `MESHTASTIC_MCP_FLASH_LOG` | File path to tee pio/esptool/nrfutil/picotool output. `run-tests.sh` sets this to `tests/flash.log` so the TUI can stream live flash progress. |
| `MESHTASTIC_MCP_TCP_HOST` | `host` or `host:port` of a `meshtasticd` daemon (e.g. the `native-macos` build). Surfaces it in `list_devices` as `tcp://host:port` so `connect()`-based tools target it transparently. Default port 4403. |
| `MESHTASTIC_UHUBCTL_LOCATION_<ROLE>` | Pin a role to a specific uhubctl hub location (e.g. `1-1.3`). Wins over VID auto-detection — use when multiple devices share a VID. |
| `MESHTASTIC_UHUBCTL_PORT_<ROLE>` | Pin a role to a specific hub port number. Required alongside `LOCATION_<ROLE>`. |
| `MESHTASTIC_UI_CAMERA_BACKEND` | Camera backend for UI tier + `capture_screen` tool: `opencv` / `ffmpeg` / `null` / `auto` (default). |
| `MESHTASTIC_UI_CAMERA_DEVICE` | Generic camera device (index or path). Used by the UI tier when no per-role var is set. |
| `MESHTASTIC_UI_CAMERA_DEVICE_<ROLE>` | Per-role camera pinning (e.g. `MESHTASTIC_UI_CAMERA_DEVICE_ESP32S3=0` for the OLED-bearing heltec-v3). |
This directory contains YAML configuration files for meshtasticd. Each file describes a specific hardware configuration, including the LoRa module and pin assignments. These configurations are used by meshtasticd to correctly interface with the hardware.
## Metadata structure
Each configuration file includes a `Meta` section that provides information about the configuration.
This configuration is consumed by configuration-selection tools.
```yaml
Meta:
name:MeshAdv-Pi E22-900M30S# A unique identifier for this configuration.
support:community# community, official, or deprecated; determined by Meshtastic Leads.
compatible:# A list of compatible products or platforms.
- raspberry-pi
```
`name`: A unique identifier for the configuration, typically reflecting the hardware it supports.
`support`: Indicates the level of support for this configuration. It can be one of the following:
-`community`: Supported by the Meshtastic community. Meshtastic Members may not possess, or have not tested this configuration.
-`official`: Fully supported by Meshtastic. Meshtastic Members have tested and verified this configuration.
-`deprecated`: No longer recommended for deployment by Meshtastic.
`compatible`: A list of compatible products or platforms that can use this configuration.
This will vary depending on the intended use case / platform.
Multiple compatible entries can be included. E.g. Armbian `BOARD` value or OpenWrt `TARGET` value.
These tags can be consumed by different configuration-selection tools, filtering based upon their platform/etc.
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.