Adds the modjam package: a MeshCore-backed test station service for Pi (IDLE + RUNNING states, cuesheet-driven), a control station REPL for the Mac, and a UDP simulator that swaps in for the radio when SIMULATOR=true (drops cross-config packets and a configurable fraction of test-payload datagrams to mimic real radio loss). docker-compose runs three sim stations + control on a bridge net. TSV log format matches the reference traces. Pytest suite covers protocol codec, cuesheet builder, TSV logger, config loader, and UDP radio packet routing/loss; .forgejo/workflows runs it on push to main and on PRs. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
3.4 KiB
mesh-control
Operator quickstart for the LoRa modulation jam test framework. Protocol/spec lives in reference/concept.md.
Components
modjam-station— runs on each Test Station (Raspberry Pi or simulator container) talking to a MeshCore node over USB serial.modjam-control— interactive REPL on the macbook (or simulator container) that sends START/STOP commands.- Simulator —
docker composewith three test stations (A, B, C) and one control container, swapping the radio for UDP.
Install (host: Pi or Mac)
python3.11 -m venv .venv && source .venv/bin/activate
pip install -e .
Drop a config at ~/modjam-config.json. Use sim/station-a.json as a template; add "port": "/dev/ttyUSB0" (Pi) or "/dev/tty.usbmodem1301" (Mac) for the attached MeshCore node.
Run:
modjam-station # on each Pi
modjam-control # on the Mac
Simulator
Three stations + control, no hardware:
docker compose build
docker compose up -d station-a station-b station-c
docker compose run --rm control
Inside the control REPL:
> start name=sim1 stations=A,B,C bw=500 sf=8 cr=5 pow=22 duration=20 padding=10 spacing=2 size=40 at=1
> stop
> quit
TSV logs appear in ./sim/logs/ (one per station per run, format matches reference/A.tsv / reference/B.tsv).
SIMULATOR=true is what flips the radio backend from MeshCore-USB to UDP. The UDP backend drops cross-config datagrams, so receivers only "hear" senders whose freq/bw/sf/cr/channel match — same behavior as real radios.
The simulator also drops a fraction of received test packets at random to mimic real-world packet loss. Default is 15%; override with SIM_PACKET_LOSS=0.0 (no loss) up to 1.0. Protocol traffic (START/STOP, heartbeats, next/done) is never dropped — only payloads matching the test-packet pattern. Each station gets a deterministic per-station RNG seed so reruns are repeatable; set SIM_SEED=... to vary.
SIM_PACKET_LOSS=0.3 docker compose up -d station-a station-b station-c
REPL commands
| Command | Effect |
|---|---|
start [k=v ...] |
encode and broadcast a START command (see reference/concept.md for keys: name, f, bw, sf, cr, pow, size, stations, duration, padding, spacing, at) |
stop |
multicast STOP — running stations return to IDLE |
help |
show command list |
quit |
exit |
Layout
modjam/
├── cli.py # entrypoints
├── config.py # ~/modjam-config.json loader
├── protocol.py # `<cmd>[:<station>]|k:v,…` codec + heartbeat/next/done formatters
├── cuesheet.py # build cuesheet from START params
├── station.py # IDLE/RUNNING state machine
├── control.py # REPL + rx tail
├── log.py # TSV logger
└── radio/
├── base.py # Radio ABC
├── meshcore.py # MeshCore over USB serial
├── udp.py # simulator radio
└── factory.py # picks backend from SIMULATOR env
Tests
pip install -e '.[test]'
pytest
CI runs the same suite via .forgejo/workflows/test.yml on push to main and on PRs.
Scope
v1: IDLE + RUNNING only. RESULTS / DOWNLINKING is not implemented yet (see reference/concept.md and notes.md for the protocol).