Files
meshtastic/scripts/meshtasticator_e2e
eric 58b18d6260 fix(ci): make run_e2e.sh executable; invoke via bash in workflow
First dispatch (run 10535) failed instantly with Permission denied on the
orchestrator: the file lost its executable bit in the branch move. Set mode
100755 and call through bash so a future mode regression cannot break the job.
2026-09-07 21:30:01 -07:00
..

Meshtasticator e2e — radio-level validation for hermes-meshtastic

This directory is the radio-level counterpart to the contract suite (plugin/tests/contract). The contract suite guarantees the adapter matches the Hermes gateway API; it cannot prove the adapter actually talks to a Meshtastic node. These e2e tests boot the Meshtasticator interactive simulator — which runs the real MeshtasticD device software per node with a simulated LoRa PHY — and drive the adapter against a live simulated node.

Layer Validated by
Adapter ↔ Hermes gateway API plugin/tests/contract (no deployment)
Adapter ↔ real MeshtasticD TCP/protobuf session this suite
Inbound text across the (simulated) LoRa mesh this suite
Outbound chunked sends arriving at another node this suite
True RF (silicon LoRa, real propagation, duty cycle) physical radio only

Layout

scripts/meshtasticator_e2e/
  run_e2e.sh                  orchestrator: boots N nodes, runs pytest, tears down
  tcp_forward.py              localhost->daemon TCP forward (remote docker daemons)
  README.md                   this file
plugin/tests/e2e/
  test_meshtasticator_e2e.py  the e2e assertions (skip-gated)
.gitea/workflows/
  meshtasticator-e2e.yaml     on-demand CI job (docker-enabled runner)

Prerequisites

  • uv + Python 3.13.
  • A contract venv with the real gateway and the plugin (with deps) installed:
    scripts/contract-test.sh                 # creates plugin/.venv-contract (hermes-agent pinned)
    uv pip install --python plugin/.venv-contract/bin/python -e plugin
    
    (-e plugin installs the meshtastic client library the tests also use.)
  • A clone of Meshtasticator: git clone https://github.com/meshtastic/meshtasticator
  • Runtime for the simulator — pick one:
    • Docker (simplest, works on macOS/Windows): the simulator pulls the meshtastic/meshtasticd image on first run (slow once). Node TCP API ports are published on your docker host's localhost, which is what the simulator itself expects.
    • Linux native: build MeshtasticD's native PlatformIO target and pass the build dir with --program.
  • Optional but recommended: an isolated simulator venv, because Meshtasticator pins old client libs (meshtastic~=2.6.1 in its requirements.txt):
    uv venv --python 3.13 /tmp/sim-venv
    uv pip install --python /tmp/sim-venv/bin/python \
      -r <meshtasticator>/requirements.txt docker
    export SIM_VENV=/tmp/sim-venv     # else the script uses plain `python3`
    

Run

# Docker mode (default)
MESHTASTICATOR_DIR=/path/to/meshtasticator \
  scripts/meshtasticator_e2e/run_e2e.sh

# Linux native mode
MESHTASTICATOR_DIR=/path/to/meshtasticator \
  scripts/meshtasticator_e2e/run_e2e.sh \
    --mode native --program /path/to/firmware/.pio/build/native

# Tune the scenario
  ... --nodes 3 --channel-index 0 --host 127.0.0.1

The orchestrator boots the simulator, waits until N node TCP API ports accept connections (Meshtasticator assigns node n → port 4404 + n), then runs pytest plugin/tests/e2e with MESHTASTICATOR_E2E=1. Simulator output lands in meshtasticator-sim.log at the repo root.

Remote docker daemon (DOCKER_HOST=tcp://host:2375): the simulator's own control connections assume nodes listen on localhost, so the script starts a tcp_forward.py per node port (localhost → daemon host) before booting the simulator. Nothing is forwarded when the daemon is local (unset/unix DOCKER_HOST).

Without MESHTASTICATOR_E2E=1 the e2e module skips, so ordinary unit/CI runs stay fast and gateway-free.

CI

.gitea/workflows/meshtasticator-e2e.yaml runs the whole flow on a docker-enabled runner (on demand via workflow_dispatch until validated, then flip on the commented pull_request trigger): it builds the Hermes contract env (pinned like release.yaml), installs the plugin with deps, clones Meshtasticator at a pinned commit, installs the simulator's own requirements in an isolated venv, and calls run_e2e.sh --mode docker. Simulator logs print on failure. Only one e2e run at a time (the sim hardcodes the Meshtastic container/volume name on the shared daemon).

What the tests do

  1. Inbound: peer node 1 broadcasts text; the adapter (connected to node 0) must receive it as a real gateway MessageEvent (host/port/channel_index come from E2E_PORTS/E2E_HOST/E2E_CHANNEL_INDEX).
  2. Outbound: the adapter sends a >180-char message; it must be chunked and the final chunk ((i/n) … word59) must arrive at peer node 1.

A _ProbeAdapter subclass records inbound events instead of dispatching to a Hermes agent handler (no agent runs in these tests; dispatch is already covered by the contract suite).

Status / open items (v1 scaffold)

This is an initial, structurally validated scaffold. A full green run still needs to happen on a Linux/Docker host with a MeshtasticD build, and these items are expected to need iteration there:

  • Node adjacency: the simulator's default random placement may put nodes out of range. For deterministic tests, seed close-together coordinates (the sim supports --from-file with an out/nodeConfig.yaml).
  • Channel semantics: tests default to channel index 0 (stock primary channel). Mirroring production (channel 1 / private channel) requires configuring matching channels on the simulated nodes.
  • Reconnect flow: killing/restarting a single simulated node needs per-node lifecycle control (native mode subprocesses; docker mode currently runs all nodes in one container), so the adapter's watchdog reconnect is not yet exercised here. See the watchdog unit coverage in the adapter for now.
  • CI validation run: .gitea/workflows/meshtasticator-e2e.yaml is drafted and on-demand only; it still needs a first green run on the docker-enabled runner (topology assumptions: docker reachable from the job, node ports on the job's localhost via the automatic tcp forwards). Trigger it via workflow_dispatch, watch the sim log step, and only then enable the pull_request trigger.