186 lines
6.6 KiB
Markdown
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()`.
|