Skip to main content

meshtastic-device-ops

Discover, connect to, drive, configure, observe, and flash Meshtastic devices through the Meshtastic MCP server. Use when an agent needs to bring up a radio (serial or TCP), read or change device/channel config, send messages, inspect the node DB, watch live packets/telemetry/logs via the recorder, recover a bricked device, or build/flash firmware. Covers the non-e2e MCP tool surface; for cross-plane device↔app testing use the `meshtastic-e2e` skill.

Ir para a instalação

Informações da origem

Repositório
meshtastic/meshtastic-mcp
Última atividade na origem
26 de agosto de 2026 às 18:21
Idioma detectado do SKILL.md
inglês
Estrelas
15
Forks
7

Opções de instalação

Por padrão, está selecionado o prompt que primeiro revisa a origem. Você pode mudar para um comando direto ou baixar uma cópia local.

Revise os arquivos de origem

Leia o SKILL.md e os arquivos complementares exibidos pelo SkillsMP antes de decidir se vai instalar.

Exibindo SKILL.md

SKILL.md
Instruções da origem · Visualização somente leitura
name
meshtastic-device-ops
license
GPL-3.0-only
description
Discover, connect to, drive, configure, observe, and flash Meshtastic devices through the Meshtastic MCP server. Use when an agent needs to bring up a radio (serial or TCP), read or change device/channel config, send messages, inspect the node DB, watch live packets/telemetry/logs via the recorder, recover a bricked device, or build/flash firmware. Covers the non-e2e MCP tool surface; for cross-plane device↔app testing use the `meshtastic-e2e` skill.
# Meshtastic device operations (via the MCP server) The Meshtastic MCP server exposes a large tool surface, plus resources and prompts. This skill maps common operator intents to the right tools and the safe order to use them. ## First: know your environment - **`doctor`** (tool) or the **`meshtastic://doctor`** resource — what's installed, what's missing, and the exact command to acquire it. Call this first if any tool fails with a missing-prerequisite error, or before an e2e/flash run. - **`meshtastic://capabilities`** resource — one line: which capability groups are active (`core`, `firmware`, `android`, `apple`). - Prereq binaries are gated: `firmware` tools (build/flash/boards/userprefs) register only when `MESHTASTIC_FIRMWARE_ROOT` + `pio` are present. Core (admin/recorder/transport) is always on. ## Connect Two transports, same admin surface: - **Serial:** a USB port from `list_devices` (e.g. `/dev/cu.usbmodem101`). - **TCP:** `tcp://host:port` (a networked node, or a virtual `meshtasticd` at `127.0.0.1:4403`). **Prefer the CLI subcommands for read-only discovery** — they cost no MCP schema tokens and work without the server running: ```bash meshtastic-mcp devices # find ports (bash, ~0 schema tokens) meshtastic-mcp devices --all # include non-Meshtastic serial ports meshtastic-mcp info <port> # firmware version, region, node identity meshtastic-mcp nodes <port> # mesh peers (long/short name, SNR, last-heard) meshtastic-mcp boards # list all PlatformIO board envs meshtastic-mcp boards --arch esp32s3 # filter by architecture meshtastic-mcp boards get heltec-v3 # full metadata for one board # All accept --json for structured output. ``` Fall back to MCP tools when already in an MCP session or when you need board metadata for a follow-on `build_start`: ``` list_devices # find candidates (include_unknown=true to see every port) device_info <port> # firmware version, node identity, channel summary list_nodes <port> # the node DB (long/short name, SNR, last-heard, position) ``` One MCP call per serial port at a time — the port lock is **exclusive and non-blocking**: contention fails fast with "busy … Retry shortly" (it does not queue). Open → act → close. ## Configure (mutating — confirm-gated) ``` get_config <port> <section> # lora | device | position | power | network | display | ... set_config <port> <section> <field>=<value> ... get_channel_url <port> # the shareable channel URL (keys) set_channel_url <port> <url> set_owner <port> --long ... --short ... ``` After a write, **reboot then re-read** to prove it persisted to NVS, not just RAM (`reboot <port>` → `get_config`). Region (`lora.region`) and `network.enabled_protocols` are the two that bite — see `meshtastic-e2e` `topology.md`. ## Vanity identity (chosen node id / app colour) A node's number is `crc32(x25519_public_key)` and every app paints it with the low 24 bits read as RGB, so both are chosen by grinding keys, not by setting a field. ``` vanity_grind_start color=crimson tol=6 # or pattern=dc80, or both; returns job_id vanity_grind_poll <job_id> # hits[]; NEVER use one with verified=false vanity_preview <private_key> # what any key gives you — no device, no GPU vanity_apply <private_key> <port> confirm=true ``` - `tol` is free and finds a hit orders of magnitude sooner — always offer it. - Grinding needs `mvgrind` (`doctor` prints how to build it); preview/apply do not. - `vanity_apply` **replaces the identity**, drops the old NodeNum from the node's own DB, and reboots the board. It needs `lora.region` set (keygen is skipped while UNSET) and refuses an unclamped key. Keep the previous key if a way back matters. - Hits are private keys. Don't echo them further than needed; see `docs/vanity.md`. ## Message + observe ``` send_text <port> <text> [--dest <nodeId>] # broadcast (^all) or directed ``` The **recorder** is always capturing to JSONL; query windows instead of tailing: ``` packets_window # recent RX/TX packets (portnum, from/to, payload) — wire truth telemetry_timeline # device/environment metrics over time logs_window # firmware log lines events_window # recorder-marked events mark_event # drop a labeled marker to anchor a later query recorder_status / recorder_pause / recorder_resume / recorder_export ``` For app-visible delivery vs wire truth (broadcast shows an error icon in a flat mesh even when delivered) see `meshtastic-e2e` `references/loop-outbound.md`. ## Recover + flash (firmware capability) ```bash # Board lookup — use the CLI (no schema overhead): meshtastic-mcp boards --query <slug> # find the env name meshtastic-mcp boards get <env> # confirm arch + upload_speed ``` ``` # Build + flash via MCP (async to avoid 60 s client timeout): build_start <env> # returns build_id immediately build_poll <build_id> # poll until status=done pio_flash <env> <port> / erase_and_flash <env> <port> / update_flash <env> <port> touch_1200bps <port> # bounce into the bootloader (nRF/RP2040) ``` Chip-specific escape hatches when pio can't help: `esptool_*`, `nrfutil_*`, `picotool_*` (raw passthroughs; destructive ones are confirm-gated). For a wedged USB device, power-cycle the hub port with `uhubctl_list` / `uhubctl_power` / `uhubctl_cycle`. > **Linux:** `uhubctl` requires udev rules to work without root. Run > `meshtastic-mcp doctor` — it will detect the permission issue and print > the exact `sudo curl … && sudo udevadm trigger` command to fix it. ### BLE OTA DFU (manual — no MCP tool covers this yet) Every `flash`/`pio_flash`/`nrfutil_dfu` path above is USB (serial or UF2). There is no MCP tool for the *wireless* leg — the Nordic legacy BLE DFU that the Meshtastic Android app uses for its in-app bootloader/firmware upgrade, and that an Adafruit/OTAFIX-family nRF52 bootloader (e.g. `meshtastic/Adafruit_nRF52_Bootloader_OTAFIX`) speaks natively. To validate that path from a dev machine instead of a phone: 1. **Buttonless jump from app mode is one of two GATT services, chosen at compile time — check which before assuming a UUID.** `NRF52Bluetooth.cpp` picks `BLEDfuSecure` (Nordic Secure DFU, service `0xFE59`, control characteristic `8ec90003-f315-4f60-9fb8-838830daea50`) only when the board's `variant.h` defines `BLE_DFU_SECURE` — today that's `wio-t1000-s` alone. Every other nRF52 board, **including RAK4631**, falls through to plain `BLEDfu` (Adafruit's Bluefruit library): legacy service `00001530-...`, control characteristic `00001531-...`. Either way: enable notifications/indications on the control characteristic, write `0x01` to it, and the node disconnects and reboots into the bootloader, advertising under a new BLE name (OTAFIX boards use `<BOARD>_DFU`, e.g. `4631_DFU` for RAK4631 — see that repo's README "BLE advertising names" table). 2. **Legacy DFU transfer in bootloader mode.** The bootloader's own GATT service is always the older Nordic Legacy DFU (`00001530-...`, control point `...1531`, data `...1532`) regardless of which service the app used to jump there. [`recrof/nrf_dfu_py`](https://github.com/recrof/nrf_dfu_py) (pure Python + `bleak`) speaks **only** this legacy service (its `DFU_SERVICE_UUID` constant is the `00001530-...` one, full stop) — it has no path for a `BLE_DFU_SECURE` board like `wio-t1000-s`, so it's a match for RAK4631 and most other nRF52 targets, not a universal tool. Clone it, `pip install bleak`, then from that checkout: `python3 dfu_cli.py --scan <firmware-or-bootloader.zip> <device-name-or-addr>`. Use the `*-ota.zip` release asset (not the `.uf2`/`.hex`) — that's the format this DFU protocol expects. One call does both legs unassisted — its `jump_to_bootloader()` sends the exact same 2-byte legacy opcode write described in step 1, then it rescans and transfers — so giving it the **app-mode** name/address up front is usually enough; you don't need a separate manual jump. Do the jump as its own step only when you need to debug the jump in isolation (its post-jump bootloader rescan matches by substring against a literal `"DFU"`/MAC-increment heuristic, not the board's exact advertised name, so once already in bootloader mode, re-running against the bootloader's own `<BOARD>_DFU` name is the more reliable retry). 3. **Finding the device's BLE name is a scan-and-match, and a name prefix can be ambiguous — resolve it to exactly one device before acting.** The app's advertised name is `<short_name>_<hex><hex>` where the hex suffix is the last two bytes of the nRF52's FICR `DEVICEADDR` (`getDeviceName()` in `firmware/src/main.cpp`) — **not** derived from `my_node_num`/`device_info`'s node id in any way you can compute offline. Scan (`BleakScanner.discover`) and match by the known `short_name` prefix, but confirm the scan turned up exactly one match before connecting: `nrf_dfu_py` (and most such tools) connects to the first match among the names/addresses you give it, so an ambiguous prefix on a mesh with more than one device sharing it can jump or flash the wrong node. 4. **`bluetooth.mode = RANDOM_PIN` needs a human (or the app) watching for the passkey — an unattended agent session can't complete pairing, screen or no screen.** The firmware sends the 6-digit passkey to `BluetoothStatus` (the app's pairing UI reads it from there) and additionally shows it on-screen `#if HAS_SCREEN` — some RAK4631 builds do have one (e.g. `rak_wismesh_pocket` is explicitly "rak4631 pin map + OLED" in `platformio.ini`; don't assume `hw_model: RAK4631` alone tells you whether the running build has a display). `capture_screen` (see *Hardware UI checks* above) can read an OLED when one's present — worth reaching for on a future attempt, since neither it nor a live debug log was actually checked in real time here; the passkey/display behavior below is reasoned from firmware source after the fact, not confirmed against what this device actually showed. Whether the passkey is visible in a live debug log (`set_debug_log_api`) depends on the exact firmware build — `onPairingPasskey`'s `LOG_INFO` included the passkey digits in `v2.7.26.54e0d8d` (what this was tested against) and still does on `develop`, but a firmware security fix logged only `match_request` for a stretch of the 2.7.x line in between (redacting pairing secrets from logs) — don't assume the log line carries it on an arbitrary build; the app's pairing UI is the one path guaranteed to receive it regardless. Neither was being watched in a scripted `bleak` session here, so the OS pairing prompt sat with nothing to type in and the connection was dropped. This is expected `RANDOM_PIN` behavior, not a bug — switch to `FIXED_PIN` (a value you already know) for scripted/headless testing instead of chasing it. `get_config`-read the current `bluetooth.mode`/`fixed_pin` *before* changing anything, and restore them (`set_config` + `reboot` + `get_config` to confirm) once testing is done — a device left in `FIXED_PIN` carries a known, reusable pairing credential indefinitely otherwise. Separately, firmware `develop` (2.8) does carry two real nRF52 BLE-pairing fixes not yet in 2.7.x that are worth knowing about if pairing looks flaky on a 2.7.x build: a passkey callback that wasn't restored after a BT disable/re-enable cycle without a reboot (#11027), and a BLE-task stack overflow that could crash the device mid-pairing on nrf52840 targets (#11190). **macOS-specific friction**, all one-time per machine/device pair, not per session: - Bluetooth must be explicitly on (Control Center) — `bleak`/CoreBluetooth error clearly (`BleakBluetoothNotAvailableError: POWERED_OFF`) when it isn't, so this fails fast rather than silently. - The terminal app driving the script needs Bluetooth permission granted (System Settings → Privacy & Security → Bluetooth) — without it, scanning either errors or (confusingly) just finds nothing. - A newly-enumerated USB device can trigger a silent macOS "accessory" permission popup that hides the port from `list_devices`/`ls /dev/cu.*` until approved — if a device that was just flashed or reset seems to vanish from USB entirely, check for that popup before assuming a bad flash. ## Hardware UI (OLED) checks `send_input_event` drives the device's buttons; `capture_screen` grabs the OLED (camera/OCR optional — see `doctor` for the `[ui]` extra). This is device-only; for app UI use `meshtastic-e2e`. ## Grounded answers - **`android_docs_search` / `android_docs_fetch`** — Android/Compose/API questions answered from the official Knowledge Base (no guessing) when working with the Android app. ## Prompts (slash workflows) - **`bringup_device`** — connect + verify a healthy baseline (firmware, region, node count). - **`inbound_loop`** / **`triage_e2e_failure`** — e2e workflows (see the `meshtastic-e2e` skill). ## Hard rules 1. One call per serial port at a time (exclusive non-blocking lock). 2. Mutations are confirm-gated and reversible-by-reboot only for RAM writes — re-read after reboot. 3. `factory_reset(full=true)` wipes BLE bonds + the identity key; `full=false` keeps them. 4. Prefer the recorder windows over ad-hoc reads — they're timestamped and align with app snapshots. 5. Never apply a ground key whose `verified` is false, and never apply one without telling the operator the old node id first — the change is not reversible without the previous key.
Ver no GitHub