Push Tracker
ria-toolkit-oss/docs/agent_tx_protocol.md

186 lines
6.6 KiB
Markdown

# Agent TX Protocol
Operator-facing reference for the TX streaming extensions to the agent
WebSocket protocol. Implementation plan: [agent_tx_implementation_plan.md](./agent_tx_implementation_plan.md).
Cross-repo design: [agent_tx_plan.md](./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:
```bash
# 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
```
```bash
# 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:
```jsonc
{
"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)
```jsonc
// 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_size` complex samples =
`buffer_size * 2 * 4` bytes.
- 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)
```jsonc
{ "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 * 4` bytes. 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:
```json
{
"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()`.