Add reliable Android BLE ride recorder

This commit is contained in:
Jay
2026-08-20 12:40:39 -04:00
parent 3bad2275cc
commit 6e408a3048
40 changed files with 2781 additions and 120 deletions
+68 -19
View File
@@ -12,9 +12,10 @@ This milestone does four things:
4. Maps both sensors into a shared enclosure frame and carries the metadata
needed to derive calibrated readings without replacing raw data.
The wired sensor path is proven. This milestone adds the first reliable BLE
transport and a macOS-compatible reference capture client; phone-side storage
remains the next consumer implementation.
The wired sensor path and reliable BLE transport are proven. The repository now
also contains an Android 12+ prototype recorder that stores the authoritative
binary stream durably before acknowledging each frame. The macOS reference
client remains available for transport diagnosis.
## Wiring
@@ -70,33 +71,59 @@ Future fusion, integration, and filtering must derive each `dt` from those
timestamps rather than assume a uniform 10 ms interval. Sensor DRDY/FIFO
acquisition remains the later refinement for reducing the jitter itself.
The ESP32-C3 schedules nominal 100 Hz polling in a dedicated acquisition task,
but each
sensor has an independent internal
The 200-second Android acceptance capture provides the current loaded-system
characterization. Of 19,983 contiguous intervals, 8,745 (43.8%) were off-grid.
Across all intervals, absolute deviation was 0 us median, 40 us p95, 240 us p99,
and 1,630 us maximum; among off-grid intervals it was 10 us median, 60 us p95,
and 750 us p99. Cumulative error was -726 us. These timestamp statistics are a
lower bound on acquisition disturbance because firmware timestamps the poll
before performing both I2C reads. Six ADXL345 overrun flags show that preemption
between the timestamp and the physical read occasionally crossed a sensor sample
boundary even though the recorded timestamp delta did not expose the full delay.
The loss is rare (0.03%), explicitly flagged, and reinforces the planned
data-ready/FIFO acquisition refinement.
After the BLE session handshake, the ESP32-C3 schedules nominal 100 Hz polling
in a dedicated acquisition task, but each sensor has an independent internal
sample clock. The status registers are read immediately before each XYZ read so a
consumer can distinguish a fresh sample from a repeated poll and identify gyro
overruns. Hardware data-ready interrupts and FIFO acquisition are deferred to the
later sensor-side acquisition refinement.
Completed samples enter a 1024-record RAM queue, providing 10.24 seconds of
transport-outage tolerance at 100 Hz when the transport reports backpressure or
failure accurately. A failed write retains and retries its packet while this
queue accumulates the backlog. A lower-priority output task batches up to eight
records into versioned `TRK1` frames, isolating acquisition from brief transport
stalls. CRC, packet and sample sequences, timestamps, and cumulative
loss/overrun counters make permanent loss detectable by the receiver.
Completed samples enter a statically reserved 3072-record RAM queue, providing
30.72 seconds of transport-outage tolerance at 100 Hz when the transport reports
backpressure or failure accurately. A failed write retains and retries its
packet while this queue accumulates the backlog. A lower-priority output task
batches up to eight records into versioned `TRK1` frames, isolating acquisition
from brief transport stalls. CRC, packet and sample sequences, timestamps, and
cumulative loss/overrun counters make permanent loss detectable by the receiver.
The 30.72-second depth is based on end-to-end phone behavior rather than the
visible Bluetooth-off interval alone. A five-second Android Bluetooth outage
produced roughly 12.5 seconds of effective transport interruption and overflowed
the prior 10.24-second queue by 223 samples. Repeating the same test with the
3,072-record queue delivered all 19,984 samples contiguously; one replay was
durably deduplicated and all loss counters remained zero. The capture and its
completed Android sidecar are preserved under `tests/fixtures/`.
The static queue occupies 96 KiB. The BLE build leaves 132,389 bytes of DRAM for
task stacks, NimBLE runtime allocation, and future features; the 200-second
acceptance capture proves the present configuration, but another large buffer or
memory-heavy feature requires a fresh runtime and build-time memory review.
The default build exposes a custom NimBLE GATT service named `TrikkeSensor`.
Each unchanged `TRK1` frame is fragmented as needed, persisted by the receiver,
and then acknowledged by exact packet sequence. A missing ACK causes a replay;
disconnect or subscription loss retains the same frame and restarts it from byte
zero after reconnection. The receiver deduplicates these deliberate replays.
Acquisition remains stopped until a valid session-token handshake establishes a
clean capture boundary. Each unchanged `TRK1` frame is then fragmented as needed,
persisted by the receiver, and acknowledged by exact packet sequence. A missing
ACK causes a replay; disconnect or subscription loss retains the same frame and
restarts it from byte zero after a same-token reconnection. The receiver
deduplicates these deliberate replays.
The preserved USB telemetry option uses ESP-IDF's interrupt-driven USB
Serial/JTAG driver behind
a transport-neutral state machine. A complete frame is submitted atomically to
the driver ring and remains pending across bounded drain timeouts; firmware does
not resubmit it ambiguously or dequeue another frame. The 1024-sample queue
not resubmit it ambiguously or dequeue another frame. The 3072-sample queue
therefore also protects a connected endpoint that temporarily stops draining.
USB drain confirms that bytes left the device endpoint, not that the capture
@@ -141,7 +168,29 @@ python3 tools/capture_ble.py
The client scans for `TrikkeSensor`, stores only complete CRC-valid `TRK1`
frames, flushes the binary and CSV outputs, and only then writes the application
ACK. See [the BLE transport specification](docs/ble-transport-v1.md).
ACK. See [the BLE transport specification](docs/ble-transport-v2.md).
## Android recorder
The `android/` project is the prototype ride recorder. Each new recording sends
an idempotent session token that gives the C3 a repeatable software session
boundary with empty queues, reset counters, and acquisition starting only after
the phone is ready. Reconnects reuse that token and preserve the outage backlog.
The app reassembles and validates complete frames, appends each new frame to
app-private storage, calls `fsync`, and only then sends `ACK1`. Exact replays are
acknowledged without being appended twice. Recording runs in a connected-device
foreground service with a partial wake lock so it can continue while the screen
is off.
Build its debug APK with:
```sh
cd android
./gradlew testDebugUnitTest lintDebug assembleDebug
```
See [the Android recorder guide](android/README.md) for installation, capture,
export, and the first coordinated phone test.
## TRK1 output and USB validation