You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: universal-telemetry-software/README.md
+97-2Lines changed: 97 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -37,6 +37,101 @@ The deployed car service sets `ROLE=car` explicitly in `deploy/car-telemetry.ser
37
37
38
38
---
39
39
40
+
## Reliability & Retransmission
41
+
42
+
The link between car and base is a **best-effort UDP stream with a pull-based recovery channel over TCP**. The car never waits for acknowledgements — it fires CAN batches over UDP as fast as it reads them and keeps a short backlog in memory. The base station detects the gaps and asks for the missing pieces back. This keeps the live path low-latency while still recovering most dropped packets a few seconds later.
43
+
44
+
All of this lives in `src/data.py`: the car side in `run_car()` (`udp_sender`, `handle_resend`), the base side in `run_base()` (`udp_receiver`, `missing_reporter`).
45
+
46
+
### Data flow
47
+
48
+
```mermaid
49
+
sequenceDiagram
50
+
participant CAN as CAN bus
51
+
participant Car as Car (udp_sender)
52
+
participant RB as Ring buffer (60s)
53
+
participant Base as Base (udp_receiver)
54
+
participant MR as Base (missing_reporter)
55
+
56
+
CAN->>Car: raw frames
57
+
Note over Car: batch 20 msgs OR 50ms<br/>assign seq_num (uint64)
58
+
Car->>RB: store (seq, batch, t)
59
+
Car--)Base: UDP :5005 [seq | count | msgs]
60
+
Note over Base: track expected_seq<br/>seq > expected ⇒ gap<br/>add skipped seqs to missing set
61
+
loop every 10s, if missing set non-empty
62
+
MR->>Car: TCP :5006 {"missing": [seq, ...]}
63
+
Car->>RB: look up seqs still in buffer
64
+
Car-->>MR: JSON [{seq, msgs:[{t,id,d}]}]
65
+
Note over MR: re-publish to Redis<br/>mark seq recovered
66
+
end
67
+
```
68
+
69
+
### Sender: batching, sequencing, ring buffer (car)
70
+
71
+
-**Batching** — CAN frames are accumulated and flushed when the batch hits `BATCH_SIZE` (20 messages) **or**`BATCH_TIMEOUT` (50 ms) elapses, whichever comes first. Each flush is one UDP datagram.
72
+
-**Sequencing** — every batch gets a monotonic `seq_num` (uint64). This is the unit of loss detection and retransmission; individual CAN messages are never tracked, only whole batches.
73
+
-**Wire format** — the UDP payload is a 10-byte header followed by fixed-size messages:
74
+
75
+
| Field | Type | Bytes | Notes |
76
+
|-------|------|-------|-------|
77
+
|`seq`|`uint64` big-endian (`!Q`) | 8 | batch sequence number |
78
+
|`count`|`uint16` big-endian (`!H`) | 2 | number of messages in this batch |
79
+
|`messages`|`count × CANMessage`| 20 each | packed via `CANMessage.pack()`|
80
+
81
+
-**Ring buffer** — after sending, the car also appends `(seq, batch, timestamp)` to an in-memory `deque` and evicts anything older than `BUFFER_DURATION` (**60 seconds**). This is the *only* copy available for retransmission. A gap that isn't requested within ~60s is gone for good on the live link (it may still be recovered later from the car's own CSV log, but not through this channel).
82
+
83
+
### Receiver: gap detection state machine (base)
84
+
85
+
The base tracks a single `expected_seq` and classifies every incoming datagram:
86
+
87
+
| Condition | Meaning | Action |
88
+
|-----------|---------|--------|
89
+
|`seq == expected_seq`| in order | process; `expected_seq = seq + 1`|
90
+
|`seq > expected_seq`|**gap**| add every seq in `[expected_seq, seq)` to the `missing` set; process; advance `expected_seq`|
91
+
|`seq < expected_seq`, in `missing` set | late/out-of-order arrival | remove from `missing`, count as recovered |
92
+
|`seq < expected_seq`, not in `missing`| duplicate | ignore |
The `missing` set is capped at **1000 entries**; when it overflows the oldest sequence is evicted (and will never be requested). A per-second status map (`0` = missing, `1` = UDP, `2` = TCP-recovered) is maintained purely for the PECAN link-health visualization.
A `missing_reporter` task wakes every `MISSING_CHECK_INTERVAL` (**10 seconds**). If the `missing` set is non-empty it opens a TCP connection to the car's resend server and sends:
100
+
101
+
```json
102
+
{ "missing": [12043, 12044, 12051] }
103
+
```
104
+
105
+
Only the **100 highest** (most recent) missing sequences are sent per cycle — `sorted(missing)[-100:]` — on the assumption that older gaps have already aged out of the car's 60s buffer. The car's `handle_resend` looks each one up in the ring buffer and returns whatever it still holds:
Recovered messages are re-published to the same `can_messages` Redis channel as the live path, so downstream consumers (WebSocket bridge, TimescaleDB) see one merged stream.
114
+
115
+
> **Format note:** the resend response uses a JSON shape (`t`/`id`/`d`-hex) that differs from the binary `CANMessage` on the primary UDP path. Both are normalized to the same `{time, canId, data}` shape before hitting Redis, but the two encodings are worth keeping in mind when touching either path.
116
+
117
+
### Tuning knobs & limits
118
+
119
+
| Constant | Default | Location | Effect |
120
+
|----------|---------|----------|--------|
121
+
|`BATCH_SIZE`|`20`|`src/data.py`| messages per UDP datagram |
122
+
|`BATCH_TIMEOUT`|`0.05` s |`src/data.py`| max latency before a partial batch is flushed |
123
+
|`BUFFER_DURATION`|`60` s |`src/data.py`| how far back the car can retransmit |
124
+
|`MISSING_CHECK_INTERVAL`|`10` s |`src/data.py`| how often the base requests resends |
**Known limitations** (by design — this is a soft-real-time telemetry link, not a guaranteed-delivery bus):
128
+
129
+
- Recovery is best-effort. Nothing is retried indefinitely; a gap older than ~60s or beyond the 1000-entry `missing` cap is dropped from the live stream.
130
+
- The `[-100:]` request slice favors recent gaps. Under a sustained large backlog, the oldest missing sequences can starve.
131
+
- There is no end-to-end ACK, so the car has no knowledge of what the base did or didn't receive — all loss detection is receiver-driven.
132
+
133
+
---
134
+
40
135
## Hardware Setup (Ubuntu)
41
136
42
137
This section covers setting up a CAN HAT (e.g. MCP2515-based) on Ubuntu before running the software.
@@ -238,8 +333,8 @@ Images are built for both `linux/amd64` and `linux/arm64` (Raspberry Pi).
238
333
239
334
| Port | Protocol | Purpose |
240
335
|------|----------|---------|
241
-
| 5005 | UDP | CAN data streaming |
242
-
| 5006 | TCP | Packet retransmission |
336
+
| 5005 | UDP | CAN data streaming (see [Reliability & Retransmission](#reliability--retransmission)) |
0 commit comments