Add configuration slider for double tapping
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
#include "ble_config.h"
|
||||
#include "tap.h"
|
||||
#include <Adafruit_LittleFS.h>
|
||||
#include <InternalFileSystem.h>
|
||||
|
||||
@@ -8,7 +9,7 @@ extern File cfgFile;
|
||||
// ─── BLE Config Service objects ───────────────────────────────────────────────
|
||||
#ifdef FEATURE_CONFIG_SERVICE
|
||||
BLEService cfgService (0x1234);
|
||||
BLECharacteristic cfgBlob (0x1235); // ConfigBlob R/W 16 bytes
|
||||
BLECharacteristic cfgBlob (0x1235); // ConfigBlob R/W 20 bytes
|
||||
BLECharacteristic cfgCommand (0x1236); // Command W 1 byte
|
||||
#ifdef FEATURE_TELEMETRY
|
||||
BLECharacteristic cfgTelemetry(0x1237); // Telemetry R/N 24 bytes
|
||||
@@ -54,9 +55,17 @@ void saveConfig() {
|
||||
#ifdef FEATURE_CONFIG_SERVICE
|
||||
void pushConfigBlob() {
|
||||
ConfigBlob b;
|
||||
b.sensitivity = cfg.sensitivity; b.deadZone = cfg.deadZone;
|
||||
b.accelStrength = cfg.accelStrength; b.curve = (uint8_t)cfg.curve;
|
||||
b.axisFlip = cfg.axisFlip; b.chargeMode = (uint8_t)cfg.chargeMode; b._pad = 0;
|
||||
b.sensitivity = cfg.sensitivity;
|
||||
b.deadZone = cfg.deadZone;
|
||||
b.accelStrength = cfg.accelStrength;
|
||||
b.curve = (uint8_t)cfg.curve;
|
||||
b.axisFlip = cfg.axisFlip;
|
||||
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;
|
||||
cfgBlob.write((uint8_t*)&b, sizeof(b));
|
||||
}
|
||||
#endif
|
||||
@@ -64,6 +73,9 @@ void pushConfigBlob() {
|
||||
void factoryReset() {
|
||||
cfg = CFG_DEFAULTS; saveConfig();
|
||||
applyChargeMode(cfg.chargeMode);
|
||||
#ifdef FEATURE_TAP_DETECTION
|
||||
applyTapThreshold();
|
||||
#endif
|
||||
#ifdef FEATURE_CONFIG_SERVICE
|
||||
if (!safeMode) pushConfigBlob();
|
||||
#endif
|
||||
@@ -84,14 +96,23 @@ void onConfigBlobWrite(uint16_t h, BLECharacteristic* c, uint8_t* d, uint16_t l)
|
||||
cfg.sensitivity = b->sensitivity;
|
||||
cfg.deadZone = b->deadZone;
|
||||
cfg.accelStrength = b->accelStrength;
|
||||
if (b->curve <= 2) cfg.curve = (CurveType)b->curve;
|
||||
if (b->curve <= 2) cfg.curve = (CurveType)b->curve;
|
||||
cfg.axisFlip = b->axisFlip;
|
||||
if (b->chargeMode <= 2) { cfg.chargeMode = (ChargeMode)b->chargeMode; applyChargeMode(cfg.chargeMode); }
|
||||
#ifdef FEATURE_TAP_DETECTION
|
||||
if (b->tapThreshold >= 1 && b->tapThreshold <= 31) {
|
||||
cfg.tapThreshold = b->tapThreshold;
|
||||
applyTapThreshold();
|
||||
}
|
||||
if (b->tapAction <= 3) cfg.tapAction = (TapAction)b->tapAction;
|
||||
cfg.tapKey = b->tapKey;
|
||||
cfg.tapMod = b->tapMod;
|
||||
#endif
|
||||
saveConfig();
|
||||
Serial.print("[CFG] Written — sens="); Serial.print(cfg.sensitivity,0);
|
||||
Serial.print(" dz="); Serial.print(cfg.deadZone,3);
|
||||
Serial.print(" curve="); Serial.print(cfg.curve);
|
||||
Serial.print(" chg="); Serial.println(cfg.chargeMode);
|
||||
Serial.print(" tapThr="); Serial.print(cfg.tapThreshold);
|
||||
Serial.print(" tapAction="); Serial.println(cfg.tapAction);
|
||||
}
|
||||
|
||||
void onCommandWrite(uint16_t h, BLECharacteristic* c, uint8_t* d, uint16_t l) {
|
||||
|
||||
@@ -60,6 +60,16 @@ enum CurveType : uint8_t { CURVE_LINEAR=0, CURVE_SQUARE=1, CURVE_SQRT=2 };
|
||||
enum ChargeMode : uint8_t { CHARGE_OFF=0, CHARGE_SLOW=1, CHARGE_FAST=2 };
|
||||
enum ChargeStatus: uint8_t { CHGSTAT_DISCHARGING=0, CHGSTAT_CHARGING=1, CHGSTAT_FULL=2 };
|
||||
|
||||
// ─── Tap action types ─────────────────────────────────────────────────────────
|
||||
// TAP_ACTION_KEY: fires a raw HID keycode (tapKey) with optional modifier (tapMod).
|
||||
// Modifier byte: bit0=Ctrl, bit1=Shift, bit2=Alt, bit3=GUI (same as HID modifier byte).
|
||||
enum TapAction : uint8_t {
|
||||
TAP_ACTION_LEFT = 0,
|
||||
TAP_ACTION_RIGHT = 1,
|
||||
TAP_ACTION_MIDDLE = 2,
|
||||
TAP_ACTION_KEY = 3,
|
||||
};
|
||||
|
||||
// ─── Config (stored in flash) ─────────────────────────────────────────────────
|
||||
struct Config {
|
||||
uint32_t magic;
|
||||
@@ -69,11 +79,15 @@ struct Config {
|
||||
CurveType curve;
|
||||
uint8_t axisFlip;
|
||||
ChargeMode chargeMode;
|
||||
uint8_t tapThreshold; // 1–31 → REG_TAP_THS_6D bits[4:0]; 1 LSB = 62.5 mg at ±2g
|
||||
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)
|
||||
};
|
||||
extern Config cfg;
|
||||
extern const Config CFG_DEFAULTS;
|
||||
|
||||
// ─── ConfigBlob (over BLE, 16 bytes) ─────────────────────────────────────────
|
||||
// ─── ConfigBlob (over BLE, 20 bytes) ─────────────────────────────────────────
|
||||
struct __attribute__((packed)) ConfigBlob {
|
||||
float sensitivity; // [0]
|
||||
float deadZone; // [4]
|
||||
@@ -81,9 +95,13 @@ struct __attribute__((packed)) ConfigBlob {
|
||||
uint8_t curve; // [12]
|
||||
uint8_t axisFlip; // [13]
|
||||
uint8_t chargeMode; // [14]
|
||||
uint8_t _pad; // [15]
|
||||
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]
|
||||
};
|
||||
static_assert(sizeof(ConfigBlob) == 16, "ConfigBlob must be 16 bytes");
|
||||
static_assert(sizeof(ConfigBlob) == 20, "ConfigBlob must be 20 bytes");
|
||||
|
||||
// ─── TelemetryPacket (24 bytes) ───────────────────────────────────────────────
|
||||
#ifdef FEATURE_TELEMETRY
|
||||
|
||||
@@ -58,7 +58,8 @@ File cfgFile(InternalFS);
|
||||
// ─── Config definitions ───────────────────────────────────────────────────────
|
||||
Config cfg;
|
||||
const Config CFG_DEFAULTS = {
|
||||
CONFIG_MAGIC, 600.0f, 0.060f, 0.08f, CURVE_LINEAR, 0x00, CHARGE_SLOW
|
||||
CONFIG_MAGIC, 600.0f, 0.060f, 0.08f, CURVE_LINEAR, 0x00, CHARGE_SLOW,
|
||||
/*tapThreshold=*/12, /*tapAction=*/TAP_ACTION_LEFT, /*tapKey=*/0, /*tapMod=*/0
|
||||
};
|
||||
|
||||
// ─── Telemetry definition ─────────────────────────────────────────────────────
|
||||
|
||||
129
source/tap.cpp
129
source/tap.cpp
@@ -7,42 +7,115 @@
|
||||
extern BLEHidAdafruit blehid;
|
||||
|
||||
// ─── Tap detection setup ──────────────────────────────────────────────────────
|
||||
// REG_TAP_THS_6D bits[4:0] = tapThreshold (1–31); 1 LSB = FS/32 = 62.5 mg at ±2g.
|
||||
// REG_INT_DUR2 at ODR=416 Hz:
|
||||
// SHOCK[7:6] = 2 → 38 ms max tap duration
|
||||
// QUIET[5:4] = 2 → 19 ms refractory after tap
|
||||
// DUR[3:0] = 6 → 115 ms max inter-tap window for double detection
|
||||
void applyTapThreshold() {
|
||||
uint8_t thr = cfg.tapThreshold;
|
||||
if (thr < 1) thr = 1;
|
||||
if (thr > 31) thr = 31;
|
||||
imuWriteReg(REG_TAP_THS_6D, thr & 0x1F);
|
||||
}
|
||||
|
||||
void setupTapDetection() {
|
||||
imuWriteReg(REG_CTRL1_XL, 0x60); // ODR=416Hz, FS=±2g
|
||||
imuWriteReg(REG_TAP_CFG, 0x8E); // INT_EN + LIR + TAP_Z/Y/X
|
||||
imuWriteReg(REG_TAP_THS_6D, 0x0C); // threshold 750 mg (was 500 mg — too easy to false-trigger)
|
||||
imuWriteReg(REG_INT_DUR2, 0x7A); // DUR=7(538ms), QUIET=2(19ms), SHOCK=2(38ms)
|
||||
imuWriteReg(REG_WAKE_UP_THS, 0x80); // enable double-tap
|
||||
imuWriteReg(REG_MD1_CFG, 0x48); // route taps to INT1
|
||||
Serial.println("[TAP] Engine configured — single=LEFT, double=RIGHT");
|
||||
imuWriteReg(REG_CTRL1_XL, 0x60); // ODR=416 Hz, FS=±2g
|
||||
imuWriteReg(REG_TAP_CFG, 0x8E); // TIMER_EN + LIR + TAP_Z/Y/X enabled
|
||||
applyTapThreshold();
|
||||
imuWriteReg(REG_INT_DUR2, 0x62); // SHOCK=2(38ms), QUIET=2(19ms), DUR=6(115ms)
|
||||
imuWriteReg(REG_WAKE_UP_THS, 0x80); // bit7=1 → single + double tap both enabled
|
||||
imuWriteReg(REG_MD1_CFG, 0x48); // route single-tap(0x08) + double-tap(0x40) → INT1
|
||||
Serial.print("[TAP] threshold="); Serial.print(cfg.tapThreshold);
|
||||
Serial.print(" (~"); Serial.print(cfg.tapThreshold * 62.5f, 0); Serial.println(" mg)");
|
||||
}
|
||||
|
||||
// ─── Tap processing ───────────────────────────────────────────────────────────
|
||||
void processTaps(unsigned long now) {
|
||||
// Release held button after CLICK_HOLD_MS
|
||||
if (clickButtonDown && (now - clickDownMs >= CLICK_HOLD_MS)) {
|
||||
blehid.mouseButtonPress(clickButton, false);
|
||||
clickButtonDown = false; clickButton = 0;
|
||||
// Only double-tap is mapped to an action. Single-tap is ignored (it always fires
|
||||
// before the double is confirmed and cannot be reliably disambiguated on this
|
||||
// hardware without an unacceptable latency penalty).
|
||||
//
|
||||
// The LSM6DS3 sets SINGLE_TAP immediately on first contact — we wait until
|
||||
// DOUBLE_TAP is set (within the hardware DUR window of 115 ms) before acting.
|
||||
// An additional TAP_CONFIRM_MS guard ensures the TAP_SRC register has settled.
|
||||
//
|
||||
// IMPORTANT: call mouseButtonPress(bitmask) — single arg only. The two-arg
|
||||
// overload takes (conn_hdl, buttons) and sends the wrong button value.
|
||||
|
||||
static enum { TAP_IDLE, TAP_PENDING, TAP_EXECUTING } tapState = TAP_IDLE;
|
||||
static unsigned long tapPendingMs = 0;
|
||||
static uint8_t pendingButton = 0; // 0 = key action pending
|
||||
|
||||
// After DOUBLE_TAP fires we add a small settle guard before committing.
|
||||
static const unsigned long TAP_CONFIRM_MS = 20;
|
||||
|
||||
static void fireTapAction(unsigned long now) {
|
||||
switch (cfg.tapAction) {
|
||||
case TAP_ACTION_LEFT:
|
||||
blehid.mouseButtonPress(MOUSE_BUTTON_LEFT);
|
||||
pendingButton = MOUSE_BUTTON_LEFT;
|
||||
Serial.println("[TAP] Double → LEFT click");
|
||||
statLeftClicks++;
|
||||
break;
|
||||
case TAP_ACTION_RIGHT:
|
||||
blehid.mouseButtonPress(MOUSE_BUTTON_RIGHT);
|
||||
pendingButton = MOUSE_BUTTON_RIGHT;
|
||||
Serial.println("[TAP] Double → RIGHT click");
|
||||
statRightClicks++;
|
||||
break;
|
||||
case TAP_ACTION_MIDDLE:
|
||||
blehid.mouseButtonPress(MOUSE_BUTTON_MIDDLE);
|
||||
pendingButton = MOUSE_BUTTON_MIDDLE;
|
||||
Serial.println("[TAP] Double → MIDDLE click");
|
||||
statLeftClicks++;
|
||||
break;
|
||||
case TAP_ACTION_KEY: {
|
||||
uint8_t keys[6] = {cfg.tapKey, 0, 0, 0, 0, 0};
|
||||
blehid.keyboardReport(cfg.tapMod, keys);
|
||||
pendingButton = 0;
|
||||
Serial.print("[TAP] Double → KEY 0x"); Serial.println(cfg.tapKey, HEX);
|
||||
statLeftClicks++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (clickButtonDown) return; // Don't start a new click while one is held
|
||||
clickButtonDown = true; clickDownMs = now;
|
||||
tapState = TAP_EXECUTING;
|
||||
}
|
||||
|
||||
// The LSM6DS3 (with D_TAP_EN) already disambiguates at hardware level:
|
||||
// SINGLE_TAP is only set after the DUR window expires with no second tap.
|
||||
// DOUBLE_TAP is set immediately when the second tap arrives within DUR.
|
||||
// We trust this directly — no software delay needed.
|
||||
void processTaps(unsigned long now) {
|
||||
// ── Release ───────────────────────────────────────────────────────────────
|
||||
if (tapState == TAP_EXECUTING) {
|
||||
if (now - clickDownMs >= CLICK_HOLD_MS) {
|
||||
if (pendingButton) {
|
||||
blehid.mouseButtonRelease();
|
||||
} else {
|
||||
// Key action: release all keys
|
||||
uint8_t noKeys[6] = {};
|
||||
blehid.keyboardReport(0, noKeys);
|
||||
}
|
||||
clickButton = 0; clickButtonDown = false;
|
||||
tapState = TAP_IDLE;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// ── Poll TAP_SRC ──────────────────────────────────────────────────────────
|
||||
uint8_t tapSrc = imuReadReg(REG_TAP_SRC);
|
||||
if (!(tapSrc & 0x40)) return; // TAP_IA not set — no event
|
||||
bool tapIA = !!(tapSrc & 0x40);
|
||||
bool doubleTap = !!(tapSrc & 0x10);
|
||||
|
||||
if (tapSrc & 0x10) { // DOUBLE_TAP → right click
|
||||
Serial.println("[TAP] Double → RIGHT");
|
||||
blehid.mouseButtonPress(MOUSE_BUTTON_RIGHT, true);
|
||||
clickButton = MOUSE_BUTTON_RIGHT; clickButtonDown = true; clickDownMs = now;
|
||||
statRightClicks++;
|
||||
} else if (tapSrc & 0x20) { // SINGLE_TAP → left click
|
||||
Serial.println("[TAP] Single → LEFT");
|
||||
blehid.mouseButtonPress(MOUSE_BUTTON_LEFT, true);
|
||||
clickButton = MOUSE_BUTTON_LEFT; clickButtonDown = true; clickDownMs = now;
|
||||
statLeftClicks++;
|
||||
if (tapState == TAP_IDLE) {
|
||||
if (tapIA && doubleTap) {
|
||||
tapPendingMs = now;
|
||||
tapState = TAP_PENDING;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (tapState == TAP_PENDING) {
|
||||
if (now - tapPendingMs >= TAP_CONFIRM_MS) {
|
||||
fireTapAction(now);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,5 +3,6 @@
|
||||
|
||||
#ifdef FEATURE_TAP_DETECTION
|
||||
void setupTapDetection();
|
||||
void applyTapThreshold();
|
||||
void processTaps(unsigned long now);
|
||||
#endif
|
||||
|
||||
Reference in New Issue
Block a user