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

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_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)

{ "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:

{
  "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().