Run 10538 booted the full simulator and ran pytest — both failures were test-side: the installed meshtastic client moved sendText from Node to the Interface, and the outbound chunked message never arrived at the peer (cause still unknown). Fix the sendText call and add in-run diagnostics so the next failure is self-explaining: - e2e: outbound-timeout failure now reports what arrived at the peer and the simulator log tail; sim now boots with -v (meshtastic debug logging). - run_e2e.sh: on pytest failure in docker mode, dump per-node meshtasticd logs (meshtasticator-nodes.log) before cleanup removes the container.
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). Readiness is probed on the upstream host (the daemon), never
on the forwarders themselves.
Other headless adaptations the script makes automatically:
- patches
lib/gui.pyin the pinned Meshtasticator checkout: forces the Agg backend (the code callsmatplotlib.use("TkAgg")at import) and makesmove_figure()a no-op when no Tk window manager exists (its unconditionalcanvas.manager.windowaccess crashes under Agg at Graph construction). Only the interactive GUI modes, never used by this harness, are lost, - warms the
meshtastic/meshtasticdimage (docker pull) before booting the simulator so the pull is not on the simulator's node-boot critical path, - feeds the simulator's interactive
cmdloopfrom a FIFO held open until teardown — an EOF stdin (e.g. CI) would otherwise make it exit right after booting the nodes.
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.