Hi everyone,
I'm working on a research project where I use an ESP32 with two ICM-20948 IMUs (addresses 0x68 and 0x69) and an AD7124 ADC to measure paddle biomechanics.
Streaming works, acquisition works, and raw values look correct — but I’m experiencing major issues with IMU calibration AND with sensor saturation, and after weeks of debugging I still can’t understand what is going on.
I am attaching here:
rest.csv → IMUs completely at rest
paddling → IMUs measuring paddle strokes
Full ESP32 code (complete .ino) → exactly what I’m running
These files should help anyone reproduce and understand the behaviour I’m getting.
1. How I calibrate the IMUs (MATLAB)
I calibrate both IMUs externally in MATLAB:
Accelerometer — 6-pose method
-
+X, –X
-
+Y, –Y
-
+Z, –Z
-
compute mean raw value for each pose
-
solve for bias (3×1) and gain/scale (3×1)
Gyroscope
- keep IMUs still → compute bias per axis
Magnetometer
-
rotate in all directions
-
compute 3×1 hard-iron bias
-
compute 3×3 soft-iron matrix
The resulting coefficients seem consistent.
2. How I apply calibration on the ESP32
Accelerometer:
ax = (ax_raw - A_BIAS[0]) * A_GAIN[0];
ay = (ay_raw - A_BIAS[1]) * A_GAIN[1];
az = (az_raw - A_BIAS[2]) * A_GAIN[2];
Gyroscope:
gx -= G_BIAS[0];
gy -= G_BIAS[1];
gz -= G_BIAS[2];
Magnetometer:
float v[3] = {mx - M_BIAS[0], my - M_BIAS[1], mz - M_BIAS[2]};
mx = M_K[0][0]*v[0] + M_K[0][1]*v[1] + M_K[0][2]*v[2];
my = M_K[1][0]*v[0] + M_K[1][1]*v[1] + M_K[1][2]*v[2];
mz = M_K[2][0]*v[0] + M_K[2][1]*v[1] + M_K[2][2]*v[2];
Everything follows standard IMU calibration procedures.
3. What goes wrong (with CSV proof attached)
A) Raw acceleration is correct, but calibrated acceleration becomes physically impossible
From paddle_001.csv (resting IMUs):
IMU1:
-
raw magnitude ≈ 9.9 m/s²
-
calibrated magnitude ≈ 9.9 m/s²
→ OK
IMU2:
-
raw magnitude ≈ 9.8 m/s² (correct)
-
calibrated magnitude ≈ 6.1 m/s² (WRONG)
→ physically impossible for a static sensor
The raw data shows IMU2 is working, but the calibrated output is totally wrong even though I am applying the same calibration method used successfully in other projects.
B) Gyroscope calibration behaves the same
IMU1 → low bias, stable.
IMU2 → bias corrected values still show ~1.5 dps at rest.
Again, raw data looks normal — the corrected data does not.
C) Saturation happens at the same values regardless of selected range
Even when I use:
imu.setAccRange(ICM20948_ACC_RANGE_4G);
imu.setAccRange(ICM20948_ACC_RANGE_8G);
imu.setAccRange(ICM20948_ACC_RANGE_16G);
I always get clipping at ~1.6 g on both IMUs.
Same for the gyroscope: clipping threshold does not change even though I configure ranges up to ±2000 dps.
This suggests that either the library is not setting the registers, or something overwrites them after configuration.
4. What I have already tried
-
removed
imu.autoOffsets()completely -
verified unit consistency (g → m/s², dps → rad/s)
-
confirmed that raw values differ between ranges (but they don’t)
-
compared to known-good calibrations
-
checked if the library resets configs after
readSensor() -
repeated calibration several times
-
swapped IMU addresses (same behaviour)
-
inspected the GitHub issues for
ICM20948_WE(no clear answer)
5. What I need help understanding
A) Why does IMU2 calibration fail even though raw data is correct?
-
Could the library be internally re-scaling the sensors after reading?
-
Are my bias/gain equations being applied incorrectly?
-
Is the ICM20948_WE library changing full-scale ranges silently?
B) Why do both ACC and GYRO saturate at the same limits regardless of selected range?
-
Is
setAccRange()known to fail? -
Should I write directly to the accelerometer/gyroscope register banks?
-
Could this be an MPU-specific bug?
C) Is there something fundamentally incompatible between the library output and external calibration?
files.zip (473.9 KB)
If needed, I can also upload:
-
calibration MATLAB scripts
-
plots of raw vs calibrated magnitude
-
register dump of both IMUs before and after configuration
I would really appreciate any guidance — this issue is blocking sensor validation and I’m running out of debugging ideas.
Thank you!
/* =======================================================================
PADDLE LOGGER — Aquisição (ESP32 + 2x ICM-20948 + AD7124)
-> Guarda QUATERNIONS (q0..q3), RAW & CORR (acc, gyro) + EXTENSÓMETROS RAW
-> Amostragem alvo: 80 Hz
======================================================================= */
#include <Arduino.h>
#include <SPI.h>
#include <SPIFFS.h>
#include <WiFi.h>
#include <WebServer.h>
#include <WebSocketsServer.h>
#include <Wire.h>
#include <ICM20948_WE.h>
#include <NHB_AD7124.h>
// AHRS (Adafruit)
#include <Adafruit_AHRS.h> // Library: "Adafruit AHRS"
Adafruit_Madgwick madg1; // IMU1
Adafruit_Madgwick madg2; // IMU2
/* ---------------- Wi-Fi / HTTP / WS ---------------- */
const char* AP_SSID = "PaddleLogger";
const char* AP_PASS = "12345678";
WebServer server(80);
WebSocketsServer ws(81);
/* ---------------- IMUs ---------------- */
#define ICM20948_ADDR_1 0x68
#define ICM20948_ADDR_2 0x69
ICM20948_WE imu1(ICM20948_ADDR_1);
ICM20948_WE imu2(ICM20948_ADDR_2);
/* ---------------- AD7124 (2 canais diferenciais) ---------------- */
const uint8_t ADC_CS = 5;
Ad7124 adc(ADC_CS, 8000000);
const int CH_COUNT = 2; // 2 extensómetros (raw)
const AD7124_GainSel PGA_GAIN = AD7124_Gain_128;
const bool BIPOLAR = true;
const AD7124_Filters FILT = AD7124_Filter_SINC4;
const uint16_t FS_BITS = 4;
/* ---------------- Amostragem / Tempo ---------------- */
const float FS_HZ = 80.0f; // alvo 80 Hz
const uint32_t DT_US = (uint32_t)(1000000.0f / FS_HZ);
/* ---------------- Estados ---------------- */
volatile bool stream_on = false;
volatile bool record_on = false;
File logFile;
uint32_t fileIndex = 0;
/* ======================================================
CALIBRAÇÃO FINAL — IMU1 & IMU2
Valores obtidos nos testes:
- 6 poses (ACC)
- repouso (GYRO)
- esfera (MAG)
====================================================== */
/* ---------------- CALIBRAÇÕES (vão ser atualizadas) ---------------- */
// IMU1 - ACC
const float A1_BIAS[3] = { -0.0009460f, 0.5182857f, 0.1845963f };
const float A1_GAIN[3] = { 1.0116220f, 1.0199814f, 1.0162910f };
// IMU1 - GYRO
const float G1_BIAS[3] = { 0.3220197f, 2.0629826f, -0.8690901f };
// IMU1 - MAG
const float M1_BIAS[3] = { -19.36425f, -4.84686f, 19.18474f };
const float M1_K[3][3] = {{ 1.00574812f, 0.01188779f, 0.01074084f },
{ 0.01188779f, 0.96194669f, -0.00903937f },
{ 0.01074084f, -0.00903937f, 1.03397035f },
};
// IMU2 - ACC
const float A2_BIAS[3] = { -0.0752043f, -0.1399603f, 0.4248510f };
const float A2_GAIN[3] = { 1.0131034f, 1.0192566f, 1.0003757f };
// IMU2 - GYRO
const float G2_BIAS[3] = { -1.7944321f, 0.0404470f, 0.1796118f };
// IMU2 - MAG
const float M2_BIAS[3] = { -17.41713f, -11.81091f, 33.52335f };
const float M2_K[3][3] = { { 1.01723582f, 0.02765768f, 0.01634341f },
{ 0.02765768f, 0.98203040f, -0.03115845f },
{ 0.01634341f, -0.03115845f, 1.00309211f },
};
/* ---------------- HTML (UI) ---------------- */
static void handleRoot();
void onWs(uint8_t num, WStype_t type, uint8_t* payload, size_t len);
extern const char INDEX_HTML[] PROGMEM;
/* ---------------- Utils / Helpers ---------------- */
static inline void sendLog(const String& s) {
ws.broadcastTXT(s.c_str());
}
/* Leitura IMU única (uma chamada a readSensor por amostra) */
static inline void readIMURaw(ICM20948_WE& imu,
float& ax, float& ay, float& az, // g
float& gx, float& gy, float& gz,
float& mx, float& my, float& mz) {
imu.readSensor();
xyzFloat a, g, m;
imu.getGValues(&a);
imu.getGyrValues(&g);
imu.getMagValues(&m); // <--- NOVO
ax = a.x;
ay = a.y;
az = a.z;
gx = g.x;
gy = g.y;
gz = g.z;
mx = m.x;
my = m.y;
mz = m.z;
}
/* Correções (bias + ganho no ACC; bias no GYRO) */
static inline void fixAcc1(float& ax, float& ay, float& az) {
ax = (ax - A1_BIAS[0]) * A1_GAIN[0];
ay = (ay - A1_BIAS[1]) * A1_GAIN[1];
az = (az - A1_BIAS[2]) * A1_GAIN[2];
}
static inline void fixAcc2(float& ax, float& ay, float& az) {
ax = (ax - A2_BIAS[0]) * A2_GAIN[0];
ay = (ay - A2_BIAS[1]) * A2_GAIN[1];
az = (az - A2_BIAS[2]) * A2_GAIN[2];
}
static inline void fixGyro1(float& gx, float& gy, float& gz) {
gx -= G1_BIAS[0];
gy -= G1_BIAS[1];
gz -= G1_BIAS[2];
}
static inline void fixGyro2(float& gx, float& gy, float& gz) {
gx -= G2_BIAS[0];
gy -= G2_BIAS[1];
gz -= G2_BIAS[2];
}
static inline void fixMag1(float& mx, float& my, float& mz) {
float v[3] = { mx - M1_BIAS[0], my - M1_BIAS[1], mz - M1_BIAS[2] };
float out_x = M1_K[0][0] * v[0] + M1_K[0][1] * v[1] + M1_K[0][2] * v[2];
float out_y = M1_K[1][0] * v[0] + M1_K[1][1] * v[1] + M1_K[1][2] * v[2];
float out_z = M1_K[2][0] * v[0] + M1_K[2][1] * v[1] + M1_K[2][2] * v[2];
mx = out_x;
my = out_y;
mz = out_z;
}
static inline void fixMag2(float& mx, float& my, float& mz) {
float v[3] = { mx - M2_BIAS[0], my - M2_BIAS[1], mz - M2_BIAS[2] };
float out_x = M2_K[0][0] * v[0] + M2_K[0][1] * v[1] + M2_K[0][2] * v[2];
float out_y = M2_K[1][0] * v[0] + M2_K[1][1] * v[1] + M2_K[1][2] * v[2];
float out_z = M2_K[2][0] * v[0] + M2_K[2][1] * v[1] + M2_K[2][2] * v[2];
mx = out_x;
my = out_y;
mz = out_z;
}
/* AD7124: configuração e leitura */
static void configureAdc() {
if (!adc.begin(SPI)) {
Serial.println("[ADC] begin falhou");
while (1) delay(1000);
}
adc.setAdcControl(AD7124_OpMode_Continuous, AD7124_FullPower, true, AD7124_Clk_Internal);
for (int i = 0; i < CH_COUNT; i++) {
adc.setup[i].setConfig(AD7124_Ref_Internal, PGA_GAIN, BIPOLAR, AD7124_Burnout_Off, 2.5);
adc.setup[i].setFilter(FILT, FS_BITS);
}
adc.setChannel(0, 0, AD7124_Input_AIN2, AD7124_Input_AIN3, true);
adc.setChannel(1, 1, AD7124_Input_AIN4, AD7124_Input_AIN5, true);
//Serial.println("[ADC] OK (2ch diff, G=128, SINC4, FS=4)");
}
static inline void readAdcPairAvg(double& ch0, double& ch1) {
const int N = 4;
double s0 = 0, s1 = 0;
Ad7124_Readings r[CH_COUNT];
for (int i = 0; i < N; i++) {
if (adc.readRaw(r, CH_COUNT) >= 0) {
s0 += (double)r[0].value;
s1 += (double)r[1].value;
}
}
ch0 = s0 / N;
ch1 = s1 / N;
}
/* IMU init comum */
static bool setupIMU(ICM20948_WE& imu) {
if (!imu.init()) return false;
imu.setAccRange(ICM20948_ACC_RANGE_8G);
imu.setGyrRange(ICM20948_GYRO_RANGE_2000);
imu.setAccDLPF(ICM20948_DLPF_6);
imu.setGyrDLPF(ICM20948_DLPF_6);
imu.setAccSampleRateDivider(5);
imu.setGyrSampleRateDivider(5);
imu.autoOffsets();
// --- MAGNETÓMETRO: OBRIGATÓRIO ---
if (!imu.initMagnetometer()) {
Serial.println("[IMU] Magnetometer init FAIL");
} else {
Serial.println("[IMU] Magnetometer OK");
imu.setMagOpMode(AK09916_CONT_MODE_100HZ);
}
return true;
}
/* ---------------- CSV: header e gravação ---------------- */
static String csvHeader() {
return "t_s,"
// IMU 1
"g1x_raw_dps,g1y_raw_dps,g1z_raw_dps,"
"a1x_raw_g,a1y_raw_g,a1z_raw_g,"
"m1x_raw_uT,m1y_raw_uT,m1z_raw_uT," // <--- NOVO
"g1x_dps,g1y_dps,g1z_dps,"
"a1x_ms2,a1y_ms2,a1z_ms2,"
"q10,q11,q12,q13,"
// IMU 2
"g2x_raw_dps,g2y_raw_dps,g2z_raw_dps,"
"a2x_raw_g,a2y_raw_g,a2z_raw_g,"
"m2x_raw_uT,m2y_raw_uT,m2z_raw_uT," // <--- NOVO
"g2x_dps,g2y_dps,g2z_dps,"
"a2x_ms2,a2y_ms2,a2z_ms2,"
"q20,q21,q22,q23,"
"strain0_raw,strain1_raw\n";
}
static bool startRecording() {
if (record_on && logFile) return true;
String fname = "/log_" + String(++fileIndex) + ".csv";
logFile = SPIFFS.open(fname, FILE_WRITE);
if (!logFile) {
sendLog("STAT:REC open FAIL");
return false;
}
logFile.print(csvHeader());
logFile.flush();
record_on = true;
sendLog("STAT:REC started -> " + fname);
return true;
}
static void stopRecording() {
if (logFile) {
logFile.flush();
logFile.close();
}
record_on = false;
sendLog("STAT:REC stopped");
}
/* ---------------- HTTP / WS ---------------- */
static void handleRoot() {
server.send_P(200, "text/html", INDEX_HTML);
}
void onWs(uint8_t num, WStype_t type, uint8_t* payload, size_t len) {
if (type == WStype_CONNECTED) {
ws.sendTXT(num, "STAT:Conectado.");
ws.sendTXT(num, String("STAT:Stream=") + (stream_on ? "ON" : "OFF") + " Rec=" + (record_on ? "ON" : "OFF"));
return;
}
if (type != WStype_TEXT) return;
String msg((char*)payload, len);
msg.trim();
if (msg == "STREAM_ON") {
stream_on = true;
sendLog("STAT:STREAM ON");
} else if (msg == "STREAM_OFF") {
stream_on = false;
sendLog("STAT:STREAM OFF");
} else if (msg == "REC_START") {
startRecording();
} else if (msg == "REC_STOP") {
stopRecording();
} else if (msg == "NEWFILE") {
++fileIndex;
sendLog("STAT:Next file index " + String(fileIndex + 1));
} else if (msg == "PING") {
ws.sendTXT(num, "PONG");
}
}
/* ---------------- Binary Packet (Browser CSV) ---------------- */
struct SamplePacket {
uint32_t sample_id;
float t_s;
float g1x_raw, g1y_raw, g1z_raw;
float a1x_raw, a1y_raw, a1z_raw;
float m1x_raw, m1y_raw, m1z_raw; // <--- NOVO
float g1x, g1y, g1z;
float a1x, a1y, a1z;
float q10, q11, q12, q13;
float g2x_raw, g2y_raw, g2z_raw;
float a2x_raw, a2y_raw, a2z_raw;
float m2x_raw, m2y_raw, m2z_raw; // <--- NOVO
float g2x, g2y, g2z;
float a2x, a2y, a2z;
float q20, q21, q22, q23;
int32_t strain0;
int32_t strain1;
};
static uint32_t sample_id = 0;
/* ---------------- setup / loop ---------------- */
void setup() {
Serial.begin(115200);
delay(100);
// FS
if (!SPIFFS.begin(true)) Serial.println("[SPIFFS] falhou");
// Wi-Fi + HTTP + WS
WiFi.softAP(AP_SSID, AP_PASS);
server.on("/", handleRoot);
server.begin();
ws.begin();
ws.onEvent(onWs);
// I2C
Wire.begin();
Wire.setClock(400000);
// ADC + IMUs
configureAdc();
if (!setupIMU(imu1)) Serial.println("[IMU1] init falhou");
if (!setupIMU(imu2)) Serial.println("[IMU2] init falhou");
// AHRS
madg1.begin(FS_HZ);
madg2.begin(FS_HZ);
Serial.println("[PaddleLogger] pronto em http://192.168.4.1/");
}
void loop() {
server.handleClient();
ws.loop();
static uint32_t next_us = micros(); // agenda 1ª amostra agora
const uint32_t now = micros();
if ((int32_t)(now - next_us) < 0) return; // ainda não é hora
next_us += DT_US;
// 1) IMU RAW
float a1x_g, a1y_g, a1z_g, g1x_dps, g1y_dps, g1z_dps;
float m1x_uT, m1y_uT, m1z_uT; // <--- NOVO
float a2x_g, a2y_g, a2z_g, g2x_dps, g2y_dps, g2z_dps;
float m2x_uT, m2y_uT, m2z_uT; // <--- NOVO
readIMURaw(imu1, a1x_g, a1y_g, a1z_g, g1x_dps, g1y_dps, g1z_dps, m1x_uT, m1y_uT, m1z_uT);
readIMURaw(imu2, a2x_g, a2y_g, a2z_g, g2x_dps, g2y_dps, g2z_dps, m2x_uT, m2y_uT, m2z_uT);
const float g1x_raw = g1x_dps, g1y_raw = g1y_dps, g1z_raw = g1z_dps;
const float a1x_raw = a1x_g, a1y_raw = a1y_g, a1z_raw = a1z_g;
const float m1x_raw = m1x_uT, m1y_raw = m1y_uT, m1z_raw = m1z_uT; // <--- NOVO
const float g2x_raw = g2x_dps, g2y_raw = g2y_dps, g2z_raw = g2z_dps;
const float a2x_raw = a2x_g, a2y_raw = a2y_g, a2z_raw = a2z_g;
const float m2x_raw = m2x_uT, m2y_raw = m2y_uT, m2z_raw = m2z_uT; // <--- NOVO
// 3) Calibração (bias/ganho)
fixGyro1(g1x_dps, g1y_dps, g1z_dps);
fixAcc1(a1x_g, a1y_g, a1z_g);
fixMag1(m1x_uT, m1y_uT, m1z_uT);
fixGyro2(g2x_dps, g2y_dps, g2z_dps);
fixAcc2(a2x_g, a2y_g, a2z_g);
fixMag2(m2x_uT, m2y_uT, m2z_uT);
// 4) Converter p/ SI (Madgwick)
const float G = 9.80665f;
const float a1x_ms2 = a1x_g * G, a1y_ms2 = a1y_g * G, a1z_ms2 = a1z_g * G;
const float a2x_ms2 = a2x_g * G, a2y_ms2 = a2y_g * G, a2z_ms2 = a2z_g * G;
const float g1x_rads = g1x_dps * DEG_TO_RAD, g1y_rads = g1y_dps * DEG_TO_RAD, g1z_rads = g1z_dps * DEG_TO_RAD;
const float g2x_rads = g2x_dps * DEG_TO_RAD, g2y_rads = g2y_dps * DEG_TO_RAD, g2z_rads = g2z_dps * DEG_TO_RAD;
// 5) AHRS -> quaternions (USAR 9-DoF)
// madg.update(gx, gy, gz, ax, ay, az, mx, my, mz) <--- Madgwick 9-DoF
madg1.update(g1x_rads, g1y_rads, g1z_rads, a1x_ms2, a1y_ms2, a1z_ms2, m1x_uT, m1y_uT, m1z_uT); // <--- ALTERADO
madg2.update(g2x_rads, g2y_rads, g2z_rads, a2x_ms2, a2y_ms2, a2z_ms2, m2x_uT, m2y_uT, m2z_uT); // <--- ALTERADO
float q10, q11, q12, q13, q20, q21, q22, q23;
madg1.getQuaternion(&q10, &q11, &q12, &q13);
madg2.getQuaternion(&q20, &q21, &q22, &q23);
// 6) Extensómetros RAW
double s0, s1;
readAdcPairAvg(s0, s1);
// 7) Tempo relativo (s)
static uint32_t t_start = micros();
const float t_s = (micros() - t_start) / 1e6f;
// 8) CSV line
String line;
line.reserve(300); // Aumentar reserva
line += String(t_s, 6) + ",";
line += String(g1x_raw, 3) + "," + String(g1y_raw, 3) + "," + String(g1z_raw, 3) + ",";
line += String(a1x_raw, 4) + "," + String(a1y_raw, 4) + "," + String(a1z_raw, 4) + ",";
line += String(m1x_raw, 2) + "," + String(m1y_raw, 2) + "," + String(m1z_raw, 2) + ","; // <--- NOVO
line += String(g1x_dps, 3) + "," + String(g1y_dps, 3) + "," + String(g1z_dps, 3) + ",";
line += String(a1x_ms2, 3) + "," + String(a1y_ms2, 3) + "," + String(a1z_ms2, 3) + ",";
line += String(q10, 6) + "," + String(q11, 6) + "," + String(q12, 6) + "," + String(q13, 6) + ",";
line += String(g2x_raw, 3) + "," + String(g2y_raw, 3) + "," + String(g2z_raw, 3) + ",";
line += String(a2x_raw, 4) + "," + String(a2y_raw, 4) + "," + String(a2z_raw, 4) + ",";
line += String(m2x_raw, 2) + "," + String(m2y_raw, 2) + "," + String(m2z_raw, 2) + ","; // <--- NOVO
line += String(g2x_dps, 3) + "," + String(g2y_dps, 3) + "," + String(g2z_dps, 3) + ",";
line += String(a2x_ms2, 3) + "," + String(a2y_ms2, 3) + "," + String(a2z_ms2, 3) + ",";
line += String(q20, 6) + "," + String(q21, 6) + "," + String(q22, 6) + "," + String(q23, 6) + ",";
line += String((int32_t)s0) + "," + String((int32_t)s1) + "\n";
//if (stream_on) ws.broadcastTXT(line);
// if (stream_on) ws.broadcastTXT(line); // ← desativado (agora binário)
/* ---- Envio Binário ---- */
if (stream_on) {
SamplePacket pkt;
pkt.sample_id = sample_id++;
pkt.t_s = t_s;
pkt.g1x_raw = g1x_raw;
pkt.g1y_raw = g1y_raw;
pkt.g1z_raw = g1z_raw;
pkt.a1x_raw = a1x_raw;
pkt.a1y_raw = a1y_raw;
pkt.a1z_raw = a1z_raw;
pkt.m1x_raw = m1x_raw;
pkt.m1y_raw = m1y_raw;
pkt.m1z_raw = m1z_raw; // <--- NOVO
pkt.g1x = g1x_dps;
pkt.g1y = g1y_dps;
pkt.g1z = g1z_dps;
pkt.a1x = a1x_ms2;
pkt.a1y = a1y_ms2;
pkt.a1z = a1z_ms2;
pkt.q10 = q10;
pkt.q11 = q11;
pkt.q12 = q12;
pkt.q13 = q13;
pkt.g2x_raw = g2x_raw;
pkt.g2y_raw = g2y_raw;
pkt.g2z_raw = g2z_raw;
pkt.a2x_raw = a2x_raw;
pkt.a2y_raw = a2y_raw;
pkt.a2z_raw = a2z_raw;
pkt.m2x_raw = m2x_uT;
pkt.m2y_raw = m2y_uT;
pkt.m2z_raw = m2z_uT; // <--- NOVO
pkt.g2x = g2x_dps;
pkt.g2y = g2y_dps;
pkt.g2z = g2z_dps;
pkt.a2x = a2x_ms2;
pkt.a2y = a2y_ms2;
pkt.a2z = a2z_ms2;
pkt.q20 = q20;
pkt.q21 = q21;
pkt.q22 = q22;
pkt.q23 = q23;
pkt.strain0 = (int32_t)s0;
pkt.strain1 = (int32_t)s1;
ws.broadcastBIN((uint8_t*)&pkt, sizeof(pkt));
}
if (record_on && logFile) {
logFile.print(line);
static uint32_t n = 0;
if ((++n % 80) == 0) logFile.flush();
}
}
const char INDEX_HTML[] PROGMEM = R"HTML(
<!doctype html><html><head>
<meta charset="utf-8">
<title>Paddle Logger</title>
<style>
body{font-family:Arial,Helvetica,sans-serif;margin:16px;background:#fafafa;color:#222}
h2{margin:0 0 12px 0;font-size:20px}
.row{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:10px}
button{padding:10px 14px;border:1px solid #bbb;border-radius:8px;background:#fff;cursor:pointer}
button:active{transform:translateY(1px)}
#log{background:#fff;border:1px solid #ddd;border-radius:8px;padding:10px;height:45vh;overflow:auto;white-space:pre;font-size:12px}
</style>
</head><body>
<h2>Paddle Logger</h2>
<div class="row">
<button id="bStart">Start Stream</button>
<button id="bStop">Stop Stream</button>
<button id="bDownload">Download CSV</button>
</div>
<pre id="log">Pronto.</pre>
<script>
let ws;
let rows = [];
let fileIndex = 1;
function log(s){
const el = document.getElementById('log');
el.textContent += "\n" + s;
el.scrollTop = el.scrollHeight;
}
function connect(){
ws = new WebSocket("ws://" + location.hostname + ":81/");
ws.binaryType = "arraybuffer";
ws.onopen = ()=> log("[WS] Conectado");
ws.onclose = ()=>{ log("[WS] Desligado — a reconectar..."); setTimeout(connect, 1000); };
ws.onmessage = ev => {
if (typeof ev.data === "string") {
log(ev.data);
return;
}
const dv = new DataView(ev.data);
let o = 0;
const sample_id = dv.getUint32(o, true); o+=4;
const t_s = dv.getFloat32(o, true); o+=4;
// IMU 1 RAW
const g1x_raw = dv.getFloat32(o,true); o+=4;
const g1y_raw = dv.getFloat32(o,true); o+=4;
const g1z_raw = dv.getFloat32(o,true); o+=4;
const a1x_raw = dv.getFloat32(o,true); o+=4;
const a1y_raw = dv.getFloat32(o,true); o+=4;
const a1z_raw = dv.getFloat32(o,true); o+=4;
const m1x_raw = dv.getFloat32(o,true); o+=4; // <-- NOVO
const m1y_raw = dv.getFloat32(o,true); o+=4; // <-- NOVO
const m1z_raw = dv.getFloat32(o,true); o+=4; // <-- NOVO
// IMU 1 CALIBRATED / AHRS
const g1x = dv.getFloat32(o,true); o+=4;
const g1y = dv.getFloat32(o,true); o+=4;
const g1z = dv.getFloat32(o,true); o+=4;
const a1x = dv.getFloat32(o,true); o+=4;
const a1y = dv.getFloat32(o,true); o+=4;
const a1z = dv.getFloat32(o,true); o+=4;
const q10 = dv.getFloat32(o,true); o+=4;
const q11 = dv.getFloat32(o,true); o+=4;
const q12 = dv.getFloat32(o,true); o+=4;
const q13 = dv.getFloat32(o,true); o+=4;
// IMU 2 RAW
const g2x_raw = dv.getFloat32(o,true); o+=4;
const g2y_raw = dv.getFloat32(o,true); o+=4;
const g2z_raw = dv.getFloat32(o,true); o+=4;
const a2x_raw = dv.getFloat32(o,true); o+=4;
const a2y_raw = dv.getFloat32(o,true); o+=4;
const a2z_raw = dv.getFloat32(o,true); o+=4;
const m2x_raw = dv.getFloat32(o,true); o+=4; // <-- NOVO
const m2y_raw = dv.getFloat32(o,true); o+=4; // <-- NOVO
const m2z_raw = dv.getFloat32(o,true); o+=4; // <-- NOVO
// IMU 2 CALIBRATED / AHRS
const g2x = dv.getFloat32(o,true); o+=4;
const g2y = dv.getFloat32(o,true); o+=4;
const g2z = dv.getFloat32(o,true); o+=4;
const a2x = dv.getFloat32(o,true); o+=4;
const a2y = dv.getFloat32(o,true); o+=4;
const a2z = dv.getFloat32(o,true); o+=4;
const q20 = dv.getFloat32(o,true); o+=4;
const q21 = dv.getFloat32(o,true); o+=4;
const q22 = dv.getFloat32(o,true); o+=4;
const q23 = dv.getFloat32(o,true); o+=4;
// Strain Gauges
const strain0 = dv.getInt32(o,true); o+=4;
const strain1 = dv.getInt32(o,true); o+=4;
rows.push([
sample_id, t_s,
// IMU 1 RAW
g1x_raw,g1y_raw,g1z_raw,
a1x_raw,a1y_raw,a1z_raw,
m1x_raw,m1y_raw,m1z_raw, // <-- NOVO
// IMU 1 CALIBRATED
g1x,g1y,g1z,
a1x,a1y,a1z,
q10,q11,q12,q13,
// IMU 2 RAW
g2x_raw,g2y_raw,g2z_raw,
a2x_raw,a2y_raw,a2z_raw,
m2x_raw,m2y_raw,m2z_raw, // <-- NOVO
// IMU 2 CALIBRATED
g2x,g2y,g2z,
a2x,a2y,a2z,
q20,q21,q22,q23,
// STRAIN
strain0,strain1
]);
};
}
connect();
function send(cmd){ if(ws && ws.readyState===1) ws.send(cmd); }
document.getElementById('bStart').onclick = ()=>{
rows = [];
send("STREAM_ON");
log("[CMD] START");
};
document.getElementById('bStop').onclick = ()=>{
send("STREAM_OFF");
log("[CMD] STOP");
};
document.getElementById('bDownload').onclick = ()=>{
if (rows.length === 0) { alert("Sem dados!"); return; }
const header = [
"sample_id","t_s",
// IMU 1
"g1x_raw","g1y_raw","g1z_raw",
"a1x_raw","a1y_raw","a1z_raw",
"m1x_raw","m1y_raw","m1z_raw", // <-- NOVO
"g1x","g1y","g1z",
"a1x","a1y","a1z",
"q10","q11","q12","q13",
// IMU 2
"g2x_raw","g2y_raw","g2z_raw",
"a2x_raw","a2y_raw","a2z_raw",
"m2x_raw","m2y_raw","m2z_raw", // <-- NOVO
"g2x","g2y","g2z",
"a2x","a2y","a2z",
"q20","q21","q22","q23",
"strain0","strain1"
];
let csv = header.join(",") + "\r\n";
rows.forEach(r => { csv += r.join(",") + "\r\n"; });
const blob = new Blob([csv], {type:"text/csv"});
const a = document.createElement("a");
const fname = "paddle_" + String(fileIndex).padStart(3,"0") + ".csv";
fileIndex++;
a.href = URL.createObjectURL(blob);
a.download = fname;
a.click();
};
</script>
</body></html>
)HTML";

