checkpoint: working sensor acquisition and axis validation

This commit is contained in:
Jay
2026-08-17 09:38:18 -04:00
commit f1ac20fb4f
16 changed files with 721 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
CompileFlags:
Remove: [-f*, -m*]
+7
View File
@@ -0,0 +1,7 @@
build/
sdkconfig
captures/
sdkconfig.old
dependencies.lock
managed_components/
.DS_Store
+11
View File
@@ -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"
]
}
+5
View File
@@ -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)
+81
View File
@@ -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.
+6
View File
@@ -0,0 +1,6 @@
idf_component_register(
SRCS "adxl345.c"
INCLUDE_DIRS "include"
REQUIRES esp_driver_i2c
)
+123
View File
@@ -0,0 +1,123 @@
#include "adxl345.h"
#include <stddef.h>
#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;
}
+34
View File
@@ -0,0 +1,34 @@
#pragma once
#include <stdint.h>
#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
+6
View File
@@ -0,0 +1,6 @@
idf_component_register(
SRCS "l3g4200d.c"
INCLUDE_DIRS "include"
REQUIRES esp_driver_i2c
)
+34
View File
@@ -0,0 +1,34 @@
#pragma once
#include <stdint.h>
#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
+131
View File
@@ -0,0 +1,131 @@
#include "l3g4200d.h"
#include <stddef.h>
#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;
}
+49
View File
@@ -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.
+5
View File
@@ -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
)
+152
View File
@@ -0,0 +1,152 @@
#include <inttypes.h>
#include <stdio.h>
#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);
}
}
+9
View File
@@ -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
+66
View File
@@ -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())