Skip to main content

meshtastic-e2e

Closed-loop end-to-end testing of a Meshtastic device and a Meshtastic app (Android, Apple, or the Compose desktop app) together. Use when validating that an action on the radio (send a text, change config, a node beacon, a power-cycle) surfaces correctly in the app UI, or that an action in the app produces the correct on-air/on-device result. Drives the firmware device via the Meshtastic MCP server and the app via the Android CLI (`android` + `adb`), the Apple toolchain (`xcrun simctl` + `idb`), or the Compose Hot Reload MCP server for desktop, then cross-asserts one plane against the other.

Quellinformationen

Repository
meshtastic/meshtastic-mcp
Letzte Quellaktivität
10. September 2026 um 19:22
Erkannte Sprache von SKILL.md
Englisch
Sterne
17
Forks
7

Installationsoptionen

Standardmäßig ist der Prompt ausgewählt, der zuerst die Quelle prüft. Sie können zu einem direkten Befehl wechseln oder eine lokale Kopie herunterladen.

Quelldateien prüfen

Lesen Sie SKILL.md und alle von SkillsMP angezeigten Begleitdateien, bevor Sie sich für eine Installation entscheiden.

Datei-Explorer
18 Dateien

SKILL.md wird angezeigt

SKILL.md
Quellanweisungen · Schreibgeschützte Vorschau
name
meshtastic-e2e
license
GPL-3.0-only
description
Closed-loop end-to-end testing of a Meshtastic device and a Meshtastic app (Android, Apple, or the Compose desktop app) together. Use when validating that an action on the radio (send a text, change config, a node beacon, a power-cycle) surfaces correctly in the app UI, or that an action in the app produces the correct on-air/on-device result. Drives the firmware device via the Meshtastic MCP server and the app via the Android CLI (`android` + `adb`), the Apple toolchain (`xcrun simctl` + `idb`), or the Compose Hot Reload MCP server for desktop, then cross-asserts one plane against the other.
# Meshtastic Device ↔ App E2E Testing Two independent planes, each with its own **stimulus** and **oracle**. A closed-loop test stimulates one plane and asserts on the *other*. | Plane | Stimulus | Oracle (observation) | |---|---|---| | **Device** (Meshtastic MCP) | `send_text`, `set_config`, `push_fake_nodedb`, `send_input_event`, `uhubctl_cycle`, traceroute | recorder `packets_window` / `telemetry_timeline` / `logs_window` / `events_window`, `device_info`, `list_nodes`, serial logs, `capture_screen`+OCR | | **App — Android** (android CLI + adb) | `adb shell input tap/text/swipe`, `android run` | `android layout --diff`, `android screen capture [--annotate]` | | **App — Android, agent-driven** (Artemis MCP) | `mobile_run_task` runs a whole journey out of context | **none — never its own oracle**; the device plane decides (see `references/journeys.md`) | | **App — Apple** (xcrun simctl + idb) | `idb ui tap/text`, `simctl install/launch` | `idb ui describe-all`, `simctl io screenshot` | | **App — Desktop** (Compose Hot Reload MCP) | `click` / `type_text` / `scroll` (by `nodeId`) | `get_semantic_tree`, `get_logs`, `get_ui_error` | ## Reference files (load the one you need) - `references/topology.md` — two-radio wiring, app-over-TCP, the uhubctl USB switch, preflight. - `references/harness.md` — the hard rules (marker tokens, bounded polling, recorder oracle, adb nav primitives, verdict format). **Read this before any loop.** - `references/mesh_e2e.py` — runnable device-plane helper (`devices`/`info`/`send`/`recv-text`/`watch-tx`/`traceroute`/`recorder`); emits grep-able `PASS …`/`FAIL …` lines. - `references/loop-inbound.md` — device → app message render - `references/loop-outbound.md` — app → device wire truth - `references/loop-node-sync.md` — node appears in app node list - `references/loop-config-writeback.md` — app setting persists on device (RAM + NVS) - `references/loop-resilience.md` — uhubctl power-cycle fault injection - `references/emulator-lab.md` — hardware-free Android: AVD app over TCP + native UDP mesh - `references/replay-app-features.md` — **drive app behavior via the replay engine**: feature injection (geofence/waypoints/alerts via `replay_inject`), sustained-load/stress (`conference-stress` preset, `duration`/`rate` pacing, self-verifying `achieved_rate`), mDNS/Bonjour auto-discovery, traceroute-log population, and tapback/bot-plane reactions; logcat/notification oracles + gotchas - `references/simulator-apple.md` — hardware-free Apple: iOS Simulator / macOS app over TCP (`127.0.0.1`) + native UDP mesh - `references/journeys.md` — **journey-driven UI (recommended over hardcoded coordinates)**: drive the app from a goal via the live a11y tree, version-resilient. Ships journey XML in `references/journeys/`. For Android, hand the journey to **Artemis** (`mobile_run_task`) rather than looping inline — same XML, runs out of context. Its verification is fail-open, so the recorder stays the verdict. - `references/vision-oracle.md` — use a screenshot + vision as the assertion when the a11y tree is empty (WebView/Canvas/animation). - `references/triage.md` — dual-plane root-cause analysis of a FAIL (pairs with the `triage_e2e_failure` MCP prompt). The device plane (`mesh_e2e.py`, native nodes, recorder) is platform-neutral and shared; only the app plane differs (Android CLI+adb, Apple `xcrun simctl`+`idb`, or the Compose Hot Reload MCP server for desktop). ## Fast app bring-up — use this before any manual UI driving Never hand-walk onboarding and the manual-IP dialog on a debug build; the app has a scripted path (and hand-walking it is how sessions waste ten minutes of taps): ```bash adb shell am start -n <pkg>/org.meshtastic.app.MainActivity \ --ez skip_onboarding true \ -a android.intent.action.VIEW -d "https://meshtastic.org/connections?address=t10.0.2.2:4403" ``` - `skip_onboarding` is MainActivity's debug-only `EXTRA_SKIP_ONBOARDING` — bypasses the whole intro flow on a fresh install (release builds ignore it). - `/connections?address=t<host>:<port>` auto-connects (transport prefixes: `t` TCP, `x` BLE MAC, `s` serial). `address=n` disconnects. Full path list: `docs/en/developer/navigation-and-deep-links.md` in the app repo. - Builds after 2.8.1 pop a trust dialog ("Connect to this device?") on link-initiated connects — one exact-match tap on **Connect**. `avd.connect_app_to_tcp()` does all of this (bypass, deep link, dialog, fallback taps) — prefer it over raw adb. - Driving Settings/UI with a sim attached? Start it with `sim_profile={"traceroute_pairs_per_hour": 0}` — the default sim injects traceroute responses that pop a modal over whatever you are testing (`replay_stop` also works once the node DB is populated; the app keeps its nodes). ## Topology (read first) Three supported topologies — pick the one that matches your hardware: | Topology | Device plane | App plane | DUT address | |---|---|---|---| | **Emulator lab** (no hardware) | `meshtasticd` native nodes via UDP multicast | Android AVD | `10.0.2.2:<port>` | | **Physical Android** | Real radios via USB serial | USB-attached Android phone | `adb reverse` → `127.0.0.1:<port>` | | **Physical Apple** | Real radios via USB serial | iOS Simulator only (see caveat) | `127.0.0.1:<port>` | | **Desktop app** | Real radios via USB serial, or native nodes over UDP | Compose `:desktopApp` on the host | `127.0.0.1:<port>` | For **physical Android**: `avd.tcp_dut_address(port, serial=<phone_serial>)` sets up the `adb reverse` tunnel automatically and returns the correct address. Pass it to `connect_app_to_tcp()`. UI observation falls back from `android layout` to `adb exec-out uiautomator dump` transparently — all helpers (`poll_for_text`, `find_text`, `_tap_text`) work on both emulators and physical phones. > **iOS physical devices — not supported.** `apple_sim.py` targets the iOS Simulator only > (`xcrun simctl`, `idb`). Physical iPhone/iPad requires code signing + provisioning profiles, > `libimobiledevice`/`usbmuxd` for device comms, and XCTest/XCUITest for UI automation > (idb is simulator-only). This is a separate, significant project — not a configuration change. Full wiring detail and the single-radio app-over-TCP workaround in `references/topology.md`. One radio cannot be both tester and DUT (the serial port lock is exclusive). ### Desktop app plane (Compose Hot Reload MCP) CMP 1.12+ is what bundles the MCP-capable Compose Hot Reload, and `Meshtastic-Android` is on 1.12.0 — so this plane is available there today. Its tracked `.mcp.json` registers the server as `compose-hot-reload` (`:desktopApp:hotMcpServer`), so a session opened in that repo drives the **live** desktop app with no rebuild between assertions. Start it with `./gradlew :desktopApp:hotRun`, then: - **Poll `status` until `connected: true`** before any other call — the server accepts requests before the app has connected to it. - **`get_semantic_tree` is the oracle:** roles, text, `selected`/`focused`, actions and bounds — everything the journey-driven approach in `references/journeys.md` needs. `click` / `type_text` / `scroll` address nodes by `nodeId` from that tree. `take_screenshot` exists but its output depends on the host renderer; prefer the tree. - **`reload`** applies source edits into the running app (`await_reload` when started with `--auto`), so a fix can be re-asserted without restarting the mesh setup. The device plane is unchanged: same radios, same recorder, same `mesh_e2e.py`. ## Prerequisites Before running any loop, verify all of these. Missing items cause silent failures, not clear errors. | Requirement | Check | How to fix | |---|---|---| | `MESHTASTIC_FIRMWARE_ROOT` set | `echo $MESHTASTIC_FIRMWARE_ROOT` | Set to your firmware checkout path | | Two radios (TESTER + DUT) | `list_devices()` → ≥2 ports with `likely_meshtastic=true` | Plug in both radios; use TCP DUT if only one physical radio | | App reachable on the plane you are testing | Android: `adb shell pm list packages \| grep meshtastic` · Apple: `xcrun simctl listapps` · Desktop: MCP `status` → `connected: true` | `android run` / `adb install`; `simctl install`; `./gradlew :desktopApp:hotRun` | | Recorder running (process-global) | `recorder_status()` → `running=true` | Auto-starts on first MCP serial call; captures every interface | | `uhubctl` available (Loop 5 only) | `uhubctl -l` returns hub info | `brew install uhubctl`; set `MESHTASTIC_UHUBCTL_LOCATION_TESTER` | | `doctor()` returns `ok=true` | Call `doctor()` | Run each `fix_commands` entry in order | ## Bootstrap ```bash export MESHTASTIC_FIRMWARE_ROOT="$HOME/meshtastic/firmware" MCP="$MESHTASTIC_FIRMWARE_ROOT/mcp-server/.venv/bin/python" S="$HOME/.agents/skills/meshtastic-e2e/references/mesh_e2e.py" $MCP "$S" devices # list tester radios TESTER=/dev/cu.usbmodem101 # pick one adb devices # confirm the phone (DUT) is attached adb shell pm list packages | grep meshtastic # confirm the app is installed ``` The MCP server (registered as user-scope `meshtastic`) exposes the 53 tools directly when running inside Claude Code; the `$MCP "$S" …` helper is the standalone/CI path. ## Hard rules (mesh is async + lossy — respect these or get flaky tests) 1. **Marker token per message.** Never assert on a bare "hello". Embed a unique token (`E2E-$(date +%s)-$RANDOM`) so a busy 80-node mesh can't false-positive your grep. 2. **Bounded polling, never `sleep N` then assert once.** Poll every 1 s up to a deadline. Use these ceilings: single-hop broadcast → 20 s; directed/PKI send → 30 s; multi-hop (≥2) → 45 s. Mesh delivery is best-effort; these cover the 99th-percentile lab case. 3. **Warm up directed/PKI sends.** Directed + encrypted sends need bilateral NodeInfo (both sides hold each other's current pubkey). Broadcast (`^all`) first to exchange, or send to a node already in both DBs. 4. **The recorder is the device-side source of truth.** It timestamps every RX packet to JSONL; align those timestamps with `android layout` snapshots. Start it before the stimulus, query the window after. 5. **One MCP call per serial port at a time** (exclusive lock): open → act → close. 6. **`layout` can fail on WebView/animation** — fall back to `screen capture --annotate` + visual/OCR inspection. ## Loop 1 — inbound message (device → app) Stimulate from the tester radio; assert the bubble renders in the app. ```bash TOKEN="E2E-$(date +%s)" # 1. device stimulus: tester radio broadcasts (or directs to the DUT node) $MCP -m meshtastic_mcp.cli ... # or drive send_text via the meshtastic API: $MCP -u -c " import meshtastic.serial_interface as si i=si.SerialInterface('$TESTER'); i.sendText('$TOKEN', wantAck=False); i.close()" # 2. app oracle: poll the UI tree for the token (bounded) for t in $(seq 1 30); do android layout 2>/dev/null | grep -q "$TOKEN" && { echo "PASS: rendered"; break; } sleep 1 done ``` Open the messages screen in the app first (`adb shell input` to navigate, or `android run` to the messages activity). Use `layout --diff` to keep only the changed bubble in context. ## Loop 2 — outbound message (app → device) Type+send in the app; assert the wire truth on the tester radio's recorder. 1. Start the recorder, then drive the app: ```bash # ensure recorder is capturing (it auto-starts when the MCP server is live; # standalone: open a SerialInterface to TESTER and call get_recorder().start()) ``` 2. App stimulus — focus the compose field, type the token, tap send: ```bash android layout --pretty | jq '.[] | select(.interactions|index("focusable"))' # find input adb shell input tap <cx> <cy> # focus compose field (must show "focused") adb shell input text "$TOKEN" adb shell input tap <send_cx> <send_cy> ``` 3. Device oracle — the tester must *receive* it; query the recorder window: ```bash $MCP -c "from meshtastic_mcp import log_query as q,json; print(json.dumps(q.packets_window(max=20)))" | grep -i "TEXT_MESSAGE_APP" ``` Decode the payload hex / matched text for `$TOKEN`. PASS when a `TEXT_MESSAGE_APP` packet from the DUT node carrying the token lands within the deadline. ## Loop 3 — node sync (device → app) `push_fake_nodedb` (or a real beacon) on the DUT radio → assert the node appears in the app's node list (`android layout` over the nodes screen, match long_name/short_name). Backstop the device truth with `list_nodes`. ## Loop 4 — config write-back (app → device) Change a setting in the app (e.g. region, device role, a channel name) → assert it persisted on the DUT radio: ```bash $MCP -c "from meshtastic_mcp import admin,json; print(json.dumps(admin.get_config('lora', port='$DUT')))" ``` Reboot the radio (`reboot` MCP tool) and re-read to prove NVS persistence, not just RAM. ## Loop 5 — resilience / fault injection Mid-conversation, power-cycle a relay or the peer with `uhubctl_cycle` (needs `uhubctl`): assert the app shows the node go **offline → online** and that a queued message recovers once the path heals. This is the app-facing mirror of the firmware suite's `test_peer_offline_recovery`. ## Loop 6 — ATAK/iTAK render (sim TAK squad → TAK client) Validates the TAK plane. Meshtastic apps (≥2.8) bridge mesh TAK traffic to a connected ATAK/iTAK client via an **in-app local TAK server** that emits CoT (the deprecated `IMeshService` plugin is gone). Two layers: - **Bridge-semantics (no emulator, in CI):** the sim emits a TAKPacketV2 squad (`replay_start(source=..., sim_profile={"tak": {"team_nodes": N, "wire": "v2"}})` — v2 rides portnum 78 `ATAK_PLUGIN_V2`; v1 rides 72). `replay/tak_server.py` `capture_to_cot_events()` reproduces the bridge's wire→TAKPacketV2→CoT path; `tests/unit/test_tak_bridge.py` asserts the CoT is well-formed, typed (`a-f-G-U-C` PLI), and carries the right callsign/position/GeoChat. Needs the `[tak]` extra (meshtastic-tak SDK). - **App-plane (opt-in, needs an emulator + ATAK-CIV), bidirectional:** `scripts/ci_atak_app_loop.py` stands up `CotTakServer` from that squad, points ATAK-CIV at `10.0.2.2:<port>` (a pushed streaming-input `.pref`), launches it, and asserts (receive) a squad callsign marker renders **and** (send) ATAK's own self-PLI streams back, is captured, and converts to a mesh TAKPacketV2.
Auf GitHub ansehen
Diese SKILL.md ist sehr gross, daher zeigt SkillsMP hier nur den ersten Abschnitt. Auf GitHub ansehen