Compare commits
6 Commits
5ab13a525a
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 4aa369ca73 | |||
| a7082c9022 | |||
| 14e9c96f55 | |||
| 0fc38a5e1b | |||
| 8ab07adfc6 | |||
| 2abc226652 |
+10
-4
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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 // 0–63
|
||||
#define SLEEP_WAKEUP_THS 6 // 0–63
|
||||
#endif
|
||||
|
||||
// Number of consecutive 26 Hz samples that must exceed the threshold.
|
||||
#ifndef SLEEP_WAKEUP_DUR
|
||||
#define SLEEP_WAKEUP_DUR 2 // 0–3
|
||||
#define SLEEP_WAKEUP_DUR 1 // 0–3
|
||||
#endif
|
||||
|
||||
// GPIO pin connected to LSM6DS3 INT1.
|
||||
|
||||
Reference in New Issue
Block a user