From f1ac20fb4ffebd2409f9fe2aaf1b7f2855e7b322 Mon Sep 17 00:00:00 2001 From: Jay Date: Mon, 17 Aug 2026 09:38:18 -0400 Subject: [PATCH] checkpoint: working sensor acquisition and axis validation --- .clangd | 2 + .gitignore | 7 ++ .vscode/settings.json | 11 ++ CMakeLists.txt | 5 + README.md | 81 +++++++++++++ components/adxl345/CMakeLists.txt | 6 + components/adxl345/adxl345.c | 123 ++++++++++++++++++++ components/adxl345/include/adxl345.h | 34 ++++++ components/l3g4200d/CMakeLists.txt | 6 + components/l3g4200d/include/l3g4200d.h | 34 ++++++ components/l3g4200d/l3g4200d.c | 131 +++++++++++++++++++++ docs/axis-validation-2026-08-16.md | 49 ++++++++ main/CMakeLists.txt | 5 + main/trikke_sensor_main.c | 152 +++++++++++++++++++++++++ sdkconfig.defaults | 9 ++ tools/capture_serial.py | 66 +++++++++++ 16 files changed, 721 insertions(+) create mode 100644 .clangd create mode 100644 .gitignore create mode 100644 .vscode/settings.json create mode 100644 CMakeLists.txt create mode 100644 README.md create mode 100644 components/adxl345/CMakeLists.txt create mode 100644 components/adxl345/adxl345.c create mode 100644 components/adxl345/include/adxl345.h create mode 100644 components/l3g4200d/CMakeLists.txt create mode 100644 components/l3g4200d/include/l3g4200d.h create mode 100644 components/l3g4200d/l3g4200d.c create mode 100644 docs/axis-validation-2026-08-16.md create mode 100644 main/CMakeLists.txt create mode 100644 main/trikke_sensor_main.c create mode 100644 sdkconfig.defaults create mode 100644 tools/capture_serial.py diff --git a/.clangd b/.clangd new file mode 100644 index 0000000..437f255 --- /dev/null +++ b/.clangd @@ -0,0 +1,2 @@ +CompileFlags: + Remove: [-f*, -m*] diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..79c2e04 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +build/ +sdkconfig +captures/ +sdkconfig.old +dependencies.lock +managed_components/ +.DS_Store diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..4735953 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,11 @@ +{ + "idf.currentSetup": "/Users/jay/.espressif/v6.0.2/esp-idf", + "idf.port": "/dev/tty.usbmodem1134101", + "idf.customExtraVars": {}, + "clangd.path": "/Users/jay/.espressif/tools/esp-clang/esp-20.1.1_20250829/esp-clang/bin/clangd", + "clangd.arguments": [ + "--background-index", + "--query-driver=**", + "--compile-commands-dir=/Users/jay/embedded_dev/trikkeSensor/build" + ] +} diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..67be53f --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,5 @@ +cmake_minimum_required(VERSION 3.22) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +idf_build_set_property(MINIMAL_BUILD ON) +project(trikke_sensor) diff --git a/README.md b/README.md new file mode 100644 index 0000000..8b9c225 --- /dev/null +++ b/README.md @@ -0,0 +1,81 @@ +# 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 100 Hz raw acquisition. +3. Emits timestamped, sensor-native raw readings over the XIAO USB connection. +4. Maps both sensors into a shared enclosure coordinate frame for validation. + +BLE transport and phone-side storage come after the wired sensor path is proven. + +## 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`. + +## Sensor configuration + +- ADXL345: 100 Hz output rate, full-resolution mode, +/-8 g. Nominal scale is + 3.9 mg/LSB. +- L3G4200D: 100 Hz output rate, 25 Hz bandwidth, +/-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 +``` + +No calibration, filtering, or sensor fusion is performed yet. + +## 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 monitor +``` + +Exit the serial monitor with `Ctrl-]`. + +## USB output + +After startup metadata, records use CSV: + +```text +sequence,timestamp_us,accel_x_raw,accel_y_raw,accel_z_raw,gyro_x_raw,gyro_y_raw,gyro_z_raw,accel_native_x_raw,accel_native_y_raw,accel_native_z_raw,gyro_native_x_raw,gyro_native_y_raw,gyro_native_z_raw +``` + +`timestamp_us` is the ESP32-C3 monotonic microsecond timer since boot. The axes +in the first six sample columns use the enclosure frame above. The appended native +columns are temporary diagnostics for the coordinated orientation test. diff --git a/components/adxl345/CMakeLists.txt b/components/adxl345/CMakeLists.txt new file mode 100644 index 0000000..8cec00a --- /dev/null +++ b/components/adxl345/CMakeLists.txt @@ -0,0 +1,6 @@ +idf_component_register( + SRCS "adxl345.c" + INCLUDE_DIRS "include" + REQUIRES esp_driver_i2c +) + diff --git a/components/adxl345/adxl345.c b/components/adxl345/adxl345.c new file mode 100644 index 0000000..e29d355 --- /dev/null +++ b/components/adxl345/adxl345.c @@ -0,0 +1,123 @@ +#include "adxl345.h" + +#include + +#define ADXL345_REG_DEVID 0x00 +#define ADXL345_REG_BW_RATE 0x2C +#define ADXL345_REG_POWER_CTL 0x2D +#define ADXL345_REG_DATA_FORMAT 0x31 +#define ADXL345_REG_DATAX0 0x32 + +#define ADXL345_DEVID_VALUE 0xE5 +#define ADXL345_BW_RATE_100_HZ 0x0A +#define ADXL345_POWER_MEASURE 0x08 +#define ADXL345_FORMAT_FULL_8G 0x0A +#define ADXL345_TIMEOUT_MS 100 + +static esp_err_t read_registers(i2c_master_dev_handle_t device, uint8_t start_register, + uint8_t *data, size_t length) +{ + return i2c_master_transmit_receive(device, &start_register, 1, data, length, + ADXL345_TIMEOUT_MS); +} + +static esp_err_t write_register(i2c_master_dev_handle_t device, uint8_t reg, uint8_t value) +{ + const uint8_t bytes[] = {reg, value}; + return i2c_master_transmit(device, bytes, sizeof(bytes), ADXL345_TIMEOUT_MS); +} + +static esp_err_t verify_register(i2c_master_dev_handle_t device, uint8_t reg, uint8_t expected) +{ + uint8_t actual = 0; + esp_err_t err = read_registers(device, reg, &actual, 1); + if (err != ESP_OK) { + return err; + } + return actual == expected ? ESP_OK : ESP_ERR_INVALID_RESPONSE; +} + +esp_err_t adxl345_init(adxl345_t *sensor, i2c_master_bus_handle_t bus, uint32_t bus_speed_hz) +{ + if (sensor == NULL || bus == NULL) { + return ESP_ERR_INVALID_ARG; + } + + *sensor = (adxl345_t){0}; + static const uint8_t candidate_addresses[] = {0x53, 0x1D}; + + for (size_t i = 0; i < sizeof(candidate_addresses); ++i) { + const i2c_device_config_t config = { + .dev_addr_length = I2C_ADDR_BIT_LEN_7, + .device_address = candidate_addresses[i], + .scl_speed_hz = bus_speed_hz, + }; + + i2c_master_dev_handle_t device = NULL; + esp_err_t err = i2c_master_bus_add_device(bus, &config, &device); + if (err != ESP_OK) { + return err; + } + + uint8_t device_id = 0; + err = read_registers(device, ADXL345_REG_DEVID, &device_id, 1); + if (err == ESP_OK && device_id == ADXL345_DEVID_VALUE) { + sensor->device = device; + sensor->address = candidate_addresses[i]; + break; + } + + i2c_master_bus_rm_device(device); + } + + if (sensor->device == NULL) { + return ESP_ERR_NOT_FOUND; + } + + // Configure while in standby, then enter measurement mode. + esp_err_t err = write_register(sensor->device, ADXL345_REG_POWER_CTL, 0x00); + if (err == ESP_OK) { + err = write_register(sensor->device, ADXL345_REG_DATA_FORMAT, ADXL345_FORMAT_FULL_8G); + } + if (err == ESP_OK) { + err = write_register(sensor->device, ADXL345_REG_BW_RATE, ADXL345_BW_RATE_100_HZ); + } + if (err == ESP_OK) { + err = write_register(sensor->device, ADXL345_REG_POWER_CTL, ADXL345_POWER_MEASURE); + } + if (err == ESP_OK) { + err = verify_register(sensor->device, ADXL345_REG_DATA_FORMAT, ADXL345_FORMAT_FULL_8G); + } + if (err == ESP_OK) { + err = verify_register(sensor->device, ADXL345_REG_BW_RATE, ADXL345_BW_RATE_100_HZ); + } + if (err == ESP_OK) { + err = verify_register(sensor->device, ADXL345_REG_POWER_CTL, ADXL345_POWER_MEASURE); + } + + return err; +} + +esp_err_t adxl345_read_raw(const adxl345_t *sensor, adxl345_sample_t *sample) +{ + if (sensor == NULL || sensor->device == NULL || sample == NULL) { + return ESP_ERR_INVALID_ARG; + } + + uint8_t data[6] = {0}; + esp_err_t err = read_registers(sensor->device, ADXL345_REG_DATAX0, data, sizeof(data)); + if (err != ESP_OK) { + return err; + } + + sample->x = (int16_t)((uint16_t)data[0] | ((uint16_t)data[1] << 8)); + sample->y = (int16_t)((uint16_t)data[2] | ((uint16_t)data[3] << 8)); + sample->z = (int16_t)((uint16_t)data[4] | ((uint16_t)data[5] << 8)); + return ESP_OK; +} + +uint8_t adxl345_address(const adxl345_t *sensor) +{ + return sensor != NULL ? sensor->address : 0; +} + diff --git a/components/adxl345/include/adxl345.h b/components/adxl345/include/adxl345.h new file mode 100644 index 0000000..c151a2f --- /dev/null +++ b/components/adxl345/include/adxl345.h @@ -0,0 +1,34 @@ +#pragma once + +#include + +#include "driver/i2c_master.h" +#include "esp_err.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct { + i2c_master_dev_handle_t device; + uint8_t address; +} adxl345_t; + +typedef struct { + int16_t x; + int16_t y; + int16_t z; +} adxl345_sample_t; + +/** Detect, identify, and configure an ADXL345 for 100 Hz, full-resolution +/-8 g. */ +esp_err_t adxl345_init(adxl345_t *sensor, i2c_master_bus_handle_t bus, uint32_t bus_speed_hz); + +/** Read one sensor-native, uncalibrated XYZ sample. */ +esp_err_t adxl345_read_raw(const adxl345_t *sensor, adxl345_sample_t *sample); + +uint8_t adxl345_address(const adxl345_t *sensor); + +#ifdef __cplusplus +} +#endif + diff --git a/components/l3g4200d/CMakeLists.txt b/components/l3g4200d/CMakeLists.txt new file mode 100644 index 0000000..7968500 --- /dev/null +++ b/components/l3g4200d/CMakeLists.txt @@ -0,0 +1,6 @@ +idf_component_register( + SRCS "l3g4200d.c" + INCLUDE_DIRS "include" + REQUIRES esp_driver_i2c +) + diff --git a/components/l3g4200d/include/l3g4200d.h b/components/l3g4200d/include/l3g4200d.h new file mode 100644 index 0000000..2609ed7 --- /dev/null +++ b/components/l3g4200d/include/l3g4200d.h @@ -0,0 +1,34 @@ +#pragma once + +#include + +#include "driver/i2c_master.h" +#include "esp_err.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct { + i2c_master_dev_handle_t device; + uint8_t address; +} l3g4200d_t; + +typedef struct { + int16_t x; + int16_t y; + int16_t z; +} l3g4200d_sample_t; + +/** Detect, identify, and configure an L3G4200D for 100 Hz, 25 Hz BW, +/-500 dps. */ +esp_err_t l3g4200d_init(l3g4200d_t *sensor, i2c_master_bus_handle_t bus, uint32_t bus_speed_hz); + +/** Read one sensor-native, uncalibrated XYZ sample. */ +esp_err_t l3g4200d_read_raw(const l3g4200d_t *sensor, l3g4200d_sample_t *sample); + +uint8_t l3g4200d_address(const l3g4200d_t *sensor); + +#ifdef __cplusplus +} +#endif + diff --git a/components/l3g4200d/l3g4200d.c b/components/l3g4200d/l3g4200d.c new file mode 100644 index 0000000..82f4424 --- /dev/null +++ b/components/l3g4200d/l3g4200d.c @@ -0,0 +1,131 @@ +#include "l3g4200d.h" + +#include + +#define L3G4200D_REG_WHO_AM_I 0x0F +#define L3G4200D_REG_CTRL1 0x20 +#define L3G4200D_REG_CTRL2 0x21 +#define L3G4200D_REG_CTRL3 0x22 +#define L3G4200D_REG_CTRL4 0x23 +#define L3G4200D_REG_CTRL5 0x24 +#define L3G4200D_REG_OUT_X_L 0x28 + +#define L3G4200D_WHO_AM_I_VALUE 0xD3 +#define L3G4200D_CTRL1_100_HZ 0x1F +#define L3G4200D_CTRL4_500_DPS 0x90 +#define L3G4200D_AUTO_INCREMENT 0x80 +#define L3G4200D_TIMEOUT_MS 100 + +static esp_err_t read_registers(i2c_master_dev_handle_t device, uint8_t start_register, + uint8_t *data, size_t length) +{ + return i2c_master_transmit_receive(device, &start_register, 1, data, length, + L3G4200D_TIMEOUT_MS); +} + +static esp_err_t write_register(i2c_master_dev_handle_t device, uint8_t reg, uint8_t value) +{ + const uint8_t bytes[] = {reg, value}; + return i2c_master_transmit(device, bytes, sizeof(bytes), L3G4200D_TIMEOUT_MS); +} + +static esp_err_t verify_register(i2c_master_dev_handle_t device, uint8_t reg, uint8_t expected) +{ + uint8_t actual = 0; + esp_err_t err = read_registers(device, reg, &actual, 1); + if (err != ESP_OK) { + return err; + } + return actual == expected ? ESP_OK : ESP_ERR_INVALID_RESPONSE; +} + +esp_err_t l3g4200d_init(l3g4200d_t *sensor, i2c_master_bus_handle_t bus, uint32_t bus_speed_hz) +{ + if (sensor == NULL || bus == NULL) { + return ESP_ERR_INVALID_ARG; + } + + *sensor = (l3g4200d_t){0}; + static const uint8_t candidate_addresses[] = {0x69, 0x68}; + + for (size_t i = 0; i < sizeof(candidate_addresses); ++i) { + const i2c_device_config_t config = { + .dev_addr_length = I2C_ADDR_BIT_LEN_7, + .device_address = candidate_addresses[i], + .scl_speed_hz = bus_speed_hz, + }; + + i2c_master_dev_handle_t device = NULL; + esp_err_t err = i2c_master_bus_add_device(bus, &config, &device); + if (err != ESP_OK) { + return err; + } + + uint8_t device_id = 0; + err = read_registers(device, L3G4200D_REG_WHO_AM_I, &device_id, 1); + if (err == ESP_OK && device_id == L3G4200D_WHO_AM_I_VALUE) { + sensor->device = device; + sensor->address = candidate_addresses[i]; + break; + } + + i2c_master_bus_rm_device(device); + } + + if (sensor->device == NULL) { + return ESP_ERR_NOT_FOUND; + } + + // Configure control registers before enabling normal mode in CTRL1. + esp_err_t err = write_register(sensor->device, L3G4200D_REG_CTRL1, 0x00); + if (err == ESP_OK) { + err = write_register(sensor->device, L3G4200D_REG_CTRL2, 0x00); + } + if (err == ESP_OK) { + err = write_register(sensor->device, L3G4200D_REG_CTRL3, 0x00); + } + if (err == ESP_OK) { + // Block data updates while a six-byte sample is being read; select +/-500 dps. + err = write_register(sensor->device, L3G4200D_REG_CTRL4, L3G4200D_CTRL4_500_DPS); + } + if (err == ESP_OK) { + err = write_register(sensor->device, L3G4200D_REG_CTRL5, 0x00); + } + if (err == ESP_OK) { + // 100 Hz ODR, 25 Hz bandwidth, normal mode, all axes enabled. + err = write_register(sensor->device, L3G4200D_REG_CTRL1, L3G4200D_CTRL1_100_HZ); + } + if (err == ESP_OK) { + err = verify_register(sensor->device, L3G4200D_REG_CTRL1, L3G4200D_CTRL1_100_HZ); + } + if (err == ESP_OK) { + err = verify_register(sensor->device, L3G4200D_REG_CTRL4, L3G4200D_CTRL4_500_DPS); + } + + return err; +} + +esp_err_t l3g4200d_read_raw(const l3g4200d_t *sensor, l3g4200d_sample_t *sample) +{ + if (sensor == NULL || sensor->device == NULL || sample == NULL) { + return ESP_ERR_INVALID_ARG; + } + + uint8_t data[6] = {0}; + uint8_t start_register = L3G4200D_REG_OUT_X_L | L3G4200D_AUTO_INCREMENT; + esp_err_t err = read_registers(sensor->device, start_register, data, sizeof(data)); + if (err != ESP_OK) { + return err; + } + + sample->x = (int16_t)((uint16_t)data[0] | ((uint16_t)data[1] << 8)); + sample->y = (int16_t)((uint16_t)data[2] | ((uint16_t)data[3] << 8)); + sample->z = (int16_t)((uint16_t)data[4] | ((uint16_t)data[5] << 8)); + return ESP_OK; +} + +uint8_t l3g4200d_address(const l3g4200d_t *sensor) +{ + return sensor != NULL ? sensor->address : 0; +} + diff --git a/docs/axis-validation-2026-08-16.md b/docs/axis-validation-2026-08-16.md new file mode 100644 index 0000000..07f510b --- /dev/null +++ b/docs/axis-validation-2026-08-16.md @@ -0,0 +1,49 @@ +# Enclosure Axis Validation — 2026-08-16 + +The enclosure was tested with the battery end treated as the bottom of the +reference orientation. The shared enclosure frame is +X right, +Y toward the +top, and +Z toward the cover. + +## Capture integrity + +- 50,001 consecutive samples over 500.000 seconds +- Effective sample rate: exactly 100 Hz +- Timestamp interval: 10,000 us for every sample +- Sequence gaps: 0 +- Axis-mapping mismatches between mapped and native columns: 0 + +The raw capture is `captures/enclosure_axis_test.csv` and is intentionally +excluded from source control. + +## Accelerometer orientation results + +| Position | Mean enclosure counts | Expected dominant axis | Result | +| --- | ---: | ---: | --- | +| Flat, cover up | (+4, -5, +258) | +Z | Pass | +| Left edge down | (+258, -3, +10) | +X | Pass | +| Right edge down | (-260, -9, +16) | -X | Pass | +| Battery edge down | (-3, +255, +11) | +Y | Pass | +| Top edge down | (+5, -264, +15) | -Y | Pass | +| Cover side down | (-7, -7, -234) | -Z | Pass | + +The paired-face results give an initial calibration estimate of: + +```text +offset_counts = (-1.30, -4.45, +12.20) +counts_per_g = (258.90, 259.85, 245.70) +``` + +These values are recorded as candidates only; calibration is not yet applied +in firmware. + +## Gyroscope rotation results + +- The deliberate X rotation was X-dominant in 1,261 dynamic samples, versus + 44 Y-dominant and 35 Z-dominant samples. +- The deliberate Z rotation was Z-dominant in all 1,008 dynamic samples. No X + or Y sample crossed the same 500-count activity threshold. +- Earlier side-to-side orientation changes were Y-dominant, confirming the + remaining gyro axis. + +The mounted sensor axes and the firmware's enclosure-frame mapping are +consistent. diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt new file mode 100644 index 0000000..a969aab --- /dev/null +++ b/main/CMakeLists.txt @@ -0,0 +1,5 @@ +idf_component_register( + SRCS "trikke_sensor_main.c" + INCLUDE_DIRS "." + REQUIRES adxl345 l3g4200d esp_timer esp_driver_gpio esp_driver_i2c +) diff --git a/main/trikke_sensor_main.c b/main/trikke_sensor_main.c new file mode 100644 index 0000000..a9b81c1 --- /dev/null +++ b/main/trikke_sensor_main.c @@ -0,0 +1,152 @@ +#include +#include + +#include "adxl345.h" +#include "driver/gpio.h" +#include "driver/i2c_master.h" +#include "esp_err.h" +#include "esp_log.h" +#include "esp_timer.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "l3g4200d.h" + +// Seeed Studio XIAO ESP32-C3: D4/SDA = GPIO6, D5/SCL = GPIO7. +#define TRIKKE_I2C_PORT I2C_NUM_0 +#define TRIKKE_I2C_SDA_GPIO GPIO_NUM_6 +#define TRIKKE_I2C_SCL_GPIO GPIO_NUM_7 +#define TRIKKE_I2C_FREQ_HZ 400000 +#define TRIKKE_SAMPLE_RATE_HZ 100 +#define TRIKKE_SAMPLE_TICKS pdMS_TO_TICKS(1000 / TRIKKE_SAMPLE_RATE_HZ) + +static const char *TAG = "trikke"; + +typedef struct { + int32_t x; + int32_t y; + int32_t z; +} trikke_axes_sample_t; + +static trikke_axes_sample_t map_accel_to_enclosure(const adxl345_sample_t *native) +{ + // Enclosure frame: +X right, +Y toward the top, +Z toward the cover. + // Mounted ADXL345: native +Y right, native +X down, native +Z toward cover. + return (trikke_axes_sample_t) { + .x = native->y, + .y = -(int32_t)native->x, + .z = native->z, + }; +} + +static trikke_axes_sample_t map_gyro_to_enclosure(const l3g4200d_sample_t *native) +{ + // The mounted L3G4200D axes already match the enclosure frame. + return (trikke_axes_sample_t) { + .x = native->x, + .y = native->y, + .z = native->z, + }; +} + +static esp_err_t init_i2c(i2c_master_bus_handle_t *bus) +{ + const i2c_master_bus_config_t config = { + .i2c_port = TRIKKE_I2C_PORT, + .sda_io_num = TRIKKE_I2C_SDA_GPIO, + .scl_io_num = TRIKKE_I2C_SCL_GPIO, + .clk_source = I2C_CLK_SRC_DEFAULT, + .glitch_ignore_cnt = 7, + .flags.enable_internal_pullup = true, + }; + + return i2c_new_master_bus(&config, bus); +} + +void app_main(void) +{ + // Emit each CSV record immediately while testing over USB. + setvbuf(stdout, NULL, _IOLBF, 0); + + ESP_LOGI(TAG, "Trikke motion telemetry prototype v0"); + ESP_LOGI(TAG, "I2C: SDA=GPIO%d, SCL=GPIO%d, clock=%d Hz", + TRIKKE_I2C_SDA_GPIO, TRIKKE_I2C_SCL_GPIO, TRIKKE_I2C_FREQ_HZ); + + i2c_master_bus_handle_t bus = NULL; + esp_err_t err = init_i2c(&bus); + if (err != ESP_OK) { + ESP_LOGE(TAG, "I2C initialization failed: %s", esp_err_to_name(err)); + return; + } + + adxl345_t accelerometer = {0}; + err = adxl345_init(&accelerometer, bus, TRIKKE_I2C_FREQ_HZ); + if (err != ESP_OK) { + ESP_LOGE(TAG, + "ADXL345 not found at 0x53 or 0x1D (expected DEVID 0xE5): %s", + esp_err_to_name(err)); + return; + } + ESP_LOGI(TAG, "ADXL345 detected at 0x%02X; 100 Hz, +/-8 g, full resolution", + adxl345_address(&accelerometer)); + + l3g4200d_t gyroscope = {0}; + err = l3g4200d_init(&gyroscope, bus, TRIKKE_I2C_FREQ_HZ); + if (err != ESP_OK) { + ESP_LOGE(TAG, + "L3G4200D not found at 0x69 or 0x68 (expected WHO_AM_I 0xD3): %s", + esp_err_to_name(err)); + return; + } + ESP_LOGI(TAG, "L3G4200D detected at 0x%02X; 100 Hz, 25 Hz BW, +/-500 dps", + l3g4200d_address(&gyroscope)); + + // Discard the gyroscope's visible startup transient before beginning the stream. + vTaskDelay(pdMS_TO_TICKS(500)); + + printf("# format=trikke_axis_validation_v1\n"); + printf("# accel_scale_g_per_lsb=0.0039,gyro_scale_dps_per_lsb=0.0175\n"); + printf("# enclosure_axes=+x:right,+y:top,+z:toward_cover\n"); + printf("# mapping=accel(x,y,z)=(native_y,-native_x,native_z);" + "gyro(x,y,z)=(native_x,native_y,native_z)\n"); + printf("sequence,timestamp_us,accel_x_raw,accel_y_raw,accel_z_raw," + "gyro_x_raw,gyro_y_raw,gyro_z_raw," + "accel_native_x_raw,accel_native_y_raw,accel_native_z_raw," + "gyro_native_x_raw,gyro_native_y_raw,gyro_native_z_raw\n"); + + uint32_t sequence = 0; + uint32_t read_error_count = 0; + TickType_t last_wake = xTaskGetTickCount(); + + while (true) { + adxl345_sample_t accel = {0}; + l3g4200d_sample_t gyro = {0}; + const int64_t timestamp_us = esp_timer_get_time(); + + const esp_err_t accel_err = adxl345_read_raw(&accelerometer, &accel); + const esp_err_t gyro_err = l3g4200d_read_raw(&gyroscope, &gyro); + + if (accel_err == ESP_OK && gyro_err == ESP_OK) { + const trikke_axes_sample_t enclosure_accel = map_accel_to_enclosure(&accel); + const trikke_axes_sample_t enclosure_gyro = map_gyro_to_enclosure(&gyro); + + printf("%" PRIu32 ",%" PRId64 ",%" PRId32 ",%" PRId32 ",%" PRId32 + ",%" PRId32 ",%" PRId32 ",%" PRId32 + ",%" PRId16 ",%" PRId16 ",%" PRId16 + ",%" PRId16 ",%" PRId16 ",%" PRId16 "\n", + sequence, timestamp_us, + enclosure_accel.x, enclosure_accel.y, enclosure_accel.z, + enclosure_gyro.x, enclosure_gyro.y, enclosure_gyro.z, + accel.x, accel.y, accel.z, + gyro.x, gyro.y, gyro.z); + } else { + ++read_error_count; + ESP_LOGE(TAG, + "sample %" PRIu32 " read failed (accel=%s, gyro=%s, total_errors=%" PRIu32 ")", + sequence, esp_err_to_name(accel_err), esp_err_to_name(gyro_err), + read_error_count); + } + + ++sequence; + xTaskDelayUntil(&last_wake, TRIKKE_SAMPLE_TICKS); + } +} diff --git a/sdkconfig.defaults b/sdkconfig.defaults new file mode 100644 index 0000000..d88d576 --- /dev/null +++ b/sdkconfig.defaults @@ -0,0 +1,9 @@ +# XIAO ESP32-C3 USB-C connector uses the chip's USB Serial/JTAG console. +CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG=y + +# A 1 kHz RTOS tick gives the 100 Hz acquisition task a 1 ms scheduler quantum. +CONFIG_FREERTOS_HZ=1000 + +# The XIAO ESP32-C3 carries 4 MB of flash. +CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y + diff --git a/tools/capture_serial.py b/tools/capture_serial.py new file mode 100644 index 0000000..d07965c --- /dev/null +++ b/tools/capture_serial.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 +"""Capture Trikke sensor CSV output until interrupted with Ctrl-C.""" + +import argparse +import signal +import sys +from datetime import datetime +from pathlib import Path + +import serial + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser() + parser.add_argument("--port", default="/dev/cu.usbmodem1134101") + parser.add_argument("--baud", type=int, default=115200) + parser.add_argument("--output", type=Path) + return parser.parse_args() + + +def main() -> int: + args = parse_args() + output = args.output or Path("captures") / datetime.now().strftime( + "motion_%Y%m%d_%H%M%S.csv" + ) + output.parent.mkdir(parents=True, exist_ok=True) + + stop_requested = False + + def request_stop(_signum: int, _frame: object) -> None: + nonlocal stop_requested + stop_requested = True + + signal.signal(signal.SIGINT, request_stop) + signal.signal(signal.SIGTERM, request_stop) + + sample_count = 0 + print(f"Recording {args.port} to {output}; press Ctrl-C to stop", flush=True) + + try: + with serial.Serial(args.port, args.baud, timeout=0.25) as sensor, output.open( + "w", encoding="utf-8", newline="" + ) as capture: + while not stop_requested: + raw_line = sensor.readline() + if not raw_line: + continue + + line = raw_line.decode("utf-8", errors="replace").rstrip("\r\n") + capture.write(line + "\n") + + if line and line[0].isdigit() and line.count(",") == 13: + sample_count += 1 + if sample_count % 500 == 0: + capture.flush() + print(f" {sample_count} samples captured", flush=True) + except serial.SerialException as exc: + print(f"Serial error: {exc}", file=sys.stderr) + return 1 + + print(f"Stopped after {sample_count} samples; saved {output}", flush=True) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())