From 25dfb6c24c9ea3e73869457899f033a8c3cc16c9 Mon Sep 17 00:00:00 2001 From: Jan Date: Mon, 6 Apr 2026 07:53:06 +0200 Subject: [PATCH] Hardening audio stream Content-Type guard --- README.md | 678 ++++++------- docs/API.md | 793 ++++++++-------- docs/pro-runtime-hardening-workboard.md | 1153 ++++++++++++----------- internal/control/control.go | 33 +- internal/control/control_test.go | 33 + 5 files changed, 1377 insertions(+), 1313 deletions(-) diff --git a/README.md b/README.md index ad73b7c..ff7d2fb 100644 --- a/README.md +++ b/README.md @@ -1,338 +1,340 @@ -# fm-rds-tx - -Go-based FM stereo transmitter with RDS. Supports ADALM-Pluto (PlutoSDR) and SoapySDR-compatible TX devices. - -## Status - -**Current status:** `v0.7.0-pre` — hardware bring-up milestone - -What is already in place: -- complete DSP chain: audio -> pre-emphasis -> stereo encoding -> RDS -> MPX -> limiter -> FM modulation -- real hardware TX paths for PlutoSDR / SoapySDR backends -- continuous TX engine with runtime telemetry -- dry-run, offline generation, and simulated TX modes -- HTTP control plane with live config patching and runtime/status endpoints -- browser UI on `/` -- live audio ingestion via stdin or HTTP stream input - -Current engineering focus: -- deterministic runtime behavior -- fault handling / recovery -- observability and runtime telemetry -- hardware-validated signal quality - -For the active runtime-hardening track, see: -- `docs/pro-runtime-hardening-workboard.md` - -## Signal path - -```text -Audio Source -> PreEmphasis(50us/75us/off) -> StereoEncoder(19k + 38k DSB-SC) --> RDS(57k BPSK) -> MPX Combiner -> Limiter -> FM Modulator(+/-75kHz) --> optional split-rate FM upsampling -> SDR backend -> RF output -``` - -For deeper DSP details, see: -- `docs/DSP-CHAIN.md` - -## Prerequisites - -### Go -- Go version from `go.mod` (currently Go 1.22) - -### Native SDR dependencies -Depending on backend, native libraries are required: - -- **SoapySDR backend** - - build with `-tags soapy` - - requires SoapySDR native library (`SoapySDR.dll` / `libSoapySDR.so` / `libSoapySDR.dylib`) - - on Windows, PothosSDR is the expected setup - -- **Pluto backend** - - uses native `libiio` - - Windows expects `libiio.dll` - - Linux build/runtime expects `pkg-config` + `libiio` - -### Hardware / legal -- validate RF output, deviation, filtering, and power with proper measurement equipment -- use only within applicable legal and regulatory constraints - -## Quick start - -## Build - -```powershell -# Build CLI tools without hardware-specific build tags: -go build ./cmd/fmrtx -go build ./cmd/offline - -# Build fmrtx with SoapySDR support: -go build -tags soapy ./cmd/fmrtx -``` - -## Quick verification - -```powershell -# Print effective config -go run ./cmd/fmrtx -print-config - -# Run tests -go test ./... - -# Basic dry-run summary -go run ./cmd/fmrtx --dry-run --dry-output build/dryrun/frame.json -``` - -For additional build/test commands, see: -- `docs/README.md` - -## Common usage flows - -### 1) List available SDR devices - -```powershell -.\fmrtx.exe --list-devices -``` - -### 2) Dry-run / config verification - -```powershell -.\fmrtx.exe --dry-run --dry-output build/dryrun/frame.json - -# Write dry-run JSON to stdout -.\fmrtx.exe --dry-run --dry-output - -``` - -### 3) Offline IQ/composite generation - -```powershell -go run ./cmd/offline -duration 2s -output build/offline/composite.iqf32 - -# Optional output rate override -go run ./cmd/offline -duration 500ms -output build/offline/composite.iqf32 -output-rate 228000 -``` - -### 4) Simulated transmit path - -```powershell -go run ./cmd/fmrtx --simulate-tx --simulate-output build/sim/simulated-soapy.iqf32 --simulate-duration 250ms -``` - -### 5) Real TX with config file - -```powershell -# Start TX service with manual start over HTTP -.\fmrtx.exe --tx --config docs/config.plutosdr.json - -# Start and begin transmitting immediately -.\fmrtx.exe --tx --tx-auto-start --config docs/config.plutosdr.json -``` - -### 6) Live audio via stdin - -```powershell -ffmpeg -i "http://svabi.ch:8443/stream" -f s16le -ar 44100 -ac 2 - | .\fmrtx.exe --tx --tx-auto-start --audio-stdin --config docs/config.plutosdr.json -``` - -### 7) Custom audio input rate - -```powershell -ffmpeg -i source.wav -f s16le -ar 48000 -ac 2 - | .\fmrtx.exe --tx --tx-auto-start --audio-stdin --audio-rate 48000 --config docs/config.plutosdr.json -``` - -### 8) HTTP audio ingest - -Start the control plane with `--audio-http` to accept raw PCM pushes on `/audio/stream` and feed them into the live encoder: - -```powershell -ffmpeg -i music.mp3 -f s16le -ar 44100 -ac 2 - | curl -X POST --data-binary @- http://localhost:8088/audio/stream -``` - -## CLI overview - -## `fmrtx` -Important runtime modes and flags include: -- `--tx` -- `--tx-auto-start` -- `--dry-run` -- `--dry-output ` -- `--simulate-tx` -- `--simulate-output ` -- `--simulate-duration ` -- `--config ` -- `--print-config` -- `--list-devices` -- `--audio-stdin` -- `--audio-rate ` -- `--audio-http` - -## `offline` -Useful flags include: -- `-duration ` -- `-output ` -- `-output-rate ` - -If the README is too high-level for the exact CLI surface, check: -- `cmd/fmrtx/main.go` -- `cmd/offline/main.go` - -## HTTP control plane - -Base URL: `http://{listenAddress}` (default typically `127.0.0.1:8088`) - -Security note: -- keep the control plane bound locally unless you intentionally place it behind a trusted and hardened access layer - -### Main endpoints - -```text -GET / browser UI -GET /healthz health check -GET /status current config/status snapshot -GET /runtime live engine / driver / audio telemetry -GET /config full config -POST /config patch config / live updates -GET /dry-run synthetic frame summary -POST /tx/start start transmission -POST /tx/stop stop transmission -POST /audio/stream push raw S16LE stereo PCM into live stream buffer -``` - -### What the control plane covers -- TX start / stop -- runtime status and driver telemetry -- config inspection -- live patching of selected parameters -- dry-run inspection -- browser-accessible control UI -- optional HTTP audio ingest (enable with `--audio-http`) - -### Live config notes -`POST /config` supports live updates for selected fields such as: -- frequency -- stereo enable/disable -- pilot / RDS injection levels -- RDS enable/disable -- limiter settings -- PS / RadioText - -Some parameters are saved but not live-applied and require restart. - -For the full API contract, examples, live-patch semantics, and `/audio/stream` details, see: -- `docs/API.md` - -## Configuration - -Sample configs: -- `docs/config.sample.json` -- `docs/config.plutosdr.json` -- `docs/config.orangepi-pluto-soapy.json` - -Important config areas include: -- `fm.*` -- `rds.*` -- `audio.*` -- `backend.*` -- `control.*` - -Examples of relevant fields you may want to inspect: -- `fm.outputDrive` -- `fm.mpxGain` -- `fm.bs412Enabled` -- `fm.bs412ThresholdDBr` -- `fm.fmModulationEnabled` -- `backend.kind` -- `backend.driver` -- `backend.deviceArgs` -- `backend.uri` -- `backend.deviceSampleRateHz` -- `backend.outputPath` -- `control.listenAddress` - -For deeper config/API behavior, refer to: -- `internal/config/config.go` -- `docs/API.md` -- `docs/config.sample.json` - -## Development and testing - -Useful commands: - -```powershell -go test ./... -go run ./cmd/fmrtx -print-config -go run ./cmd/fmrtx -config docs/config.sample.json -go run ./cmd/fmrtx --dry-run --dry-output build/dryrun/frame.json -go run ./cmd/fmrtx --simulate-tx --simulate-output build/sim/simulated-soapy.iqf32 --simulate-duration 250ms -go run ./cmd/offline -duration 500ms -output build/offline/composite.iqf32 -``` - -See also: -- `docs/README.md` - -## PlutoSDR / backend notes - -- PlutoSDR commonly runs with a device-side sample rate above composite rate, so split-rate mode may be used automatically -- SoapySDR backend is suitable for Soapy-compatible TX hardware -- backend/device settings are selected through config rather than hardcoded paths -- runtime telemetry should be used to inspect effective TX state during operation - -## Repository layout - -```text -cmd/ - fmrtx/ main CLI - offline/ offline generator -internal/ - app/ TX engine + runtime state - audio/ audio input, resampling, tone generation, stream buffering - config/ config schema and validation - control/ HTTP control plane + browser UI - dryrun/ dry-run JSON summaries - dsp/ DSP primitives - mpx/ MPX combiner - offline/ full offline composite generation - output/ output/backend abstractions - platform/ backend abstractions and device/runtime stats - platform/soapysdr/ CGO SoapySDR binding - platform/plutosdr/ Pluto/libiio backend code - rds/ RDS encoder - stereo/ stereo encoder -docs/ - API.md - DSP-CHAIN.md - README.md - config.sample.json - config.plutosdr.json - config.orangepi-pluto-soapy.json - pro-runtime-hardening-workboard.md -scripts/ -examples/ -``` - -## Planning / workboard - -For the current runtime-hardening / professionalization track, see: -- `docs/pro-runtime-hardening-workboard.md` - -This is the living workboard for: -- status tracking -- confirmed findings -- open technical decisions -- verification notes -- implementation progress - -## Release / project docs - -Additional project docs: -- `CHANGELOG.md` -- `RELEASE.md` -- `docs/README.md` -- `docs/API.md` -- `docs/DSP-CHAIN.md` -- `docs/NOTES.md` - -## Legal note - -This project is intended only for lawful use within relevant license and regulatory constraints. -RF output, deviation, filtering, and transmitted power must be validated with proper measurement equipment. +# fm-rds-tx + +Go-based FM stereo transmitter with RDS. Supports ADALM-Pluto (PlutoSDR) and SoapySDR-compatible TX devices. + +## Status + +**Current status:** `v0.7.0-pre` — hardware bring-up milestone + +What is already in place: +- complete DSP chain: audio -> pre-emphasis -> stereo encoding -> RDS -> MPX -> limiter -> FM modulation +- real hardware TX paths for PlutoSDR / SoapySDR backends +- continuous TX engine with runtime telemetry +- dry-run, offline generation, and simulated TX modes +- HTTP control plane with live config patching and runtime/status endpoints +- browser UI on `/` +- live audio ingestion via stdin or HTTP stream input + +Current engineering focus: +- deterministic runtime behavior +- fault handling / recovery +- observability and runtime telemetry +- hardware-validated signal quality + +For the active runtime-hardening track, see: +- `docs/pro-runtime-hardening-workboard.md` + +## Signal path + +```text +Audio Source -> PreEmphasis(50us/75us/off) -> StereoEncoder(19k + 38k DSB-SC) +-> RDS(57k BPSK) -> MPX Combiner -> Limiter -> FM Modulator(+/-75kHz) +-> optional split-rate FM upsampling -> SDR backend -> RF output +``` + +For deeper DSP details, see: +- `docs/DSP-CHAIN.md` + +## Prerequisites + +### Go +- Go version from `go.mod` (currently Go 1.22) + +### Native SDR dependencies +Depending on backend, native libraries are required: + +- **SoapySDR backend** + - build with `-tags soapy` + - requires SoapySDR native library (`SoapySDR.dll` / `libSoapySDR.so` / `libSoapySDR.dylib`) + - on Windows, PothosSDR is the expected setup + +- **Pluto backend** + - uses native `libiio` + - Windows expects `libiio.dll` + - Linux build/runtime expects `pkg-config` + `libiio` + +### Hardware / legal +- validate RF output, deviation, filtering, and power with proper measurement equipment +- use only within applicable legal and regulatory constraints + +## Quick start + +## Build + +```powershell +# Build CLI tools without hardware-specific build tags: +go build ./cmd/fmrtx +go build ./cmd/offline + +# Build fmrtx with SoapySDR support: +go build -tags soapy ./cmd/fmrtx +``` + +## Quick verification + +```powershell +# Print effective config +go run ./cmd/fmrtx -print-config + +# Run tests +go test ./... + +# Basic dry-run summary +go run ./cmd/fmrtx --dry-run --dry-output build/dryrun/frame.json +``` + +For additional build/test commands, see: +- `docs/README.md` + +## Common usage flows + +### 1) List available SDR devices + +```powershell +.\fmrtx.exe --list-devices +``` + +### 2) Dry-run / config verification + +```powershell +.\fmrtx.exe --dry-run --dry-output build/dryrun/frame.json + +# Write dry-run JSON to stdout +.\fmrtx.exe --dry-run --dry-output - +``` + +### 3) Offline IQ/composite generation + +```powershell +go run ./cmd/offline -duration 2s -output build/offline/composite.iqf32 + +# Optional output rate override +go run ./cmd/offline -duration 500ms -output build/offline/composite.iqf32 -output-rate 228000 +``` + +### 4) Simulated transmit path + +```powershell +go run ./cmd/fmrtx --simulate-tx --simulate-output build/sim/simulated-soapy.iqf32 --simulate-duration 250ms +``` + +### 5) Real TX with config file + +```powershell +# Start TX service with manual start over HTTP +.\fmrtx.exe --tx --config docs/config.plutosdr.json + +# Start and begin transmitting immediately +.\fmrtx.exe --tx --tx-auto-start --config docs/config.plutosdr.json +``` + +### 6) Live audio via stdin + +```powershell +ffmpeg -i "http://svabi.ch:8443/stream" -f s16le -ar 44100 -ac 2 - | .\fmrtx.exe --tx --tx-auto-start --audio-stdin --config docs/config.plutosdr.json +``` + +### 7) Custom audio input rate + +```powershell +ffmpeg -i source.wav -f s16le -ar 48000 -ac 2 - | .\fmrtx.exe --tx --tx-auto-start --audio-stdin --audio-rate 48000 --config docs/config.plutosdr.json +``` + +### 8) HTTP audio ingest + +Start the control plane with `--audio-http` to accept raw PCM pushes on `/audio/stream` and feed them into the live encoder: + +Set `Content-Type` to `application/octet-stream` (or `audio/L16`) when posting audio data: + +```powershell +ffmpeg -i music.mp3 -f s16le -ar 44100 -ac 2 - | curl -X POST -H "Content-Type: application/octet-stream" --data-binary @- http://localhost:8088/audio/stream +``` + +## CLI overview + +## `fmrtx` +Important runtime modes and flags include: +- `--tx` +- `--tx-auto-start` +- `--dry-run` +- `--dry-output ` +- `--simulate-tx` +- `--simulate-output ` +- `--simulate-duration ` +- `--config ` +- `--print-config` +- `--list-devices` +- `--audio-stdin` +- `--audio-rate ` +- `--audio-http` + +## `offline` +Useful flags include: +- `-duration ` +- `-output ` +- `-output-rate ` + +If the README is too high-level for the exact CLI surface, check: +- `cmd/fmrtx/main.go` +- `cmd/offline/main.go` + +## HTTP control plane + +Base URL: `http://{listenAddress}` (default typically `127.0.0.1:8088`) + +Security note: +- keep the control plane bound locally unless you intentionally place it behind a trusted and hardened access layer + +### Main endpoints + +```text +GET / browser UI +GET /healthz health check +GET /status current config/status snapshot +GET /runtime live engine / driver / audio telemetry +GET /config full config +POST /config patch config / live updates +GET /dry-run synthetic frame summary +POST /tx/start start transmission +POST /tx/stop stop transmission +POST /audio/stream push raw S16LE stereo PCM into live stream buffer (Content-Type: application/octet-stream or audio/L16 required) +``` + +### What the control plane covers +- TX start / stop +- runtime status and driver telemetry +- config inspection +- live patching of selected parameters +- dry-run inspection +- browser-accessible control UI +- optional HTTP audio ingest (enable with `--audio-http`) + +### Live config notes +`POST /config` supports live updates for selected fields such as: +- frequency +- stereo enable/disable +- pilot / RDS injection levels +- RDS enable/disable +- limiter settings +- PS / RadioText + +Some parameters are saved but not live-applied and require restart. + +For the full API contract, examples, live-patch semantics, and `/audio/stream` details, see: +- `docs/API.md` + +## Configuration + +Sample configs: +- `docs/config.sample.json` +- `docs/config.plutosdr.json` +- `docs/config.orangepi-pluto-soapy.json` + +Important config areas include: +- `fm.*` +- `rds.*` +- `audio.*` +- `backend.*` +- `control.*` + +Examples of relevant fields you may want to inspect: +- `fm.outputDrive` +- `fm.mpxGain` +- `fm.bs412Enabled` +- `fm.bs412ThresholdDBr` +- `fm.fmModulationEnabled` +- `backend.kind` +- `backend.driver` +- `backend.deviceArgs` +- `backend.uri` +- `backend.deviceSampleRateHz` +- `backend.outputPath` +- `control.listenAddress` + +For deeper config/API behavior, refer to: +- `internal/config/config.go` +- `docs/API.md` +- `docs/config.sample.json` + +## Development and testing + +Useful commands: + +```powershell +go test ./... +go run ./cmd/fmrtx -print-config +go run ./cmd/fmrtx -config docs/config.sample.json +go run ./cmd/fmrtx --dry-run --dry-output build/dryrun/frame.json +go run ./cmd/fmrtx --simulate-tx --simulate-output build/sim/simulated-soapy.iqf32 --simulate-duration 250ms +go run ./cmd/offline -duration 500ms -output build/offline/composite.iqf32 +``` + +See also: +- `docs/README.md` + +## PlutoSDR / backend notes + +- PlutoSDR commonly runs with a device-side sample rate above composite rate, so split-rate mode may be used automatically +- SoapySDR backend is suitable for Soapy-compatible TX hardware +- backend/device settings are selected through config rather than hardcoded paths +- runtime telemetry should be used to inspect effective TX state during operation + +## Repository layout + +```text +cmd/ + fmrtx/ main CLI + offline/ offline generator +internal/ + app/ TX engine + runtime state + audio/ audio input, resampling, tone generation, stream buffering + config/ config schema and validation + control/ HTTP control plane + browser UI + dryrun/ dry-run JSON summaries + dsp/ DSP primitives + mpx/ MPX combiner + offline/ full offline composite generation + output/ output/backend abstractions + platform/ backend abstractions and device/runtime stats + platform/soapysdr/ CGO SoapySDR binding + platform/plutosdr/ Pluto/libiio backend code + rds/ RDS encoder + stereo/ stereo encoder +docs/ + API.md + DSP-CHAIN.md + README.md + config.sample.json + config.plutosdr.json + config.orangepi-pluto-soapy.json + pro-runtime-hardening-workboard.md +scripts/ +examples/ +``` + +## Planning / workboard + +For the current runtime-hardening / professionalization track, see: +- `docs/pro-runtime-hardening-workboard.md` + +This is the living workboard for: +- status tracking +- confirmed findings +- open technical decisions +- verification notes +- implementation progress + +## Release / project docs + +Additional project docs: +- `CHANGELOG.md` +- `RELEASE.md` +- `docs/README.md` +- `docs/API.md` +- `docs/DSP-CHAIN.md` +- `docs/NOTES.md` + +## Legal note + +This project is intended only for lawful use within relevant license and regulatory constraints. +RF output, deviation, filtering, and transmitted power must be validated with proper measurement equipment. diff --git a/docs/API.md b/docs/API.md index c5fdebc..e7f89b0 100644 --- a/docs/API.md +++ b/docs/API.md @@ -1,396 +1,397 @@ -# 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} -``` - -`engine.state` spiegelt jetzt die Runtime-State-Maschine wider (idle, arming, prebuffering, running, degraded, muted, faulted, stopping) und bietet eine erste beobachtbare Basis für Fault-Transitions. - - ---- - -### `GET /status` - -Current transmitter status (read-only snapshot). Runtime indicator, alert, and queue stats from the running TX controller are mirrored here for quick health checks. - -**Response:** -```json -{ - "service": "fm-rds-tx", - "backend": "pluto", - "frequencyMHz": 100.0, - "stereoEnabled": true, - "rdsEnabled": true, - "preEmphasisTauUS": 50, - "limiterEnabled": true, - "fmModulationEnabled": true, - "runtimeIndicator": "normal", - "runtimeAlert": "", - "queue": { - "capacity": 3, - "depth": 1, - "fillLevel": 0.33, - "health": "low" - } -} -``` - -`runtimeIndicator` is derived from the engine queue health plus any late buffers observed in the last 5 seconds and can be "normal", "degraded", or "queueCritical". `runtimeAlert` surfaces a short reason (e.g. "queue health low" or "late buffers") when the indicator is not "normal", but late-buffer alerts expire after a few seconds once cycle times settle so the signal doesn't stay stuck on degraded. The cumulative `lateBuffers` counter returned by `/runtime` still shows how many late cycles have occurred since start for post-mortem diagnosis. - ---- - -### `GET /runtime` - -Live engine and driver telemetry. Only populated when TX is active. - -**Response:** -```json -{ - "engine": { - "state": "running", - "runtimeStateDurationSeconds": 12.4, - "chunksProduced": 12345, - "totalSamples": 1408950000, - "underruns": 0, - "lastError": "", - "uptimeSeconds": 3614.2, - "faultCount": 2, - "lastFault": { - "time": "2026-04-06T00:00:00Z", - "reason": "queueCritical", - "severity": "faulted", - "message": "queue health critical for 5 checks" - }, - "faultHistory": [ - { - "time": "2026-04-06T00:00:00Z", - "reason": "queueCritical", - "severity": "faulted", - "message": "queue health critical for 5 checks" - } - ], - "transitionHistory": [ - { - "time": "2026-04-06T00:00:00Z", - "from": "running", - "to": "degraded", - "severity": "warn" - } - ] - }, - "driver": { - "txEnabled": true, - "streamActive": true, - "framesWritten": 12345, - "samplesWritten": 1408950000, - "underruns": 0, - "underrunStreak": 0, - "maxUnderrunStreak": 0, - "effectiveSampleRateHz": 2280000 - } -} -``` -`engine.state` spiegelt jetzt die Runtime-State-Maschine wider (idle, arming, prebuffering, running, degraded, muted, faulted, stopping) und bietet eine erste beobachtbare Basis für Fault-Transitions. - -`runtimeStateDurationSeconds` sagt, wie viele Sekunden die Engine bereits im aktuellen Runtime-Zustand verweilt. So erkennt man schnell, ob `muted`/`degraded` zu lange dauern oder ob ein Übergang gerade frisch begonnen hat. - -`transitionHistory` liefert die jüngsten Übergänge (from/to, severity, timestamp) damit API und UI die Runtime History synchronisieren können. - -`driver.underrunStreak` reports how many consecutive reads returned silence, and `driver.maxUnderrunStreak` captures the longest such run since the engine started. Together they help differentiate short glitches from persistent underrun storms and can be plotted alongside queue health sparkline telemetry. - - ---- - -### `POST /runtime/fault/reset` - -Manually acknowledge a `faulted` runtime state so the supervisor can re-enter the recovery path (the engine moves back to `degraded` once the reset succeeds). - -**Response:** -```json -{"ok": true} -``` - -**Errors:** -- `405 Method Not Allowed` if the request is not a POST -- `503 Service Unavailable` when no TX controller is attached (`--tx` mode not active) -- `409 Conflict` when the engine is not currently faulted or the reset was rejected (e.g. still throttled) - ---- - -### `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). - -The control snapshot (GET /config) only reflects new values once they pass validation and, if the TX engine is running, after the live update succeeded. That keeps the API from reporting desired values that were rejected or still pending. - -**Request body:** JSON with any subset of patchable fields. - -**Content-Type:** `application/json` (charset parameters allowed). Requests without it are rejected with 415 Unsupported Media Type. - -**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–10 | Composite output level multiplier (empfohlen 1..4). | -| `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 - ---- - -### `POST /audio/stream` - -Push raw audio data into the live stream buffer. Format: **S16LE stereo PCM** at the configured `--audio-rate` (default 44100 Hz). - -Requires `--audio-stdin`, `--audio-http`, or another configured stream source to feed the buffer. - -**Request:** Binary body, `application/octet-stream`, raw S16LE stereo PCM bytes. - -**Response:** -```json -{ - "ok": true, - "frames": 4096, - "stats": { - "available": 12000, - "capacity": 131072, - "buffered": 0.09, - "bufferedDurationSeconds": 0.27, - "highWatermark": 15000, - "highWatermarkDurationSeconds": 0.34, - "written": 890000, - "underruns": 0, - "overflows": 0 - } -} -``` - -**Example:** -```bash -# Push a file -ffmpeg -i song.mp3 -f s16le -ar 44100 -ac 2 - | \ - curl -X POST --data-binary @- http://pluto:8088/audio/stream -``` - -**Errors:** -- `405` if not POST -- `503` if no audio stream configured - ---- - -## Audio Streaming - -### Stdin pipe (primary method) - -Pipe any audio source through ffmpeg into the transmitter: - -```bash -# Internet radio stream -ffmpeg -i "http://stream.example.com/radio.mp3" -f s16le -ar 44100 -ac 2 - | \ - fmrtx --tx --tx-auto-start --audio-stdin --config config.json - -# Local music file -ffmpeg -i music.flac -f s16le -ar 44100 -ac 2 - | \ - fmrtx --tx --tx-auto-start --audio-stdin - -# Playlist (ffmpeg concat) -ffmpeg -f concat -i playlist.txt -f s16le -ar 44100 -ac 2 - | \ - fmrtx --tx --tx-auto-start --audio-stdin - -# PulseAudio / ALSA capture (Linux) -parecord --format=s16le --rate=44100 --channels=2 - | \ - fmrtx --tx --tx-auto-start --audio-stdin - -# Custom sample rate (e.g. 48kHz source) -ffmpeg -i source.wav -f s16le -ar 48000 -ac 2 - | \ - fmrtx --tx --tx-auto-start --audio-stdin --audio-rate 48000 -``` - -### HTTP audio push - -Push audio from a remote machine via the HTTP API. Run the server with `--audio-http` (and typically `--tx`/`--tx-auto-start`) so the `/audio/stream` endpoint is available. - -```bash -# From another machine on the network -ffmpeg -i music.mp3 -f s16le -ar 44100 -ac 2 - | \ - curl -X POST --data-binary @- http://pluto-host:8088/audio/stream -``` - -### Audio buffer - -The stream uses a lock-free ring buffer (default: 2 seconds at input rate). Buffer stats are available in `GET /runtime` under `audioStream`: - -```json -{ - "audioStream": { - "available": 12000, - "capacity": 131072, - "buffered": 0.09, - "bufferedDurationSeconds": 0.27, - "highWatermark": 15000, - "highWatermarkDurationSeconds": 0.34, - "written": 890000, - "underruns": 0, - "overflows": 0 - } -} -``` - -- **underruns**: DSP consumed faster than audio arrived (silence inserted) -- **overflows**: Audio arrived faster than DSP consumed (data dropped) -- **buffered**: Fill ratio (0.0 = empty, 1.0 = full) -- **bufferedDurationSeconds**: Approximate seconds of audio queued in the buffer (`available` frames divided by the sample rate) -- **highWatermark**: Highest observed buffer occupancy (frames) since the buffer was created -- **highWatermarkDurationSeconds**: Equivalent peak time (`highWatermark` frames divided by the sample rate) - -When no audio is streaming, the transmitter falls back to the configured tone generator or silence. +# 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} +``` + +`engine.state` spiegelt jetzt die Runtime-State-Maschine wider (idle, arming, prebuffering, running, degraded, muted, faulted, stopping) und bietet eine erste beobachtbare Basis für Fault-Transitions. + + +--- + +### `GET /status` + +Current transmitter status (read-only snapshot). Runtime indicator, alert, and queue stats from the running TX controller are mirrored here for quick health checks. + +**Response:** +```json +{ + "service": "fm-rds-tx", + "backend": "pluto", + "frequencyMHz": 100.0, + "stereoEnabled": true, + "rdsEnabled": true, + "preEmphasisTauUS": 50, + "limiterEnabled": true, + "fmModulationEnabled": true, + "runtimeIndicator": "normal", + "runtimeAlert": "", + "queue": { + "capacity": 3, + "depth": 1, + "fillLevel": 0.33, + "health": "low" + } +} +``` + +`runtimeIndicator` is derived from the engine queue health plus any late buffers observed in the last 5 seconds and can be "normal", "degraded", or "queueCritical". `runtimeAlert` surfaces a short reason (e.g. "queue health low" or "late buffers") when the indicator is not "normal", but late-buffer alerts expire after a few seconds once cycle times settle so the signal doesn't stay stuck on degraded. The cumulative `lateBuffers` counter returned by `/runtime` still shows how many late cycles have occurred since start for post-mortem diagnosis. + +--- + +### `GET /runtime` + +Live engine and driver telemetry. Only populated when TX is active. + +**Response:** +```json +{ + "engine": { + "state": "running", + "runtimeStateDurationSeconds": 12.4, + "chunksProduced": 12345, + "totalSamples": 1408950000, + "underruns": 0, + "lastError": "", + "uptimeSeconds": 3614.2, + "faultCount": 2, + "lastFault": { + "time": "2026-04-06T00:00:00Z", + "reason": "queueCritical", + "severity": "faulted", + "message": "queue health critical for 5 checks" + }, + "faultHistory": [ + { + "time": "2026-04-06T00:00:00Z", + "reason": "queueCritical", + "severity": "faulted", + "message": "queue health critical for 5 checks" + } + ], + "transitionHistory": [ + { + "time": "2026-04-06T00:00:00Z", + "from": "running", + "to": "degraded", + "severity": "warn" + } + ] + }, + "driver": { + "txEnabled": true, + "streamActive": true, + "framesWritten": 12345, + "samplesWritten": 1408950000, + "underruns": 0, + "underrunStreak": 0, + "maxUnderrunStreak": 0, + "effectiveSampleRateHz": 2280000 + } +} +``` +`engine.state` spiegelt jetzt die Runtime-State-Maschine wider (idle, arming, prebuffering, running, degraded, muted, faulted, stopping) und bietet eine erste beobachtbare Basis für Fault-Transitions. + +`runtimeStateDurationSeconds` sagt, wie viele Sekunden die Engine bereits im aktuellen Runtime-Zustand verweilt. So erkennt man schnell, ob `muted`/`degraded` zu lange dauern oder ob ein Übergang gerade frisch begonnen hat. + +`transitionHistory` liefert die jüngsten Übergänge (from/to, severity, timestamp) damit API und UI die Runtime History synchronisieren können. + +`driver.underrunStreak` reports how many consecutive reads returned silence, and `driver.maxUnderrunStreak` captures the longest such run since the engine started. Together they help differentiate short glitches from persistent underrun storms and can be plotted alongside queue health sparkline telemetry. + + +--- + +### `POST /runtime/fault/reset` + +Manually acknowledge a `faulted` runtime state so the supervisor can re-enter the recovery path (the engine moves back to `degraded` once the reset succeeds). + +**Response:** +```json +{"ok": true} +``` + +**Errors:** +- `405 Method Not Allowed` if the request is not a POST +- `503 Service Unavailable` when no TX controller is attached (`--tx` mode not active) +- `409 Conflict` when the engine is not currently faulted or the reset was rejected (e.g. still throttled) + +--- + +### `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). + +The control snapshot (GET /config) only reflects new values once they pass validation and, if the TX engine is running, after the live update succeeded. That keeps the API from reporting desired values that were rejected or still pending. + +**Request body:** JSON with any subset of patchable fields. + +**Content-Type:** `application/json` (charset parameters allowed). Requests without it are rejected with 415 Unsupported Media Type. + +**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–10 | Composite output level multiplier (empfohlen 1..4). | +| `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 + +--- + +### `POST /audio/stream` + +Push raw audio data into the live stream buffer. Format: **S16LE stereo PCM** at the configured `--audio-rate` (default 44100 Hz). + +Requires `--audio-stdin`, `--audio-http`, or another configured stream source to feed the buffer. + +**Request:** Binary body, `application/octet-stream`, raw S16LE stereo PCM bytes. Set `Content-Type` to `application/octet-stream` or `audio/L16`; other media types are rejected. + +**Response:** +```json +{ + "ok": true, + "frames": 4096, + "stats": { + "available": 12000, + "capacity": 131072, + "buffered": 0.09, + "bufferedDurationSeconds": 0.27, + "highWatermark": 15000, + "highWatermarkDurationSeconds": 0.34, + "written": 890000, + "underruns": 0, + "overflows": 0 + } +} +``` + +**Example:** +```bash +# Push a file +ffmpeg -i song.mp3 -f s16le -ar 44100 -ac 2 - | \ + curl -X POST -H "Content-Type: application/octet-stream" --data-binary @- http://pluto:8088/audio/stream +``` + +**Errors:** +- `405` if not POST +- `415` if Content-Type is missing or unsupported (must be `application/octet-stream` or `audio/L16`) +- `503` if no audio stream configured + +--- + +## Audio Streaming + +### Stdin pipe (primary method) + +Pipe any audio source through ffmpeg into the transmitter: + +```bash +# Internet radio stream +ffmpeg -i "http://stream.example.com/radio.mp3" -f s16le -ar 44100 -ac 2 - | \ + fmrtx --tx --tx-auto-start --audio-stdin --config config.json + +# Local music file +ffmpeg -i music.flac -f s16le -ar 44100 -ac 2 - | \ + fmrtx --tx --tx-auto-start --audio-stdin + +# Playlist (ffmpeg concat) +ffmpeg -f concat -i playlist.txt -f s16le -ar 44100 -ac 2 - | \ + fmrtx --tx --tx-auto-start --audio-stdin + +# PulseAudio / ALSA capture (Linux) +parecord --format=s16le --rate=44100 --channels=2 - | \ + fmrtx --tx --tx-auto-start --audio-stdin + +# Custom sample rate (e.g. 48kHz source) +ffmpeg -i source.wav -f s16le -ar 48000 -ac 2 - | \ + fmrtx --tx --tx-auto-start --audio-stdin --audio-rate 48000 +``` + +### HTTP audio push + +Push audio from a remote machine via the HTTP API. Run the server with `--audio-http` (and typically `--tx`/`--tx-auto-start`) so the `/audio/stream` endpoint is available. + +```bash +# From another machine on the network +ffmpeg -i music.mp3 -f s16le -ar 44100 -ac 2 - | \ + curl -X POST -H "Content-Type: application/octet-stream" --data-binary @- http://pluto-host:8088/audio/stream +``` + +### Audio buffer + +The stream uses a lock-free ring buffer (default: 2 seconds at input rate). Buffer stats are available in `GET /runtime` under `audioStream`: + +```json +{ + "audioStream": { + "available": 12000, + "capacity": 131072, + "buffered": 0.09, + "bufferedDurationSeconds": 0.27, + "highWatermark": 15000, + "highWatermarkDurationSeconds": 0.34, + "written": 890000, + "underruns": 0, + "overflows": 0 + } +} +``` + +- **underruns**: DSP consumed faster than audio arrived (silence inserted) +- **overflows**: Audio arrived faster than DSP consumed (data dropped) +- **buffered**: Fill ratio (0.0 = empty, 1.0 = full) +- **bufferedDurationSeconds**: Approximate seconds of audio queued in the buffer (`available` frames divided by the sample rate) +- **highWatermark**: Highest observed buffer occupancy (frames) since the buffer was created +- **highWatermarkDurationSeconds**: Equivalent peak time (`highWatermark` frames divided by the sample rate) + +When no audio is streaming, the transmitter falls back to the configured tone generator or silence. diff --git a/docs/pro-runtime-hardening-workboard.md b/docs/pro-runtime-hardening-workboard.md index de45aa1..304d61f 100644 --- a/docs/pro-runtime-hardening-workboard.md +++ b/docs/pro-runtime-hardening-workboard.md @@ -1,576 +1,577 @@ -# Pro Runtime Hardening Workboard - -Status: living document -Branch: `feature/pro-runtime-hardening` - -Dieses Dokument ist das **Arbeitsdokument** zur schrittweisen Umsetzung des Konzepts aus `fm-rds-tx_pro_runtime_hardening_concept.json`. - -Ziel ist **nicht** nur eine hübsche Roadmap, sondern ein Ort, an dem wir konkret markieren können: -- **wo** wir im Code stehen, -- **welche Lücken** bestätigt sind, -- **welche Entscheidungen** gefallen sind, -- **welche Arbeiten** offen / in Arbeit / erledigt sind, -- **welche Risiken** noch bestehen, -- **welche Akzeptanzkriterien** wirklich nachgewiesen wurden. - ---- - -## 1. Arbeitsregeln für dieses Dokument - -### Statuswerte -- `TODO` → noch nicht begonnen -- `IN PROGRESS` → aktiv in Arbeit -- `BLOCKED` → sinnvoll erkannt, aber blockiert -- `DONE` → umgesetzt -- `VERIFIED` → umgesetzt **und** sinnvoll geprüft -- `DEFERRED` → bewusst nach hinten verschoben -- `REJECTED` → bewusst verworfen - -### Nachweispflicht -Ein Punkt gilt erst als wirklich fertig, wenn eingetragen ist: -1. **Code-Ort(e)** -2. **Was geändert wurde** -3. **Wie verifiziert wurde** -4. **Welche Restrisiken bleiben** - -### Update-Regel -Wenn wir an einem Workstream arbeiten, soll dieses Dokument mitgezogen werden. -Kein „ist im Kopf klar“. Der Stand kommt hier rein. - ---- - -## 2. Gesamtüberblick - -## Gesamtstatus -- Projektphase: `Umsetzung (WS-01)` -- Technischer Fokus aktuell: `Entkoppelter TX-Pfad (FrameQueue + Writer)` -- Nächster sinnvoller Startpunkt laut Konzept: `WS-01 Deterministische Echtzeit-TX-Pipeline mit entkoppeltem Writer` -- Vorangegangene Workstreams: `WS-03 Semantische Korrektheit und konsistent angewandte Config` (abgeschlossen) - -## Repo-bezogene bestätigte Ausgangslage - -| Thema | Status | Notiz | -|---|---|---| -| TX-Engine aktuell als synchroner Single-Loop | CONFIRMED | `internal/app/engine.go` | -| Persistenter DSP-Zustand im Generator vorhanden | CONFIRMED | `internal/offline/generator.go` | -| HTTP-Control vorhanden | CONFIRMED | `internal/control/control.go` | -| Config-Validation vorhanden, aber nicht überall semantisch konsistent | CONFIRMED | `internal/config/config.go` + Runtime-Pfade | -| Device/Capability-Modell vorhanden, aber noch nicht streng genug | CONFIRMED | `internal/platform/soapy.go` | -| Lock-freier SPSC-Audio-Ringbuffer vorhanden | CONFIRMED | `internal/audio/stream.go` | - -## Bereits bekannte bestätigte Inkonsistenzen - -| ID | Status | Beschreibung | Ort | -|---|---|---|---| -| CFG-SEM-001 | CONFIRMED | `fm.outputDrive` wird in Validation und Runtime nicht konsistent behandelt | `internal/config/config.go`, `internal/app/engine.go` | -| CTL-UX-001 | RESOLVED | `handleAudioStream()` beschreibt `--audio-http`; der CLI-Schalter ist nun vorhanden und setzt den Stream-Puffer für `/audio/stream` direkt. | `internal/control/control.go`, `cmd/fmrtx/main.go` | - ---- - -## 3. Prioritätenmodell - -| Priorität | Bedeutung | -|---|---| -| P0 | Technische Perfektion und Determinismus | -| P1 | Betriebssicherheit und Fehlerbeherrschung | -| P2 | Hardware-Wahrheit und RF-Qualität | -| P3 | Sichere und saubere Runtime-Steuerung | -| P4 | Deployment-, Release- und Service-Reife | - ---- - -## 4. Umsetzungstracker nach Workstream - -# WS-03 — Semantische Korrektheit und harte Config-/Runtime-Konsistenz -**Priorität:** P0 -**Gesamtstatus:** IN PROGRESS - -## Ziel -Ein einziger, eindeutig definierter Parameterraum. Jeder Wert hat exakt eine Bedeutung und identische Constraints in Config, HTTP-API, Runtime und Telemetrie. - -## Warum dieser Workstream zuerst -Wenn Semantik und Grenzwerte nicht sauber vereinheitlicht sind, bauen spätere Runtime- und Fault-Mechanismen auf unstabilem Fundament. - -## Aufgaben - -### WS-03-T1 — Parameterinventar erstellen -- **Status:** VERIFIED -- **Owner:** Builder A -- **Code-Orte:** - - `internal/config/config.go` - - `internal/app/engine.go` - - `internal/control/control.go` - - `internal/offline/generator.go` -- **Ziel:** - Alle öffentlich und intern verwendeten Parameter inventarisieren mit: - - Name - - Typ - - Einheit - - Bereich - - Default - - hot-reload-fähig ja/nein - - safety class - - Telemetrie-Name -- **Offene Fragen:** - - Wo leben heute implizite Parameter, die nicht sauber dokumentiert sind? - - Welche Runtime-Werte sind abgeleitet statt direkt konfigurierbar? -- **Nachweis:** - - `docs/ws-03-parameter-inventory.md` enthält das inventarisierte Parameter-Tableau und referenziert Config/Control/Engine. - - Live-Nutzung über `internal/control/control.go` → `LivePatch` dokumentiert. -- **Restrisiken:** - - versteckte Semantik in Helper-Funktionen übersehen - -### WS-03-T2 — Validation vereinheitlichen -- **Status:** VERIFIED -- **Owner:** Builder A -- **Code-Orte:** - - `internal/config/config.go` - - `internal/app/engine.go` - - `internal/app/engine_test.go` - - `internal/control/control.go` -- **Ziel:** - `Config.Validate()`, Runtime-Update-Pfade und API-Patch-Validierung dürfen nicht divergieren. -- **Bereits bekannter Startpunkt:** - - `fm.outputDrive` -- **Nachweis:** - - CFG-SEM-001: `outputDrive`-Validation in `Engine.UpdateConfig` jetzt 0..10 (wie `Config.Validate`). - - Tests (`go test ./...`) fangen neue Range ab und besitzen aktualisierten `engine_test`-Check. - - Live-Patch fließt durch `txBridge` und `LivePatch` (control) → `LiveConfigUpdate`. -- **Restrisiken:** - - weitere Inkonsistenzen erst beim Inventar sichtbar - -### WS-03-T3 — DesiredConfig / AppliedConfig einführen -- **Status:** IN PROGRESS -- **Owner:** Lead Coderaffe -- **Code-Orte:** - - `internal/app/engine.go` - - `internal/control/control.go` - - ggf. Config-/Statusmodelle -- **Ziel:** - API und Runtime sollen trennen zwischen: - - gewünschter Konfiguration - - tatsächlich angewandter Konfiguration - - aktuellem Runtime-Zustand -- **Nachweis:** - - `internal/control/control.go` wartet mit Snapshot-Updates, bis LivePatch erfolgreich war. - - `internal/control/control_test.go` deckt ab, dass abgelehnte Live-Updates keine neue `GET /config`-Ansicht schreiben. -- **Restrisiken:** - - Die API liefert noch nicht beide Sichten gleichzeitig; weitere Workstreams müssen Desired/Applied explizit zurückgeben. - -## WS-03 Entscheidungslog -| Datum | Entscheidung | Notiz | -|---|---|---| -| 2026-04-05 | CFG-SEM-001: `fm.outputDrive` | Live-Validierung auf 0..10 angeglichen, Tests angepasst, Parameterinventar dokumentiert. | -| 2026-04-05 | WS-03-T3: Desired/Applied-Gate | Control-API zeigt Snapshots nur noch, wenn LivePatch erfolgreich angewendet wurde; Tests verhindern irreführende Wunschwerte. | - -## WS-03 Verifikation -| Datum | Fokus | Ergebnis | -|---|---|---| -| 2026-04-05 | `go test ./...` | ✅ Bestätigt `Engine.UpdateConfig`, `LivePatch` und Parameter-Range sowie Inventar-Dokumentation. Neue Control-Tests sichern Desired/Applied-Gate. | - ---- - -# WS-01 — Deterministische Echtzeit-TX-Pipeline mit entkoppeltem Writer -**Priorität:** P0 -**Gesamtstatus:** IN PROGRESS - -## Ziel -Generator/Upsampler und Hardwarewriter werden als getrennte Stufen mit kleinem, kontrolliertem Frame-Puffer betrieben. - -## Aktueller Stand -- Der TX-Pfad ist laut Konzept aktuell noch synchron gekoppelt: - `GenerateFrame -> optional FMUpsampler.Process -> driver.Write` -- Das ist elegant, aber nicht pro-level-hart gegenüber Write-Spikes und Blockaden. - -## Aufgaben - -### WS-01-T1 — FrameQueue einführen -- **Status:** VERIFIED -- **Owner:** Lead Coderaffe -- **Code-Orte:** - - `internal/output/frame_queue.go` - - `internal/output/frame_queue_test.go` - - `internal/app/engine.go` -- **Ziel:** - Bounded Queue mit fester Kapazität, sichtbarem Füllstand, Counter- / Statistikzugriff und klarer Trennung zwischen Generator und Writer. -- **Zu entscheiden:** - - Puffern vor oder nach Upsampling → Device-Frame-Ebene (Queue lebt nach dem Upsampler) für Writer-Simplifizierung. - - Referenzkapazität: `runtime.frameQueueCapacity` (default 3) bleibt konfigurierbar. -- **Akzeptanzpunkte:** - - Keine unbounded Queue. - - Fill-Level (High/Low) ist aus `QueueStats` sichtbar. - - Queue-Health-Indikator (`queue.health`) liefert `critical`, `low` oder `normal` aus dem Fill-Level. EngineStats.`queue` zeigt den Status ebenfalls. - - Drop/Repeat/Mute-Counter sind vorhanden und testbar. -- **Nachweis:** - - `FrameQueue`-Implementierung (`internal/output/frame_queue.go`) liefert kapazitätsgesteuerte Push/Pop-Logik und Counters. - - Engine-Run nutzt Queue vor dem Writer und zeigt `QueueStats` in `EngineStats`. - - Tests (`internal/output/frame_queue_test.go` + `go test ./...`) decken Push/Pop, Timeout-Counters, Stats und den neuen Queue-Health-Indikator ab. -- **Restrisiken:** - - Die Queue wird aktuell synchron getrieben; ein dedizierter Writer-Worker fehlt noch. - - Queue-Close erwartet, dass Generator/Writer vor dem Schließen stoppen, sonst droht Panik beim Schreiben. - -### WS-01-T2 — Writer-Worker einführen -- **Status:** VERIFIED -- **Owner:** Lead Coderaffe -- **Code-Orte:** - - `internal/app/engine.go` (run loop, `writerLoop`, `cloneFrame`, Stats) - - `internal/dsp/*` (FMUpsampler / Resampler copy `GeneratedAt` für Cycle-Metriken) -- **Ziel:** - Generator/Upsampler liefern Frames in die FrameQueue, `driver.Write()` läuft nur noch im dedizierten Writer. -- **Akzeptanzpunkte:** - - `writerLoop()` ist die einzige Stelle mit `driver.Write()` und zieht aus der Queue. - - FrameQueue ist ein echter Puffer (Generator klont Frames, Writer poppt) und `EngineStats.Queue` zeigt den Füllstand. - - Write- und Cycle-Latenzen plus `LateBuffers` bleiben in `EngineStats` sichtbar (`MaxWriteMs`, `LateBuffers`, `MaxCycleMs`). -- **Nachweis:** - - `go test ./...` (Engine + Queue + DSP) läuft erfolgreich. - - `EngineStats` berichtet weiterhin über Queue-/Writer-Metriken. -- **Restrisiken:** - - Frame-Klonierung pro Chunk erhöht Heap-Pressure; spätere Workstreams sollten Pooling / Zero-Copy prüfen. - -### WS-01-T3 — Supervisor-Schicht einführen -- **Status:** TODO -- **Owner:** offen -- **Code-Orte:** - - `internal/app/engine.go` -- **Ziel:** - Queue-Füllstand, Late-Rate und Fehlerhäufigkeit überwachen und in Runtime-Zustände überführen. -- **Akzeptanzpunkte:** - - State-Entscheidungen sind explizit - - kein implizites Weiterwursteln bei Schieflage - -## Offene Architekturfragen -- Ist `capacity_frames = 3` ein guter Startwert oder nur Konzept-Default? -- Sollte im Fault-Fall `repeat last safe frame` erlaubt sein oder von Anfang an nur `mute`? -- Wie eng koppeln wir WS-01 mit WS-02, ohne Overengineering zu erzeugen? - -## WS-01 Entscheidungslog -| Datum | Entscheidung | Notiz | -|---|---|---| -| 2026-04-05 | FrameQueue mit Engine-Integration | Queue lebt nach dem Upsampler auf DeviceFrame-Ebene, Kapazität via `runtime.frameQueueCapacity`, `EngineStats` zeigt `QueueStats`, Tests decken Timeouts und Counters ab. | -| 2026-04-05 | Queue-Health-Indikator | `QueueStats.Health` gibt `critical`/`low`/`normal` zurück und `txBridge` leitet `EngineStats.Queue` ins `/runtime`-JSON. | -| 2026-04-05 | Runtime-Indikator | `EngineStats.RuntimeIndicator` kombiniert `queue.health` + `lateBuffers`, `/runtime` zeigt `engine.runtimeIndicator`. | -| 2026-04-05 | /status runtime indicator | `/status` reuses `txBridge.TXStats()` and now reports `runtimeIndicator` alongside the config snapshot for quick ops. | -| 2026-04-05 | /status queue stats | `/status` spiegelt das `queue`-Objekt aus `txBridge.TXStats()` für schnelle Queue-Checks, API-Doku und `TestStatusReportsQueueStats` fangen den neuen Key ab. | - -## WS-01 Verifikation -| Datum | Fokus | Ergebnis | -|---|---|---| -| 2026-04-05 | FrameQueue + Engine integration | ✅ `go test ./...` (im `internal`-Modul incl. `frame_queue_test.go`) | -| 2026-04-05 | Queue-Health-Indikator | go test ./... deckt `TestFrameQueueHealthIndicator` und `queue.health` ab. | -| 2026-04-05 | Runtime-Indikator | OK `go test ./...` deckt `runtimeIndicator` sowie `/runtime`-Exposition von `engine.runtimeIndicator`. | -| 2026-04-05 | Runtime API queue health | ✅ `/runtime` liefert jetzt `engine.queue.health` dank `txBridge.TXStats`. | -| 2026-04-05 | /status runtime indicator | ✅ `/status` gibt jetzt `runtimeIndicator` aus (`control_test` deckt den neuen Key). | -| 2026-04-05 | /status queue stats | ✅ `TestStatusReportsQueueStats` plus `docs/API.md` zeigen, dass `queue` korrekt durchgereicht wird. | - ---- - -# WS-02 — Explizite Runtime-State-Maschine und Fault-Handling -**Priorität:** P0 -**Gesamtstatus:** IN PROGRESS - -## Ziel -Einführen eines klaren Betriebsmodells mit Fault-, Recovery- und Muted-Zuständen. - -## Fortschritt -- EngineStats liefert das Runtime-State-Feld (`idle`, `arming`, `prebuffering`, `running`) und reagiert nun auf Queue-Gesundheit bzw. späte Buffers, indem es bei `low`/`critical` oder späten Buffern in `degraded` wechselt und sonst auf `running` zurückkehrt. -- `evaluateRuntimeState` escalates persistent `critical` queues from `degraded` to `muted`, while `FaultReasonQueueCritical` surfaces `muted` severity so the mute transition stays observable. -- `evaluateRuntimeState` now waits for a short healthy streak before leaving `muted`, logging a degraded-severity recovery event once the queue settles. -- Persistent queue-critical streaks while `muted` now escalate to `faulted` with `FaultSeverityFaulted`, keeping `RuntimeStateFaulted` observable. -- `EngineStats` and `txBridge` now expose transition/fault counters plus `lastFault`, surfacing the new telemetry through `/runtime`. -- Control-plane UI now renders those WS-02 transition counters, fault count, and last-fault summary so operators can watch runtime escalations without digging through logs. -- Control-plane now exposes `POST /runtime/fault/reset` so operators can acknowledge `faulted` state; `TestRuntimeFaultReset*` covers the new HTTP path. -- Control-plane UI now also offers a Danger Zone `Reset Fault` button that calls the same endpoint so operators can acknowledge faults from the dashboard. - -- Control-plane UI now posts an ops toast/log entry whenever the runtime state shifts so escalations and manual acknowledgements are immediately visible. -- Control-plane UI now keeps a compact Transition History panel beside the Fault History so operators can see recent runtime shifts without scrolling the activity log. - - -## Zielzustände laut Konzept -- `idle` -- `arming` -- `prebuffering` -- `running` -- `degraded` -- `muted` -- `faulted` -- `stopping` - -## Aufgaben - -### WS-02-T1 — Fault-Klassifikation definieren -- **Status:** TODO -- **Owner:** offen -- **Beispiele:** - - Treiberfehler - - Write-Time-Budget überschritten - - Queue leer - - Queue dauerhaft kritisch - - Selbsttest fehlgeschlagen - - unerlaubtes Live-Update - -### WS-02-T2 — Reaktionsstrategie definieren -- **Status:** TODO -- **Owner:** offen -- **Ziel:** - Pro Fehlerklasse klar definieren: - - warn only - - degraded - - muted - - faulted - -### WS-02-T3 — Fault-Historie und Event-Log einführen -- **Status:** TODO -- **Owner:** offen -- **Ziel:** - Zustandswechsel und Faults auditierbar machen. - -## Offene Designfragen -- Wie fein granular darf die State-Maschine werden, ohne unwartbar zu werden? -- Welche Transitionen sind wirklich produktiv relevant und welche nur „theoretisch schön“? - -## WS-02 Entscheidungslog -| Datum | Entscheidung | Notiz | -|---|---|---| -| 2026-04-05 | Faulted escalation on persistent critical queue | `muted` now surfaces `RuntimeStateFaulted` when queue health stays critical and metrics capture every transition. | -| 2026-04-05 | Manual fault reset endpoint | Added `POST /runtime/fault/reset` so operators can acknowledge `faulted` before the supervisor re-enters recovery. | -| 2026-04-05 | Fault-reset UI shortcut | Danger Zone now hosts a Reset Fault button wired to `/runtime/fault/reset` so operators get an in-app acknowledgement path without manual HTTP calls. | -| 2026-04-06 | Runtime transition visibility cue | Control UI now posts toast/log entries for runtime state shifts so ops instantly sees escalations and manual reset acknowledgements. | -| 2026-04-06 | Transition history panel | Added a compact Transition History panel next to the Fault History so the last few runtime state shifts stay visible even when the activity log is full. | - -## WS-02 Verifikation -| Datum | Fokus | Ergebnis | -|---|---|---| -| 2026-04-05 | Faulted path + transition counters | `go test ./...` exercises `TestEngineFaultsAfterMutedCriticalStreak` and `TestRuntimeTransitionCounters`, while `/runtime` now surfaces `engine.degradedTransitions`, `engine.mutedTransitions`, `engine.faultedTransitions`, `engine.faultCount`, and the last fault via `txBridge`. | -| 2026-04-05 | Runtime fault reset API | `go test ./...` now runs `TestRuntimeFaultReset*`, verifying the new HTTP path and controller error scenarios. | -| 2026-04-06 | Runtime transition visibility | ✅ `go test ./...`; manual UI smoke verification still pending to ensure the toast/log flow shows every runtime shift. | - ---- - -# WS-04 — Observability, Telemetrie und Diagnosefähigkeit -**Priorität:** P1 -**Gesamtstatus:** TODO - -## Ziel -Vollständige Sichtbarkeit auf Runtime, Queue, Writer, Generator, RF-Selbsttests und API-Aktivität schaffen. - -## Aufgaben - -### WS-04-T1 — Strukturiertes Logging -- **Status:** TODO -- **Owner:** offen - -### WS-04-T2 — Prometheus-/Metrics-Schicht -- **Status:** TODO -- **Owner:** offen - -### WS-04-T3 — Debug-/Profiling-Endpunkte -- **Status:** TODO -- **Owner:** offen - -## Gewünschte Beispielmetriken -- `engine_chunks_generated_total` -- `engine_late_buffers_total` -- `engine_fault_transitions_total` -- `writer_write_duration_seconds` -- `queue_fill_ratio` -- `queue_dropped_frames_total` -- `queue_muted_frames_total` -- `driver_write_errors_total` -- `audio_stream_underruns_total` -- `audio_stream_overflows_total` -- `rf_selftest_pilot_db` -- `rf_selftest_rds_57k_db` - -## WS-04 Entscheidungslog -| Datum | Entscheidung | Notiz | -| --- | --- | --- | -| 2026-04-06 | High-watermark trend sparkline | Captured audio high-watermark duration history and surface it as a new Health-panel sparkline for queue pressure visibility. | -| 2026-04-06 | Queue fill visibility | Added queue fill ratio health line and sparklines to highlight real-time queue pressure alongside high-watermark trends. | -| 2026-04-07 | Underrun streak telemetry | StreamStats now expose current and max underrun streak counters so queue diagnostics can see repeated underruns without touching the metrics stack. | - -## WS-04 Verifikation -| Datum | Fokus | Ergebnis | -| --- | --- | --- | -| 2026-04-06 | High-watermark trend sparkline | `go test ./...` plus manual UI check confirm the new sparkline updates with runtime audio stats. | -| 2026-04-06 | Queue fill visibility | `go test ./...` plus UI smoke check confirm queue fill stats stay available and the new sparkline/health line react to queue health changes. | -| 2026-04-07 | Underrun streak telemetry | `go test ./internal/audio` confirms the new streak counters plus Stats coverage so the API surfaces the same names. | - ---- - -# WS-05 — Sichere und erwachsene Control-Plane -**Priorität:** P1 / P3-nah -**Gesamtstatus:** TODO - -## Ziel -API transport- und anwendungsseitig härten, state-aware machen und auditierbar gestalten. - -## Aufgaben - -### WS-05-T1 — Auth und Deploy-Modi definieren -- **Status:** TODO -- **Owner:** offen -- **Zielmodi:** - - localhost-only - - trusted-lan - - secured-remote - -### WS-05-T2 — HTTP-Server härten -- **Status:** TODO -- **Owner:** offen -- **Mindestpunkte:** - - ReadTimeout - - WriteTimeout - - IdleTimeout - - ReadHeaderTimeout - - Body-Size-Limits - - Content-Type-Validierung - - Method Enforcement - -### WS-05-T3 — API semantisch aufräumen -- **Status:** TODO -- **Owner:** offen -- **Ziel:** - - DesiredConfig vs AppliedConfig vs RuntimeState - - idempotente Start/Stop-Endpunkte - - transaktionsartige Apply-/Reject-Antworten - - Audit-Log pro Eingriff - -## Frühe Quick-Wins -Diese Punkte könnten ggf. vorgezogen werden, auch wenn WS-05 formal nach WS-01/02 kommt: -- HTTP-Timeouts -- Body-Limits -- sicherer Standard-Bind-Modus - -## WS-05 Entscheidungslog -- Noch leer - -## WS-05 Verifikation -| Datum | Fokus | Ergebnis | -|---|---|---| -| 2026-04-05 | `/audio/stream` rejects non-POST requests | `TestAudioStreamRejectsNonPost` enforces POST-only access to `/audio/stream` before a stream source is configured | - ---- - -# WS-06 — Hardware-in-the-loop und externe RF-Wahrheitsprüfung -**Priorität:** P2 -**Gesamtstatus:** TODO - -## Ziel -Nicht nur intern richtig rechnen, sondern extern nachweisen, dass tatsächlich korrekt gesendet wird. - -## Status -- Konzept vorhanden -- noch kein eingetragener HIL-Arbeitsstand in diesem Dokument - -## Offene Kernfragen -- Welches Referenz-Setup wird verbindlich? -- Welche Testfrequenz / Standarddauer / Schutzmaßnahmen gelten? -- Welcher externe Decoder / Empfänger gilt als Referenz? - ---- - -# WS-07 — Device-aware Capability- und Kalibrierungsmodell -**Priorität:** P2 -**Gesamtstatus:** TODO - -## Ziel -Fähigkeiten und Kalibrierungen nicht implizit, sondern explizit pro Device modellieren. - -## Noch offen -- Capability-Schema konkretisieren -- Kalibrierungsprofil definieren -- Device-aware Validation einbauen - ---- - -# WS-08 — Signal-Selbstüberwachung im Betrieb -**Priorität:** P2 -**Gesamtstatus:** TODO - -## Ziel -Pilot, Stereo, RDS und Composite-Anomalien im Betrieb erkennen. - -## Noch offen -- Goertzel/FFT-Strategie festlegen -- Schwellwerte definieren -- in Fault-Logik einspeisen - ---- - -# WS-09 — Teststrategie erweitern -**Priorität:** P3/P4-nah -**Gesamtstatus:** TODO - -## Ziel -Von Unit-Tests zu echter Qualitätsabsicherung: Golden Vectors, Long-Run, Race, Fuzzing, API-Mutation, HIL. - -## Noch offen -- Testpyramide konkretisieren -- Nightly-/CI-Fähigkeit bestimmen - ---- - -# WS-10 — Service-Reife, Packaging und Reproduzierbarkeit -**Priorität:** P4 -**Gesamtstatus:** TODO - -## Ziel -Build-, Release- und Betriebsartefakte reproduzierbar und teamtauglich machen. - -## Noch offen -- Build-Metadaten -- Service-Units -- Config-Versionierung / Migration - ---- - -## 5. Übergreifende Regeln - -### Musts -- Jeder neue Runtime-Zustand muss per API und Telemetrie sichtbar sein. -- Jede Recovery-, Drop- oder Mute-Strategie braucht Counter, Logs und Tests. -- Keine neue Config-Option ohne klaren Typ, Bereich, Einheit, Default und Hot-Reload-Klassifikation. -- Hardware-nahe Änderungen brauchen mindestens Simulations- und HIL-Validierung. -- Alle Faults müssen eine maschinenlesbare Ursache und eine menschenlesbare Zusammenfassung haben. - -### Must Not -- Keine unbounded Queues. -- Keine stillen Fallbacks ohne Telemetrie. -- Keine teilweise angewandten Live-Config-Änderungen ohne explizite Rückmeldung. -- Keine unterschiedlichen Grenzwerte zwischen Config, API und Runtime. -- Keine sicherheitsrelevanten HTTP-Endpunkte ohne Härtung im Remote-Betrieb. - ---- - -## 6. Aktuelle offene Entscheidungen - -| ID | Status | Frage | Notiz | -|---|---|---|---| -| DEC-001 | RESOLVED | Puffern wir auf CompositeFrame- oder DeviceFrame-Ebene? | Queue lebt nach dem Upsampler (DeviceFrame-Ebene) gemäß `internal/app/engine.go`-Integrationsschleife. | -| DEC-002 | OPEN | Fault-Recovery zuerst mit `mute`, `repeat last safe frame` oder beidem? | Muss technisch und RF-seitig sauber bewertet werden | -| DEC-003 | OPEN | Ziehen wir minimale WS-05-Basis-Härtungen vor? | Timeouts/Body-Limits evtl. früher sinnvoll | -| DEC-004 | OPEN | Wie gross/simpel halten wir die erste State-Maschine? | Gefahr von Overengineering | - ---- - -## 7. Nächste sinnvolle Schritte - -### Empfohlener Start -1. **WS-03-T1 Parameterinventar erstellen** *(abgeschlossen)* -2. **bekannte Inkonsistenzen (CFG-SEM-001, CTL-UX-001) konkret verifizieren** -3. **DesiredConfig / AppliedConfig / RuntimeState Zielmodell grob skizzieren** -4. Danach Architekturarbeit an **WS-01 + WS-02** starten -5. **Aktuell:** WS-01-T2 Writer-Worker einführen (Queue → Driver), danach WS-01-T3 Supervisor + WS-02 Runtime-State. - -### Vor dem ersten grossen Umbau klären -- Was ist „minimal sinnvoll“ für Milestone 1? -- Welche Dinge sind harte Must-haves und welche nur spätere Veredelung? -- Wo wollen wir bewusst nicht sofort maximal abstrahieren? - ---- - -## 8. Änderungsprotokoll - -| Datum | Änderung | Person / Agent | -|---|---|---| -| 2026-04-05 | Initiales Arbeitsdokument aus `fm-rds-tx_pro_runtime_hardening_concept.json` erstellt | Alfred | +# Pro Runtime Hardening Workboard + +Status: living document +Branch: `feature/pro-runtime-hardening` + +Dieses Dokument ist das **Arbeitsdokument** zur schrittweisen Umsetzung des Konzepts aus `fm-rds-tx_pro_runtime_hardening_concept.json`. + +Ziel ist **nicht** nur eine hübsche Roadmap, sondern ein Ort, an dem wir konkret markieren können: +- **wo** wir im Code stehen, +- **welche Lücken** bestätigt sind, +- **welche Entscheidungen** gefallen sind, +- **welche Arbeiten** offen / in Arbeit / erledigt sind, +- **welche Risiken** noch bestehen, +- **welche Akzeptanzkriterien** wirklich nachgewiesen wurden. + +--- + +## 1. Arbeitsregeln für dieses Dokument + +### Statuswerte +- `TODO` → noch nicht begonnen +- `IN PROGRESS` → aktiv in Arbeit +- `BLOCKED` → sinnvoll erkannt, aber blockiert +- `DONE` → umgesetzt +- `VERIFIED` → umgesetzt **und** sinnvoll geprüft +- `DEFERRED` → bewusst nach hinten verschoben +- `REJECTED` → bewusst verworfen + +### Nachweispflicht +Ein Punkt gilt erst als wirklich fertig, wenn eingetragen ist: +1. **Code-Ort(e)** +2. **Was geändert wurde** +3. **Wie verifiziert wurde** +4. **Welche Restrisiken bleiben** + +### Update-Regel +Wenn wir an einem Workstream arbeiten, soll dieses Dokument mitgezogen werden. +Kein „ist im Kopf klar“. Der Stand kommt hier rein. + +--- + +## 2. Gesamtüberblick + +## Gesamtstatus +- Projektphase: `Umsetzung (WS-01)` +- Technischer Fokus aktuell: `Entkoppelter TX-Pfad (FrameQueue + Writer)` +- Nächster sinnvoller Startpunkt laut Konzept: `WS-01 Deterministische Echtzeit-TX-Pipeline mit entkoppeltem Writer` +- Vorangegangene Workstreams: `WS-03 Semantische Korrektheit und konsistent angewandte Config` (abgeschlossen) + +## Repo-bezogene bestätigte Ausgangslage + +| Thema | Status | Notiz | +|---|---|---| +| TX-Engine aktuell als synchroner Single-Loop | CONFIRMED | `internal/app/engine.go` | +| Persistenter DSP-Zustand im Generator vorhanden | CONFIRMED | `internal/offline/generator.go` | +| HTTP-Control vorhanden | CONFIRMED | `internal/control/control.go` | +| Config-Validation vorhanden, aber nicht überall semantisch konsistent | CONFIRMED | `internal/config/config.go` + Runtime-Pfade | +| Device/Capability-Modell vorhanden, aber noch nicht streng genug | CONFIRMED | `internal/platform/soapy.go` | +| Lock-freier SPSC-Audio-Ringbuffer vorhanden | CONFIRMED | `internal/audio/stream.go` | + +## Bereits bekannte bestätigte Inkonsistenzen + +| ID | Status | Beschreibung | Ort | +|---|---|---|---| +| CFG-SEM-001 | CONFIRMED | `fm.outputDrive` wird in Validation und Runtime nicht konsistent behandelt | `internal/config/config.go`, `internal/app/engine.go` | +| CTL-UX-001 | RESOLVED | `handleAudioStream()` beschreibt `--audio-http`; der CLI-Schalter ist nun vorhanden und setzt den Stream-Puffer für `/audio/stream` direkt. | `internal/control/control.go`, `cmd/fmrtx/main.go` | + +--- + +## 3. Prioritätenmodell + +| Priorität | Bedeutung | +|---|---| +| P0 | Technische Perfektion und Determinismus | +| P1 | Betriebssicherheit und Fehlerbeherrschung | +| P2 | Hardware-Wahrheit und RF-Qualität | +| P3 | Sichere und saubere Runtime-Steuerung | +| P4 | Deployment-, Release- und Service-Reife | + +--- + +## 4. Umsetzungstracker nach Workstream + +# WS-03 — Semantische Korrektheit und harte Config-/Runtime-Konsistenz +**Priorität:** P0 +**Gesamtstatus:** IN PROGRESS + +## Ziel +Ein einziger, eindeutig definierter Parameterraum. Jeder Wert hat exakt eine Bedeutung und identische Constraints in Config, HTTP-API, Runtime und Telemetrie. + +## Warum dieser Workstream zuerst +Wenn Semantik und Grenzwerte nicht sauber vereinheitlicht sind, bauen spätere Runtime- und Fault-Mechanismen auf unstabilem Fundament. + +## Aufgaben + +### WS-03-T1 — Parameterinventar erstellen +- **Status:** VERIFIED +- **Owner:** Builder A +- **Code-Orte:** + - `internal/config/config.go` + - `internal/app/engine.go` + - `internal/control/control.go` + - `internal/offline/generator.go` +- **Ziel:** + Alle öffentlich und intern verwendeten Parameter inventarisieren mit: + - Name + - Typ + - Einheit + - Bereich + - Default + - hot-reload-fähig ja/nein + - safety class + - Telemetrie-Name +- **Offene Fragen:** + - Wo leben heute implizite Parameter, die nicht sauber dokumentiert sind? + - Welche Runtime-Werte sind abgeleitet statt direkt konfigurierbar? +- **Nachweis:** + - `docs/ws-03-parameter-inventory.md` enthält das inventarisierte Parameter-Tableau und referenziert Config/Control/Engine. + - Live-Nutzung über `internal/control/control.go` → `LivePatch` dokumentiert. +- **Restrisiken:** + - versteckte Semantik in Helper-Funktionen übersehen + +### WS-03-T2 — Validation vereinheitlichen +- **Status:** VERIFIED +- **Owner:** Builder A +- **Code-Orte:** + - `internal/config/config.go` + - `internal/app/engine.go` + - `internal/app/engine_test.go` + - `internal/control/control.go` +- **Ziel:** + `Config.Validate()`, Runtime-Update-Pfade und API-Patch-Validierung dürfen nicht divergieren. +- **Bereits bekannter Startpunkt:** + - `fm.outputDrive` +- **Nachweis:** + - CFG-SEM-001: `outputDrive`-Validation in `Engine.UpdateConfig` jetzt 0..10 (wie `Config.Validate`). + - Tests (`go test ./...`) fangen neue Range ab und besitzen aktualisierten `engine_test`-Check. + - Live-Patch fließt durch `txBridge` und `LivePatch` (control) → `LiveConfigUpdate`. +- **Restrisiken:** + - weitere Inkonsistenzen erst beim Inventar sichtbar + +### WS-03-T3 — DesiredConfig / AppliedConfig einführen +- **Status:** IN PROGRESS +- **Owner:** Lead Coderaffe +- **Code-Orte:** + - `internal/app/engine.go` + - `internal/control/control.go` + - ggf. Config-/Statusmodelle +- **Ziel:** + API und Runtime sollen trennen zwischen: + - gewünschter Konfiguration + - tatsächlich angewandter Konfiguration + - aktuellem Runtime-Zustand +- **Nachweis:** + - `internal/control/control.go` wartet mit Snapshot-Updates, bis LivePatch erfolgreich war. + - `internal/control/control_test.go` deckt ab, dass abgelehnte Live-Updates keine neue `GET /config`-Ansicht schreiben. +- **Restrisiken:** + - Die API liefert noch nicht beide Sichten gleichzeitig; weitere Workstreams müssen Desired/Applied explizit zurückgeben. + +## WS-03 Entscheidungslog +| Datum | Entscheidung | Notiz | +|---|---|---| +| 2026-04-05 | CFG-SEM-001: `fm.outputDrive` | Live-Validierung auf 0..10 angeglichen, Tests angepasst, Parameterinventar dokumentiert. | +| 2026-04-05 | WS-03-T3: Desired/Applied-Gate | Control-API zeigt Snapshots nur noch, wenn LivePatch erfolgreich angewendet wurde; Tests verhindern irreführende Wunschwerte. | + +## WS-03 Verifikation +| Datum | Fokus | Ergebnis | +|---|---|---| +| 2026-04-05 | `go test ./...` | ✅ Bestätigt `Engine.UpdateConfig`, `LivePatch` und Parameter-Range sowie Inventar-Dokumentation. Neue Control-Tests sichern Desired/Applied-Gate. | + +--- + +# WS-01 — Deterministische Echtzeit-TX-Pipeline mit entkoppeltem Writer +**Priorität:** P0 +**Gesamtstatus:** IN PROGRESS + +## Ziel +Generator/Upsampler und Hardwarewriter werden als getrennte Stufen mit kleinem, kontrolliertem Frame-Puffer betrieben. + +## Aktueller Stand +- Der TX-Pfad ist laut Konzept aktuell noch synchron gekoppelt: + `GenerateFrame -> optional FMUpsampler.Process -> driver.Write` +- Das ist elegant, aber nicht pro-level-hart gegenüber Write-Spikes und Blockaden. + +## Aufgaben + +### WS-01-T1 — FrameQueue einführen +- **Status:** VERIFIED +- **Owner:** Lead Coderaffe +- **Code-Orte:** + - `internal/output/frame_queue.go` + - `internal/output/frame_queue_test.go` + - `internal/app/engine.go` +- **Ziel:** + Bounded Queue mit fester Kapazität, sichtbarem Füllstand, Counter- / Statistikzugriff und klarer Trennung zwischen Generator und Writer. +- **Zu entscheiden:** + - Puffern vor oder nach Upsampling → Device-Frame-Ebene (Queue lebt nach dem Upsampler) für Writer-Simplifizierung. + - Referenzkapazität: `runtime.frameQueueCapacity` (default 3) bleibt konfigurierbar. +- **Akzeptanzpunkte:** + - Keine unbounded Queue. + - Fill-Level (High/Low) ist aus `QueueStats` sichtbar. + - Queue-Health-Indikator (`queue.health`) liefert `critical`, `low` oder `normal` aus dem Fill-Level. EngineStats.`queue` zeigt den Status ebenfalls. + - Drop/Repeat/Mute-Counter sind vorhanden und testbar. +- **Nachweis:** + - `FrameQueue`-Implementierung (`internal/output/frame_queue.go`) liefert kapazitätsgesteuerte Push/Pop-Logik und Counters. + - Engine-Run nutzt Queue vor dem Writer und zeigt `QueueStats` in `EngineStats`. + - Tests (`internal/output/frame_queue_test.go` + `go test ./...`) decken Push/Pop, Timeout-Counters, Stats und den neuen Queue-Health-Indikator ab. +- **Restrisiken:** + - Die Queue wird aktuell synchron getrieben; ein dedizierter Writer-Worker fehlt noch. + - Queue-Close erwartet, dass Generator/Writer vor dem Schließen stoppen, sonst droht Panik beim Schreiben. + +### WS-01-T2 — Writer-Worker einführen +- **Status:** VERIFIED +- **Owner:** Lead Coderaffe +- **Code-Orte:** + - `internal/app/engine.go` (run loop, `writerLoop`, `cloneFrame`, Stats) + - `internal/dsp/*` (FMUpsampler / Resampler copy `GeneratedAt` für Cycle-Metriken) +- **Ziel:** + Generator/Upsampler liefern Frames in die FrameQueue, `driver.Write()` läuft nur noch im dedizierten Writer. +- **Akzeptanzpunkte:** + - `writerLoop()` ist die einzige Stelle mit `driver.Write()` und zieht aus der Queue. + - FrameQueue ist ein echter Puffer (Generator klont Frames, Writer poppt) und `EngineStats.Queue` zeigt den Füllstand. + - Write- und Cycle-Latenzen plus `LateBuffers` bleiben in `EngineStats` sichtbar (`MaxWriteMs`, `LateBuffers`, `MaxCycleMs`). +- **Nachweis:** + - `go test ./...` (Engine + Queue + DSP) läuft erfolgreich. + - `EngineStats` berichtet weiterhin über Queue-/Writer-Metriken. +- **Restrisiken:** + - Frame-Klonierung pro Chunk erhöht Heap-Pressure; spätere Workstreams sollten Pooling / Zero-Copy prüfen. + +### WS-01-T3 — Supervisor-Schicht einführen +- **Status:** TODO +- **Owner:** offen +- **Code-Orte:** + - `internal/app/engine.go` +- **Ziel:** + Queue-Füllstand, Late-Rate und Fehlerhäufigkeit überwachen und in Runtime-Zustände überführen. +- **Akzeptanzpunkte:** + - State-Entscheidungen sind explizit + - kein implizites Weiterwursteln bei Schieflage + +## Offene Architekturfragen +- Ist `capacity_frames = 3` ein guter Startwert oder nur Konzept-Default? +- Sollte im Fault-Fall `repeat last safe frame` erlaubt sein oder von Anfang an nur `mute`? +- Wie eng koppeln wir WS-01 mit WS-02, ohne Overengineering zu erzeugen? + +## WS-01 Entscheidungslog +| Datum | Entscheidung | Notiz | +|---|---|---| +| 2026-04-05 | FrameQueue mit Engine-Integration | Queue lebt nach dem Upsampler auf DeviceFrame-Ebene, Kapazität via `runtime.frameQueueCapacity`, `EngineStats` zeigt `QueueStats`, Tests decken Timeouts und Counters ab. | +| 2026-04-05 | Queue-Health-Indikator | `QueueStats.Health` gibt `critical`/`low`/`normal` zurück und `txBridge` leitet `EngineStats.Queue` ins `/runtime`-JSON. | +| 2026-04-05 | Runtime-Indikator | `EngineStats.RuntimeIndicator` kombiniert `queue.health` + `lateBuffers`, `/runtime` zeigt `engine.runtimeIndicator`. | +| 2026-04-05 | /status runtime indicator | `/status` reuses `txBridge.TXStats()` and now reports `runtimeIndicator` alongside the config snapshot for quick ops. | +| 2026-04-05 | /status queue stats | `/status` spiegelt das `queue`-Objekt aus `txBridge.TXStats()` für schnelle Queue-Checks, API-Doku und `TestStatusReportsQueueStats` fangen den neuen Key ab. | + +## WS-01 Verifikation +| Datum | Fokus | Ergebnis | +|---|---|---| +| 2026-04-05 | FrameQueue + Engine integration | ✅ `go test ./...` (im `internal`-Modul incl. `frame_queue_test.go`) | +| 2026-04-05 | Queue-Health-Indikator | go test ./... deckt `TestFrameQueueHealthIndicator` und `queue.health` ab. | +| 2026-04-05 | Runtime-Indikator | OK `go test ./...` deckt `runtimeIndicator` sowie `/runtime`-Exposition von `engine.runtimeIndicator`. | +| 2026-04-05 | Runtime API queue health | ✅ `/runtime` liefert jetzt `engine.queue.health` dank `txBridge.TXStats`. | +| 2026-04-05 | /status runtime indicator | ✅ `/status` gibt jetzt `runtimeIndicator` aus (`control_test` deckt den neuen Key). | +| 2026-04-05 | /status queue stats | ✅ `TestStatusReportsQueueStats` plus `docs/API.md` zeigen, dass `queue` korrekt durchgereicht wird. | + +--- + +# WS-02 — Explizite Runtime-State-Maschine und Fault-Handling +**Priorität:** P0 +**Gesamtstatus:** IN PROGRESS + +## Ziel +Einführen eines klaren Betriebsmodells mit Fault-, Recovery- und Muted-Zuständen. + +## Fortschritt +- EngineStats liefert das Runtime-State-Feld (`idle`, `arming`, `prebuffering`, `running`) und reagiert nun auf Queue-Gesundheit bzw. späte Buffers, indem es bei `low`/`critical` oder späten Buffern in `degraded` wechselt und sonst auf `running` zurückkehrt. +- `evaluateRuntimeState` escalates persistent `critical` queues from `degraded` to `muted`, while `FaultReasonQueueCritical` surfaces `muted` severity so the mute transition stays observable. +- `evaluateRuntimeState` now waits for a short healthy streak before leaving `muted`, logging a degraded-severity recovery event once the queue settles. +- Persistent queue-critical streaks while `muted` now escalate to `faulted` with `FaultSeverityFaulted`, keeping `RuntimeStateFaulted` observable. +- `EngineStats` and `txBridge` now expose transition/fault counters plus `lastFault`, surfacing the new telemetry through `/runtime`. +- Control-plane UI now renders those WS-02 transition counters, fault count, and last-fault summary so operators can watch runtime escalations without digging through logs. +- Control-plane now exposes `POST /runtime/fault/reset` so operators can acknowledge `faulted` state; `TestRuntimeFaultReset*` covers the new HTTP path. +- Control-plane UI now also offers a Danger Zone `Reset Fault` button that calls the same endpoint so operators can acknowledge faults from the dashboard. + +- Control-plane UI now posts an ops toast/log entry whenever the runtime state shifts so escalations and manual acknowledgements are immediately visible. +- Control-plane UI now keeps a compact Transition History panel beside the Fault History so operators can see recent runtime shifts without scrolling the activity log. + + +## Zielzustände laut Konzept +- `idle` +- `arming` +- `prebuffering` +- `running` +- `degraded` +- `muted` +- `faulted` +- `stopping` + +## Aufgaben + +### WS-02-T1 — Fault-Klassifikation definieren +- **Status:** TODO +- **Owner:** offen +- **Beispiele:** + - Treiberfehler + - Write-Time-Budget überschritten + - Queue leer + - Queue dauerhaft kritisch + - Selbsttest fehlgeschlagen + - unerlaubtes Live-Update + +### WS-02-T2 — Reaktionsstrategie definieren +- **Status:** TODO +- **Owner:** offen +- **Ziel:** + Pro Fehlerklasse klar definieren: + - warn only + - degraded + - muted + - faulted + +### WS-02-T3 — Fault-Historie und Event-Log einführen +- **Status:** TODO +- **Owner:** offen +- **Ziel:** + Zustandswechsel und Faults auditierbar machen. + +## Offene Designfragen +- Wie fein granular darf die State-Maschine werden, ohne unwartbar zu werden? +- Welche Transitionen sind wirklich produktiv relevant und welche nur „theoretisch schön“? + +## WS-02 Entscheidungslog +| Datum | Entscheidung | Notiz | +|---|---|---| +| 2026-04-05 | Faulted escalation on persistent critical queue | `muted` now surfaces `RuntimeStateFaulted` when queue health stays critical and metrics capture every transition. | +| 2026-04-05 | Manual fault reset endpoint | Added `POST /runtime/fault/reset` so operators can acknowledge `faulted` before the supervisor re-enters recovery. | +| 2026-04-05 | Fault-reset UI shortcut | Danger Zone now hosts a Reset Fault button wired to `/runtime/fault/reset` so operators get an in-app acknowledgement path without manual HTTP calls. | +| 2026-04-06 | Runtime transition visibility cue | Control UI now posts toast/log entries for runtime state shifts so ops instantly sees escalations and manual reset acknowledgements. | +| 2026-04-06 | Transition history panel | Added a compact Transition History panel next to the Fault History so the last few runtime state shifts stay visible even when the activity log is full. | + +## WS-02 Verifikation +| Datum | Fokus | Ergebnis | +|---|---|---| +| 2026-04-05 | Faulted path + transition counters | `go test ./...` exercises `TestEngineFaultsAfterMutedCriticalStreak` and `TestRuntimeTransitionCounters`, while `/runtime` now surfaces `engine.degradedTransitions`, `engine.mutedTransitions`, `engine.faultedTransitions`, `engine.faultCount`, and the last fault via `txBridge`. | +| 2026-04-05 | Runtime fault reset API | `go test ./...` now runs `TestRuntimeFaultReset*`, verifying the new HTTP path and controller error scenarios. | +| 2026-04-06 | Runtime transition visibility | ✅ `go test ./...`; manual UI smoke verification still pending to ensure the toast/log flow shows every runtime shift. | + +--- + +# WS-04 — Observability, Telemetrie und Diagnosefähigkeit +**Priorität:** P1 +**Gesamtstatus:** TODO + +## Ziel +Vollständige Sichtbarkeit auf Runtime, Queue, Writer, Generator, RF-Selbsttests und API-Aktivität schaffen. + +## Aufgaben + +### WS-04-T1 — Strukturiertes Logging +- **Status:** TODO +- **Owner:** offen + +### WS-04-T2 — Prometheus-/Metrics-Schicht +- **Status:** TODO +- **Owner:** offen + +### WS-04-T3 — Debug-/Profiling-Endpunkte +- **Status:** TODO +- **Owner:** offen + +## Gewünschte Beispielmetriken +- `engine_chunks_generated_total` +- `engine_late_buffers_total` +- `engine_fault_transitions_total` +- `writer_write_duration_seconds` +- `queue_fill_ratio` +- `queue_dropped_frames_total` +- `queue_muted_frames_total` +- `driver_write_errors_total` +- `audio_stream_underruns_total` +- `audio_stream_overflows_total` +- `rf_selftest_pilot_db` +- `rf_selftest_rds_57k_db` + +## WS-04 Entscheidungslog +| Datum | Entscheidung | Notiz | +| --- | --- | --- | +| 2026-04-06 | High-watermark trend sparkline | Captured audio high-watermark duration history and surface it as a new Health-panel sparkline for queue pressure visibility. | +| 2026-04-06 | Queue fill visibility | Added queue fill ratio health line and sparklines to highlight real-time queue pressure alongside high-watermark trends. | +| 2026-04-07 | Underrun streak telemetry | StreamStats now expose current and max underrun streak counters so queue diagnostics can see repeated underruns without touching the metrics stack. | + +## WS-04 Verifikation +| Datum | Fokus | Ergebnis | +| --- | --- | --- | +| 2026-04-06 | High-watermark trend sparkline | `go test ./...` plus manual UI check confirm the new sparkline updates with runtime audio stats. | +| 2026-04-06 | Queue fill visibility | `go test ./...` plus UI smoke check confirm queue fill stats stay available and the new sparkline/health line react to queue health changes. | +| 2026-04-07 | Underrun streak telemetry | `go test ./internal/audio` confirms the new streak counters plus Stats coverage so the API surfaces the same names. | + +--- + +# WS-05 — Sichere und erwachsene Control-Plane +**Priorität:** P1 / P3-nah +**Gesamtstatus:** TODO + +## Ziel +API transport- und anwendungsseitig härten, state-aware machen und auditierbar gestalten. + +## Aufgaben + +### WS-05-T1 — Auth und Deploy-Modi definieren +- **Status:** TODO +- **Owner:** offen +- **Zielmodi:** + - localhost-only + - trusted-lan + - secured-remote + +### WS-05-T2 — HTTP-Server härten +- **Status:** TODO +- **Owner:** offen +- **Mindestpunkte:** + - ReadTimeout + - WriteTimeout + - IdleTimeout + - ReadHeaderTimeout + - Body-Size-Limits + - Content-Type-Validierung + - Method Enforcement + +### WS-05-T3 — API semantisch aufräumen +- **Status:** TODO +- **Owner:** offen +- **Ziel:** + - DesiredConfig vs AppliedConfig vs RuntimeState + - idempotente Start/Stop-Endpunkte + - transaktionsartige Apply-/Reject-Antworten + - Audit-Log pro Eingriff + +## Frühe Quick-Wins +Diese Punkte könnten ggf. vorgezogen werden, auch wenn WS-05 formal nach WS-01/02 kommt: +- HTTP-Timeouts +- Body-Limits +- sicherer Standard-Bind-Modus + +## WS-05 Entscheidungslog +- 2026-04-06: `/audio/stream` now enforces a binary `Content-Type` (`application/octet-stream` or `audio/L16`) before queuing any samples. + +## WS-05 Verifikation +| Datum | Fokus | Ergebnis | +|---|---|---| +| 2026-04-05 | `/audio/stream` rejects non-POST requests | `TestAudioStreamRejectsNonPost` enforces POST-only access to `/audio/stream` before a stream source is configured | +| 2026-04-06 | `/audio/stream` enforces binary Content-Type headers | `TestAudioStreamRejectsMissingContentType` and `TestAudioStreamRejectsUnsupportedContentType` confirm 415 when the media type is missing or wrong | + +--- + +# WS-06 — Hardware-in-the-loop und externe RF-Wahrheitsprüfung +**Priorität:** P2 +**Gesamtstatus:** TODO + +## Ziel +Nicht nur intern richtig rechnen, sondern extern nachweisen, dass tatsächlich korrekt gesendet wird. + +## Status +- Konzept vorhanden +- noch kein eingetragener HIL-Arbeitsstand in diesem Dokument + +## Offene Kernfragen +- Welches Referenz-Setup wird verbindlich? +- Welche Testfrequenz / Standarddauer / Schutzmaßnahmen gelten? +- Welcher externe Decoder / Empfänger gilt als Referenz? + +--- + +# WS-07 — Device-aware Capability- und Kalibrierungsmodell +**Priorität:** P2 +**Gesamtstatus:** TODO + +## Ziel +Fähigkeiten und Kalibrierungen nicht implizit, sondern explizit pro Device modellieren. + +## Noch offen +- Capability-Schema konkretisieren +- Kalibrierungsprofil definieren +- Device-aware Validation einbauen + +--- + +# WS-08 — Signal-Selbstüberwachung im Betrieb +**Priorität:** P2 +**Gesamtstatus:** TODO + +## Ziel +Pilot, Stereo, RDS und Composite-Anomalien im Betrieb erkennen. + +## Noch offen +- Goertzel/FFT-Strategie festlegen +- Schwellwerte definieren +- in Fault-Logik einspeisen + +--- + +# WS-09 — Teststrategie erweitern +**Priorität:** P3/P4-nah +**Gesamtstatus:** TODO + +## Ziel +Von Unit-Tests zu echter Qualitätsabsicherung: Golden Vectors, Long-Run, Race, Fuzzing, API-Mutation, HIL. + +## Noch offen +- Testpyramide konkretisieren +- Nightly-/CI-Fähigkeit bestimmen + +--- + +# WS-10 — Service-Reife, Packaging und Reproduzierbarkeit +**Priorität:** P4 +**Gesamtstatus:** TODO + +## Ziel +Build-, Release- und Betriebsartefakte reproduzierbar und teamtauglich machen. + +## Noch offen +- Build-Metadaten +- Service-Units +- Config-Versionierung / Migration + +--- + +## 5. Übergreifende Regeln + +### Musts +- Jeder neue Runtime-Zustand muss per API und Telemetrie sichtbar sein. +- Jede Recovery-, Drop- oder Mute-Strategie braucht Counter, Logs und Tests. +- Keine neue Config-Option ohne klaren Typ, Bereich, Einheit, Default und Hot-Reload-Klassifikation. +- Hardware-nahe Änderungen brauchen mindestens Simulations- und HIL-Validierung. +- Alle Faults müssen eine maschinenlesbare Ursache und eine menschenlesbare Zusammenfassung haben. + +### Must Not +- Keine unbounded Queues. +- Keine stillen Fallbacks ohne Telemetrie. +- Keine teilweise angewandten Live-Config-Änderungen ohne explizite Rückmeldung. +- Keine unterschiedlichen Grenzwerte zwischen Config, API und Runtime. +- Keine sicherheitsrelevanten HTTP-Endpunkte ohne Härtung im Remote-Betrieb. + +--- + +## 6. Aktuelle offene Entscheidungen + +| ID | Status | Frage | Notiz | +|---|---|---|---| +| DEC-001 | RESOLVED | Puffern wir auf CompositeFrame- oder DeviceFrame-Ebene? | Queue lebt nach dem Upsampler (DeviceFrame-Ebene) gemäß `internal/app/engine.go`-Integrationsschleife. | +| DEC-002 | OPEN | Fault-Recovery zuerst mit `mute`, `repeat last safe frame` oder beidem? | Muss technisch und RF-seitig sauber bewertet werden | +| DEC-003 | OPEN | Ziehen wir minimale WS-05-Basis-Härtungen vor? | Timeouts/Body-Limits evtl. früher sinnvoll | +| DEC-004 | OPEN | Wie gross/simpel halten wir die erste State-Maschine? | Gefahr von Overengineering | + +--- + +## 7. Nächste sinnvolle Schritte + +### Empfohlener Start +1. **WS-03-T1 Parameterinventar erstellen** *(abgeschlossen)* +2. **bekannte Inkonsistenzen (CFG-SEM-001, CTL-UX-001) konkret verifizieren** +3. **DesiredConfig / AppliedConfig / RuntimeState Zielmodell grob skizzieren** +4. Danach Architekturarbeit an **WS-01 + WS-02** starten +5. **Aktuell:** WS-01-T2 Writer-Worker einführen (Queue → Driver), danach WS-01-T3 Supervisor + WS-02 Runtime-State. + +### Vor dem ersten grossen Umbau klären +- Was ist „minimal sinnvoll“ für Milestone 1? +- Welche Dinge sind harte Must-haves und welche nur spätere Veredelung? +- Wo wollen wir bewusst nicht sofort maximal abstrahieren? + +--- + +## 8. Änderungsprotokoll + +| Datum | Änderung | Person / Agent | +|---|---|---| +| 2026-04-05 | Initiales Arbeitsdokument aus `fm-rds-tx_pro_runtime_hardening_concept.json` erstellt | Alfred | diff --git a/internal/control/control.go b/internal/control/control.go index 07cb355..283ac96 100644 --- a/internal/control/control.go +++ b/internal/control/control.go @@ -52,11 +52,17 @@ type Server struct { } const ( - maxConfigBodyBytes = 64 << 10 // 64 KiB - configContentTypeHeader = "application/json" - noBodyErrMsg = "request must not include a body" + maxConfigBodyBytes = 64 << 10 // 64 KiB + configContentTypeHeader = "application/json" + noBodyErrMsg = "request must not include a body" + audioStreamContentTypeError = "Content-Type must be application/octet-stream or audio/L16" ) +var audioStreamAllowedMediaTypes = []string{ + "application/octet-stream", + "audio/l16", +} + func isJSONContentType(r *http.Request) bool { ct := strings.TrimSpace(r.Header.Get("Content-Type")) if ct == "" { @@ -110,6 +116,23 @@ func rejectBody(w http.ResponseWriter, r *http.Request) bool { return false } +func isAudioStreamContentType(r *http.Request) bool { + ct := strings.TrimSpace(r.Header.Get("Content-Type")) + if ct == "" { + return false + } + mediaType, _, err := mime.ParseMediaType(ct) + if err != nil { + return false + } + for _, allowed := range audioStreamAllowedMediaTypes { + if strings.EqualFold(mediaType, allowed) { + return true + } + } + return false +} + func (s *Server) SetTXController(tx TXController) { s.mu.Lock() s.tx = tx @@ -248,6 +271,10 @@ func (s *Server) handleAudioStream(w http.ResponseWriter, r *http.Request) { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } + if !isAudioStreamContentType(r) { + http.Error(w, audioStreamContentTypeError, http.StatusUnsupportedMediaType) + return + } s.mu.RLock() stream := s.streamSrc s.mu.RUnlock() diff --git a/internal/control/control_test.go b/internal/control/control_test.go index 846b24d..e20a0b3 100644 --- a/internal/control/control_test.go +++ b/internal/control/control_test.go @@ -307,6 +307,7 @@ func TestAudioStreamRequiresSource(t *testing.T) { srv := NewServer(cfgpkg.Default()) rec := httptest.NewRecorder() req := httptest.NewRequest(http.MethodPost, "/audio/stream", bytes.NewReader(nil)) + req.Header.Set("Content-Type", "application/octet-stream") srv.Handler().ServeHTTP(rec, req) if rec.Code != http.StatusServiceUnavailable { t.Fatalf("expected 503 when audio stream missing, got %d", rec.Code) @@ -321,6 +322,7 @@ func TestAudioStreamPushesPCM(t *testing.T) { pcm := []byte{0, 0, 0, 0} rec := httptest.NewRecorder() req := httptest.NewRequest(http.MethodPost, "/audio/stream", bytes.NewReader(pcm)) + req.Header.Set("Content-Type", "application/octet-stream") srv.Handler().ServeHTTP(rec, req) if rec.Code != 200 { t.Fatalf("expected 200, got %d", rec.Code) @@ -355,6 +357,37 @@ func TestAudioStreamRejectsNonPost(t *testing.T) { } } +func TestAudioStreamRejectsMissingContentType(t *testing.T) { + cfg := cfgpkg.Default() + srv := NewServer(cfg) + srv.SetStreamSource(audio.NewStreamSource(256, 44100)) + rec := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodPost, "/audio/stream", bytes.NewReader([]byte{0, 0})) + srv.Handler().ServeHTTP(rec, req) + if rec.Code != http.StatusUnsupportedMediaType { + t.Fatalf("expected 415 when Content-Type missing, got %d", rec.Code) + } + if !strings.Contains(rec.Body.String(), "Content-Type must be") { + t.Fatalf("unexpected response body: %q", rec.Body.String()) + } +} + +func TestAudioStreamRejectsUnsupportedContentType(t *testing.T) { + cfg := cfgpkg.Default() + srv := NewServer(cfg) + srv.SetStreamSource(audio.NewStreamSource(256, 44100)) + rec := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodPost, "/audio/stream", bytes.NewReader([]byte{0, 0})) + req.Header.Set("Content-Type", "text/plain") + srv.Handler().ServeHTTP(rec, req) + if rec.Code != http.StatusUnsupportedMediaType { + t.Fatalf("expected 415 for unsupported Content-Type, got %d", rec.Code) + } + if !strings.Contains(rec.Body.String(), "Content-Type must be") { + t.Fatalf("unexpected response body: %q", rec.Body.String()) + } +} + func TestTXStartWithoutController(t *testing.T) { srv := NewServer(cfgpkg.Default()) rec := httptest.NewRecorder()