#!/usr/bin/env bash # Boot N Meshtasticator nodes and run the plugin's radio-level e2e tests # (plugin/tests/e2e) against node 0. # # Meshtasticator's interactive simulator (https://meshtastic.org/docs/software/meshtasticator/) # runs the real MeshtasticD device software per node and simulates the LoRa # PHY, so these tests exercise the adapter's real TCP/protobuf session, # inbound/outbound text flow across the simulated mesh, and chunked sends — # without physical radios. It cannot validate silicon-level RF. # # Prerequisites # * Linux host, OR Docker (macOS/Windows): the simulator auto-falls back to # Docker on non-Linux and can build MeshtasticD itself (`-d`). # * Native Linux mode: build MeshtasticD's 'native' target (PlatformIO, # select 'native') and pass --program . # * uv + python 3.13 for the contract venv (plugin/.venv-contract). # # Usage # MESHTASTICATOR_DIR=/path/to/meshtasticator scripts/meshtasticator_e2e/run_e2e.sh # # docker mode (default): ... run_e2e.sh --mode docker # # native mode: ... run_e2e.sh --mode native --program /path/to/firmware/.pio/build/native # # node count / channel: ... run_e2e.sh --nodes 3 --channel-index 0 # # Environment # MESHTASTICATOR_DIR path to a clone of github.com/meshtastic/meshtasticator # E2E_VENV python env with hermes-agent + plugin deps installed # (default: plugin/.venv-contract — set it up with # `uv pip install -e plugin/.hermes-src` equivalents via # scripts/contract-test.sh, then `uv pip install -e plugin`) set -euo pipefail REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" SIM_DIR="${MESHTASTICATOR_DIR:?set MESHTASTICATOR_DIR to a meshtasticator clone}" NODES=3 MODE=docker PROGRAM="" CHANNEL_INDEX=0 HOST=127.0.0.1 VENV="${E2E_VENV:-$REPO_ROOT/plugin/.venv-contract}" while [ $# -gt 0 ]; do case "$1" in --nodes) NODES="$2"; shift 2 ;; --mode) MODE="$2"; shift 2 ;; --program) PROGRAM="$2"; shift 2 ;; --channel-index) CHANNEL_INDEX="$2"; shift 2 ;; --host) HOST="$2"; shift 2 ;; *) echo "unknown arg: $1" >&2; exit 2 ;; esac done command -v uv >/dev/null 2>&1 || { echo "uv required" >&2; exit 1; } [ -d "$SIM_DIR" ] || { echo "meshtasticator not found at $SIM_DIR" >&2; exit 1; } [ -x "$VENV/bin/python" ] || { echo "e2e venv missing at $VENV (see scripts/contract-test.sh)" >&2; exit 1; } export MPLBACKEND=Agg # headless matplotlib (no display needed) SIM_LOG="$REPO_ROOT/meshtasticator-sim.log" SIM_ARGS=("$NODES") if [ "$MODE" = native ]; then [ -n "$PROGRAM" ] && [ -d "$PROGRAM" ] || { echo "--mode native needs --program " >&2; exit 2; } SIM_ARGS+=(-p "$PROGRAM") else SIM_ARGS+=(-d) fi echo "== booting Meshtasticator ($MODE) with $NODES node(s) from $SIM_DIR ==" ( cd "$SIM_DIR" python3 interactiveSim.py "${SIM_ARGS[@]}" >"$SIM_LOG" 2>&1 ) & SIM_PID=$! PORTS="" cleanup() { echo "== tearing down simulator (pid $SIM_PID) ==" kill "$SIM_PID" 2>/dev/null || true wait "$SIM_PID" 2>/dev/null || true if [ "$MODE" = docker ]; then # The sim container may outlive the process on abrupt kills. docker ps -q --filter "ancestor=meshtastic-meshtasticd" 2>/dev/null | xargs -r docker rm -f >/dev/null 2>&1 || true fi } trap cleanup EXIT echo "== waiting for $NODES node TCP API port(s) to accept connections ==" READY="" for _ in $(seq 1 180); do # up to ~5 min: docker builds firmware on first run READY="$("$VENV/bin/python" - "$HOST" "$NODES" <<'PY' || true import socket, sys host, count = sys.argv[1], int(sys.argv[2]) found = [] # Meshtasticator assigns node n -> TCP port starting at 4404 (see # lib/interactive.py TCP_PORT_OFFSET); probe a small window to be safe. for port in range(4403, 4403 + 16): try: with socket.create_connection((host, port), timeout=0.5): found.append(port) except OSError: pass if len(found) >= count: break print(",".join(map(str, found[:count]))) PY )" if [ -n "$READY" ]; then break fi sleep 2 done if [ -z "$READY" ]; then echo "error: no Meshtasticator node ports became reachable on $HOST (see $SIM_LOG)" >&2 exit 1 fi echo " node ports: $READY" echo "== running radio-level e2e tests ==" ( cd "$REPO_ROOT" MESHTASTICATOR_E2E=1 \ E2E_HOST="$HOST" \ E2E_PORTS="$READY" \ E2E_CHANNEL_INDEX="$CHANNEL_INDEX" \ "$VENV/bin/python" -m pytest plugin/tests/e2e -q "$@" )