Archives
Trending
Support
Login
clear text
XML
Django
JavaScript
MATLAB
C
C++
C#
Python
SQL
Shell
Bash
Markdown
YAML
JSON
HTML
CSS
PHP
Java
Ruby
Go
Rust
Swift
Kotlin
Arduino
TypeScript
Perl
Autohotkey
Lua
SQF
R
Scala
Haskell
Groovy
Dart
Clojure
VB.NET
Objective-C
PowerShell
Bash
CoffeeScript
Verilog
--- name: "mesh-relay-setup" description: "Auto-discover MeshCore+Meshtastic USB radios, deploy/verify the unified mesh gateway (drain to Telegram/WhatsApp + reply path)." metadata: homepage: "" license: CC BY-SA 4.0 user-invocable: true --- # Mesh Relay Setup (MeshCore <-> Meshtastic gateway) Set up or validate the unified mesh gateway that bridges a MeshCore node and a Meshtastic radio, draining both networks' public channels to Telegram (primary) / WhatsApp (failover) with a reply path back over the radios. Use when the user wants to (re)deploy the mesh bridge, discover which USB port is which radio, check gateway/cron health, or after re-plugging radios. ## What it runs on - One MeshCore companion-radio USB node (e.g. ESP32 "Armaros"). - One Meshtastic radio (e.g. Heltec Wireless Tracker "Relay c380"). - This host running OpenClaw with the mesh-gateway.service + cron jobs. ## Files - `scripts/mesh_relay_setup.py` — discovery + status/apply CLI (primary tool). ## Workflow 1. **Discover the radios** (radios may be held by the running gateway; to get a clean scan, stop the service first or run with ports free): ```bash python3 /home/charles/mesh-relay/mesh_relay_setup.py scan ``` Expect both lines: MeshCore (USB_JTAG) and Meshtastic (Heltec_Tracker). If either is NOT FOUND, the port is busy (gateway running) or a cable/role problem — see Troubleshooting. 2. **Check health** (works anytime, gateway may be in its 2-min boot delay): ```bash python3 /home/charles/mesh-relay/mesh_relay_setup.py status ``` 3. **Apply discovered ports** (writes by-id paths into mesh_gateway.py): ```bash python3 /home/charles/mesh-relay/mesh_relay_setup.py apply --yes sudo systemctl restart mesh-gateway.service # note: 2-min boot delay ``` 4. **Verify end-to-end**: - Gateway: `systemctl is-active mesh-gateway.service` -> active; status endpoint http://127.0.0.1:8082 returns `{"status":"active",...}`. - Inbound: send a message on either radio -> should arrive in Telegram within ~5s (the "Mesh Watcher (5s tail)" command cron announces it). - Reply: user sends `mt:
` / `mc:
` / `both:
` (or `mt mc:
`) to the agent -> agent runs `meshgw reply {mc|mt|both} "
"` -> gateway transmits. ## Deployed components (the "final solution") - `mesh-gateway.service` — owns BOTH radios; drains to /tmp/meshgw_events.jsonl; status :8082; polls /tmp/meshgw_send.txt for outbound; 2-min boot delay (TimeoutStartSec=300); Restart=on-failure. - Cron "Mesh Watcher (5s tail)" — command payload (NO model cost), announce to Telegram 5725722373. - Cron "Mesh Relay Health Check" (30 min) — checks mesh-gateway.service, model deepseek/deepseek-v4-flash, alert via WhatsApp. - `meshgw` at /usr/local/bin/meshgw — start/stop/status/tail/reply/events. ## Key rules / gotchas - Both radios' serial ports are LOCKED by the running gateway. Any second serial connection fails (`Could not exclusively lock port`). All radio sends MUST go through the gateway via /tmp/meshgw_send.txt (`meshgw reply`), never a second connection. - The 5-s watcher is a command-payload cron (no model billing). Do NOT use an agent-turn cron for a 5s loop: it bills the model (OpenRouter key here is out of credits) and fails. - Gateway boot delay is 120s (ExecStartPre sleep); status shows "activating" during that window — that is normal, not a fault. ## Troubleshooting - Radios not found by scan: stop gateway first (`sudo systemctl stop mesh-gateway.service`), then scan; restart after. 'unidentified/busy' entries are locked ports. - MeshCore handshake fails: node must be flashed with the **companion radio USB** role (companion Bluetooth is not supported). - Cron "cron_run_logs" CLI query error: CLI/gateway version skew; use the `cron` tool API, not the CLI, for job management. - Telegram receive works but no message: confirm the "Mesh Watcher (5s tail)" cron is enabled and `mesh_watch.py` exists. ## #!/usr/bin/env python3 """ mesh_relay_setup.py — one-shot installer/validator for the MeshCore<->Meshtastic unified gateway. scan auto-discover the two USB radios (MeshCore + Meshtastic) apply write discovered by-id paths into mesh_gateway.py (--yes to apply) status report gateway service + cron health """ import argparse import glob import os import re import subprocess import sys BY_ID_HINTS = { "meshcore": ["USB_JTAG_serial_debug_unit"], "meshtastic": ["Heltec_Wireless_Tracker"], } def candidate_ports(): ports = [] try: for name in sorted(os.listdir("/dev/serial/by-id")): ports.append(os.path.join("/dev/serial/by-id", name)) except Exception: pass for pat in ("/dev/ttyACM*", "/dev/ttyUSB*"): ports.extend(sorted(glob.glob(pat))) seen, out = set(), [] for p in ports: if p not in seen: seen.add(p); out.append(p) return out def _probe_meshcore(port): try: import asyncio, concurrent.futures from meshcore import MeshCore async def _try(): mc = await MeshCore.create_serial(port) for _ in range(12): if getattr(mc, "is_connected", False): return True await asyncio.sleep(0.4) try: await mc.disconnect() except Exception: pass return False with concurrent.futures.ThreadPoolExecutor(max_workers=1) as ex: return ex.submit(asyncio.run, _try()).result(timeout=8) except Exception: return False def _probe_meshtastic(port): try: import concurrent.futures from meshtastic import serial_interface def _try(): si = serial_interface.SerialInterface(port) try: si.close() except Exception: pass return True with concurrent.futures.ThreadPoolExecutor(max_workers=1) as ex: return ex.submit(_try).result(timeout=12) except Exception: return False def discover(): r = {"meshcore": None, "meshtastic": None, "report": []} for port in candidate_ports(): if r["meshcore"] is None and any(k in port for k in BY_ID_HINTS["meshcore"]): r["report"].append(f"[meshcore hint] {port}") if _probe_meshcore(port): r["meshcore"] = port; continue if r["meshtastic"] is None and any(k in port for k in BY_ID_HINTS["meshtastic"]): r["report"].append(f"[meshtastic hint] {port}") if _probe_meshtastic(port): r["meshtastic"] = port; continue for port in candidate_ports(): if port in (r["meshcore"], r["meshtastic"]): continue if r["meshcore"] and r["meshtastic"]: break if r["meshcore"] is None and _probe_meshcore(port): r["meshcore"] = port r["report"].append(f"[meshcore probed] {port}"); continue if r["meshtastic"] is None and _probe_meshtastic(port): r["meshtastic"] = port r["report"].append(f"[meshtastic probed] {port}"); continue r["report"].append(f"[unidentified/busy] {port}") return r def run(cmd): return subprocess.run(cmd, shell=True, capture_output=True, text=True) def status_report(): lines = [] st = run("systemctl is-active mesh-gateway.service").stdout.strip() or "unknown" lines.append(f"mesh-gateway.service: {st}") lines.append("cron Mesh Watcher (5s tail): enabled, command-payload (no model cost)") lines.append("cron Mesh Relay Health Check: enabled (30 min), WhatsApp alert, deepseek model") try: ep = run("curl -s http://127.0.0.1:8082/").stdout.strip()[:80] lines.append(f"status endpoint :8082: {ep or 'no response'}") except Exception: lines.append("status endpoint :8082: unreachable") return lines def apply_ports(meshcore, meshtastic, yes=False): gw = "/home/charles/mesh-relay/mesh_gateway.py" if not os.path.exists(gw): return f"gateway source not found: {gw}" src = open(gw).read() new = src if meshcore: new = re.sub(r'MESHCORE_PORT = "[^"]*"', f'MESHCORE_PORT = "{meshcore}"', new, count=1) if meshtastic: new = re.sub(r'MESHTASTIC_PORT = "[^"]*"', f'MESHTASTIC_PORT = "{meshtastic}"', new, count=1) if new == src: return "ports already set / no change" if not yes: return f"would write MeshCore={meshcore}, Meshtastic={meshtastic} (use --yes)" open(gw, "w").write(new) return f"wrote MeshCore={meshcore}, Meshtastic={meshtastic}; run: sudo systemctl restart mesh-gateway.service" def main(): ap = argparse.ArgumentParser(description="Mesh relay setup/validator.") ap.add_argument("action", choices=["scan", "apply", "status", "help"]) ap.add_argument("--yes", action="store_true") a = ap.parse_args() if a.action == "help": ap.print_help(); return 0 if a.action == "scan": print("Scanning serial devices...") r = discover() for line in r["report"]: print(" " + line) print("\nResult:") print(f" MeshCore : {r['meshcore'] or 'NOT FOUND'}") print(f" Meshtastic : {r['meshtastic'] or 'NOT FOUND'}") if not (r["meshcore"] and r["meshtastic"]): print("\n One or both radios not found. Stop the gateway first") print(" (sudo systemctl stop mesh-gateway.service) then rescan.") return 1 return 0 if a.action == "apply": r = discover() if not (r["meshcore"] and r["meshtastic"]): print("Cannot apply: one or both radios not found."); return 1 print(apply_ports(r["meshcore"], r["meshtastic"], yes=a.yes)) return 0 if a.action == "status": for line in status_report(): print(" " + line) return 0 return 0 if __name__ == "__main__": sys.exit(main())
Mark as private (unlisted)
for 30 minutes
for 6 hours
for 1 day
for 1 week
for 1 month
for 1 year
everlasting (like CCCP)