Compare commits

...

6 Commits

Author SHA1 Message Date
nikrozman 4aa369ca73 Double click support 2026-03-24 23:39:06 +01:00
nikrozman a7082c9022 Disable tap detection by default, simplify tracking 2026-03-24 23:30:06 +01:00
nikrozman 14e9c96f55 Improve sleep and add button waking 2026-03-24 23:29:34 +01:00
nikrozman 0fc38a5e1b Simplify tracking 2026-03-24 23:11:17 +01:00
nikrozman 8ab07adfc6 Add button support 2026-03-24 22:56:21 +01:00
nikrozman 2abc226652 Micro-adjust 3D model to support current PCB 2026-03-24 22:54:49 +01:00
7 changed files with 134 additions and 124 deletions
+10 -4
View File
@@ -41,9 +41,9 @@ BAT_L, BAT_W, BAT_H = 50.0, 12.0, 12.0
BAT_X, BAT_Y = BRD_X + BRD_L + 8.0 + 5.0, (W - BAT_W) / 2.0
BAT_CLIP_Y = 8.0
BTN_X, BTN_CY, BTN_HOLE_R = 28.0, W / 2.0, 10.0
CAP_SHAFT_R, CAP_SHAFT_H = 9.6, WALL
CAP_SHAFT_R, CAP_SHAFT_H = 9.6, WALL + 1.0 # +1mm taller shaft so cap sits flush
CAP_RIM_R, CAP_RIM_H = 12.0, 1.5
NUBBIN_R, NUBBIN_H = 4.2, 1.0
CAP_CAVITY_R, CAP_CAVITY_H = 5.2, 2.5 # Hollow cavity replaces nubbin — clears button dome
BTN_DOME_R, BTN_DOME_SAG = 14.0, 0.6
PCB_BOT_Z = SPLIT_Z + 1.5
@@ -128,7 +128,11 @@ for cx, cy, ix, iy in [(BRD_X, BRD_Y, 1, 1), (BRD_X+BRD_L, BRD_Y, -1, 1),
POST_R = 1.75
POST_TAPER_EXTRA = 0.3 # Extra radius at base
POST_TAPER_H = 6.0 # Height over which the taper blends to nominal radius
for px, py in [(BTN_X+ox, BTN_CY+oy) for ox in [-POST_OFFS_X, POST_OFFS_X] for oy in [-POST_OFFS_Y, POST_OFFS_Y]]:
BACK_POST_SHIFT = POST_R # Shift back posts by half a post diameter
for ox in [-POST_OFFS_X, POST_OFFS_X]:
for oy in [-POST_OFFS_Y, POST_OFFS_Y]:
px = BTN_X + ox + (BACK_POST_SHIFT if ox > 0 else 0)
py = BTN_CY + oy
post = cyl(POST_R, POST_H, px, py, WALL)
# Tapered cone base: wider at bottom, blends to post radius at POST_TAPER_H
taper = Part.makeCone(POST_R + POST_TAPER_EXTRA, POST_R, POST_TAPER_H,
@@ -175,7 +179,9 @@ top_shell = top_shell.cut(box(rec_w, rec_d, RIDGE_H+TOL, L/2-rec_w/2, W-GROOVE_T
# Button & Cap
top_shell = top_shell.cut(cyl(BTN_HOLE_R, H, BTN_X, BTN_CY, SPLIT_Z))
top_shell = top_shell.cut(Part.makeSphere(BTN_DOME_R, Base.Vector(BTN_X, BTN_CY, H - WALL - BTN_DOME_R + BTN_DOME_SAG)))
cap = cyl(CAP_SHAFT_R, CAP_SHAFT_H).fuse(cyl(CAP_RIM_R, CAP_RIM_H, 0, 0, -CAP_RIM_H)).fuse(cyl(NUBBIN_R, NUBBIN_H, 0, 0, -CAP_RIM_H - NUBBIN_H))
cap = cyl(CAP_SHAFT_R, CAP_SHAFT_H).fuse(cyl(CAP_RIM_R, CAP_RIM_H, 0, 0, -CAP_RIM_H))
# Hollow cavity in bottom of shaft — button dome nests inside instead of a protruding nubbin
cap = cap.cut(cyl(CAP_CAVITY_R, CAP_CAVITY_H, 0, 0, -CAP_RIM_H))
cap_placed = cap.copy(); cap_placed.translate(Base.Vector(BTN_X, BTN_CY, H - CAP_SHAFT_H))
# ─── REGISTER ────────────────────────────────────────────────────────────────
+51 -7
View File
@@ -4,8 +4,18 @@
#include <bluefruit.h>
extern BLEHidAdafruit blehid;
extern Config cfg;
static uint8_t physBtnMask = 0; // bitmask of currently-pressed physical buttons
static uint8_t physBtnMask = 0;
static uint8_t rawMaskPrev = 0;
static unsigned long debounceMs = 0;
static const unsigned long DEBOUNCE_MS = 20;
// Double-press detection for left button
static const unsigned long DOUBLE_PRESS_MS = 400; // max gap between two releases
static const unsigned long KEY_HOLD_MS = 60; // how long to hold the key down
static unsigned long lastLeftReleaseMs = 0;
static unsigned long keyDownUntil = 0;
// Setup
void setupPhysicalButtons() {
@@ -36,16 +46,50 @@ void setupPhysicalButtons() {
void processPhysicalButtons() {
if (!Bluefruit.connected()) return;
uint8_t newMask = 0;
if (BTN_LEFT_PIN != BTN_PIN_NONE && digitalRead(BTN_LEFT_PIN) == LOW) newMask |= MOUSE_BUTTON_LEFT;
if (BTN_RIGHT_PIN != BTN_PIN_NONE && digitalRead(BTN_RIGHT_PIN) == LOW) newMask |= MOUSE_BUTTON_RIGHT;
if (BTN_MIDDLE_PIN != BTN_PIN_NONE && digitalRead(BTN_MIDDLE_PIN) == LOW) newMask |= MOUSE_BUTTON_MIDDLE;
unsigned long now = millis();
if (newMask != physBtnMask) {
// Release held key combo after KEY_HOLD_MS
if (keyDownUntil && now >= keyDownUntil) {
uint8_t noKeys[6] = {};
blehid.keyboardReport(0, noKeys);
keyDownUntil = 0;
Serial.println("[BTN] key release");
}
uint8_t rawMask = 0;
if (BTN_LEFT_PIN != BTN_PIN_NONE && digitalRead(BTN_LEFT_PIN) == LOW) rawMask |= MOUSE_BUTTON_LEFT;
if (BTN_RIGHT_PIN != BTN_PIN_NONE && digitalRead(BTN_RIGHT_PIN) == LOW) rawMask |= MOUSE_BUTTON_RIGHT;
if (BTN_MIDDLE_PIN != BTN_PIN_NONE && digitalRead(BTN_MIDDLE_PIN) == LOW) rawMask |= MOUSE_BUTTON_MIDDLE;
if (rawMask != rawMaskPrev) { rawMaskPrev = rawMask; debounceMs = now; }
if (rawMask != physBtnMask && (now - debounceMs >= DEBOUNCE_MS)) {
uint8_t newMask = rawMask;
uint8_t pressed = newMask & ~physBtnMask;
uint8_t released = physBtnMask & ~newMask;
physBtnMask = newMask;
if (physBtnMask) blehid.mouseButtonPress(physBtnMask);
else blehid.mouseButtonRelease();
Serial.print("[BTN] mask=0x"); Serial.println(physBtnMask, HEX);
if (pressed & MOUSE_BUTTON_LEFT) Serial.println("[BTN] L press");
if (pressed & MOUSE_BUTTON_RIGHT) Serial.println("[BTN] R press");
if (pressed & MOUSE_BUTTON_MIDDLE) Serial.println("[BTN] M press");
if (released & MOUSE_BUTTON_LEFT) {
unsigned long gap = lastLeftReleaseMs ? (now - lastLeftReleaseMs) : 0;
Serial.print("[BTN] L release - gap="); Serial.print(gap);
Serial.print("ms (max="); Serial.print(DOUBLE_PRESS_MS); Serial.println("ms)");
// Double-press detection: two short presses → fire key combo
if (lastLeftReleaseMs && (gap <= DOUBLE_PRESS_MS)) {
uint8_t keys[6] = {cfg.tapKey, 0, 0, 0, 0, 0};
blehid.keyboardReport(cfg.tapMod, keys);
keyDownUntil = now + KEY_HOLD_MS;
lastLeftReleaseMs = 0;
Serial.print("[BTN] Double-press → key 0x"); Serial.print(cfg.tapKey, HEX);
Serial.print(" mod 0x"); Serial.println(cfg.tapMod, HEX);
} else {
lastLeftReleaseMs = now;
}
}
if (released & MOUSE_BUTTON_RIGHT) Serial.println("[BTN] R release");
if (released & MOUSE_BUTTON_MIDDLE) Serial.println("[BTN] M release");
}
}
+4 -7
View File
@@ -5,7 +5,7 @@
#define FEATURE_CONFIG_SERVICE
#define FEATURE_TELEMETRY
#define FEATURE_IMU_STREAM
#define FEATURE_TAP_DETECTION
// #define FEATURE_TAP_DETECTION
#define FEATURE_TEMP_COMPENSATION
#define FEATURE_AUTO_RECAL
#define FEATURE_BATTERY_MONITOR
@@ -60,7 +60,7 @@
// Physical button pin assignments (hardcoded - set to 0xFF to disable a button)
// Valid pin numbers: 0-10 (Arduino D0-D10 on XIAO nRF52840 Sense)
#define BTN_PIN_NONE 0xFF
#define BTN_LEFT_PIN BTN_PIN_NONE // e.g. 0 for D0
#define BTN_LEFT_PIN 1 // D1, active-low to GND
#define BTN_RIGHT_PIN BTN_PIN_NONE // e.g. 1 for D1
#define BTN_MIDDLE_PIN BTN_PIN_NONE // e.g. 2 for D2
@@ -158,8 +158,7 @@ struct __attribute__((packed)) ImuPacket {
static_assert(sizeof(ImuPacket) == 14, "ImuPacket must be 14 bytes");
#endif
// Tuning constants
extern const float ALPHA;
// Tuning constants
extern const int LOOP_RATE_MS;
extern const int BIAS_SAMPLES;
extern const int IDLE_FRAMES;
@@ -184,10 +183,8 @@ extern const float BATT_CRITICAL;
extern const unsigned long AUTO_RECAL_MS;
#endif
// Global state
extern float angleX, angleY;
// Global state
extern float accumX, accumY;
extern float gravX, gravY, gravZ;
extern float biasGX, biasGY, biasGZ;
extern float calTempC;
extern float cachedTempC;
+1 -3
View File
@@ -34,9 +34,7 @@ void calibrateGyroBias() {
biasGY = (float)(sy/BIAS_SAMPLES);
biasGZ = (float)(sz/BIAS_SAMPLES);
calTempC = readIMUTemp();
angleX = angleY = accumX = accumY = 0.0f;
// Seed gravity estimate from current accel so projection is correct immediately
gravX = imu.readFloatAccelX(); gravY = imu.readFloatAccelY(); gravZ = imu.readFloatAccelZ();
accumX = accumY = 0.0f;
#ifdef FEATURE_TELEMETRY
statRecalCount++;
+33 -81
View File
@@ -17,7 +17,6 @@
#include "imu.h"
#include "ble_config.h"
#include "battery.h"
#include "tap.h"
#include "buttons.h"
#include <bluefruit.h>
#include <Adafruit_LittleFS.h>
@@ -46,7 +45,7 @@ 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_KEY, /*tapKey=*/0x04, /*tapMod=*/0x03, // Ctrl+Shift+A
/*jerkThreshold=*/2000.0f, /*tapFreezeEnabled=*/1, /*featureFlags=*/FLAG_ALL_DEFAULT
};
@@ -55,11 +54,9 @@ const Config CFG_DEFAULTS = {
TelemetryPacket telem = {};
#endif
// Tuning constants
const float ALPHA = 0.96f;
// Tuning constants
const int LOOP_RATE_MS = 10;
const float SMOOTH_LOW_RPS = 0.15f; // below this → heavy EMA smoothing (~8°/s)
const float SMOOTH_HIGH_RPS = 0.50f; // above this → no smoothing (~29°/s)
const float SMOOTH_ALPHA = 0.65f; // single-pole low-pass for cursor smoothing
const int BIAS_SAMPLES = 200;
const int IDLE_FRAMES = 150;
const unsigned long BATT_REPORT_MS = 20000;
@@ -83,11 +80,8 @@ const float BATT_CRITICAL = 3.10f;
const unsigned long AUTO_RECAL_MS = 5UL * 60UL * 1000UL;
#endif
// Global state definitions
float angleX = 0, angleY = 0;
// Global state definitions
float accumX = 0, accumY = 0;
// Low-pass filtered gravity estimate in device frame (for roll-independent axis projection)
float gravX = 0, gravY = 0, gravZ = 1.0f;
float biasGX = 0, biasGY = 0, biasGZ = 0;
float calTempC = 25.0f;
float cachedTempC = 25.0f;
@@ -121,10 +115,6 @@ bool pendingReset = false;
bool pendingOTA = false;
#endif
// Jerk-based shock detection - freeze cursor during tap impacts, doesn't work well yet!
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;
@@ -225,8 +215,6 @@ 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();
sleepManagerInit();
@@ -297,17 +285,19 @@ void loop() {
}
#endif
// Serial commands: 'c' = calibrate, 'r' = factory reset
// Serial commands: 'c' = calibrate, 'r' = factory reset, 'd' = axis diagnostic
static unsigned long diagUntil = 0;
while (Serial.available()) {
char cmd = Serial.read();
if (cmd == 'c') { Serial.println("[SERIAL] Calibrate"); pendingCal = true; }
if (cmd == 'r') { Serial.println("[SERIAL] Reset"); pendingReset = true; }
if (cmd == 'd') { Serial.println("[DIAG] Printing raw gyro for 10s — pan, nod, roll one at a time"); diagUntil = now + 10000; }
#ifdef FEATURE_OTA
if (cmd == 'o') { Serial.println("[SERIAL] OTA DFU"); pendingOTA = true; }
#endif
}
if (pendingCal) { pendingCal = false; calibrateGyroBias(); prevAx = imu.readFloatAccelX(); prevAy = imu.readFloatAccelY(); prevAz = imu.readFloatAccelZ(); }
if (pendingCal) { pendingCal = false; calibrateGyroBias(); }
if (pendingReset) { pendingReset = false; factoryReset(); }
#ifdef FEATURE_OTA
if (pendingOTA) {
@@ -371,62 +361,32 @@ void loop() {
if (cfg.featureFlags & FLAG_TEMP_COMP_ENABLED)
correction = TEMP_COMP_COEFF_DPS_C * (cachedTempC - calTempC);
#endif
float gx = (imu.readFloatGyroX() - biasGX - correction) * (PI/180.0f);
float gy = (imu.readFloatGyroY() - biasGY - correction) * (PI/180.0f);
float gz = (imu.readFloatGyroZ() - biasGZ - correction) * (PI/180.0f);
float gx = (imu.readFloatGyroX() - biasGX) * (PI/180.0f); // roll (unused for cursor)
float gy = (imu.readFloatGyroY() - biasGY - correction) * (PI/180.0f); // pitch → cursor Y
float gz = (imu.readFloatGyroZ() - biasGZ - correction) * (PI/180.0f); // yaw → cursor X
float ax = imu.readFloatAccelX();
float ay = imu.readFloatAccelY();
float az = imu.readFloatAccelZ();
// Axis diagnostic — send 'd' over serial to enable
if (diagUntil && now < diagUntil) {
static unsigned long lastDiagPrint = 0;
if (now - lastDiagPrint >= 100) { lastDiagPrint = now;
Serial.print("[DIAG] gx="); Serial.print(gx,3);
Serial.print(" gy="); Serial.print(gy,3);
Serial.print(" gz="); Serial.println(gz,3);
}
} else if (diagUntil) { diagUntil = 0; Serial.println("[DIAG] Done"); }
// Jerk-based shock detection - freeze cursor during tap impacts, doesn't work well yet
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 = cfg.tapFreezeEnabled && ((jerkSq > cfg.jerkThreshold) || (now < shockFreezeUntil));
if (cfg.tapFreezeEnabled && jerkSq > cfg.jerkThreshold) shockFreezeUntil = now + SHOCK_FREEZE_MS;
// Direct axis mapping (empirically verified via diagnostic)
float yawRate = gz; // gyroZ = pan left/right → cursor X
float pitchRate = gy; // gyroY = nod up/down → cursor Y
// Complementary filter
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
const float GRAV_LP = 0.05f;
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;
float gnx = gravX/gN, gny = gravY/gN, gnz = gravZ/gN;
float ry = -gnz, rz = gny;
float rN = sqrtf(ry*ry + rz*rz);
if (rN < 0.01f) { ry = -1.0f; rz = 0.0f; rN = 1.0f; }
ry /= rN; rz /= rN;
// Yaw (cursor X) = angular velocity component around gravity (vertical)
// Pitch (cursor Y) = angular velocity component around screen-right
float yawRate = gx*gnx + gy*gny + gz*gnz;
float pitchRate = -(gy*ry + gz*rz);
// Projected rates amplify residual gyro bias (especially GY drift on pitch axis).
float fYaw = (fabsf(yawRate) > cfg.deadZone) ? yawRate : 0.0f;
float fPitch = (fabsf(pitchRate) > cfg.deadZone * 3.0f) ? pitchRate : 0.0f;
// Dead zone (equal for both axes)
float fYaw = (fabsf(yawRate) > cfg.deadZone) ? yawRate : 0.0f;
float fPitch = (fabsf(pitchRate) > cfg.deadZone) ? pitchRate : 0.0f;
#ifdef DEBUG
{ static unsigned long lastDiag = 0;
if (now - lastDiag >= 500) { lastDiag = now;
Serial.print("[PROJ] grav="); Serial.print(gnx,2); Serial.print(","); Serial.print(gny,2); Serial.print(","); Serial.print(gnz,2);
Serial.print(" R="); Serial.print(ry,2); Serial.print(","); Serial.print(rz,2);
Serial.print(" gyro="); Serial.print(gx,2); Serial.print(","); Serial.print(gy,2); Serial.print(","); Serial.print(gz,2);
Serial.print("[IMU] gyro="); Serial.print(gx,2); Serial.print(","); Serial.print(gy,2); Serial.print(","); Serial.print(gz,2);
Serial.print(" yaw="); Serial.print(yawRate,3); Serial.print(" pitch="); Serial.println(pitchRate,3);
}
}
@@ -440,7 +400,7 @@ void loop() {
#ifdef FEATURE_AUTO_RECAL
if ((cfg.featureFlags & FLAG_AUTO_RECAL_ENABLED) && idle && idleStartMs != 0 && (now - idleStartMs >= AUTO_RECAL_MS)) {
Serial.println("[AUTO-CAL] Long idle - recalibrating...");
idleStartMs = 0; calibrateGyroBias(); prevAx = imu.readFloatAccelX(); prevAy = imu.readFloatAccelY(); prevAz = imu.readFloatAccelZ(); return;
idleStartMs = 0; calibrateGyroBias(); return;
}
#endif
@@ -449,12 +409,7 @@ void loop() {
static float smoothX = 0.0f, smoothY = 0.0f;
if (shocked) {
// Shock freeze - discard accumulated sub-pixel motion and suppress output
smoothX = smoothY = 0.0f;
accumX = accumY = 0.0f;
flags |= 0x08; // bit3 = shock freeze active
} else if (idle) {
if (idle) {
smoothX = smoothY = 0.0f;
accumX = accumY = 0.0f;
flags |= 0x01;
@@ -463,13 +418,9 @@ void loop() {
float rawY = applyAcceleration(applyCurve(-fPitch * cfg.sensitivity * dt));
if (cfg.axisFlip & 0x01) rawX = -rawX;
if (cfg.axisFlip & 0x02) rawY = -rawY;
// Tiered velocity smoothing: heavy EMA when nearly still, none when fast.
// Thresholds are in rad/s (angular rate), independent of sensitivity setting.
float speed = sqrtf(fYaw*fYaw + fPitch*fPitch);
float alpha = (speed < SMOOTH_LOW_RPS) ? 0.25f :
(speed < SMOOTH_HIGH_RPS) ? 0.65f : 1.00f;
smoothX = smoothX * (1.0f - alpha) + rawX * alpha;
smoothY = smoothY * (1.0f - alpha) + rawY * alpha;
// Single-pole low-pass smoothing
smoothX = smoothX * (1.0f - SMOOTH_ALPHA) + rawX * SMOOTH_ALPHA;
smoothY = smoothY * (1.0f - SMOOTH_ALPHA) + rawY * SMOOTH_ALPHA;
accumX += smoothX; accumY += smoothY;
moveX = (int8_t)constrain((int)accumX, -127, 127);
moveY = (int8_t)constrain((int)accumY, -127, 127);
@@ -485,6 +436,7 @@ void loop() {
if (now < streamBackoffUntil) {
// Backing off - host TX buffer congested, skip to avoid 100ms block
} else {
float ax = imu.readFloatAccelX(), ay = imu.readFloatAccelY(), az = imu.readFloatAccelZ();
ImuPacket pkt;
pkt.gyroX_mDPS = (int16_t)constrain(gx*(180.f/PI)*1000.f, -32000, 32000);
pkt.gyroZ_mDPS = (int16_t)constrain(gz*(180.f/PI)*1000.f, -32000, 32000);
+33 -20
View File
@@ -61,11 +61,16 @@ static void lsmWrite(uint8_t reg, uint8_t val) {
Wire1.endTransmission();
}
// ISR
// ISR
static void imuInt1ISR() {
imuWakeFlag = true;
}
static volatile bool btnWakeFlag = false;
static void btnWakeISR() {
btnWakeFlag = true;
}
// Arm wakeup interrupt
static void armWakeupInterrupt() {
lsmWrite(SLP_WAKE_UP_DUR, (uint8_t)((SLEEP_WAKEUP_DUR & 0x03) << 4));
@@ -127,6 +132,15 @@ static void enterImuLP() {
armWakeupInterrupt();
// Arm button wake interrupt
#if BTN_LEFT_PIN != BTN_PIN_NONE
btnWakeFlag = false;
attachInterrupt(digitalPinToInterrupt(BTN_LEFT_PIN), btnWakeISR, FALLING);
#endif
// Turn off all LEDs for sleep
digitalWrite(LED_RED, HIGH); digitalWrite(LED_GREEN, HIGH); digitalWrite(LED_BLUE, HIGH);
lpEnteredMs = millis();
sleepStage = SLEEP_IMU_LP;
Serial.print("[SLEEP] IMU LP entered - idle for ");
@@ -153,9 +167,9 @@ static void enterDeepSleep() {
Serial.println("[SLEEP] Deep sleep - WFE on INT1");
Serial.flush();
digitalWrite(LED_RED, LOW); delay(80); digitalWrite(LED_RED, HIGH);
digitalWrite(LED_RED, HIGH); digitalWrite(LED_GREEN, HIGH); digitalWrite(LED_BLUE, HIGH);
while (!imuWakeFlag) {
while (!imuWakeFlag && !btnWakeFlag) {
(void)lsmRead(SLP_WAKE_UP_SRC);
sd_app_evt_wait();
}
@@ -172,6 +186,11 @@ void sleepManagerWakeIMU() {
disarmWakeupInterrupt();
// Detach button wake interrupt — normal polling takes over
#if BTN_LEFT_PIN != BTN_PIN_NONE
detachInterrupt(digitalPinToInterrupt(BTN_LEFT_PIN));
#endif
// Only recalibrate if gyro was off long enough for thermal drift to accumulate,
// or if waking from full deep sleep. Short LP naps reuse the existing bias.
unsigned long lpDuration = millis() - lpEnteredMs;
@@ -186,24 +205,9 @@ void sleepManagerWakeIMU() {
lpEnteredMs = 0;
// Reset motion filter state to prevent a cursor jump on the first frame.
// After sleep: angleX/Y are stale, gravX/Y/Z drifted, accumX/Y is dirty,
// and lastTime is old so dt would be huge on the first loop iteration.
// Zeroing these here means the first frame integrates 0 motion cleanly.
extern float angleX, angleY;
extern float accumX, accumY;
extern float gravX, gravY, gravZ;
extern float prevAx, prevAy, prevAz;
extern unsigned long lastTime;
angleX = angleY = 0.0f;
accumX = accumY = 0.0f;
// Reseed gravity from current accel so projection is correct immediately.
// Can't call imu.readFloat* here (gyro not fully settled), but accel is
// already running - read it directly via Wire1.
// Simpler: just reset to neutral [0,0,1] and let the LP filter converge
// over the first ~20 frames (200 ms) of real use.
gravX = 0.0f; gravY = 0.0f; gravZ = 1.0f;
prevAx = 0.0f; prevAy = 0.0f; prevAz = 0.0f;
// Set lastTime to now so the first dt = 0 rather than (now - sleepEntryTime)
lastTime = millis();
sleepStage = SLEEP_AWAKE;
@@ -238,14 +242,23 @@ void sleepManagerInit() {
// Returns true → caller must skip IMU reads this iteration.
bool sleepManagerUpdate(unsigned long nowMs, bool idle, bool bleConnected) {
// ISR wakeup
// ISR wakeup (IMU motion or button press)
bool woke = false;
if (imuWakeFlag) {
imuWakeFlag = false;
Serial.print("[SLEEP] INT1 fired - stage="); Serial.println((int)sleepStage);
woke = true;
}
if (btnWakeFlag) {
btnWakeFlag = false;
Serial.print("[SLEEP] Button fired - stage="); Serial.println((int)sleepStage);
woke = true;
}
if (woke) {
if (sleepStage == SLEEP_DEEP || sleepStage == SLEEP_IMU_LP) {
sleepManagerWakeIMU();
} else {
(void)lsmRead(SLP_WAKE_UP_SRC); // normal-mode edge, clear latch only
(void)lsmRead(SLP_WAKE_UP_SRC);
}
}
+2 -2
View File
@@ -11,12 +11,12 @@
// LSM6DS3 wakeup threshold: 1 LSB = 7.8 mg at ±2 g FS (±2g range).
#ifndef SLEEP_WAKEUP_THS
#define SLEEP_WAKEUP_THS 16 // 063
#define SLEEP_WAKEUP_THS 6 // 063
#endif
// Number of consecutive 26 Hz samples that must exceed the threshold.
#ifndef SLEEP_WAKEUP_DUR
#define SLEEP_WAKEUP_DUR 2 // 03
#define SLEEP_WAKEUP_DUR 1 // 03
#endif
// GPIO pin connected to LSM6DS3 INT1.