# 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.