Extend sensor logger: tagged CSV, GPS/step/tilt/touch channels, Settings toggle

Refactors IMULogger to a general sensor data logger with tagged CSV format:
  ms,type,d0,d1,...,d8
Types: A=accel, G=gyro, M=mag, S=step, W=wrist_tilt, P=GPS, T=touch

Each sensor pushes its own tagged sample independently instead of
the old combined IMU-only row format. GPS logged at 1Hz when active,
step/tilt events logged on occurrence, touch logged on press.

Adds Data Logger switch to the Settings screen with live sample
count and duration display.

Reorders includes so IMULogger.h is available to sensor callbacks.
This commit is contained in:
GlassOnTin 2026-03-31 04:22:18 +01:00
commit 782710e52e
3 changed files with 143 additions and 59 deletions

56
Gui.h
View file

@ -155,10 +155,16 @@ uint16_t *gui_screenshot_buf = NULL;
void display_unblank();
extern float pmu_temperature;
extern volatile uint32_t imu_step_count;
// IMU logger toggle — set by .ino after IMULogger.h is included
// Sensor logger toggle — set by .ino after IMULogger.h is included
typedef bool (*gui_log_toggle_fn_t)();
static gui_log_toggle_fn_t gui_log_toggle_fn = NULL;
static bool gui_imu_logging = false;
// Forward declarations for IMULogger.h variables (defined later in compilation)
extern bool imu_logging;
extern uint32_t imu_log_samples;
extern uint32_t imu_log_start_ms;
// Forward declaration for touch logging (defined in IMULogger.h)
void sensor_log_touch(int16_t x, int16_t y, bool pressed);
#ifndef PMU_TEMP_MIN
#define PMU_TEMP_MIN -30
#endif
@ -208,6 +214,9 @@ static void gui_touch_read_cb(lv_indev_t *indev, lv_indev_data_t *data) {
data->point.y = ty;
data->state = LV_INDEV_STATE_PRESSED;
last_unblank_event = millis();
#if HAS_SD
sensor_log_touch(tx, ty, true);
#endif
} else {
data->state = LV_INDEV_STATE_RELEASED;
}
@ -439,6 +448,8 @@ static lv_obj_t *gui_set_disp_val = NULL;
static lv_obj_t *gui_set_bcn_roller = NULL;
static lv_obj_t *gui_set_gps_roller = NULL;
static lv_obj_t *gui_set_bcn_sw = NULL;
static lv_obj_t *gui_set_log_sw = NULL;
static lv_obj_t *gui_set_log_status = NULL;
static void gui_set_disp_cb(lv_event_t *e) {
lv_obj_t *slider = (lv_obj_t *)lv_event_get_target(e);
@ -477,6 +488,15 @@ static void gui_set_gps_model_cb(lv_event_t *e) {
}
}
static void gui_set_log_cb(lv_event_t *e) {
bool on = lv_obj_has_state(gui_set_log_sw, LV_STATE_CHECKED);
if (on) {
if (gui_log_toggle_fn) gui_log_toggle_fn();
} else {
if (gui_log_toggle_fn && imu_logging) gui_log_toggle_fn();
}
}
static void gui_create_settings_screen(lv_obj_t *parent) {
gui_style_black_container(parent);
@ -555,6 +575,17 @@ static void gui_create_settings_screen(lv_obj_t *parent) {
lv_obj_set_style_text_color(gui_set_gps_roller, lv_color_hex(GUI_COL_BLACK), LV_PART_SELECTED);
lv_roller_set_selected(gui_set_gps_roller, gps_dynamic_model, LV_ANIM_OFF);
lv_obj_add_event_cb(gui_set_gps_roller, gui_set_gps_model_cb, LV_EVENT_VALUE_CHANGED, NULL);
// --- Sensor logger ---
gui_create_rule(cont, 320);
gui_label_at(cont, &lv_font_montserrat_14, GUI_COL_MID, "Data logger", GUI_PAD, 335);
gui_set_log_sw = lv_switch_create(cont);
lv_obj_set_pos(gui_set_log_sw, GUI_W - GUI_PAD - 50, 330);
lv_obj_set_size(gui_set_log_sw, 50, 26);
lv_obj_set_style_bg_color(gui_set_log_sw, lv_color_hex(GUI_COL_DIM), 0);
lv_obj_set_style_bg_color(gui_set_log_sw, lv_color_hex(GUI_COL_GREEN), LV_PART_INDICATOR | LV_STATE_CHECKED);
lv_obj_add_event_cb(gui_set_log_sw, gui_set_log_cb, LV_EVENT_VALUE_CHANGED, NULL);
gui_set_log_status = gui_label_at(cont, &lv_font_montserrat_14, GUI_COL_DIM, "", GUI_PAD, 365);
}
// ---------------------------------------------------------------------------
@ -611,12 +642,13 @@ static void gui_update_data() {
bool on_watch = (cur_tile == gui_tile_watch);
bool on_radio = (cur_tile == gui_tile_radio);
bool on_gps = (cur_tile == gui_tile_gps);
bool on_settings = (cur_tile == gui_tile_set);
// ---- Watch face ----
// Time always updates (cheap, changes rarely)
lv_label_set_text_fmt(gui_time_label, "%02d:%02d", rtc_hour, rtc_minute);
if (!on_watch && !on_radio && !on_gps) return;
if (!on_watch && !on_radio && !on_gps && !on_settings) return;
// ---- Watch face details (only when visible) ----
if (on_watch) {
@ -812,6 +844,26 @@ static void gui_update_data() {
}
}
#endif
// ---- Settings screen (logger status) ----
if (on_settings && gui_set_log_status) {
#if HAS_SD
if (imu_logging) {
uint32_t dur = (millis() - imu_log_start_ms) / 1000;
lv_label_set_text_fmt(gui_set_log_status, "%lu samples %lus", imu_log_samples, dur);
lv_obj_set_style_text_color(gui_set_log_status, lv_color_hex(GUI_COL_GREEN), 0);
if (!lv_obj_has_state(gui_set_log_sw, LV_STATE_CHECKED))
lv_obj_add_state(gui_set_log_sw, LV_STATE_CHECKED);
} else {
lv_label_set_text(gui_set_log_status, "SD card ready");
lv_obj_set_style_text_color(gui_set_log_status, lv_color_hex(GUI_COL_DIM), 0);
if (lv_obj_has_state(gui_set_log_sw, LV_STATE_CHECKED))
lv_obj_clear_state(gui_set_log_sw, LV_STATE_CHECKED);
}
#else
lv_label_set_text(gui_set_log_status, "No SD card");
#endif
}
}
// ---------------------------------------------------------------------------

View file

@ -1,8 +1,9 @@
// IMU Data Logger — streams BHI260AP sensor data to SD card as CSV
// Start/stop via remote debug command 'L' or long-press button
// Sensor Data Logger — streams all sensor data to SD card as CSV
// Start/stop via remote debug command 'L' or Settings screen toggle
//
// CSV format: timestamp_ms,ax,ay,az,gx,gy,gz,mx,my,mz
// Accelerometer/gyro/magnetometer in raw int16 units
// CSV format: timestamp_ms,type,d0,d1,d2,d3,d4,d5,d6,d7,d8
// Type: A=accel, G=gyro, M=mag, S=step, W=wrist_tilt, P=GPS, T=touch
// IMU values in raw int16 units, GPS in scaled integers
// Timestamp is millis() at time of callback
#ifndef IMULOGGER_H
@ -14,67 +15,87 @@
#include "SharedSPI.h"
// Ring buffer for sensor samples (stored in PSRAM)
struct imu_sample_t {
// Tagged samples: type char + up to 9 int32 fields
struct sensor_sample_t {
uint32_t timestamp;
int16_t ax, ay, az; // accelerometer
int16_t gx, gy, gz; // gyroscope
int16_t mx, my, mz; // magnetometer
char type; // A=accel, G=gyro, M=mag, S=step, W=wrist, P=gps, T=touch
int32_t d[9]; // data fields (meaning depends on type)
};
#define IMU_LOG_BUF_SIZE 512 // samples before flush (~10s at 50Hz)
static imu_sample_t *imu_log_buf = NULL;
static sensor_sample_t *imu_log_buf = NULL;
static volatile uint32_t imu_log_head = 0; // write position
static volatile uint32_t imu_log_tail = 0; // read position
static bool imu_logging = false;
bool imu_logging = false;
static File imu_log_file;
static uint32_t imu_log_samples = 0;
static uint32_t imu_log_start_ms = 0;
uint32_t imu_log_samples = 0;
uint32_t imu_log_start_ms = 0;
// Latest raw values (written by callbacks, read by flush)
static volatile int16_t imu_raw_ax = 0, imu_raw_ay = 0, imu_raw_az = 0;
static volatile int16_t imu_raw_gx = 0, imu_raw_gy = 0, imu_raw_gz = 0;
static volatile int16_t imu_raw_mx = 0, imu_raw_my = 0, imu_raw_mz = 0;
static volatile bool imu_accel_new = false;
// Push a tagged sample to the ring buffer (safe from callbacks)
static void sensor_log_push(char type, int32_t d0=0, int32_t d1=0, int32_t d2=0,
int32_t d3=0, int32_t d4=0, int32_t d5=0,
int32_t d6=0, int32_t d7=0, int32_t d8=0) {
if (!imu_logging || !imu_log_buf) return;
uint32_t next = (imu_log_head + 1) % IMU_LOG_BUF_SIZE;
if (next == imu_log_tail) return; // full
sensor_sample_t &s = imu_log_buf[imu_log_head];
s.timestamp = millis();
s.type = type;
s.d[0]=d0; s.d[1]=d1; s.d[2]=d2; s.d[3]=d3; s.d[4]=d4;
s.d[5]=d5; s.d[6]=d6; s.d[7]=d7; s.d[8]=d8;
imu_log_head = next;
}
// Sensor callbacks — store latest values
// Sensor callbacks — push individual tagged samples
void imu_log_accel_cb(uint8_t sensor_id, uint8_t *data, uint32_t size, uint64_t *timestamp, void *user_data) {
if (size >= 6) {
imu_raw_ax = (int16_t)(data[0] | (data[1] << 8));
imu_raw_ay = (int16_t)(data[2] | (data[3] << 8));
imu_raw_az = (int16_t)(data[4] | (data[5] << 8));
imu_accel_new = true;
// Push combined sample to ring buffer when accel fires (it's the "clock")
if (imu_logging && imu_log_buf) {
uint32_t next = (imu_log_head + 1) % IMU_LOG_BUF_SIZE;
if (next != imu_log_tail) { // not full
imu_sample_t &s = imu_log_buf[imu_log_head];
s.timestamp = millis();
s.ax = imu_raw_ax; s.ay = imu_raw_ay; s.az = imu_raw_az;
s.gx = imu_raw_gx; s.gy = imu_raw_gy; s.gz = imu_raw_gz;
s.mx = imu_raw_mx; s.my = imu_raw_my; s.mz = imu_raw_mz;
imu_log_head = next;
}
}
int16_t ax = (int16_t)(data[0] | (data[1] << 8));
int16_t ay = (int16_t)(data[2] | (data[3] << 8));
int16_t az = (int16_t)(data[4] | (data[5] << 8));
sensor_log_push('A', ax, ay, az);
}
}
void imu_log_gyro_cb(uint8_t sensor_id, uint8_t *data, uint32_t size, uint64_t *timestamp, void *user_data) {
if (size >= 6) {
imu_raw_gx = (int16_t)(data[0] | (data[1] << 8));
imu_raw_gy = (int16_t)(data[2] | (data[3] << 8));
imu_raw_gz = (int16_t)(data[4] | (data[5] << 8));
int16_t gx = (int16_t)(data[0] | (data[1] << 8));
int16_t gy = (int16_t)(data[2] | (data[3] << 8));
int16_t gz = (int16_t)(data[4] | (data[5] << 8));
sensor_log_push('G', gx, gy, gz);
}
}
void imu_log_mag_cb(uint8_t sensor_id, uint8_t *data, uint32_t size, uint64_t *timestamp, void *user_data) {
if (size >= 6) {
imu_raw_mx = (int16_t)(data[0] | (data[1] << 8));
imu_raw_my = (int16_t)(data[2] | (data[3] << 8));
imu_raw_mz = (int16_t)(data[4] | (data[5] << 8));
int16_t mx = (int16_t)(data[0] | (data[1] << 8));
int16_t my = (int16_t)(data[2] | (data[3] << 8));
int16_t mz = (int16_t)(data[4] | (data[5] << 8));
sensor_log_push('M', mx, my, mz);
}
}
// Log step counter event
void sensor_log_step(uint32_t count) {
sensor_log_push('S', (int32_t)count);
}
// Log wrist tilt event
void sensor_log_wrist_tilt() {
sensor_log_push('W');
}
// Log GPS fix (call at 1Hz from main loop when logging)
void sensor_log_gps(double lat, double lon, double alt, double speed, double hdop, uint8_t sats) {
sensor_log_push('P',
(int32_t)(lat * 1e6), (int32_t)(lon * 1e6), (int32_t)(alt * 10),
(int32_t)(speed * 100), (int32_t)(hdop * 100), sats);
}
// Log touch event
void sensor_log_touch(int16_t x, int16_t y, bool pressed) {
sensor_log_push('T', x, y, pressed ? 1 : 0);
}
// Forward declaration
void imu_log_flush();
@ -83,8 +104,8 @@ bool imu_log_start(SensorBHI260AP *bhi) {
// Allocate ring buffer in PSRAM
if (!imu_log_buf) {
imu_log_buf = (imu_sample_t *)heap_caps_malloc(
IMU_LOG_BUF_SIZE * sizeof(imu_sample_t), MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
imu_log_buf = (sensor_sample_t *)heap_caps_malloc(
IMU_LOG_BUF_SIZE * sizeof(sensor_sample_t), MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
if (!imu_log_buf) return false;
}
imu_log_head = 0;
@ -111,7 +132,7 @@ bool imu_log_start(SensorBHI260AP *bhi) {
}
// Write CSV header
imu_log_file.println("ms,ax,ay,az,gx,gy,gz,mx,my,mz");
imu_log_file.println("ms,type,d0,d1,d2,d3,d4,d5,d6,d7,d8");
// Configure sensors at 50Hz
bhi->configure(SensorBHI260AP::ACCEL_PASSTHROUGH, 50.0, 0);
@ -159,13 +180,14 @@ void imu_log_flush() {
if (!imu_logging || !imu_log_buf) return;
if (shared_spi_mutex && xSemaphoreTake(shared_spi_mutex, pdMS_TO_TICKS(50)) != pdTRUE) return;
char line[80];
char line[128];
uint32_t flushed = 0;
while (imu_log_tail != imu_log_head) {
imu_sample_t &s = imu_log_buf[imu_log_tail];
int len = snprintf(line, sizeof(line), "%lu,%d,%d,%d,%d,%d,%d,%d,%d,%d\n",
s.timestamp, s.ax, s.ay, s.az,
s.gx, s.gy, s.gz, s.mx, s.my, s.mz);
sensor_sample_t &s = imu_log_buf[imu_log_tail];
int len = snprintf(line, sizeof(line), "%lu,%c,%ld,%ld,%ld,%ld,%ld,%ld,%ld,%ld,%ld\n",
s.timestamp, s.type,
s.d[0], s.d[1], s.d[2], s.d[3], s.d[4],
s.d[5], s.d[6], s.d[7], s.d[8]);
imu_log_file.write((uint8_t *)line, len);
imu_log_tail = (imu_log_tail + 1) % IMU_LOG_BUF_SIZE;
imu_log_samples++;

View file

@ -32,24 +32,26 @@
volatile uint32_t imu_step_count = 0;
volatile bool imu_wrist_tilt = false;
// MAX98357A I2S speaker + SPM1423 PDM microphone
#include "Speaker.h"
#include "Microphone.h"
// Sensor data logger to SD card (must be before callbacks that call sensor_log_*)
#include "IMULogger.h"
// IMU sensor callbacks
void imu_step_cb(uint8_t sensor_id, uint8_t *data, uint32_t size, uint64_t *timestamp, void *user_data) {
if (size >= 4) {
imu_step_count = data[0] | (data[1] << 8) | (data[2] << 16) | (data[3] << 24);
sensor_log_step(imu_step_count);
}
}
void imu_wrist_tilt_cb(uint8_t sensor_id, uint8_t *data, uint32_t size, uint64_t *timestamp, void *user_data) {
imu_wrist_tilt = true;
sensor_log_wrist_tilt();
}
// MAX98357A I2S speaker + SPM1423 PDM microphone
#include "Speaker.h"
#include "Microphone.h"
// IMU data logger to SD card
#include "IMULogger.h"
// Shared SPI bus mutex (LoRa + SD + NFC)
#include "SharedSPI.h"
SemaphoreHandle_t shared_spi_mutex = NULL; // definition (declared extern in SharedSPI.h)
@ -2256,7 +2258,15 @@ void loop() {
#endif
}
#if HAS_SD
if (imu_logging) imu_log_flush();
if (imu_logging) {
imu_log_flush();
// Log GPS at 1Hz when logging
static uint32_t last_gps_log = 0;
if (gps_has_fix && millis() - last_gps_log >= 1000) {
sensor_log_gps(gps_lat, gps_lon, gps_alt, gps_speed, gps_hdop, gps_sats);
last_gps_log = millis();
}
}
#endif
}
#endif