From dcc50150b89d8377b14821f040099bda326319af Mon Sep 17 00:00:00 2001 From: Nik Rozman Date: Tue, 3 Mar 2026 08:49:22 +0100 Subject: [PATCH] Make tap freezing configurable, add toggles to other functions, minor UI changes --- source/ble_config.cpp | 11 +++++++---- source/config.h | 43 +++++++++++++++++++++++++++---------------- source/main.cpp | 27 ++++++++++++--------------- web/app.js | 32 ++++++++++++++++++++++++++++---- web/index.html | 19 +++++++++++++++++++ web/style.css | 6 ++++-- 6 files changed, 97 insertions(+), 41 deletions(-) diff --git a/source/ble_config.cpp b/source/ble_config.cpp index 50121f4..758abcf 100644 --- a/source/ble_config.cpp +++ b/source/ble_config.cpp @@ -72,10 +72,11 @@ void pushConfigBlob() { b.chargeMode = (uint8_t)cfg.chargeMode; b.tapThreshold = cfg.tapThreshold; b.tapAction = (uint8_t)cfg.tapAction; - b.tapKey = cfg.tapKey; - b.tapMod = cfg.tapMod; - b._pad = 0; - b.jerkThreshold = cfg.jerkThreshold; + b.tapKey = cfg.tapKey; + b.tapMod = cfg.tapMod; + b.tapFreezeEnabled = cfg.tapFreezeEnabled; + b.jerkThreshold = cfg.jerkThreshold; + b.featureFlags = cfg.featureFlags; cfgBlob.write((uint8_t*)&b, sizeof(b)); } #endif @@ -118,7 +119,9 @@ void onConfigBlobWrite(uint16_t h, BLECharacteristic* c, uint8_t* d, uint16_t l) cfg.tapKey = b->tapKey; cfg.tapMod = b->tapMod; #endif + cfg.tapFreezeEnabled = b->tapFreezeEnabled ? 1 : 0; if (b->jerkThreshold >= 100.0f && b->jerkThreshold <= 50000.0f) cfg.jerkThreshold = b->jerkThreshold; + cfg.featureFlags = b->featureFlags & (FLAG_TAP_ENABLED | FLAG_TEMP_COMP_ENABLED | FLAG_AUTO_RECAL_ENABLED); saveConfig(); Serial.print("[CFG] Written — sens="); Serial.print(cfg.sensitivity,0); Serial.print(" dz="); Serial.print(cfg.deadZone,3); diff --git a/source/config.h b/source/config.h index b9e1690..3afd5ec 100644 --- a/source/config.h +++ b/source/config.h @@ -53,7 +53,15 @@ // ─── Persistence ────────────────────────────────────────────────────────────── #define CONFIG_FILENAME "/imu_mouse_cfg.bin" -#define CONFIG_MAGIC 0xDEAD123AUL +#define CONFIG_MAGIC 0xDEAD123CUL + +// ─── Runtime feature-override flags (cfg.featureFlags bitmask) ─────────────── +// These mirror the compile-time FEATURE_* defines but can be toggled at runtime +// via the web UI and persisted in flash. Bits not listed here are reserved = 0. +#define FLAG_TAP_ENABLED 0x01 // Tap detection active (requires restart) +#define FLAG_TEMP_COMP_ENABLED 0x02 // Temperature gyro-drift compensation +#define FLAG_AUTO_RECAL_ENABLED 0x04 // Auto-recalibrate after long idle +#define FLAG_ALL_DEFAULT (FLAG_TAP_ENABLED | FLAG_TEMP_COMP_ENABLED | FLAG_AUTO_RECAL_ENABLED) // ─── Enums ──────────────────────────────────────────────────────────────────── enum CurveType : uint8_t { CURVE_LINEAR=0, CURVE_SQUARE=1, CURVE_SQRT=2 }; @@ -83,27 +91,30 @@ 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 + float jerkThreshold; // jerk² threshold for tap-freeze detection + uint8_t tapFreezeEnabled; // 1 = enable jerk-based cursor freeze during taps + uint8_t featureFlags; // bitmask of FLAG_* — runtime feature overrides }; extern Config cfg; extern const Config CFG_DEFAULTS; -// ─── ConfigBlob (over BLE, 20 bytes) ───────────────────────────────────────── +// ─── ConfigBlob (over BLE, 25 bytes) ───────────────────────────────────────── struct __attribute__((packed)) ConfigBlob { - float sensitivity; // [0] - float deadZone; // [4] - float accelStrength; // [8] - uint8_t curve; // [12] - uint8_t axisFlip; // [13] - uint8_t chargeMode; // [14] - uint8_t tapThreshold; // [15] 1–31 - uint8_t tapAction; // [16] TapAction enum - uint8_t tapKey; // [17] HID keycode - uint8_t tapMod; // [18] HID modifier - uint8_t _pad; // [19] - float jerkThreshold; // [20] jerk² tap-freeze threshold + float sensitivity; // [0] + float deadZone; // [4] + float accelStrength; // [8] + uint8_t curve; // [12] + uint8_t axisFlip; // [13] + uint8_t chargeMode; // [14] + uint8_t tapThreshold; // [15] 1–31 + uint8_t tapAction; // [16] TapAction enum + uint8_t tapKey; // [17] HID keycode + uint8_t tapMod; // [18] HID modifier + uint8_t tapFreezeEnabled; // [19] 1 = enable jerk-based cursor freeze during taps + float jerkThreshold; // [20] jerk² tap-freeze threshold + uint8_t featureFlags; // [24] FLAG_* bitmask — runtime feature overrides }; -static_assert(sizeof(ConfigBlob) == 24, "ConfigBlob must be 24 bytes"); +static_assert(sizeof(ConfigBlob) == 25, "ConfigBlob must be 25 bytes"); // ─── TelemetryPacket (24 bytes) ─────────────────────────────────────────────── #ifdef FEATURE_TELEMETRY diff --git a/source/main.cpp b/source/main.cpp index 06345c5..24d8f4b 100644 --- a/source/main.cpp +++ b/source/main.cpp @@ -60,7 +60,7 @@ 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, - /*jerkThreshold=*/2000.0f + /*jerkThreshold=*/2000.0f, /*tapFreezeEnabled=*/1, /*featureFlags=*/FLAG_ALL_DEFAULT }; // ─── Telemetry definition ───────────────────────────────────────────────────── @@ -221,7 +221,7 @@ void setup() { Serial.println("[OK] IMU ready"); #ifdef FEATURE_TAP_DETECTION - setupTapDetection(); + if (cfg.featureFlags & FLAG_TAP_ENABLED) setupTapDetection(); #endif cachedTempC = readIMUTemp(); @@ -320,7 +320,7 @@ void loop() { #endif #ifdef FEATURE_TAP_DETECTION - processTaps(now); + if (cfg.featureFlags & FLAG_TAP_ENABLED) processTaps(now); #endif if (now - lastTime < (unsigned long)LOOP_RATE_MS) return; @@ -340,17 +340,14 @@ void loop() { #endif // Gyro reads with optional temperature compensation - float gx, gy, gz; + float correction = 0.0f; #ifdef FEATURE_TEMP_COMPENSATION - float correction = TEMP_COMP_COEFF_DPS_C * (cachedTempC - calTempC); - gx = (imu.readFloatGyroX() - biasGX - correction) * (PI/180.0f); - gy = (imu.readFloatGyroY() - biasGY - correction) * (PI/180.0f); - gz = (imu.readFloatGyroZ() - biasGZ - correction) * (PI/180.0f); - #else - gx = (imu.readFloatGyroX() - biasGX) * (PI/180.0f); - gy = (imu.readFloatGyroY() - biasGY) * (PI/180.0f); - gz = (imu.readFloatGyroZ() - biasGZ) * (PI/180.0f); + 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 ax = imu.readFloatAccelX(); float ay = imu.readFloatAccelY(); @@ -362,8 +359,8 @@ void loop() { 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; + bool shocked = cfg.tapFreezeEnabled && ((jerkSq > cfg.jerkThreshold) || (now < shockFreezeUntil)); + if (cfg.tapFreezeEnabled && jerkSq > cfg.jerkThreshold) shockFreezeUntil = now + SHOCK_FREEZE_MS; // Complementary filter — gx=pitch axis, gz=yaw axis on this board layout // During shock: gyro-only integration to avoid accel spike corrupting angles @@ -427,7 +424,7 @@ void loop() { bool idle = (idleFrames >= IDLE_FRAMES); #ifdef FEATURE_AUTO_RECAL - if (idle && idleStartMs != 0 && (now - idleStartMs >= AUTO_RECAL_MS)) { + 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; } diff --git a/web/app.js b/web/app.js index f1093e2..95d4a8a 100644 --- a/web/app.js +++ b/web/app.js @@ -9,9 +9,16 @@ const CHR = { gitHash: '00001239-0000-1000-8000-00805f9b34fb', // GitHash R 8 bytes }; +// Runtime feature-override flag bitmask constants (mirror firmware FLAG_* defines) +const FLAG_TAP_ENABLED = 0x01; +const FLAG_TEMP_COMP_ENABLED = 0x02; +const FLAG_AUTO_RECAL_ENABLED = 0x04; +const FLAG_ALL_DEFAULT = FLAG_TAP_ENABLED | FLAG_TEMP_COMP_ENABLED | FLAG_AUTO_RECAL_ENABLED; + // Local shadow of the current config (kept in sync with device) const config = { sensitivity:600, deadZone:0.06, accelStrength:0.08, curve:0, axisFlip:0, chargeMode:1, - tapThreshold:12, tapAction:0, tapKey:0, tapMod:0, tapFreezeEnabled:1, jerkThreshold:2000 }; + tapThreshold:12, tapAction:0, tapKey:0, tapMod:0, tapFreezeEnabled:1, jerkThreshold:2000, + featureFlags:FLAG_ALL_DEFAULT }; let device=null, server=null, chars={}, userDisconnected=false; let currentChargeStatus=0, currentBattPct=null, currentBattVoltage=null; @@ -200,11 +207,11 @@ async function checkHashMatch() { } // ── ConfigBlob read / write ────────────────────────────────────────────────── -// ConfigBlob layout (24 bytes LE): +// ConfigBlob layout (25 bytes LE): // float sensitivity [0], float deadZone [4], float accelStrength [8] // uint8 curve [12], uint8 axisFlip [13], uint8 chargeMode [14] // uint8 tapThreshold [15], uint8 tapAction [16], uint8 tapKey [17], uint8 tapMod [18], uint8 tapFreezeEnabled [19] -// float jerkThreshold [20] +// float jerkThreshold [20], uint8 featureFlags [24] async function readConfigBlob() { if (!chars.configBlob) return; @@ -227,6 +234,11 @@ async function readConfigBlob() { if (view.byteLength >= 24) { config.jerkThreshold = view.getFloat32(20, true); } + if (view.byteLength >= 25) { + config.featureFlags = view.getUint8(24); + } else { + config.featureFlags = FLAG_ALL_DEFAULT; // old firmware — assume all on + } applyConfigToUI(); log(`Config loaded — sens=${config.sensitivity.toFixed(0)} dz=${config.deadZone.toFixed(3)} tapThr=${config.tapThreshold}`,'ok'); } catch(e) { log(`Config read error: ${e.message}`,'err'); } @@ -255,6 +267,9 @@ function applyConfigToUI() { document.getElementById('tapModShift').checked = !!(config.tapMod & 0x02); document.getElementById('tapModAlt').checked = !!(config.tapMod & 0x04); document.getElementById('tapModGui').checked = !!(config.tapMod & 0x08); + document.getElementById('capTapEnabled').checked = !!(config.featureFlags & FLAG_TAP_ENABLED); + document.getElementById('capTempComp').checked = !!(config.featureFlags & FLAG_TEMP_COMP_ENABLED); + document.getElementById('capAutoRecal').checked = !!(config.featureFlags & FLAG_AUTO_RECAL_ENABLED); } let _writeConfigTimer = null; @@ -278,9 +293,12 @@ async function _doWriteConfigBlob() { | (document.getElementById('tapModGui').checked ? 0x08 : 0); config.tapFreezeEnabled = document.getElementById('tapFreezeEnabled').checked ? 1 : 0; config.jerkThreshold = +document.getElementById('slJerkThreshold').value; + config.featureFlags = (document.getElementById('capTapEnabled').checked ? FLAG_TAP_ENABLED : 0) + | (document.getElementById('capTempComp').checked ? FLAG_TEMP_COMP_ENABLED : 0) + | (document.getElementById('capAutoRecal').checked ? FLAG_AUTO_RECAL_ENABLED : 0); // config.curve, config.chargeMode, config.tapAction, config.tapKey updated directly - const buf = new ArrayBuffer(24); + const buf = new ArrayBuffer(25); const view = new DataView(buf); view.setFloat32(0, config.sensitivity, true); view.setFloat32(4, config.deadZone, true); @@ -294,6 +312,7 @@ async function _doWriteConfigBlob() { view.setUint8(18, config.tapMod); view.setUint8(19, config.tapFreezeEnabled); view.setFloat32(20, config.jerkThreshold, true); + view.setUint8(24, config.featureFlags); try { await gattWrite(chars.configBlob, buf); @@ -330,6 +349,11 @@ function setChargeModeUI(val) { document.getElementById('ciMode').textContent = ['Off (0mA)','50 mA','100 mA'][val] ?? '--'; } +function onCapTapChange(enabled) { + writeConfigBlob(); + log('Tap detection ' + (enabled ? 'enabled' : 'disabled') + ' — restart device to apply', 'warn'); +} + function onTapFreezeChange(enabled) { config.tapFreezeEnabled = enabled ? 1 : 0; updateTapFreezeUI(enabled); diff --git a/web/index.html b/web/index.html index 09d07bf..fe97476 100644 --- a/web/index.html +++ b/web/index.html @@ -155,6 +155,25 @@ +
Device Capabilities
+
+
+
Tap Detection
+
Double-tap click action  · restart to apply
+ +
+
+
Temp Compensation
+
Gyro drift correction by temperature
+ +
+
+
Auto Recalibration
+
Recalibrate gyro after long idle period
+ +
+
+
Device Commands