# fm-rds-tx HTTP Control API Base URL: `http://{listenAddress}` (default `127.0.0.1:8088`) --- ## Endpoints ### `GET /healthz` Health check. **Response:** ```json {"ok": true} ``` --- ### `GET /status` Current transmitter status (read-only snapshot). **Response:** ```json { "service": "fm-rds-tx", "backend": "pluto", "frequencyMHz": 100.0, "stereoEnabled": true, "rdsEnabled": true, "preEmphasisTauUS": 50, "limiterEnabled": true, "fmModulationEnabled": true } ``` --- ### `GET /runtime` Live engine and driver telemetry. Only populated when TX is active. **Response:** ```json { "engine": { "state": "running", "chunksProduced": 12345, "totalSamples": 1408950000, "underruns": 0, "lastError": "", "uptimeSeconds": 3614.2 }, "driver": { "txEnabled": true, "streamActive": true, "framesWritten": 12345, "samplesWritten": 1408950000, "underruns": 0, "effectiveSampleRateHz": 2280000 } } ``` --- ### `GET /config` Full current configuration (all fields, including non-patchable). **Response:** Complete `Config` JSON object. --- ### `POST /config` **Live parameter update.** Changes are applied to the running TX engine immediately — no restart required. Only include fields you want to change (PATCH semantics). **Request body:** JSON with any subset of patchable fields. **Response:** ```json {"ok": true, "live": true} ``` `"live": true` = changes were forwarded to the running engine. `"live": false` = engine not active, changes saved for next start. #### Patchable fields — DSP (applied within ~50ms) | Field | Type | Range | Description | |---|---|---|---| | `frequencyMHz` | float | 65–110 | TX center frequency. Tunes hardware LO live. | | `outputDrive` | float | 0–3 | Composite output level multiplier. | | `stereoEnabled` | bool | | Enable/disable stereo (pilot + 38kHz subcarrier). | | `pilotLevel` | float | 0–0.2 | 19 kHz pilot injection level. | | `rdsInjection` | float | 0–0.15 | 57 kHz RDS subcarrier injection level. | | `rdsEnabled` | bool | | Enable/disable RDS subcarrier. | | `limiterEnabled` | bool | | Enable/disable MPX peak limiter. | | `limiterCeiling` | float | 0–2 | Limiter ceiling (max composite amplitude). | #### Patchable fields — RDS text (applied within ~88ms) | Field | Type | Max length | Description | |---|---|---|---| | `ps` | string | 8 chars | Program Service name (station name on receiver display). | | `radioText` | string | 64 chars | RadioText message (scrolling text on receiver). | When `radioText` is updated, the RDS A/B flag toggles automatically per spec, signaling receivers to refresh their display. #### Patchable fields — other (saved, not live-applied) | Field | Type | Description | |---|---|---| | `toneLeftHz` | float | Left tone frequency (test generator). | | `toneRightHz` | float | Right tone frequency (test generator). | | `toneAmplitude` | float | Test tone amplitude (0–1). | | `preEmphasisTauUS` | float | Pre-emphasis time constant. **Requires restart.** | #### Examples ```bash # Tune to 99.5 MHz curl -X POST localhost:8088/config -d '{"frequencyMHz": 99.5}' # Switch to mono curl -X POST localhost:8088/config -d '{"stereoEnabled": false}' # Update now-playing text curl -X POST localhost:8088/config \ -d '{"ps": "MYRADIO", "radioText": "Artist - Song Title"}' # Reduce power + disable limiter curl -X POST localhost:8088/config \ -d '{"outputDrive": 0.8, "limiterEnabled": false}' # Full update curl -X POST localhost:8088/config -d '{ "frequencyMHz": 101.3, "outputDrive": 2.2, "stereoEnabled": true, "pilotLevel": 0.041, "rdsInjection": 0.021, "rdsEnabled": true, "limiterEnabled": true, "limiterCeiling": 1.0, "ps": "PIRATE", "radioText": "Broadcasting from the attic" }' ``` #### Error handling Invalid values return `400 Bad Request` with a descriptive message: ```bash curl -X POST localhost:8088/config -d '{"frequencyMHz": 200}' # → 400: frequencyMHz out of range (65-110) ``` --- ### `POST /tx/start` Start transmission. Requires `--tx` mode with hardware. **Response:** ```json {"ok": true, "action": "started"} ``` **Errors:** - `405` if not POST - `503` if no TX controller (not in `--tx` mode) - `409` if already running --- ### `POST /tx/stop` Stop transmission. **Response:** ```json {"ok": true, "action": "stopped"} ``` --- ### `GET /dry-run` Generate a synthetic frame summary without hardware. Useful for config verification. **Response:** `FrameSummary` JSON with mode, rates, source info, preview samples. --- ## Live update architecture All live updates are **lock-free** in the DSP path: | What | Mechanism | Latency | |---|---|---| | DSP params | `atomic.Pointer[LiveParams]` loaded once per chunk | ≤ 50ms | | RDS text | `atomic.Value` in encoder, read at group boundary | ≤ 88ms | | TX frequency | `atomic.Pointer` in engine, `driver.Tune()` between chunks | ≤ 50ms | No mutex, no channel, no allocation in the real-time path. The HTTP goroutine writes atomics, the DSP goroutine reads them. ## Parameters that require restart These cannot be hot-reloaded (they affect DSP pipeline structure): - `compositeRateHz` — changes sample rate of entire DSP chain - `deviceSampleRateHz` — changes hardware rate / upsampler ratio - `maxDeviationHz` — changes FM modulator scaling - `preEmphasisTauUS` — changes filter coefficients - `rds.pi` / `rds.pty` — rarely change, baked into encoder init - `audio.inputPath` — audio source selection - `backend.kind` / `backend.device` — hardware selection