|
|
|
@@ -0,0 +1,220 @@ |
|
|
|
# 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 |