Add a discovery endpoint + UI so cameras are picked from a detected list
rather than typing OpenCV indices blind.
- Name enumeration works WITHOUT OpenCV (macOS system_profiler,
Linux /sys/class/video4linux), so discovery is useful before the [ui]
extra is installed; when cv2 IS present, each not-in-use index is briefly
opened to confirm it works and read its resolution.
- GET /api/cameras/discover returns {available, cv2, cameras:[{index, name,
width, height, in_use, unavailable}]}; indices already bound to a FleetSuite
camera are marked in_use and not re-opened (their stream owns them).
- CameraManager auto-scans on mount with a "scan" button, lists detected
cameras (name · idx · resolution) with one-click add, flags missing OpenCV
with an install hint, and keeps manual-by-index as a fallback.
Verified on the bench: detects all 4 USB capture cameras with cv2 absent;
one-click add registers + marks in_use.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Background auto-enrichment in the discovery loop, FleetLog-style: when a
device is newly seen or hops ports, fire a one-shot device_info to sniff its
running firmware version, hw_model → exact pio env, region, and node num — no
manual refresh needed. hw_model resolution picks the precise variant (e.g.
T_ECHO_PLUS → t-echo-plus, HELTEC_MESH_NODE_T114 → heltec-mesh-node-t114,
RAK4631 → rak4631), not just the coarse role default.
Gated for safety: skipped entirely while a test run holds the ports;
serialized so only one device is on the wire at a time; the device's live
serial monitor is suspended for the connect and resumed after; pinned envs are
never clobbered; a connect that fails or returns incomplete metadata backs off
(60s) instead of re-hammering every poll. Disable with
MESHTASTIC_MCP_AUTO_ENRICH=0.
Also fixes a real bug: both auto-enrichment and the existing /refresh endpoint
called admin.device_info, which doesn't exist — device_info lives in
meshtastic_mcp.info. /refresh was silently 500ing before this.
The device card now shows the sniffed specs (running fw version, hw model,
region — UNSET flagged amber since it blocks TX — and resolved env).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Re-theme the SPA on the official Meshtastic design system
(github.com/meshtastic/design): the navy neutral scale, mint accent
(#67EA94), and M3 dark tokens. Tailwind's default scales are remapped onto
the brand palette in style.css so the whole UI inherits it (slate→neutral,
emerald→green, rose→error, amber→warning, sky/indigo→blue).
Differentiated from the Meshtastic app / FleetLog so it doesn't read as
either: mint is reserved strictly for live/healthy/go status (online dots,
"LIVE", pass counts), while the design system's indigo/blue becomes
FleetSuite's structural signature — wordmark, nav, section labels, card
rails — for a "test-bench instrument" character rather than a mint-dominant
consumer surface. Adds:
- a LoRa-chirp glyph + mono, tracked "FLEETSUITE" wordmark (nods to the
logo's chirp origin);
- circular node identifiers per the design standard (computed per-node
color on the ring/initials only, never a row background wash);
- instrument card rails (inset indigo→mint hairline) and indigo section
labels across the Fleet + Test Suite panels.
Frontend-only; no API changes. Verified in-browser on both tabs, no console
errors.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Camera mirror: a horizontal-flip toggle alongside rotation, persisted as a
property of the camera mount (survives reassignment). Applied client-side as
a scaleX(-1) on the MJPEG feed; new `mirror` column + POST
/api/cameras/{id}/mirror.
Per-node USB power: cut/restore/cycle VBUS to a node via uhubctl on its
PPPS-capable hub. Because uhubctl's port listing exposes only VID:PID (not
serial), the hub slot is tracked explicitly on the device row
(hub_location/hub_port):
- `locate` auto-binds when exactly one PPPS port matches the VID;
- an ambiguous match (two identical boards) is surfaced for the operator to
pin the correct slot in device settings, which then survives replug/reboot.
Power actions resolve through that mapping (falling back to a unique VID
match), are gated by the run-safety lock, and suspend any live serial monitor
so the port is free. Routes: GET /api/hubs, POST /api/devices/{serial}/locate,
PUT /api/devices/{serial}/hub-port, POST /api/devices/{serial}/power/{on,off,cycle}.
Degrades gracefully when uhubctl is absent (502 with an install hint; the
settings panel shows "uhubctl not available").
Additive db migration (mirror, hub_location, hub_port) for existing
registries. 20 web unit tests still pass; verified end-to-end in the browser.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The "Draft of FleetSuite" commit shipped the Vue SPA, scripts, pyproject
entrypoint, and unit tests, but the meshtastic_mcp.web backend they all
reference was never committed. Root cause: the repo-root .gitignore had a
bare `web` pattern (for the firmware build's top-level web/ output) that
also matched mcp-server/src/meshtastic_mcp/web/, so `git add` silently
skipped the package and a later clean wiped the untracked files. Anchored
the build-artifact patterns (.pio/pio/web/...) to the repo root so they no
longer clobber nested web/ directories.
Reconstructed the backend from the contract pinned down by the three unit
tests and the seven Pinia stores:
- db/ aiosqlite registry: devices (serial-keyed identity that follows
a node across ports, env pinning, offline marking), cameras,
flash timings, test runs/results, build ledger, settings
- services/ identity (VID->role->env, hw_model resolution), control gate
(no port action during a run), builder (SHA-keyed artifact
cache + queue), test_runner (live pytest via reportlog tail),
datadog (scrub + dashboard-compatible mappers + cursor reader),
firmware (git ref), native (Docker meshtasticd), discovery
(USB poll loop), serial_monitor (live pyserial stream),
camera_stream (MJPEG)
- ws/hub.py topic broadcast hub backing /ws
- app.py FastAPI factory: REST for every store/component + /ws, serves
the built SPA
- __main__.py the meshtastic-mcp-web entrypoint (uvicorn + pywebview window,
--browser to serve only)
All 20 web unit tests pass; verified end-to-end against real hardware
(discovery, live serial logs, REST + WebSocket).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* Clamp position precision on public / known-keys (Compliance)
* Fix review comments: bounds check, all-channel precision clamp, disabled channel guard
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* Refactor position precision comments for clarity and update channel configuration handling in AdminModule
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* Add LTO support for nrf52840 while preserving interrupt handlers
* nrf52840: enable whole-image LTO on all targets via nrf52_base
Moves -flto + the nrf52_lto.py exclusion middleware from the rak4631 env
(771018ca8) up to [nrf52_base], so every nrf52840 target inherits it.
nrf52_lto.py keeps the interrupt handlers out of LTO (framework core +
TinyUSB USBD_IRQHandler) -- they're referenced only from the asm vector
table, so whole-program LTO would otherwise drop them and the chip hangs.
HW-validated: RAK4631 (SX1262, -60KB) and muzi-base (LR1121) both boot and
init their radios. Build-verified on canaryone (fresh board, base-inherited).
Caveat: the RAK "1-Watt" variant's radio does not tolerate global-LTO
(SX126x init fails); it shares the rak4631 binary, so keep that hardware on
src-only or split it into a separate target.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* nrf52840: move -fmerge-all-constants to nrf52_base (all targets)
It had been trialed on rak4631 only; it's a general image-wide flag (the
same one stm32 uses globally, ~0.7KB), so move it up to [nrf52_base] next
to -flto so every nrf52840 target gets it instead of just rak4631.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* nrf52_lto.py: normalize path separators for Windows (Copilot review)
get_abspath() returns backslash-separated paths on Windows, so the
"/FrameworkArduino/" / "/cores/nRF5/" substring matches would miss and the
ISR-owning objects would still be LTO'd -> hang on first IRQ. Replace
backslashes with forward slashes before matching. No-op on macOS/Linux.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Fix directory handling for LTO
* Add post-link guard to check for dropped ISR handlers in nrf52 LTO
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Jason P <applewiz@mac.com>
* Added NodeDB fixtures and refactored to use std maps for better efficiency
* Defer NodeDB save during xmodem transfer to prevent mid-transfer fsFormat
* 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
* 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
* 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>
* 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>