hfmodemDocumentation

07 — Host-API mapping (TCP command / data interface)

VARA presents its host interface as two TCP sockets: a command channel on port 8300 carrying settings, session control and asynchronous notifications, and a data channel on port 8301 carrying payload bytes. A host application — Pat, Winlink Express — drives the modem entirely through these two sockets. This section specifies the framing, the command set, the notifications, and how each maps onto the ARQ session states of `05`.

The implementation of this interface in this repository is hfmodem/kestrel/host/protocol.py (message framing and command parsing) and hfmodem/kestrel/host/server.py (the two sockets).

7.1 Transport

AttributeValue
Command port8300 (TCP)
Data port8301 (TCP)
Command framingASCII text; each message terminated by a single \r (0x0D, CR). Several messages may share one TCP segment → split on \r
Data framingRaw, un-framed bytes — no length prefix, no delimiter. Bytes written are transmitted; bytes read are what the far end sent. Flow control is out-of-band via BUFFER on 8300
EncodingCommand channel ASCII; data channel binary

7.2 Commands (client → modem)

CommandArgumentsSemanticsExpected response
MYCALLc1 [c2..c5]Set up to 5 station callsignsOK
LISTENON / CQ / OFFArm/disarm inbound connect; CQ also enables CQ notificationsOK
CONNECTsrc dst (HF/SAT); FM adds via d1 d2Start an ARQ session to dstOK; then PENDING/CONNECTED/DISCONNECTED async
DISCONNECT—Graceful close (flushes TX buffer first)OK; then DISCONNECTED
ABORT—Immediate "dirty" disconnect; an accepted CQ finishesOK
BW500 / BW2300 / BW2750—Select HF bandwidth (narrow / standard / tactical)OK
COMPRESSIONOFF / TEXT / FILES / ONPayload compression; legacy ON selects TEXTOK
CHATON / OFFKeep an idle, answering link open; ON enables listening and CQ reports; OFF preserves listening and suppresses CQ reportsOK
CLEANTXBUFFER—Clear queued data only when not holding the sending turnCLEANTXBUFFER BUFFEREMPTY, CLEANTXBUFFER FAILED (trailing space), or CLEANTXBUFFER OK
CQFRAMEsrc bwSend a CQ frame (HF)OK; peers may emit CQFRAME …
VERSION—Query modem version; a later addition, outside the base command setVERSION x (or WRONG on old builds)

Most commands reply with OK (accepted) or WRONG (bad command); VERSION and CLEANTXBUFFER have the specific replies above. Bandwidth/compression/MYCALL/LISTEN are settings; CONNECT/DISCONNECT/ ABORT/CQFRAME drive the ARQ state machine (`05`).

The replacement supports CHAT timing and CQ transmission/reception. CQ reports are enabled by LISTEN CQ or CHAT ON. LISTEN ON, LISTEN OFF and CHAT OFF suppress the entire CQ notification sequence. CQFRAME accepts exactly one space between its three fields. Calls are uppercased, allow seven base characters and optional SSIDs 1–15, and may be all digits. Explicit -0, suffixes above 15, /P and underscores return WRONG. Arguments 500, 2300 and 2750 select codes 1, 2 and 3 independently of the configured modem bandwidth; other bandwidth arguments return OK without transmitting. A pending CQ or pending/active link returns WRONG. ABORT leaves an accepted CQ intact; PTT OFF follows actual audio completion. Command edges are pinned to B1/B2/B4 in cq-fix1-host.json and the CQ replay ledger; cq-fix2-modes.json records the later listen/chat notification checks. Stock's SSID overflow and early PTT OFF are deliberately not reproduced. The owner-serviced heartbeat, receive BUSY events and registration mapping are implemented by the live producers.

Kestrel also provides CQFRAMEFORCE, BEACONFORCE and bounded BROADCAST requests. These are extensions; an admission reply is not a transmit result. Its BUFFER count uses original bytes for OFF/TEXT and container bytes for FILES.

7.3 Async notifications (modem → client)

NotificationMeaningWhen emitted
OK / WRONGcommand accepted / rejectedsynchronously after each command
CONNECTED src dst [bw]ARQ link established (bw present on HF/FM, absent on SAT)on handshake completion
DISCONNECTEDsession closed by either endon graceful/abort close
PTT ON / PTT OFFkey/unkey the radio (host drives PTT)around every TX burst
BUFFER nbytes remaining in the TX queueon enqueue and on each ack-drain
PENDING / CANCELPENDINGinbound connect detected / aborted (pause scanning)on CR detect / give-up
BUSY ON / BUSY OFFchannel busy-detector stateon busy-detector edges
High speed capability (per callsign or link), or speed limited link statushigh speed capability status (gates full speed)at connect
BITRATE (N) x bps TX / … RXcurrent speed level of my next over / the frame I decoded (two spaces after ))RX immediately after SN for a decoded data over, once even for two frames; CQ uses level 1 at 18 bps; no RX bitrate for connect or short controls
IAMALIVEhost-facing keepalive~every 60 s while connected
SN x.xsignal report in dB, independent of chat modeonce per received data over, connect frame or CQ frame; no short-control reports
CQFRAME src bwa CQ frame was decodedwith CQ reports enabled, after BITRATE (1) 18 bps RX and SN
ENCRYPTION … / ENCRYPTED/UNENCRYPTED LINKlink encryption status (newer builds)at connect

