From 04d965d67a7264a1c7c211494aebda1953df7603 Mon Sep 17 00:00:00 2001 From: "info@mode42.com" Date: Fri, 7 Aug 2026 18:25:13 +0000 Subject: Initial push --- include/max25/protocol.md | 213 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 213 insertions(+) create mode 100644 include/max25/protocol.md (limited to 'include/max25/protocol.md') diff --git a/include/max25/protocol.md b/include/max25/protocol.md new file mode 100644 index 0000000..740809f --- /dev/null +++ b/include/max25/protocol.md @@ -0,0 +1,213 @@ +# M25/1 — MAX25 Terminal ↔ max25d protocol + +**Version:** M25/1 (stable for `max25-terminal` / `max25-client`) + +Line-oriented UTF-8 text. One command or response per line, terminated by `\n` (LF). Optional `\r` before `\n` is stripped by the daemon. + +Developer guide: [docs/MAX25-CLIENT.md](../../docs/MAX25-CLIENT.md) + +--- + +## Transport + +| Method | Default | Override | +|--------|---------|----------| +| TCP | `0.0.0.0:7325` | `max25d.ini` `[network]`, env `MAX25_HOST` / `MAX25_PORT` | +| Unix stream | `/run/max25/modem.sock` | `max25d.ini`, env `MAX25_UNIX`; without root use a writable path or TCP | + +The official client tries Unix first (if configured), then TCP. + +--- + +## Connection handshake + +On connect, **the daemon sends first** (client must not send before reading). + +### Without TCP auth (`tcp_password` empty) + +``` +OK +STATUS hardware= device= devices= mode= callerid= callid= ax25_ui=on|off connected=yes|no stack= serial= error=valid|invalid voice=valid|invalid +``` + +### With TCP auth (`[network] tcp_password` set — **TCP only**) + +Unix socket clients skip auth (local trust). + +``` +AUTH required +AUTH +OK +STATUS hardware=… +``` + +Client sends the password as a single line: `AUTH ` (plain text, v1). + +Wrong or missing password → `ERR auth failed` and the connection closes. + +| Client flag | Env | +|-------------|-----| +| `-P`, `--password` | `MAX25_TCP_PASSWORD` | + +`` for `stack=` is typically `running`, `stopped`, or `error`. + +Implementations **must buffer** incomplete lines across `read()` calls. + +--- + +## Client → daemon commands + +| Command | Description | +|---------|-------------| +| `PING` | Keepalive | +| `GET STATUS` | Query state → `STATUS …` then `OK` | +| `GET DEVICES` | List enabled devices → one `DEVICE …` line each, then `OK` | +| `SET DEVICE ` | Select TX target device for this session (`SELECT DEVICE` alias) | +| `SET CALLERID ` | Live source callsign (uppercase) | +| `SET CALLID ` | Live destination callsign | +| `SET AX25_UI on\|off` | Toggle AX.25 UI framing for TX | +| `CONNECT` | Attach modem session on all enabled devices (required before `SEND`) | +| `DISCONNECT` | Detach session (daemon keeps running) | +| `SEND ` | Transmit line on session-selected device — payload is remainder of line after `SEND ` (may be empty) | +| `MONITOR on\|off` | RX-only mode (`SEND` → `ERR monitor-only`) | +| `BAN ` | Block AX.25 source — incoming UI frames dropped silently | +| `UNBAN ` | Remove source from ban list | +| `BANS` | List banned callsigns → one `BAN …` line each, then `OK` | + +Command keywords are case-sensitive except `SET AX25_UI` flags (`on`/`off` case-insensitive). + +### CALLSIGN rules + +AX.25 address text (see [docs/PACKET-RADIO.md](../../docs/PACKET-RADIO.md)): + +- Call body: **1–6** chars `A–Z` `0–9` +- SSID: optional `-0` … `-15` + +Invalid `SET CALLERID` / `SET CALLID` → `ERR invalid CALLERID` / `ERR invalid CALLID`. + +--- + +## Daemon → client responses + +| Line | Meaning | +|------|---------| +| `OK` | Command succeeded | +| `ERR ` | Command failed | +| `STATUS hardware=… device=… devices=… mode=… callerid=… callid=… ax25_ui=… connected=… stack=… serial=… error=valid\|invalid voice=valid\|invalid` | State snapshot | +| `DEVICE id=… hardware=… serial=… stack=… enabled=… error=… voice=…` | One enabled device (`GET DEVICES`) | +| `RX device= ` | Received traffic from `` for display | +| `RX ` | Loopback TX echo (no serial) or legacy single-device | +| `EVENT connected` | Session attached (`CONNECT`) | +| `EVENT disconnected` | Session detached (`DISCONNECT`) | + +### Multi-line command replies + +| Command | Response sequence | +|---------|-------------------| +| `PING` | `OK` | +| `GET STATUS` | `STATUS …` → `OK` | +| `GET DEVICES` | `DEVICE …` (one per enabled id) → `OK` | +| `SET DEVICE ` / `SELECT DEVICE ` | `OK` or `ERR unknown device: …` | +| `SET CALLERID` / `SET CALLID` / `SET AX25_UI` / `MONITOR` | `OK` or `ERR …` | +| `CONNECT` | `EVENT connected` → `OK` | +| `DISCONNECT` | `EVENT disconnected` → `OK` | +| `SEND ` | `RX device= ` → `OK` (sender); other clients get `RX …` only | + +With `ax25_ui=on`, framed text looks like: `[AX25 UI >] `. + +### Errors + +| Condition | Response | +|-----------|----------| +| Unknown command | `ERR unknown command: ` | +| `SEND` without `CONNECT` | `ERR not connected` | +| `SEND` in monitor mode | `ERR monitor-only` | +| Invalid UTF-8 line | `ERR invalid utf-8` | + +--- + +## Session model + +- `max25d` holds **global** `callerid`, `callid`, `ax25_ui` for all clients. +- `device=` in `STATUS` is the **session TX target** (default: `[devices] default=` or first enabled). +- `devices=` lists all enabled device ids (comma-separated). +- `connected` is per-daemon-session state (shared across clients in current implementation). +- `MONITOR` is per-daemon global flag in current implementation. +- Hardware lifecycle (`stack=running`) is owned by `max25d` per device, not the terminal. +- Each enabled device owns one serial port exclusively (one `KissBridge` each). + +## Multi-device configuration (legacy) + +> **Host layout:** **1× Main** + optional **5+ Secondaries** per server — [ARCHITECTURE.md](../../docs/ARCHITECTURE.md#host-layout--main--secondaries). Multi-id syntax below remains for backward compatibility. + +`max25d.ini` `[devices]` section (see `share/max25/max25d.ini.example`): + +```ini +[devices] +default = tnc2c +tnc2c = /dev/ttyS4 +``` + +Legacy multi-id example (deprecated for new sites): + +```ini +pktnc2 = /dev/ttyS5 +``` + +Legacy single-device configs (`[daemon] device=` + optional `[serial]`) remain valid. + +Per-device serial overrides: `[serial.]` sections (baud, line, dtr_rts, kiss_entry). + +### Reporting (`[reporting]` in INI) + +| Key | Default | Meaning | +|-----|---------|---------| +| `error_transmissions` | `yes` | `yes`: `error=valid` when link healthy and last AX.25 decode OK; `no`: always `error=invalid` | +| `voice_transmissions` | `yes` | `yes`: `voice=valid` when acoustic/CRDOP path ready; `no`: always `voice=invalid` (TNC-only: `voice=valid`) | + +Invalid AX.25 UI frames on serial backends emit `EVENT device= error=invalid` when `error_transmissions=yes`. + +--- + +## Example session + +``` +← OK +← STATUS hardware=tncs device=tnc2c devices=tnc2c mode=standalone callerid=CB-0 callid=QST ax25_ui=on connected=no stack=stopped +→ SET DEVICE tnc2c +← OK +→ CONNECT +← EVENT connected +← OK +→ SET CALLERID DG1ABC +← OK +→ SEND 73 +← RX device=tnc2c [AX25 UI DG1ABC>QST] 73 +← OK +→ GET STATUS +← STATUS hardware=tncs device=tnc2c devices=tnc2c,pktnc2 mode=standalone callerid=DG1ABC callid=QST ax25_ui=on connected=yes stack=stopped +← OK +→ DISCONNECT +← EVENT disconnected +← OK +``` + +(`→` client, `←` daemon) + +--- + +## Reference code + +| Component | Path | +|-----------|------| +| C client library | `stacks/terminal/max25_proto.c` | +| Terminal UI | `stacks/terminal/max25_terminal.c` | +| Daemon server | `stacks/daemon/max25d` | +| Offline smoke test | `stacks/daemon/test_proto.py` | +| Multi-device tests | `stacks/daemon/test_multi_device.py` | + +--- + +## Stability + +M25/1 is the long-term binding contract for **`max25-terminal` only**. New features should extend this protocol with documented, backward-compatible lines — not introduce a parallel client protocol. -- cgit v1.3.1