6.6 KiB
Agent TX Protocol
Operator-facing reference for the TX streaming extensions to the agent WebSocket protocol. Implementation plan: agent_tx_implementation_plan.md. Cross-repo design: agent_tx_plan.md.
Regulatory note. Transmission is regulated in every jurisdiction. The agent-side interlocks documented below let you configure safe defaults for your deployment. They do not replace licensing or responsibility for your own emissions. The RIA Hub's consent modal and audit log make actions attributable — they are not a legal-compliance layer.
Opt-in
TX is disabled by default. The hub cannot make the agent transmit unless the operator has explicitly opted in on the agent host.
Two equivalent opt-in paths:
# Persist to ~/.ria/agent.json so the agent always allows TX.
ria-agent register --hub http://HUB:3005 --api-key KEY \
--allow-tx \
--tx-max-gain-db -10 \
--tx-max-duration-s 60 \
--tx-freq-range 2.4e9 2.5e9 \
--tx-freq-range 5.7e9 5.8e9
# Runtime-only override (does not touch disk).
ria-agent stream --allow-tx
Caps:
| Flag | Config key | Effect |
|---|---|---|
--tx-max-gain-db VALUE |
tx_max_gain_db |
Reject any tx_start whose tx_gain > VALUE |
--tx-max-duration-s VALUE |
tx_max_duration_s |
Auto-stop any TX session after VALUE seconds (watchdog in the TX loop) |
--tx-freq-range LO HI (repeatable) |
tx_allowed_freq_ranges |
Reject any tx_start whose tx_center_frequency falls outside all configured ranges |
The agent enforces each cap before opening the SDR. A violating
tx_start produces a tx_status: error frame and never touches hardware.
Heartbeat advertisement
Every heartbeat now includes:
{
"type": "heartbeat",
"hardware": ["mock", "pluto"],
"status": "streaming",
"capabilities": ["rx", "tx"], // "tx" present only when tx_enabled=True
"tx_enabled": true,
"sessions": { // omitted when no session is live
"rx": { "app_id": "app-1", "state": "streaming" },
"tx": { "app_id": "app-1", "state": "transmitting" }
}
}
Hubs should read capabilities to decide whether to surface TX operators
against this agent in the Screens app composer.
Control messages
Hub → agent (JSON)
// Arm the TX side. Agent validates interlocks, opens/resolves the SDR,
// and transitions into "armed". The next binary frames are consumed as
// TX IQ buffers.
{
"type": "tx_start",
"app_id": "app-1",
"radio_config": {
"device": "pluto",
"identifier": "ip:192.168.3.1",
"tx_sample_rate": 1000000,
"tx_center_frequency": 2450000000,
"tx_gain": -20, // dB; Pluto uses negative attenuation
"tx_bandwidth": 1000000, // optional
"buffer_size": 1024,
"underrun_policy": "pause" // "pause" (default) | "zero" | "repeat"
}
}
// Update parameters at the next buffer boundary. No re-arm needed.
{ "type": "tx_configure", "app_id": "app-1",
"radio_config": { "tx_gain": -25 } }
// Stop TX, drain the inbound queue, pause_tx, release the SDR (if no RX
// session is still using it). A new tx_start can follow immediately.
{ "type": "tx_stop", "app_id": "app-1" }
Hub → agent (binary)
- Raw interleaved float32 IQ, normalised to
[-1, 1]. - One WebSocket frame = one buffer =
buffer_sizecomplex samples =buffer_size * 2 * 4bytes. - Accepted only while a TX session is live. Frames outside that window are logged and dropped.
- Malformed frames (odd float count, wrong size) trigger one underrun cycle but do not crash the stream.
Agent → hub (JSON)
{ "type": "tx_status", "app_id": "app-1", "state": "armed" }
{ "type": "tx_status", "app_id": "app-1", "state": "transmitting" }
{ "type": "tx_status", "app_id": "app-1", "state": "underrun" }
{ "type": "tx_status", "app_id": "app-1", "state": "done" }
{ "type": "tx_status", "app_id": "app-1", "state": "error",
"message": "tx_gain -5 exceeds cap -15.0" }
Transitions:
tx_start tx_stop
—————————————————▶ armed ▶ transmitting ——————————▶ done
│ │
│ │ queue empties + policy="pause"
│ ▼
│ underrun ▶ done (auto-teardown)
│
└─ interlock / init failure ▶ error (no session)
Underrun policies
When the inbound TX queue is empty at a buffer boundary:
| Policy | Behavior |
|---|---|
pause (default) |
Callback returns silence, calls pause_tx(), flips the session into underrun. Watchdog emits tx_status: underrun + tx_status: done and tears down. Hub must re-issue tx_start to resume. |
zero |
Callback returns a zero-filled buffer. Session stays alive; no status change. Carrier continues with dead air. |
repeat |
Callback returns the most recently transmitted buffer. If no buffer has arrived yet, falls back to zero for that cycle. |
Choose pause for correctness-sensitive workloads (any data modulation
where zero-fill or repeat corrupts the stream). Choose zero or repeat
for continuous-carrier use cases where brief stalls are acceptable.
Concurrent RX + TX
A single app_id may hold both an RX session (start/stop) and a TX
session (tx_start/tx_stop) on the same agent at the same time. When
both reference the same (device, identifier), the agent shares a single
driver instance between the two sessions (ref-counted release on stop).
Multi-app sharing of one SDR is not supported in v1. A second tx_start
with a different app_id while another TX session is live produces
tx_status: error "tx already active on this agent".
Buffer format recap
- Direction is the only framing: hub → agent binary means TX, agent → hub binary means RX.
- Layout:
[I0, Q0, I1, Q1, …]as little-endian float32. - Size:
buffer_size * 2 * 4bytes. Mismatched sizes are treated as a single-cycle underrun (malformed frame). - Range: samples must lie in
[-1, 1]. Out-of-range values are transmitted as-is; the SDR driver may clip.
Configuration reference
~/.ria/agent.json is written by ria-agent register and read by
ria-agent stream. Minimum schema with TX:
{
"hub_url": "https://hub.example.com",
"agent_id": "agent-abc123",
"token": "rha_...",
"tx_enabled": true,
"tx_max_gain_db": -10.0,
"tx_max_duration_s": 60,
"tx_allowed_freq_ranges": [[2.4e9, 2.5e9], [5.7e9, 5.8e9]]
}
File permissions are enforced to 0600 by save().