7.4 Data-path semantics

AttributeValue
Buffering / backpressureHost writes raw bytes to port 8301; the modem queues them and reports the queue depth as BUFFER n on 8300, decrementing per ACKed block. The host throttles on BUFFER
BUFFER unitsraw host bytes still queued (independent of COMPRESSION); each ACKed block clears 43 B at level 4, 34 B at level 3
The data port is not opaqueA 4.9.0 reads what the host writes: FQ\r handed to the data port ends the link by itself, Disconnecting by FQ in the modem's own log, with nothing keyed and no DISCONNECT command sent. Measured 2026-08-26, and it cost a bench a second handover before it was found. A B2F layer that means FQ for its peer has to answer the far end's FQ with something else — the benches here answer ;OK\r
Relationship of data port to ARQ stateData port bytes are only transported over the air while CONNECTED. Bytes the host writes before the link comes up are buffered against the TX queue (and reported via BUFFER n), then flushed into the session the moment CONNECTED fires — they are not dropped. A graceful DISCONNECT flushes the queue first; ABORT discards it. In this repository, hfmodem/kestrel/host/modem_core.py's LoopbackModem is where that pre-connect buffering and flush-on-CONNECT behaviour lives

7.5 Mapping to internal state

Ties host-API events to the state machine in `05`.

Host-API eventInternal transition (05)
cmd LISTEN ONDISCONNECTED → LISTENING
cmd CONNECT src dstDISCONNECTED → CONNECTING (keys CR burst; BUSY ON)
notif PENDINGLISTENING → CONNECTING (CR decoded)
notif CONNECTED …CONNECTING → CONNECTED
notif BUFFER n (n↓)CONNECTED: a block was ACKed
cmd DISCONNECTCONNECTED → DISCONNECTING
notif DISCONNECTEDDISCONNECTING → DISCONNECTED (BUSY OFF)

7.6 Behaviour not fixed here

BW500 levels 1–4 use the spectral estimator fitted to the P0 noise ladder and P2 gate-2 paired level-4 readings (2026-10-18). Its clean ceiling is 21.4 dB. The common low-level estimator is not separately validated on paired L1–3 data. The P0-only fit missed the held-out gate by up to 3.0 dB; the final pooled fit has no independent gate holdout (only one observation at each noisy gate setting).

For higher levels, only the host SN is mapped; the decoder quality used for burst detection and rate selection is unchanged. The paired scalar measurements and transcript times are pinned in tests/kestrel/fixtures/chat-sn-pairs.json.

BW500 levelDecoder raw dB observedHost correctionMaximum paired error
534.63–34.64−8.485 dB0.95 dB
639.40–39.64−13.1685 dB1.63 dB
861.83–63.01−32.34 dB0.31 dB
1063.01−34.26 dB0.65 dB
1161.88–63.01−34.6345 dB0.72 dB
1355.63–59.5527.7 dB plateau1.70 dB

These are sparse, clean-cable calibrations, not noisy-channel validation. Beyond each observed range the nearest endpoint's additive correction is used, except L13 stays at 27.7 dB above raw 59.546675. Additive extrapolation is unvalidated. No stock paired SN exists for the noisy level-6 overs: the gate's 39.4–39.9/22.8/12.7/4.1 values are kestrel's own reports, not stock reference values.

BW500 L7, L9 and L12 now carry one additive point each, so a raw metric near the OFDM ceiling no longer reaches the host as 58-63 dB (bench 3 gate 3 reported 58.2 at L12 and 62.8 at L9). L7 is 30.7 at raw 63.01 and L9 29.7 at raw 63.01, the stock-to-stock medians on these cables; L12 is 28.7 at raw 58.21, its three paired readings. L12 is not monotone in the raw metric (stock 31.4 dB at raw 56.7858, 26.5 dB at raw 58.3450), so this point misses those by up to 4.1 dB, inside stock's own 20.3-31.5 dB spread at that level. All BW2300/BW2750 SN remain uncalibrated and are reported unchanged. The unavailable noisy captures and empty gate WAV directions cannot supply additional calibration pairs.

RX bitrate uses the bandwidth's nominal level ladder (physical table in evidence/RECORD-SEMANTICS.md §6; low levels in 01), independently of SN calibration. When decoder quality is unavailable, neither SN nor BITRATE is emitted for the data over. P0 and gate transcripts pin BW500 L1, L4–8 and L10–13, plus BW2300 L6; remaining nominal rates lack a paired host transcript in these runs.