Skip to content

WebSocket Sender Job

The WebSocketSender is an asyncio-based broadcast server that serves two distinct message types over a single WebSocket connection:

  • Type 0 — Waveform data: 4× decimated, bandpass-filtered seismic traces at 25 Hz.
  • Type 1 — State of Health (SOH): link quality metrics emitted every 5 seconds.

data_queue (100 Hz raw counts)
WebSocketSender ──── per-channel sliding window
│ │
│ bandpass filter + decimate
│ │
│ broadcast Type 0 (every 1 s)
SOHTracker (shared with Reader)
every 5 s
broadcast Type 1 (SOH)
ws://0.0.0.0:8765
├── Angular dashboard client
└── (n clients)
  • Input: websocket_queue (100 Hz raw ADC counts).
  • Side input: thread-safe SOHTracker updated by the Reader on every packet.
  • Output: JSON over WebSocket to all connected clients simultaneously.

To provide a clean seismic trace on the dashboard, each channel’s data passes through four stages before broadcast:

  1. Buffering: Incoming samples are appended to a 5-second sliding window (deque(maxlen=window_size)). Per-channel state is created lazily on first packet.
  2. Trigger: Processing fires once per step size (= 1 s × sampling rate samples). This means 100 new samples trigger a processing pass for that channel.
  3. Filtering: A bandpass filter (0.2–10 Hz) is applied to the full 5-second window via ObsPy’s Trace.filter(). This removes DC offset below 0.2 Hz and high-frequency electronic noise above 10 Hz.
  4. Decimation: Trace.decimate(factor, no_filter=False) downsamples by the configured decimation_factor (default 4), yielding a 25 Hz output with an additional anti-alias filter applied internally by ObsPy.
  5. Packetisation: The most recent step_size / decimation_factor (= 25) downsampled samples are extracted and wrapped in a SamplePayload JSON envelope.

The SOHTracker is a thread-safe dataclass that the Reader updates on every received serial byte. The WebSocketSender reads it on a 5-second timer and broadcasts the result to all connected clients as a Type 1 message.

This gives the dashboard a live picture of link quality without interfering with the data path.

FieldTypeDescription
link_qualityfloat (0–100)Percentage of bytes that resulted in a valid, checksum-verified packet. Computed as valid / (valid + errors + dropped) × 100.
checksum_errorsintPackets received with a header match (0xAA 0xBB) but a failing CRC-32.
bytes_droppedintBytes discarded during buffer re-alignment (no header found at expected position).
last_seenfloatUnix timestamp of the most recently validated packet.
connectedboolTrue while the Reader thread is alive and has seen a packet within the last 2 s.

The tracker uses a threading.Lock so the Reader can increment counters from its tight loop without racing the sender’s read.

Counters accumulate since daemon start. The metric reflects the lifetime health of the link, a brief noise burst will dilute quickly once normal operation resumes.

The Angular SOH popover renders the link_quality value with traffic-light colours:

RangeColourMeaning
≥ 95 %🟢 GreenLink healthy
80 – 95 %🟡 AmberOccasional errors — monitor
< 80 %🔴 RedSignificant data loss — investigate

The connected flag drives the status dot in the dashboard header: green and pulsing when true, red when false.


Both message types share the same top-level envelope. Clients use type to route the payload.

{
"type": <int>,
"timestamp": "<ISO-8601 UTC>",
"payload": { }
}
{
"type": 0,
"timestamp": "2026-04-01T10:23:01.000000Z",
"payload": {
"channel": "EHZ",
"timestamp": "2026-04-01T10:23:01.000000Z",
"fs": 25.0,
"data": [12300, 12305, 12289]
}
}
FieldTypeDescription
payload.channelstringSEED channel code (e.g. EHZ, EHN, EHE)
payload.timestampstringISO-8601 end-time of the delivered batch
payload.fsfloatActual sample rate after decimation (25.0 Hz)
payload.datanumber[]25 filtered, decimated samples (1 s of data)

Broadcast to all connected clients every 5 seconds, independently of whether any waveform data is being delivered.

{
"type": 1,
"timestamp": "2026-04-01T10:23:05.000000Z",
"payload": {
"link_quality": 98.7,
"checksum_errors": 3,
"bytes_dropped": 0,
"last_seen": 1743499385.12,
"connected": true
}
}
FieldTypeDescription
payload.link_qualityfloat0–100 %, higher is better
payload.checksum_errorsintCumulative CRC-32 failures since daemon start
payload.bytes_droppedintCumulative bytes discarded during re-alignment
payload.last_seenfloatUnix timestamp of last valid packet from MCU
payload.connectedboolWhether the MCU is actively streaming

The WebsocketService routes messages by type field using RxJS filter():

websocket-service.ts
getSensorUpdates(): Observable<SensorData> {
return this.onEvent<SensorData>(WebsocketMessageTypeEnum.DATA); // type 0
}
getStateOfHealth(): Observable<StateOfHealth> {
return this.onEvent<StateOfHealth>(WebsocketMessageTypeEnum.STATE_OF_HEALTH); // type 1
}

The dashboard component subscribes to getStateOfHealth() independently of the waveform subscription — an SOH update never blocks a waveform delivery. The payload is passed to a PrimeNG popover component that renders the traffic-light indicator and metric table.

TypeScript interfaces:

state-of-health.ts
export interface StateOfHealthPayload {
link_quality: number;
bytes_dropped: number;
checksum_errors: number;
last_seen: number;
connected: boolean;
}
export interface StateOfHealth extends BaseWsMessage {
payload: StateOfHealthPayload;
}

The thread is built on asyncio and uses asyncio.gather to fan out to all connected clients concurrently. A failed send (dead client) is caught silently and the client is removed from the active set without affecting other clients.

MetricValue
Waveform latency (queue → network)~5 ms
SOH broadcast interval5 s
Bandwidth per client (waveform only)~2.4 kbps
Bandwidth per client (waveform + SOH)~2.5 kbps
Concurrent clients (RPi 4)100+

config.yml (excerpt)
decimation_factor: 4 # 100 Hz ÷ 4 = 25 Hz broadcast rate

The WebSocket port (8765) and host (0.0.0.0) are currently hardcoded as defaults in the constructor and can be overridden by passing host= and port= kwargs when instantiating the thread in main.py.


  1. Confirm port 8765 is open: sudo ufw allow 8765.
  2. Open browser DevTools → Network → WS tab and verify the connection handshake returns 101 Switching Protocols.
  3. Check that decimation_factor is a supported integer (2, 4, 5, 8, 10) — non-integer or prime factors cause ObsPy’s decimate() to raise an error and skip the broadcast.

SOH shows connected: false but waveforms are arriving

Section titled “SOH shows connected: false but waveforms are arriving”

The connected flag is set false if the Reader has not produced a valid packet within the last 2 seconds. This can happen transiently during MCU settings renegotiation at startup. If it persists, check the Reader thread logs for serial errors.

High bytes_dropped with low checksum_errors

Section titled “High bytes_dropped with low checksum_errors”

Many dropped bytes with few CRC errors typically indicates the daemon is restarting frequently — causing mid-packet stream alignment loss — rather than a noisy link. Check for MCUNoResponse exceptions in daemon.log.

The counters are lifetime cumulative — a brief incident early in the run is permanently reflected in the ratio. A daemon restart resets the counters. If the current link is healthy but the historical metric looks low, this is expected behaviour rather than an ongoing fault.