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.
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 plugininstalls 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/meshtasticdimage 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
nativePlatformIO target and pass the build dir with--program.
- Docker (simplest, works on macOS/Windows): the simulator pulls the
- Optional but recommended: an isolated simulator venv, because Meshtasticator
pins old client libs (
meshtastic~=2.6.1in 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
- Inbound: peer node 1 broadcasts text; the adapter (connected to node 0)
must receive it as a real gateway
MessageEvent(host/port/channel_indexcome fromE2E_PORTS/E2E_HOST/E2E_CHANNEL_INDEX). - 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-filewith anout/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.yamlis 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 viaworkflow_dispatch, watch the sim log step, and only then enable thepull_requesttrigger.