Attempt to add jerk correction

This commit is contained in:
2026-03-02 23:53:10 +01:00
parent 4768754bef
commit a666304013
6 changed files with 215 additions and 14 deletions

View File

@@ -59,7 +59,8 @@ File cfgFile(InternalFS);
Config cfg;
const Config CFG_DEFAULTS = {
CONFIG_MAGIC, 600.0f, 0.060f, 0.08f, CURVE_LINEAR, 0x00, CHARGE_SLOW,
/*tapThreshold=*/12, /*tapAction=*/TAP_ACTION_LEFT, /*tapKey=*/0, /*tapMod=*/0
/*tapThreshold=*/12, /*tapAction=*/TAP_ACTION_LEFT, /*tapKey=*/0, /*tapMod=*/0,
/*jerkThreshold=*/2000.0f
};
// ─── Telemetry definition ─────────────────────────────────────────────────────
@@ -128,6 +129,14 @@ uint32_t loopStalls = 0; // loop iterations where dt > 20ms (behind sch
bool pendingCal = false;
bool pendingReset = false;
// ── Jerk-based shock detection — freeze cursor during tap impacts ────────────
// Jerk = da/dt (rate of change of acceleration). Normal mouse rotation produces
// smooth accel changes (low jerk); a tap is a sharp impulse (very high jerk).
// This cleanly separates taps from any intentional motion regardless of speed.
unsigned long shockFreezeUntil = 0;
float prevAx = 0, prevAy = 0, prevAz = 0; // previous frame's accel for Δa
const unsigned long SHOCK_FREEZE_MS = 80; // hold freeze after last spike
ChargeStatus lastChargeStatus = CHGSTAT_DISCHARGING;
int idleFrames = 0;
@@ -223,6 +232,8 @@ void setup() {
#endif
calibrateGyroBias();
// Seed previous-accel for jerk detection so first frame doesn't spike
prevAx = imu.readFloatAccelX(); prevAy = imu.readFloatAccelY(); prevAz = imu.readFloatAccelZ();
bledis.setManufacturer("Seeed Studio");
bledis.setModel("XIAO nRF52840 Sense");
@@ -294,7 +305,7 @@ void loop() {
if (cmd == 'r') { Serial.println("[SERIAL] Reset"); pendingReset = true; }
}
if (pendingCal) { pendingCal = false; calibrateGyroBias(); }
if (pendingCal) { pendingCal = false; calibrateGyroBias(); prevAx = imu.readFloatAccelX(); prevAy = imu.readFloatAccelY(); prevAz = imu.readFloatAccelZ(); }
if (pendingReset) { pendingReset = false; factoryReset(); }
// Heartbeat LED
@@ -345,19 +356,37 @@ void loop() {
float ay = imu.readFloatAccelY();
float az = imu.readFloatAccelZ();
// ── Jerk-based shock detection — freeze cursor during tap impacts ────────
// Jerk = da/dt. Normal rotation = smooth accel changes (low jerk);
// a tap is a sharp impulse (very high jerk).
float jx = (ax - prevAx) / dt, jy = (ay - prevAy) / dt, jz = (az - prevAz) / dt;
float jerkSq = jx*jx + jy*jy + jz*jz;
prevAx = ax; prevAy = ay; prevAz = az;
bool shocked = (jerkSq > cfg.jerkThreshold) || (now < shockFreezeUntil);
if (jerkSq > cfg.jerkThreshold) shockFreezeUntil = now + SHOCK_FREEZE_MS;
// Complementary filter — gx=pitch axis, gz=yaw axis on this board layout
angleX = ALPHA*(angleX + gx*dt) + (1.0f - ALPHA)*atan2f(ax, sqrtf(ay*ay + az*az));
angleY = ALPHA*(angleY + gz*dt) + (1.0f - ALPHA)*atan2f(ay, sqrtf(ax*ax + az*az));
// During shock: gyro-only integration to avoid accel spike corrupting angles
if (shocked) {
angleX += gx * dt;
angleY += gz * dt;
} else {
angleX = ALPHA*(angleX + gx*dt) + (1.0f - ALPHA)*atan2f(ax, sqrtf(ay*ay + az*az));
angleY = ALPHA*(angleY + gz*dt) + (1.0f - ALPHA)*atan2f(ay, sqrtf(ax*ax + az*az));
}
// ── Gravity-based axis decomposition ──────────────────────────────────────
// Low-pass filter accel to get a stable gravity estimate in device frame.
// This lets us project angular velocity onto world-aligned axes regardless
// of how the device is rolled. Device forward (pointing) axis = X.
// Confirmed by diagnostics: GX=roll, GY=nod, GZ=pan in user's hold.
// Skip update during shock to protect the gravity estimate from tap spikes.
const float GRAV_LP = 0.05f;
gravX += GRAV_LP * (ax - gravX);
gravY += GRAV_LP * (ay - gravY);
gravZ += GRAV_LP * (az - gravZ);
if (!shocked) {
gravX += GRAV_LP * (ax - gravX);
gravY += GRAV_LP * (ay - gravY);
gravZ += GRAV_LP * (az - gravZ);
}
float gN = sqrtf(gravX*gravX + gravY*gravY + gravZ*gravZ);
if (gN < 0.3f) gN = 1.0f;
@@ -400,14 +429,18 @@ void loop() {
#ifdef FEATURE_AUTO_RECAL
if (idle && idleStartMs != 0 && (now - idleStartMs >= AUTO_RECAL_MS)) {
Serial.println("[AUTO-CAL] Long idle — recalibrating...");
idleStartMs = 0; calibrateGyroBias(); return;
idleStartMs = 0; calibrateGyroBias(); prevAx = imu.readFloatAccelX(); prevAy = imu.readFloatAccelY(); prevAz = imu.readFloatAccelZ(); return;
}
#endif
int8_t moveX = 0, moveY = 0;
uint8_t flags = 0;
if (idle) {
if (shocked) {
// Shock freeze — discard accumulated sub-pixel motion and suppress output
accumX = accumY = 0.0f;
flags |= 0x08; // bit3 = shock freeze active
} else if (idle) {
accumX = accumY = 0.0f;
flags |= 0x01;
} else {