hfmodemDocumentation

Sabir host interface

Sabir uses one framed CBOR connection for commands, payloads and events. The reference TCP server binds loopback port 8400 by default and serves one application at a time. host/run_server.py runs two modems over simulated audio on ports 8400 and 8410. StationAir can bind the modem to station audio; that integration has not been validated by a two-station Sabir radio session.

The implementation is in host/messages.py, host/hostlink.py and host/modem_core.py. host/client.py is a threaded application client. tests/sabir/test_hostapi.py and test_m4.py cover the framing, telemetry and host-to-host simulated exchanges.

1. Framing and attachment

Each frame is a four-byte big-endian body length followed by one CBOR map. The maximum body is 16 MiB. An oversized prefix receives MALFORMED and closes the connection without allocating its body. Other malformed frames receive MALFORMED and leave the established connection open.

Top-level keys are unsigned integers; key 0 is the unsigned message type m. The CBOR subset comprises 64-bit integers, byte/text strings, arrays, maps, booleans, null and float64. Indefinite lengths, tags and shorter floating-point encodings are unsupported. Map keys must be integers or text and cannot repeat. The encoder uses shortest integer arguments and sorts keys by their encoded bytes. The decoder accepts nonminimal integer arguments; no canonical byte image is needed for host framing.

The first message must be Hello{m:0, proto:"1.0"}. The modem answers with Hello{proto, features, modem, profiles, identity_required:true}. features is ['data-profiles', 'objects']; profiles is [1,2]. The modem sends INCOMPATIBLE and closes on a protocol major mismatch. Invalid initial frames receive MALFORMED and close. Minor versions may add optional fields and events. Unknown message types and unknown integer field keys are ignored.

Commands execute in connection order on the modem's air thread. Events may arrive while a command is pending. There is no periodic keepalive, application flow control or radio binding command. Disconnecting the host connection aborts its modem session. TCP buffers supply backpressure.

2. Commands

Notation: u unsigned integer, s text, b bytes, f float; ? optional. ref:u? correlates command errors and may accompany any command.

mCommandFieldsBehavior
0Helloproto:s, features:[s]?, client:s?Attach handshake
1SetIdentitystation_id:sSet a 1–12 byte printable ASCII identifier, uppercased
2SetProfileprofile:uSelect the presence-beacon profile label: 1 Amateur, 2 Unrestricted
3Listenon:bool, station_id:s?Enable/disable incoming connects; optional identity replaces the modem's listening identity
4Connectpeer_id:s, station_id:s?Connect, using the optional local identity for this session
5Senddata:b, id:u?, deflate:bool?Queue one integrity-protected record
6Disconnect—Drain queued session data and close
7Abort—Abandon queued transmissions and close
8Subscribeevents:[u]Replace the optional-event subscription
9ConfigureDATA options belowConfigure an idle modem
10Beaconaddressee:u?Send a presence beacon; default addressee 0
11SendObjectObject options belowSend a connectionless object

Listen, Connect, Beacon and SendObject require SetIdentity first. SetProfile is a beacon metadata label; it does not apply bandwidth or identification checks. The station's radio configuration selects those checks. Aliases, per-Connect profiles, deadlines, nonzero streams and nonzero priority are unsupported and rejected. stream:0 and priority:0 have ordinary Send semantics. Subscribe.stats_period is ignored; telemetry has no periodic timer.

Configure accepts data_profile:s?, receive_profiles:[s]?, feedback_iters:u?, impulse_blank:f?, bandwidth_hz:u? and inactivity_timeout_s:f?. The option set is validated before applying any change and only while DISCONNECTED or LISTENING. receive_profiles contains at most eight distinct extended profile names; data_profile may prefer any implemented DATA profile, including baseline profiles. The extended receive profiles are doppler, sparse34, narrow, narrow2, narrow4 and wide256. feedback_iters is 0–3, impulse_blank 0–20, and bandwidth_hz is 500, 1500, 2300 or 2750. The inactivity timeout is finite and positive (default 180 seconds); validated peer traffic renews it and an active transmission or validated receive body is protected until completion. Radio and PTT configuration submaps are rejected. The profile registry and waveform widths are specified in SPEC.md.

SendObject accepts data:b, destination:s?, service:u?, data_profile:s?, parity:bool?, repeats:u? and message_id:b?. It requires an idle link with no previous object draining. Defaults are all-call destination *, service 0, profile workhorse, parity true and one repeat. The profile must fit the configured width. Repeats are 1–8. The identifier is 16 bytes, randomly generated when omitted. An object with multiple data fragments may carry one XOR repair fragment.

3. Events

