Go-based FM stereo transmitter with RDS, Windows-first and cross-platform
Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.

8.4KB

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:

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

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:

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:

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.