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.
Architecture
Section titled “Architecture”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
SOHTrackerupdated by the Reader on every packet. - Output: JSON over WebSocket to all connected clients simultaneously.
Signal Processing Pipeline (Type 0)
Section titled “Signal Processing Pipeline (Type 0)”To provide a clean seismic trace on the dashboard, each channel’s data passes through four stages before broadcast:
- Buffering: Incoming samples are appended to a 5-second sliding window (
deque(maxlen=window_size)). Per-channel state is created lazily on first packet. - Trigger: Processing fires once per step size (= 1 s × sampling rate samples). This means 100 new samples trigger a processing pass for that channel.
- 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. - Decimation:
Trace.decimate(factor, no_filter=False)downsamples by the configureddecimation_factor(default 4), yielding a 25 Hz output with an additional anti-alias filter applied internally by ObsPy. - Packetisation: The most recent
step_size / decimation_factor(= 25) downsampled samples are extracted and wrapped in aSamplePayloadJSON envelope.
State of Health (Type 1)
Section titled “State of Health (Type 1)”Overview
Section titled “Overview”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.
SOHTracker Metrics
Section titled “SOHTracker Metrics”| Field | Type | Description |
|---|---|---|
link_quality | float (0–100) | Percentage of bytes that resulted in a valid, checksum-verified packet. Computed as valid / (valid + errors + dropped) × 100. |
checksum_errors | int | Packets received with a header match (0xAA 0xBB) but a failing CRC-32. |
bytes_dropped | int | Bytes discarded during buffer re-alignment (no header found at expected position). |
last_seen | float | Unix timestamp of the most recently validated packet. |
connected | bool | True 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.
Link Quality Calculation
Section titled “Link Quality Calculation”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.
Dashboard Colour Coding
Section titled “Dashboard Colour Coding”The Angular SOH popover renders the link_quality value with traffic-light colours:
| Range | Colour | Meaning |
|---|---|---|
| ≥ 95 % | 🟢 Green | Link healthy |
| 80 – 95 % | 🟡 Amber | Occasional errors — monitor |
| < 80 % | 🔴 Red | Significant data loss — investigate |
The connected flag drives the status dot in the dashboard header: green and pulsing when true, red when false.
Message Specification
Section titled “Message Specification”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 — Waveform Data
Section titled “Type 0 — Waveform Data”{ "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] }}| Field | Type | Description |
|---|---|---|
payload.channel | string | SEED channel code (e.g. EHZ, EHN, EHE) |
payload.timestamp | string | ISO-8601 end-time of the delivered batch |
payload.fs | float | Actual sample rate after decimation (25.0 Hz) |
payload.data | number[] | 25 filtered, decimated samples (1 s of data) |
Type 1 — State of Health
Section titled “Type 1 — State of Health”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 }}| Field | Type | Description |
|---|---|---|
payload.link_quality | float | 0–100 %, higher is better |
payload.checksum_errors | int | Cumulative CRC-32 failures since daemon start |
payload.bytes_dropped | int | Cumulative bytes discarded during re-alignment |
payload.last_seen | float | Unix timestamp of last valid packet from MCU |
payload.connected | bool | Whether the MCU is actively streaming |
Angular Integration
Section titled “Angular Integration”The WebsocketService routes messages by type field using RxJS filter():
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:
export interface StateOfHealthPayload { link_quality: number; bytes_dropped: number; checksum_errors: number; last_seen: number; connected: boolean;}
export interface StateOfHealth extends BaseWsMessage { payload: StateOfHealthPayload;}Performance & Scalability
Section titled “Performance & Scalability”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.
| Metric | Value |
|---|---|
| Waveform latency (queue → network) | ~5 ms |
| SOH broadcast interval | 5 s |
| Bandwidth per client (waveform only) | ~2.4 kbps |
| Bandwidth per client (waveform + SOH) | ~2.5 kbps |
| Concurrent clients (RPi 4) | 100+ |
Configuration
Section titled “Configuration”decimation_factor: 4 # 100 Hz ÷ 4 = 25 Hz broadcast rateThe 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.
Troubleshooting
Section titled “Troubleshooting”No waveform data on the dashboard
Section titled “No waveform data on the dashboard”- Confirm port 8765 is open:
sudo ufw allow 8765. - Open browser DevTools → Network → WS tab and verify the connection handshake returns
101 Switching Protocols. - Check that
decimation_factoris a supported integer (2, 4, 5, 8, 10) — non-integer or prime factors cause ObsPy’sdecimate()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.
link_quality slowly degrading over days
Section titled “link_quality slowly degrading over days”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.