Files
trikkeSensors/README.md
T

192 lines
8.1 KiB
Markdown

# Trikke Motion Telemetry Logger
Prototype v0 firmware for a Seeed Studio XIAO ESP32-C3 with an ADXL345
accelerometer and L3G4200D gyroscope on a shared I2C bus.
This milestone does four things:
1. Detects and verifies both sensors by their identification registers.
2. Configures each sensor for a nominal 100 Hz raw output rate.
3. Emits framed, timestamped binary readings over reliable BLE, with the
audited direct-USB path retained as a build option.
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.
## Wiring
| Signal | XIAO pin | ESP32-C3 GPIO |
| --- | --- | --- |
| SDA | D4 | GPIO6 |
| SCL | D5 | GPIO7 |
| Sensor power | 3V3 | — |
| Sensor ground | GND | — |
Both breakouts share SDA, SCL, 3V3, and GND. The firmware checks both possible
7-bit I2C addresses for each device:
- ADXL345: `0x53` or `0x1D`; expected `DEVID` is `0xE5`.
- L3G4200D: `0x69` or `0x68`; expected `WHO_AM_I` is `0xD3`.
The XIAO ESP32-C3 external antenna is installed for the BLE transport.
## Sensor configuration
- ADXL345: nominal 100 Hz output rate, full-resolution mode, +/-8 g. Nominal scale is
3.9 mg/LSB.
- L3G4200D: nominal 100 Hz output rate, LPF2 selected with a 25 Hz cutoff,
+/-500 dps. Nominal scale is 17.5 mdps/LSB.
The enclosure coordinate frame is:
- +X points right in the reference photograph.
- +Y points toward the top of the enclosure.
- +Z points out of the board toward the enclosure cover.
The L3G4200D already matches that frame. The ADXL345 mapping is:
```text
enclosure X = native Y
enclosure Y = -native X
enclosure Z = native Z
```
Host-side software calibration is applied after enclosure-axis mapping.
Accelerometer offset and per-axis scale were measured with a six-face enclosure
test. Gyroscope zero-rate bias and polarity were measured; its 17.5 mdps/LSB scale
remains the nominal datasheet value. Mapped raw counts are stored directly, and
sensor-native counts are reconstructed losslessly from the documented mapping.
No software filtering or sensor fusion is performed yet.
The ESP32-C3 polls at exactly 100 Hz 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.
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.
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
therefore also protects a connected endpoint that temporarily stops draining.
USB drain confirms that bytes left the device endpoint, not that the capture
application persisted them. Packet/sample sequences and CRC expose loss after
the fact. BLE `COMPLETE` instead means the receiver acknowledged the exact frame
after persistence.
Measured end-to-end framing overhead is about 2.47 kB/s at 100 Hz, or 8.47
MiB/hour before BLE link overhead.
## Build and flash
ESP-IDF 6.0.2 is installed at:
```text
/Users/jay/.espressif/v6.0.2/esp-idf
```
For each new terminal:
```sh
source /Users/jay/.espressif/v6.0.2/esp-idf/export.sh
idf.py build
idf.py -p /dev/cu.usbmodem1134101 flash
```
BLE is the default. `idf.py menuconfig` -> `Telemetry transport` can select the
preserved direct USB transport for wired regression work. A separate build tree
can verify that selection without disturbing the normal BLE configuration:
```sh
idf.py -B build-usb -D SDKCONFIG=build-usb/sdkconfig \
-D 'SDKCONFIG_DEFAULTS=sdkconfig.defaults;sdkconfig.usb.defaults' build
```
Install the reference host dependencies and capture BLE telemetry with:
```sh
python3 -m pip install -r requirements.txt
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).
## TRK1 output and USB validation
After readable startup metadata, the device emits framed binary. Each sample is a
20-byte record containing mapped raw sensor counts, timing, sequence, and the two
raw status bytes. See [the complete wire-format specification](docs/binary-record-v1.md).
Binary is the authoritative capture format. The host tools render it back to the
same diagnostic CSV schema used during calibration:
```text
sequence,poll_timestamp_us,accel_x_raw,accel_y_raw,accel_z_raw,gyro_x_raw,gyro_y_raw,gyro_z_raw,accel_x_mg,accel_y_mg,accel_z_mg,gyro_x_mdps,gyro_y_mdps,gyro_z_mdps,accel_native_x_raw,accel_native_y_raw,accel_native_z_raw,gyro_native_x_raw,gyro_native_y_raw,gyro_native_z_raw,accel_int_source,gyro_status,loop_overrun_count
```
`poll_timestamp_us` is reconstructed from each frame's base timestamp and 10 us
record deltas. It represents the ESP32-C3 monotonic time immediately before status
and data reads. It is not the sensors' physical sample time. The axes in the first
six sample columns use the enclosure frame above. Calibrated acceleration is in
integer milligravity (`mg`), and bias-corrected angular rate is in integer
millidegrees per second (`mdps`). Native columns remain available for diagnostics.
Status bits:
- `accel_int_source & 0x80`: an unread ADXL345 sample existed when status was
checked. If clear, treat the following sample as stale/untrusted; a sample can
arrive in the short interval between the status and data transactions.
- `accel_int_source & 0x01`: ADXL345 unread data was overwritten.
- `gyro_status & 0x08`: L3G4200D sample is fresh.
- `gyro_status & 0x80`: L3G4200D data overran before it was read.
- `loop_overrun_count`: cumulative acquisition deadlines missed; the loop
resynchronizes after a miss instead of issuing catch-up bursts.
The binary capture tool auto-detects a single `/dev/cu.usbmodem*` device, stores
only CRC-valid frames, renders CSV, and reports packet, sample, timing, status,
drop, overrun, timestamp-saturation, and trailing-partial-byte totals. An
optional `--wire` path preserves every received byte, including startup text and
damaged or partial frames, for forensic comparison:
```sh
python tools/capture_binary.py --reset --wire captures/session.wire
```
`--reset` normalizes the USB DTR/RTS state, clears bytes from the prior session,
and resets the C3 while the new capture is already open. Omit it when attaching
to an intentionally uninterrupted stream.
An existing `.trk` stream can be decoded again without hardware:
```sh
python tools/decode_binary.py captures/session.trk captures/session.csv
```
Live capture and offline decoding use the same integrity tracker, including
wrap-aware packet/sample gap classification.
`tools/capture_serial.py` remains available only for decoding captures from the
older CSV-v3 firmware snapshots.