Add reliable Android BLE ride recorder
This commit is contained in:
@@ -106,15 +106,17 @@ does not by itself prove corruption or a hostile receiver.
|
||||
|
||||
## Buffering
|
||||
|
||||
Acquisition runs in a dedicated higher-priority task and writes complete samples
|
||||
to a 1024-entry RAM queue. The lower-priority output task batches up to eight
|
||||
records per frame. At 100 Hz this queue represents about 10.24 seconds of
|
||||
Under BLE, acquisition does not start until the version-2 session handshake has
|
||||
established a clean capture boundary. Once started, the dedicated
|
||||
higher-priority acquisition task writes complete samples to a statically
|
||||
reserved 3072-entry RAM queue. The lower-priority output task batches up to eight
|
||||
records per frame. At 100 Hz this queue represents about 30.72 seconds of
|
||||
decoupling when the transport reports backpressure or failure accurately. A
|
||||
failed write retains and retries the same encoded packet rather than dequeuing
|
||||
more samples, so the queue accumulates the outage backlog. After reconnection,
|
||||
the oldest retained data is sent first. If the queue fills, acquisition drops
|
||||
new samples rather than overwriting older ones; sequence gaps and the cumulative
|
||||
lost-sample counter expose that permanent loss.
|
||||
more samples, so the queue accumulates the outage backlog. After a same-token
|
||||
reconnection, the oldest retained data is sent first. If the queue fills,
|
||||
acquisition drops new samples rather than overwriting older ones; sequence gaps
|
||||
and the cumulative lost-sample counter expose that permanent loss.
|
||||
|
||||
The shared transport state machine distinguishes three nonfatal states. `RETRY`
|
||||
is valid only from initial submission: it means zero bytes were accepted and the
|
||||
@@ -136,7 +138,8 @@ 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
|
||||
reliable BLE reserves `COMPLETE` for an application ACK of the exact frame. See
|
||||
`ble-transport-v1.md` for fragmentation, replay, and UUIDs.
|
||||
`ble-transport-v2.md` for session establishment, 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.
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
# 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.
|
||||
|
||||
The rejected-ACK counter is diagnostic, not a pure corruption count. A valid
|
||||
duplicate ACK can arrive after the output task has already completed that frame
|
||||
and begun the next one; firmware then rejects and counts the now-stale write.
|
||||
|
||||
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. A nearby peer
|
||||
can also deny availability by subscribing and never acknowledging: firmware
|
||||
correctly retains and replays the owned frame, but the RAM queue eventually
|
||||
fills while the legitimate receiver remains excluded. Pairing and connection
|
||||
authorization are required before treating this as a hostile-environment
|
||||
logger.
|
||||
@@ -0,0 +1,119 @@
|
||||
# Reliable BLE Transport — Version 2
|
||||
|
||||
BLE carries the unchanged, CRC-protected `TRK1` frames defined in
|
||||
`binary-record-v1.md`. The default peripheral name is `TrikkeSensor`. Version 2
|
||||
adds a mandatory, idempotent recording-session handshake; the data envelope and
|
||||
application ACK remain unchanged from version 1.
|
||||
|
||||
## 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 |
|
||||
| Control | `7d2ea000-f75b-4a9b-8fbe-3d4c2a1e9c13` | Write |
|
||||
|
||||
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.
|
||||
|
||||
## Recording-session handshake
|
||||
|
||||
Every new capture generates a random uint64 session token. The reference
|
||||
receivers first write exactly 12 bytes to Control—ASCII `BGN1`, then that token
|
||||
as little-endian uint64—and enable data notifications after that write succeeds.
|
||||
Firmware also accepts the reverse order: it tracks the raw CCCD state separately
|
||||
from session authorization and treats the connection as subscribed as soon as
|
||||
both conditions are true. Notifications never flow before authorization.
|
||||
|
||||
When the token differs from the C3's active token, firmware stores it in
|
||||
RTC-retained memory and performs a controlled software restart. Acquisition and
|
||||
output tasks remain stopped after boot. The receiver reconnects and repeats the
|
||||
same `BGN1` write; firmware recognizes the retained token, authorizes that BLE
|
||||
connection, starts acquisition with empty queues and zeroed volatile counters,
|
||||
and accepts the subsequent notification subscription.
|
||||
|
||||
The Android acceptance capture measured 10.797 seconds from the user's Start
|
||||
action to the first durably persisted frame. That includes the initial control
|
||||
write, controlled restart, advertising and scan latency, GATT reconnection,
|
||||
same-token authorization, notification subscription, and first frame delivery.
|
||||
This startup interval is expected and is not part of the recorded sensor stream.
|
||||
|
||||
The same token must be written on every reconnect during one recording. That
|
||||
write is idempotent: it authorizes the new connection without restarting or
|
||||
discarding the in-flight frame and sample backlog. A different token is an
|
||||
explicit new-session boundary and deliberately discards all prior volatile
|
||||
state through the controlled restart.
|
||||
|
||||
Firmware does not start acquisition after boot and does not honor a notification
|
||||
subscription until a valid control write authorizes the connection. This avoids
|
||||
pre-session queue overflow and prevents an older client from bypassing the
|
||||
session boundary. The token provides idempotence, not authentication or secrecy.
|
||||
|
||||
RTC token recovery is intentionally accepted only after `ESP_RST_SW`. A panic,
|
||||
watchdog, brownout, or power-on reset discards the token and all volatile stream
|
||||
state. When the still-recording receiver reconnects and rewrites its unchanged
|
||||
token, firmware treats it as a new token, performs one additional controlled
|
||||
restart, and authorizes the following same-token reconnect. Packet and sample
|
||||
sequences restart at zero inside the receiver's existing file, where the reset is
|
||||
observable through integrity tracking. This favors a known clean state over
|
||||
silently treating an uncontrolled reset as continuation of the old session.
|
||||
|
||||
## 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, after the same-token control write, 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. Both reference receivers
|
||||
implement this ordering.
|
||||
|
||||
The rejected-ACK counter is diagnostic, not a pure corruption count. A valid
|
||||
duplicate ACK can arrive after the output task has already completed that frame
|
||||
and begun the next one; firmware then rejects and counts the now-stale write.
|
||||
|
||||
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.
|
||||
|
||||
The current firmware reserves a 3072-sample acquisition queue, providing 30.72
|
||||
seconds of transport-outage tolerance at the nominal 100 Hz rate. This interval
|
||||
includes link-loss detection, scanning, GATT reconnection, session
|
||||
reauthorization, notification subscription, and replay—not merely the time a
|
||||
phone's Bluetooth control is visibly off. Longer interruptions remain bounded
|
||||
and detectable through sample-sequence gaps and the queue-overflow counter.
|
||||
|
||||
Version 2 remains an unauthenticated, single-connection prototype service. It
|
||||
does not yet provide pairing, authorization, or confidentiality against a nearby
|
||||
peer; those are separate from the session and replay guarantees above. A nearby
|
||||
peer can also deny availability by connecting and withholding the control write,
|
||||
or by subscribing and never acknowledging. Pairing and connection authorization
|
||||
are required before treating this as a hostile-environment logger.
|
||||
Reference in New Issue
Block a user