# Metering WS-1 Implementation Plan Status: Draft Scope: Minimal WebSocket live-telemetry path for metering in `fm-rds-tx` ## 1. Goal Add the smallest practical live metering transport on top of the existing snapshot system. The target is: - keep `GET /measurements` - add `GET /ws/telemetry` - stream only `measurement` - use the same underlying measurement truth as `/measurements` - keep the realtime path safe - make the browser prefer WS, but fall back to snapshot polling This is WS-1, not the final telemetry system. --- ## 2. Existing Pieces We Already Have The current codebase already provides the most important semantic building blocks: - `internal/offline/generator.go` - produces `MeasurementSnapshot` - stores `latestMeasurement` - `internal/app/engine.go` - carries measurement in `EngineStats` - `internal/control/control.go` - exposes `GET /measurements` - `internal/control/ui.html` - already consumes `/measurements` - already contains browser-side meter smoothing / peak-hold style logic That means WS-1 does not need a new meter model. It only needs a new live transport path. --- ## 3. Hard Rules ## 3.1 Single source of truth There must be only one latest measurement truth: - generator/engine produces the snapshot - `/measurements` exposes it - WebSocket streams it WebSocket must not introduce a second, UI-specific measurement calculation path. ## 3.2 Realtime safety The producer path must never block on telemetry. Allowed on the realtime side: - reading latest measurement pointer - comparing sequence numbers - one non-blocking publish step Forbidden on the realtime side: - JSON encoding - HTTP work - WebSocket writes - blocking channels - slow shared locks ## 3.3 WS-1 stays small WS-1 should include only: - one endpoint: `GET /ws/telemetry` - one message class: `measurement` - one small broadcaster/hub - one bounded queue per client - one drop policy: drop old, keep newest - browser snapshot bootstrap + WS preference No topics, bundles, runtime-event multiplexing, or speculative protocol machinery. --- ## 4. Proposed Files ## New file ### `internal/control/telemetry.go` Contains: - `TelemetryMessage` - `TelemetryHub` - subscriber management - non-blocking publish logic ## Modified files ### `internal/control/control.go` - add telemetry hub to `Server` - add `GET /ws/telemetry` - add WS handler ### `internal/app/engine.go` - add optional measurement publisher hook - publish new measurement snapshots when sequence advances ### `cmd/fmrtx/main.go` - wire engine publisher to control telemetry hub ### `internal/control/ui.html` - add `connectTelemetryWS()` - bootstrap from `/measurements` - prefer WS while connected - fall back to polling when disconnected --- ## 5. Message Shape WS-1 should use a minimal typed envelope. Example: ```json { "type": "measurement", "ts": "2026-04-13T07:00:53.842Z", "seq": 128, "data": { "timestamp": "2026-04-13T07:00:53.842Z", "sampleRateHz": 228000, "chunkSamples": 11400, "chunkDurationMs": 50, "sequence": 128, "flags": { "stereoEnabled": true, "stereoMode": "DSB" }, "lrPreEncodePostWatermark": { "lRms": 0.27, "rRms": 0.27, "lPeakAbs": 0.51, "rPeakAbs": 0.51 }, "compositeFinalPreIq": { "peakAbs": 0.63, "pilotInjectionEquivalentPercent": 9.0 } } } ``` Rule: - `data` should be semantically identical to `/measurements.measurement` - envelope adds only: - `type` - `ts` - `seq` --- ## 6. TelemetryHub Design ## 6.1 Minimal responsibilities The hub should: - accept published measurement snapshots - fan them out to connected clients - use bounded per-client queues - never block the publisher ## 6.2 Minimal internal shape A small subscriber model is enough for WS-1. Conceptually: ```go type TelemetryMessage struct { Type string `json:"type"` TS time.Time `json:"ts"` Seq uint64 `json:"seq"` Data interface{} `json:"data"` } type telemetrySubscriber struct { ch chan TelemetryMessage } type TelemetryHub struct { mu sync.Mutex subscribers map[*telemetrySubscriber]struct{} } ``` ## 6.3 Publish policy Per client: - channel size should be tiny, e.g. `1` or `2` - if the channel is full: - discard older unsent frame - keep newest In short: - latest state wins - producer never blocks --- ## 7. Preferred Publish Path The realtime path should not write WebSocket frames directly. Preferred path: - generator finalizes latest measurement snapshot - engine notices a new sequence - engine calls a small measurement publisher hook - hub receives the snapshot non-blockingly - WS clients receive the transport copy later That keeps transport outside the hot path. --- ## 8. Engine Integration ## 8.1 Publisher hook Add a small hook to `Engine`, conceptually like: ```go SetMeasurementPublisher(func(*offpkg.MeasurementSnapshot)) ``` The engine should publish only when: - a new snapshot exists - the sequence advanced ## 8.2 Why the engine is a good handoff point The generator is still the source of truth. But the engine is a good place to bridge from: - chunk production - to control-plane telemetry transport because it already sits between signal generation and external control/runtime reporting. --- ## 9. Initial Snapshot on Subscribe On WebSocket connect, the server should: 1. upgrade the connection 2. subscribe the client 3. immediately send the latest known measurement snapshot if one exists 4. continue streaming subsequent updates This avoids the UI showing an empty state until the next natural update arrives. --- ## 10. WebSocket Handler Behavior ## Endpoint - `GET /ws/telemetry` ## Handler rules - upgrade connection - create subscriber - send latest known snapshot immediately if available - loop over subscriber channel - write typed messages with write deadlines - on write failure or broken client: - close connection - unsubscribe Additional safety rule: - a persistently slow or broken client may be dropped rather than buffered around --- ## 11. UI Behavior The browser flow should be: 1. load snapshot from `GET /measurements` 2. render immediately from snapshot 3. connect to WebSocket 4. if WS is healthy, prefer streamed updates 5. if WS drops, keep last known state and resume snapshot polling ## UI state additions Conceptually: ```js S.telemetry = { ws: null, wsConnected: false, wsRetryTimer: null, snapshotPollingActive: true } ``` --- ## 12. Polling Strategy with WS Current UI behavior polls `/measurements` aggressively. WS-1 should change that to: ### While WS is healthy - no continuous `/measurements` polling - keep `/runtime` and `/config` polling as needed ### While WS is disconnected - resume `/measurements` polling - lower fallback rate is acceptable This preserves `/measurements` without turning it into a redundant live-stream duplicate. --- ## 13. Observability WS-1 logging should stay minimal. Useful: - client connected - client disconnected - client dropped due to write failure/backpressure Not useful: - logging every telemetry frame --- ## 14. Risks ### A. Too much logic in the producer path Avoid. ### B. Slow client causing sticky WS writes Use deadlines and disconnect. ### C. Drift between `/measurements` and WS Prevent by using the same underlying snapshot source. ### D. Overengineering WS-1 Avoid topics, bundles, and speculative protocol work. --- ## 15. Implementation Sequence ### Step 1 Create `internal/control/telemetry.go` with: - `TelemetryMessage` - `TelemetryHub` - subscribe/unsubscribe/publish ### Step 2 Add telemetry hub ownership to `Server` in `internal/control/control.go`. ### Step 3 Add `GET /ws/telemetry` handler in control. ### Step 4 Add measurement publisher hook to `Engine`. ### Step 5 Wire engine publisher to telemetry hub in `cmd/fmrtx/main.go`. ### Step 6 Update `internal/control/ui.html`: - snapshot bootstrap - WS connect - WS preference - fallback polling ### Step 7 Test behavior manually: - no-data case - connect while TX idle - connect while TX running - disconnect/reconnect - fallback back to polling - slow/broken client handling --- ## 16. Recommendation Implement WS-1 in the smallest possible form. That means: - keep `/measurements` - add one WS endpoint - stream one message type - keep one truth - protect the realtime path - let freshness win over completeness That gives `fm-rds-tx` a real live-metering transport backbone without turning the first step into a giant telemetry framework.