192 lines
8.1 KiB
Markdown
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 512-record RAM queue, providing 5.12 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 512-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.
|