Attempt to add jerk correction
This commit is contained in:
@@ -75,6 +75,7 @@ void pushConfigBlob() {
|
||||
b.tapKey = cfg.tapKey;
|
||||
b.tapMod = cfg.tapMod;
|
||||
b._pad = 0;
|
||||
b.jerkThreshold = cfg.jerkThreshold;
|
||||
cfgBlob.write((uint8_t*)&b, sizeof(b));
|
||||
}
|
||||
#endif
|
||||
@@ -117,6 +118,7 @@ void onConfigBlobWrite(uint16_t h, BLECharacteristic* c, uint8_t* d, uint16_t l)
|
||||
cfg.tapKey = b->tapKey;
|
||||
cfg.tapMod = b->tapMod;
|
||||
#endif
|
||||
if (b->jerkThreshold >= 100.0f && b->jerkThreshold <= 50000.0f) cfg.jerkThreshold = b->jerkThreshold;
|
||||
saveConfig();
|
||||
Serial.print("[CFG] Written — sens="); Serial.print(cfg.sensitivity,0);
|
||||
Serial.print(" dz="); Serial.print(cfg.deadZone,3);
|
||||
|
||||
+4
-2
@@ -53,7 +53,7 @@
|
||||
|
||||
// ─── Persistence ──────────────────────────────────────────────────────────────
|
||||
#define CONFIG_FILENAME "/imu_mouse_cfg.bin"
|
||||
#define CONFIG_MAGIC 0xDEAD1239UL
|
||||
#define CONFIG_MAGIC 0xDEAD123AUL
|
||||
|
||||
// ─── Enums ────────────────────────────────────────────────────────────────────
|
||||
enum CurveType : uint8_t { CURVE_LINEAR=0, CURVE_SQUARE=1, CURVE_SQRT=2 };
|
||||
@@ -83,6 +83,7 @@ struct Config {
|
||||
TapAction tapAction; // what a double-tap does
|
||||
uint8_t tapKey; // HID keycode (used when tapAction == TAP_ACTION_KEY)
|
||||
uint8_t tapMod; // HID modifier byte (used when tapAction == TAP_ACTION_KEY)
|
||||
float jerkThreshold; // jerk² threshold for tap-freeze detection
|
||||
};
|
||||
extern Config cfg;
|
||||
extern const Config CFG_DEFAULTS;
|
||||
@@ -100,8 +101,9 @@ struct __attribute__((packed)) ConfigBlob {
|
||||
uint8_t tapKey; // [17] HID keycode
|
||||
uint8_t tapMod; // [18] HID modifier
|
||||
uint8_t _pad; // [19]
|
||||
float jerkThreshold; // [20] jerk² tap-freeze threshold
|
||||
};
|
||||
static_assert(sizeof(ConfigBlob) == 20, "ConfigBlob must be 20 bytes");
|
||||
static_assert(sizeof(ConfigBlob) == 24, "ConfigBlob must be 24 bytes");
|
||||
|
||||
// ─── TelemetryPacket (24 bytes) ───────────────────────────────────────────────
|
||||
#ifdef FEATURE_TELEMETRY
|
||||
|
||||
+42
-9
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user