Skip to main content

run-meshtastic-android

Run, launch, drive, and screenshot the Meshtastic app — the Compose Desktop app via hot reload (semantic clicks, live reload, window screenshots) or the Android app on an emulator (scripted deeplink bring-up, uiautomator taps, screencap). Use when asked to run the app, verify a UI change in the real app, take a screenshot, or exercise a flow end to end against a simulated radio.

Ir a la instalación

Datos de origen

Repositorio
meshtastic/Meshtastic-Android
Última actividad en el origen
30 de agosto de 2026 a las 01:21
Idioma detectado de SKILL.md
inglés
Estrellas
1847
Forks
517

Opciones de instalación

De forma predeterminada está seleccionado el prompt que primero revisa el origen. Puedes cambiar a un comando directo o descargar una copia local.

Revisa los archivos de origen

Lee SKILL.md y los archivos complementarios que muestra SkillsMP antes de decidir si quieres instalarlo.

Explorador de archivos
3 archivos

Mostrando SKILL.md

SKILL.md
Instrucciones de origen · Vista previa de solo lectura
name
run-meshtastic-android
description
Run, launch, drive, and screenshot the Meshtastic app — the Compose Desktop app via hot reload (semantic clicks, live reload, window screenshots) or the Android app on an emulator (scripted deeplink bring-up, uiautomator taps, screencap). Use when asked to run the app, verify a UI change in the real app, take a screenshot, or exercise a flow end to end against a simulated radio.
# Run Meshtastic (Desktop & Emulator) Two binaries, two drivers, one simulated radio. All paths are relative to the repo root. Both drivers are Python 3, stdlib only, and print `--- <cmd> done ---` per step on stderr. - **Desktop** (`:desktopApp`, Compose/JVM, runs on this machine): launch with the hot-reload run task, drive through `.claude/skills/run-meshtastic-android/driver.py`, which speaks MCP JSON-RPC to `:desktopApp:hotMcpServer` — semantic tree, clicks by node id, `reload` (recompile + hot-swap), window screenshots. - **Emulator** (`:androidApp` fdroid debug): drive through `.claude/skills/run-meshtastic-android/driver_emulator.py` — scripted deeplink bring-up, uiautomator-based taps, screencap. - **Radio**: neither app does much without one. `mcp__meshtastic__replay_start` (meshtastic MCP) serves a simulated Meshtastic TCP radio; the desktop app reaches it at `127.0.0.1:<port>`, an AVD at `10.0.2.2:<port>`. One client per session — run the desktop and emulator against **different ports** (e.g. 4403 and 4404). ## Prerequisites - Gradle runs go through the machine-wide queue: `~/.claude/bin/gradle-queue`. Everything after its `--` is **Gradle arguments** — it runs `./gradlew` itself (`gradle-queue -- ./gradlew tasks` fails with `Task './gradlew' not found`). - The JetBrains 25 JDK Gradle provisioned at `~/.gradle/jdks/jetbrains_s_r_o_-25-*/…/Contents/Home` (the drivers find it themselves). - Emulator leg: a running AVD (`adb devices`) with the fdroid debug build installed (`./gradlew :androidApp:installFdroidDebug` via the queue if missing). - A simulated radio, e.g. `replay_start(source="meshcon", sim_nodes=30, port=4403, rate=2, loop=true, sim_profile={"traceroute_pairs_per_hour": 0})` — mute the traceroutes or their modals bury whatever you are testing. ## Run: Desktop (agent path) Kill stray instances first — two apps fight over the pid file and the MCP server reports `connected:false` forever: ```bash pgrep -fl "MainKt|devtools.Main" # kill any hits before launching ``` Launch (the Nix dev shell's Darwin stdenv breaks the MapLibre FFI — strip it): ```bash env -u DEVELOPER_DIR -u SDKROOT -u CC -u CXX -u LD -u AR -u NM -u RANLIB -u STRIP -u NIX_CC \ JAVA_HOME=$(ls -d ~/.gradle/jdks/jetbrains_s_r_o_-25-*/*/Contents/Home | tail -1) \ PATH=/usr/bin:/bin:/usr/sbin:/sbin \ ~/.claude/bin/gradle-queue -- :desktopApp:hotRunAsync ``` `BUILD SUCCESSFUL` + `desktopApp/build/run/main/main.pid` on disk means the app is up. Drive it. Each driver invocation spawns a fresh `hotMcpServer`, auto-waits for it to attach (asynchronous — the driver polls `status` for you), runs the commands in order, and exits: ```bash python3 .claude/skills/run-meshtastic-android/driver.py tree # semantic tree (JSON, node ids) python3 .claude/skills/run-meshtastic-android/driver.py click=170 sleep=1.5 tree python3 .claude/skills/run-meshtastic-android/driver.py raise ss=/tmp/app.png python3 .claude/skills/run-meshtastic-android/driver.py reload # recompile + hot-swap edits ``` Run `driver.py` with no arguments for the full command list (`type=NODEID:TEXT`, `scroll_to=NODEID:IDX`, `restart`, `err`, `logs`, …). `tools` prints the server's live tool schemas if they've drifted. **Verified flow** (connect to a sim and see its mesh): nav-rail tabs are semantic `Tab` nodes — `Connect` opened via `click=<its id from tree>`, then the `Network` radio button, then the device row for `127.0.0.1` under Recent Network Devices. The sim's `replay_status` flips to `connected:true` within seconds and the Nodes tab fills with the sim's mesh (`RPLY Replay Observer`, …). The connection card can sit on "Reconnecting…" while packets already flow — the label lags the config download. Trust `replay_status` and the Nodes list, not the card text. **Screenshots capture the window's on-screen region**, so the window must be frontmost: always `raise` before `ss`. If `ss` shows your terminal, that's why. `hotMcpServer` and `reload` compile **outside** gradle-queue (a long-lived stdio server can't hold a slot) — check `~/.claude/bin/gradle-queue --status` before a `reload` if other sessions may be building, and keep those runs short. ### Desktop deeplink launch (no clicking — but no hot reload) The desktop app parses the same Meshtastic deeplink URIs from its **program args** (`Main.kt` accepts `meshtastic://` and `https://meshtastic.org/...`), so a connected app is one command: ```bash env -u DEVELOPER_DIR -u SDKROOT -u CC -u CXX -u LD -u AR -u NM -u RANLIB -u STRIP -u NIX_CC \ JAVA_HOME=$(ls -d ~/.gradle/jdks/jetbrains_s_r_o_-25-*/*/Contents/Home | tail -1) \ PATH=/usr/bin:/bin:/usr/sbin:/sbin \ ~/.claude/bin/gradle-queue -- :desktopApp:run --args="https://meshtastic.org/connections?address=t127.0.0.1:4403" ``` Verified against a sim the app had never connected to before, so it is the deeplink acting, not last-device auto-reconnect. Caveats, all observed: - `hotRunAsync` does **not** accept `--args` (its option list: --auto, --className, --funName, --mainClass, --stdout/--stderr only) — deeplink launch means the plain `run` task, which trades away hot reload. Long driving session → `hotRunAsync` + the driver's click path; quick "get me a connected app" → `run --args=…`. - `run` blocks, so it **holds a gradle-queue slot for the app's whole lifetime**. Keep such runs short, or other sessions' builds will queue behind your app. - The deeplink races last-device auto-reconnect: the app can connect to its remembered device first, then switch to the deeplink's target a moment later — if the remembered device is another sim, that sim briefly shows a client too. - No trust dialog blocked the localhost connect in testing (unlike the Android build, which pops one for a never-seen device). ## Run: Emulator (agent path) Scripted bring-up only — never hand-walk onboarding or the manual-IP dialog: ```bash python3 .claude/skills/run-meshtastic-android/driver_emulator.py -s emulator-5554 \ connect=t10.0.2.2:4404 wait_text=RPLY ss=/tmp/emu.png ``` `connect` force-stops the app, relaunches `org.meshtastic.app.MainActivity` with the debug-only `skip_onboarding` extra and the `/connections?address=` deeplink (`t` = TCP, `x` = BLE, `s` = serial, `n` = disconnect — full path list in `docs/en/developer/navigation-and-deep-links.md`), then waits for the trust dialog newer builds pop and taps its **Connect** button. Success looks like the Connection screen showing `RPLY Replay Observer` with a **Disconnect** button, and `replay_status` reporting `connected:true`. Other commands: `dump`, `find=TEXT`, `tap_text=TEXT`, `tap=X,Y`, `text=`, `key=`, `swipe=`, `launch`, `stop` — run with no arguments for the list. Default package is `com.geeksville.mesh.fdroid.debug` (`-p` to override). ## Run (human path) `./gradlew :desktopApp:run` (via the queue, same env hygiene) opens the window without hot reload; Ctrl-C to stop. The emulator app is just the launcher icon — but a debug build launched by icon lands on onboarding; the deeplink path above is faster even for humans. ## Stopping - Desktop: take the pid from the app's own pid file — it is a Java properties file (not a bare pid) and self-deletes on clean exit: ```bash kill $(sed -n 's/^pid=//p' desktopApp/build/run/main/main.pid) ``` If the pid file is gone but a process lingers, `pgrep -af "MainKt|devtools.Main"`, check each match's path for **this** checkout, and kill that specific PID — a bare `pkill` on the pattern can take down another checkout's or session's app. - Emulator: `driver_emulator.py -s <serial> stop`. - Sim: `replay_stop`. Sessions the sim created are real user data in the app's DB; the app's last-selected device is now the sim — switch back on the Connect screen if a real radio should reconnect. ## Gotchas - **`gradle-queue -- ./gradlew …` fails**: args after `--` go to `./gradlew`, which the wrapper runs itself. And piping its output (`| tail`) eats the exit code — check for `BUILD SUCCESSFUL` in the text, not `$?`. - **`tap_text` matches substrings**: bare `Connect` also matches "Stop **Connect**ing" and "Re**connect**ing…". The driver tries exact text first; wait on the trust dialog's title ("Connect to this device"), not its button. - **The MCP server attaches asynchronously** — a `tree` fired immediately after spawn returns "No application is currently connected". The driver auto-waits; if it times out, the app isn't running (or a stray instance holds the pid file). - **`take_screenshot` needs the window visible** — `raise` first (System Events `AXRaise` targeting the window literally named "Meshtastic Desktop"; with two java processes, pid-based frontmosting picks the wrong one). - **One client per simulated node.** Two apps pointed at the same sim don't queue — they fight, stealing the connection back and forth so both flap between Connected and Reconnecting. The desktop app holding port 4403 means the emulator needs its own `replay_start` on 4404. - **`adb shell input text` can leave a trailing space**; dialogs' Add buttons silently no-op on it. And don't press BACK to dismiss the keyboard — it closes the dialog. - **Swipe near x≈30** in lists; mid-screen swipes get eaten by embedded maps. Never busy-loop adb — pace with `adb shell sleep 2` or the emulator drops offline. - The desktop app auto-reconnects to its last device on launch — it may already be connected to a real radio when you attach; check the Connect screen before assuming the sim. ## Troubleshooting - `Task './gradlew' not found in root project` → you passed `./gradlew` after `gradle-queue --`; drop it. - `BUILD FAILED in 1s` from `hotRunAsync` with slots free → read the full output; the queue wrapper's exit code vanishes behind pipes. - Screenshot is your terminal → `raise` before `ss` (window wasn't frontmost). - `connected:false` forever from `status` → stray `MainKt` from another checkout or worktree; `pgrep -fl MainKt`, kill, relaunch. - Trust dialog never tapped, app stuck on dialog → older driver matched "Reconnecting…"; re-run `tap_text=Connect` (exact match wins now). - UI card stuck "Reconnecting…" but sim says `connected:true` → not stuck; config download in progress. Check the Nodes tab for the sim's nodes. - Connection flapping → `desktopApp/build/run/main/hotRun.stderr.txt` carries the transport-level story ("Handshake stall detected at Stage 1 … requesting forced transport restart" is the app self-recovering, not a crash). Also check that a second app isn't fighting for the same sim (one client per simulated node). - A bare `status` right after spawn can report `connected:false` while the app is fine — the server attach is asynchronous; `wait` (or any UI command, which auto-waits) is the truth.
Ver en GitHub