add reliable BLE telemetry transport

This commit is contained in:
Jay
2026-08-18 06:24:10 -04:00
parent 00f52ecf0f
commit 7617010d8e
26 changed files with 1499 additions and 47 deletions
+31 -5
View File
@@ -11,7 +11,7 @@ combined by an underlying byte transport.
| ---: | ---: | --- |
| 0 | 4 | ASCII magic `TRK1` |
| 4 | 1 | Wire version (`1`) |
| 5 | 1 | Packet type: metadata `1`, samples `2` |
| 5 | 1 | Packet type: metadata `1`, samples `2`, status `3` |
| 6 | 1 | Header size (`36`) |
| 7 | 1 | Record size (`0` or `20`) |
| 8 | 1 | Record count (`0` or 18) |
@@ -51,7 +51,11 @@ timestamp is reconstructed by cumulatively adding its delta. Firmware ends the
current packet before a delta exceeds the representable 655.35 ms range, making
the next sample the exact base timestamp of a new packet. As a defensive encoder
fallback, an unrepresentable delta is stored as `0xFFFF` and sets packet flag bit
0. Sample sequence gaps remain detectable independently.
0. Because intra-packet deltas are rounded to 10 us while each packet base keeps
the exact ESP timer value, an integrity check for an exact 10,000 us interval can
report up to +/-5 us at packet boundaries; changing the transport's packet size
changes how often that harmless quantization boundary appears. Sample sequence
gaps remain detectable independently.
Mapped raw counts are authoritative. The original sensor-native axes can be
reconstructed because the mappings are lossless:
@@ -76,6 +80,28 @@ A byte-stream receiver may begin inside an incomplete frame. It discards bytes
until a magic/header/CRC combination validates. Host tools report any rejection
before that first valid frame separately from CRC failures after synchronization.
## Status payload (32 bytes)
Status frames use packet type `3`, record size/count zero, and payload version
`1`. They are emitted at startup and approximately every five seconds. Offsets
0 and 2 are uint16 payload version and payload size; the remaining fields are
cumulative uint32 counters:
| Offset | Field |
| ---: | --- |
| 4 | Sensor read failures |
| 8 | Sample-queue overflows |
| 12 | Initial transport submissions that accepted zero bytes |
| 16 | BLE disconnects after a connection was established |
| 20 | BLE notification enqueue/send failures |
| 24 | BLE frame replays after disconnect, subscription change, or ACK timeout |
| 28 | Malformed, stale, premature, or wrong-connection ACK writes |
The header's cumulative dropped-sample count remains the sum of sensor read
failures and queue overflows, preserving version-1 receiver compatibility while
the status payload makes the causes independently observable. USB-specific BLE
counters remain zero.
## Buffering
Acquisition runs in a dedicated higher-priority task and writes complete samples
@@ -106,9 +132,9 @@ USB drain is not end-to-end application delivery confirmation. A host process
may attach after earlier frames have already left the endpoint, or fail after
the endpoint accepts them. CRC and sequence checks make resulting loss visible,
but an application acknowledgement and replay window are still required to
guarantee receipt. Accordingly, USB `COMPLETE` means endpoint drain, while the
planned reliable BLE backend will reserve `COMPLETE` for an application ACK of
the exact frame.
guarantee receipt. Accordingly, USB `COMPLETE` means endpoint drain, while
reliable BLE reserves `COMPLETE` for an application ACK of the exact frame. See
`ble-transport-v1.md` for fragmentation, replay, and UUIDs.
Receivers report bytes left in an incomplete trailing frame when capture ends.
Those bytes cannot pass CRC validation and are not silently admitted as samples.
+59
View File
@@ -0,0 +1,59 @@
# Reliable BLE Transport — Version 1
BLE carries the unchanged, CRC-protected `TRK1` frames defined in
`binary-record-v1.md`. The default peripheral name is `TrikkeSensor`.
## GATT service
| Purpose | UUID | Properties |
| --- | --- | --- |
| Service | `7d2ea000-f75b-4a9b-8fbe-3d4c2a1e9c10` | Primary service |
| Data | `7d2ea000-f75b-4a9b-8fbe-3d4c2a1e9c11` | Notify |
| ACK | `7d2ea000-f75b-4a9b-8fbe-3d4c2a1e9c12` | Write, write without response |
The firmware prefers a 256-byte ATT MTU, allowing the largest 196-byte `TRK1`
frame and its eight-byte BLE envelope to fit in one notification. Smaller MTUs
remain protocol-compatible; firmware sends at most eight fragments per bounded
poll, although sustained 100 Hz delivery still depends on the negotiated link.
## Data notification envelope
Every notification starts with an eight-byte little-endian envelope:
| Offset | Size | Field |
| ---: | ---: | --- |
| 0 | 4 | `TRK1` packet sequence |
| 4 | 2 | Byte offset within the complete `TRK1` frame |
| 6 | 2 | Complete `TRK1` frame size |
| 8 | remaining | Consecutive frame bytes at that offset |
Offset zero starts or restarts a frame. A receiver appends only consecutive
offsets for the same sequence and total size, then validates the complete
`TRK1` header and CRC. A malformed or missing fragment is not acknowledged.
## Application ACK and replay
After validating and persisting a frame, the receiver writes exactly eight bytes
to the ACK characteristic: ASCII `ACK1`, then the acknowledged packet sequence
as little-endian uint32. Firmware accepts an ACK only for the frame it currently
owns and only from the active subscribed connection.
`COMPLETE` is not reported to the output task until that ACK arrives. Until then:
- a one-second ACK timeout replays the frame from offset zero;
- disconnect or notification unsubscription preserves the frame;
- the next subscription replays it from offset zero;
- BLE polling returns `PENDING`, never `RETRY`, after ownership begins.
The ACK itself can be lost after the receiver persisted the frame. Receivers
therefore compare the sequence and raw bytes with their last persisted frame,
avoid writing a duplicate, and ACK the replay again. The reference
`tools/capture_ble.py` implements this ordering.
BLE notification success only means the fragment entered the stack. The `ACK1`
write is the end-to-end boundary. It deliberately confirms application
persistence rather than radio or ATT delivery alone.
Version 1 is an unauthenticated, single-connection prototype service. It does
not yet provide pairing, authorization, or confidentiality against a nearby
peer; those are separate from the loss/replay guarantees above.