mEventFields
32StateChangedstate:u, peer_id:s?, reason:u?
33CapabilitiesNegotiatedpeer_id:s, peer_capabilities:u, usable:{…}, peer_profiles:[u]
34LinkStatsgear:s, rung:u, snr3k_db:f?, group_snr_db:[f?], control_tier:u, throughput_bps:f, queue_bytes:u, harq_rounds:u, rebuilds:u, compression_ratio:f?
35DataReceiveddata:b, stream:0
36SendProgressid:u?, message_id:b?, sent:u?, total:u?, delivered:bool, deflated:bool?
37IdSentstation_id:s, t:f
38PeerObservedpeer_id:s, profile:u?, capabilities:{…}
39PhysicalStateptt:bool?, busy:bool?
40Errorcode:u, ref:u?
41ObjectReceivedpeer_id:s, destination:s, service:u, message_id:b, data:b, t:f

LinkStats, IdSent, PeerObserved and PhysicalState are subscription gated; all other events are always delivered. State codes are 0 DISCONNECTED, 1 LISTENING, 2 CONNECTING, 3 CONNECTED and 4 DISCONNECTING. A listening responder returns to LISTENING after its disconnect event.

Disconnect reasons are 1 remote, 2 local, 3 link failed and 4 negotiation refused; the field is omitted when unset. Error codes are 1 INCOMPATIBLE, 2 BAD_STATE, 3 NO_IDENTITY and 4 MALFORMED. The reference currently reports invalid command fields and rejected configurations as MALFORMED.

CapabilitiesNegotiated reports the completed connection's capability intersection. peer_profiles lists the exact supported DATA registry IDs; peer_capabilities is the received bitmap. usable has boolean fastctl, pback, loading and deflate entries. Mandatory record integrity does not have a negotiated capability bit.

LinkStats is emitted when the gear, control tier, queue depth or rebuild count changes. SNR or throughput changes alone do not trigger an event. The group SNR vector has eight entries, with null for groups not yet measured. control_tier is 0 for floor or 1 for fast control. queue_bytes estimates unacknowledged application bytes by mapping committed record bytes across the submitted records; a partially acknowledged compressed record is interpolated. throughput_bps is received application bits divided by elapsed time since connection. compression_ratio includes record headers and integrity trailers, so it can exceed one. Neither metric is a prediction of future throughput.

PeerObserved reports a decoded presence beacon. No automatic modem switching or gateway discovery policy is implemented by this interface. PhysicalState is modem telemetry; its presence does not prove an RF transmission.

4. Record and object delivery

Send queues one record even when its payload is empty. With id, acceptance emits SendProgress with sent=total=payload length, delivered:false, and deflated reflecting the encoding actually selected. sent here means queued, not transmitted. DEFLATE is used only when negotiated and when it shrinks the body. Delivery emits delivered:true after every record byte is acknowledged; a session failure leaves unacknowledged messages without that event. Host applications must not infer remote application processing from a link ACK.

DataReceived carries one complete, verified and decompressed record. Concatenate its payloads to obtain an ordered byte stream. There is one stream and no priority scheduler or independent stream multiplexing.

The record is [flags:1][length:3][encoded body][SHA-256:32]. Flags are 0x10 raw or 0x11 raw DEFLATE. Length counts body plus digest; the digest covers header and encoded body. Verification precedes decompression and delivery. Invalid flags, invalid compression and integrity failures abort the session. Decoded payloads are limited to 8 MiB. SHA-256 detects corruption; it does not authenticate a peer.

SendObject acceptance echoes its message_id with delivered:false. It never reports remote delivery because objects have no ACK. ObjectReceived carries a complete verified object addressed to the receiver or all-call. t is the receiver's local clock at completion, not necessarily UTC.

5. Field keys

Assigned keys identify fields; a key has meaning only in its message schema. Unused assignments do not imply support for a feature. Future changes may add optional keys; changing an existing field's interpretation requires a major host version change.

0 m1 proto2 features3 client4 modem5 profiles6 identity_required7 station_id8 aliases9 profile
10 on11 peer_id12 deadline13 ref14 data15 stream16 priority17 deflate18 id19 events
20 stats_period21 radio22 ptt23 state24 reason25 peer_capabilities26 usable27 unused28 peer_profiles29 gear
30 rung31 snr3k_db32 group_snr_db33 control_tier34 throughput_bps35 queue_bytes36 eta_s37 harq_rounds38 rebuilds39 compression_ratio
40 sent41 total42 delivered43 deflated44 t45 capabilities46 busy47 code48 detail49 addressee
50 data_profile51 receive_profiles52 feedback_iters53 impulse_blank54 bandwidth_hz55 destination56 service57 message_id58 parity59 repeats

Key 60 is inactivity_timeout_s.