Fix boot timeout
This commit is contained in:
48
README.md
Normal file
48
README.md
Normal file
@@ -0,0 +1,48 @@
|
||||
# Air mouse
|
||||
|
||||
This project is aimed at replacing/open-sourcing the Logitech presenter. It also contains some features not found on the Logitech Spotlight.
|
||||
|
||||
I used a **Seeed Studio XIAO nRF52480 Sense** board, but it required some BSP modification to work with SoftDevice S140 7.3.0. Namely the memory layout:
|
||||
```c
|
||||
/* Linker script to configure memory regions. */
|
||||
|
||||
SEARCH_DIR(.)
|
||||
GROUP(-lgcc -lc -lnosys)
|
||||
|
||||
MEMORY
|
||||
{
|
||||
FLASH (rx) : ORIGIN = 0x27000, LENGTH = 0xED000 - 0x27000
|
||||
|
||||
/* SRAM required by Softdevice depend on
|
||||
* - Attribute Table Size (Number of Services and Characteristics)
|
||||
* - Vendor UUID count
|
||||
* - Max ATT MTU
|
||||
* - Concurrent connection peripheral + central + secure links
|
||||
* - Event Len, HVN queue, Write CMD queue
|
||||
*/
|
||||
RAM (rwx) : ORIGIN = 0x2000E000, LENGTH = 0x20040000 - 0x2000E000
|
||||
}
|
||||
|
||||
SECTIONS
|
||||
{
|
||||
. = ALIGN(4);
|
||||
.svc_data :
|
||||
{
|
||||
PROVIDE(__start_svc_data = .);
|
||||
KEEP(*(.svc_data))
|
||||
PROVIDE(__stop_svc_data = .);
|
||||
} > RAM
|
||||
|
||||
.fs_data :
|
||||
{
|
||||
PROVIDE(__start_fs_data = .);
|
||||
KEEP(*(.fs_data))
|
||||
PROVIDE(__stop_fs_data = .);
|
||||
} > RAM
|
||||
} INSERT AFTER .data;
|
||||
|
||||
INCLUDE "nrf52_common.ld"
|
||||
```
|
||||
in `cores/linker/nrf52840_s140_v7.ld`
|
||||
|
||||
You can use the `web-config.html` page to configure this device.
|
||||
707
air-mouse.ino
707
air-mouse.ino
@@ -1,92 +1,128 @@
|
||||
/*
|
||||
* IMU BLE Mouse — Seeed XIAO nRF52840 Sense (v3.3)
|
||||
* IMU BLE Mouse — Seeed XIAO nRF52840 Sense (v3.4)
|
||||
* ================================================================
|
||||
* Changes vs v3.2:
|
||||
* - 10 characteristics → 4 (fixes SoftDevice RAM overflow)
|
||||
* - configAttrTableSize reduced to 1024
|
||||
* - All config params merged into one 16-byte ConfigBlob (0x1235)
|
||||
* - chargeStatus merged into TelemetryPacket._pad (0x1237)
|
||||
* - ImuStream (0x1238) and Command (0x1236) unchanged
|
||||
* - Boot-loop detection retained
|
||||
* Feature flags — comment out any line to disable that feature.
|
||||
* ATT table size is computed automatically from enabled features.
|
||||
* Start with minimal flags to isolate the SoftDevice RAM issue,
|
||||
* then re-enable one at a time.
|
||||
*
|
||||
* ── BLE Config Service (UUID 0x1234) ───────────────────────────────
|
||||
* UUID | Len | R/W/N | Description
|
||||
* ───────|─────|───────|──────────────────────────────────────────
|
||||
* 0x1235 | 16 | R/W | ConfigBlob — all settings in one write
|
||||
* 0x1236 | 1 | W | Command: 0x01=Cal 0xFF=FactoryReset
|
||||
* 0x1237 | 24 | R/N | TelemetryPacket, notified 1 Hz
|
||||
* 0x1238 | 14 | N | ImuPacket, notified ~100 Hz
|
||||
* MINIMUM (just working mouse, no BLE config):
|
||||
* leave only FEATURE_BATTERY_MONITOR + FEATURE_BOOT_LOOP_DETECT
|
||||
*
|
||||
* ── ConfigBlob (16 bytes, little-endian) ────────────────────────────
|
||||
* float sensitivity [0]
|
||||
* float deadZone [4]
|
||||
* float accelStrength [8]
|
||||
* uint8_t curve [12] 0=LINEAR 1=SQUARE 2=SQRT
|
||||
* uint8_t axisFlip [13] bit0=flipX bit1=flipY
|
||||
* uint8_t chargeMode [14] 0=OFF 1=50mA 2=100mA
|
||||
* uint8_t _pad [15]
|
||||
* RECOMMENDED first test:
|
||||
* enable FEATURE_CONFIG_SERVICE, keep TAP + STREAM + TELEMETRY off
|
||||
*
|
||||
* ── TelemetryPacket (24 bytes, little-endian) ────────────────────────
|
||||
* uint32_t uptimeSeconds [0]
|
||||
* uint32_t leftClicks [4]
|
||||
* uint32_t rightClicks [8]
|
||||
* float tempCelsius [12]
|
||||
* float biasRmsRadS [16]
|
||||
* uint16_t recalCount [20]
|
||||
* uint8_t chargeStatus [22] 0=discharging 1=charging 2=full
|
||||
* uint8_t _pad [23]
|
||||
* ── Feature flag index ───────────────────────────────────────────
|
||||
* FEATURE_CONFIG_SERVICE Custom GATT service (ConfigBlob + Command)
|
||||
* FEATURE_TELEMETRY +24-byte notify characteristic, 1 Hz
|
||||
* FEATURE_IMU_STREAM +14-byte notify characteristic, ~100 Hz
|
||||
* FEATURE_TAP_DETECTION LSM6DS3 hardware tap engine → L/R clicks
|
||||
* FEATURE_TEMP_COMPENSATION Gyro drift correction by temperature delta
|
||||
* FEATURE_AUTO_RECAL Recalibrate after AUTO_RECAL_MS idle
|
||||
* FEATURE_BATTERY_MONITOR ADC battery read + BLE Battery Service
|
||||
* FEATURE_BOOT_LOOP_DETECT .noinit crash counter → safe mode
|
||||
*
|
||||
* ── ImuPacket (14 bytes, little-endian) ─────────────────────────────
|
||||
* int16_t gyroY_mDPS [0]
|
||||
* int16_t gyroZ_mDPS [2]
|
||||
* int16_t accelX_mg [4]
|
||||
* int16_t accelY_mg [6]
|
||||
* int16_t accelZ_mg [8]
|
||||
* int8_t moveX [10]
|
||||
* int8_t moveY [11]
|
||||
* uint8_t flags [12] bit0=idle bit1=singleTap bit2=doubleTap
|
||||
* uint8_t _pad [13]
|
||||
* Dependencies:
|
||||
* FEATURE_TELEMETRY requires FEATURE_CONFIG_SERVICE
|
||||
* FEATURE_IMU_STREAM requires FEATURE_CONFIG_SERVICE
|
||||
* ================================================================
|
||||
*/
|
||||
|
||||
// ─── Feature Flags ────────────────────────────────────────────────────────────
|
||||
#define FEATURE_CONFIG_SERVICE
|
||||
#define FEATURE_TELEMETRY
|
||||
#define FEATURE_IMU_STREAM
|
||||
#define FEATURE_TAP_DETECTION
|
||||
#define FEATURE_TEMP_COMPENSATION
|
||||
#define FEATURE_AUTO_RECAL
|
||||
#define FEATURE_BATTERY_MONITOR
|
||||
#define FEATURE_BOOT_LOOP_DETECT
|
||||
|
||||
// ─── Debug ────────────────────────────────────────────────────────────────────
|
||||
// #define DEBUG
|
||||
|
||||
// ─── ATT table size ───────────────────────────────────────────────────────────
|
||||
// Must be passed to configAttrTableSize() BEFORE Bluefruit.begin().
|
||||
// Too small → SoftDevice panics. Too large → SoftDevice claims RAM the linker
|
||||
// already assigned to the app and also panics. Sweet spot is 1500-2000.
|
||||
//
|
||||
// HID service alone costs ~700 B (report map + 6 characteristics + CCCDs).
|
||||
// Add DIS (~100 B), BAS (~50 B), then our custom characteristics.
|
||||
// Use 1536 as the safe base; add 256 per notify characteristic.
|
||||
//
|
||||
// Feature cost breakdown:
|
||||
// HID + DIS + BAS baseline : ~900 B (always present)
|
||||
// CONFIG_SERVICE (blob+cmd) : ~80 B
|
||||
// TELEMETRY (R/N 24 bytes) : ~40 B
|
||||
// IMU_STREAM (N 14 bytes) : ~30 B
|
||||
//
|
||||
#define _ATT_BASE 900
|
||||
#ifdef FEATURE_CONFIG_SERVICE
|
||||
#define _ATT_CFG 80
|
||||
#else
|
||||
#define _ATT_CFG 0
|
||||
#endif
|
||||
#ifdef FEATURE_TELEMETRY
|
||||
#define _ATT_TELEM 40
|
||||
#else
|
||||
#define _ATT_TELEM 0
|
||||
#endif
|
||||
#ifdef FEATURE_IMU_STREAM
|
||||
#define _ATT_STREAM 30
|
||||
#else
|
||||
#define _ATT_STREAM 0
|
||||
#endif
|
||||
// Floor of 1536 so we never go below what HID actually needs
|
||||
#define ATT_TABLE_SIZE_CALC (_ATT_BASE + _ATT_CFG + _ATT_TELEM + _ATT_STREAM)
|
||||
#define ATT_TABLE_SIZE (ATT_TABLE_SIZE_CALC < 1536 ? 1536 : ATT_TABLE_SIZE_CALC)
|
||||
|
||||
// ─── Includes ─────────────────────────────────────────────────────────────────
|
||||
#include <bluefruit.h>
|
||||
#include <Adafruit_LittleFS.h>
|
||||
#include <InternalFileSystem.h>
|
||||
#include "LSM6DS3.h"
|
||||
#include "Wire.h"
|
||||
|
||||
// ─── Debug ────────────────────────────────────────────────────────────────────
|
||||
// #define DEBUG
|
||||
|
||||
// ─── Boot-loop detection ──────────────────────────────────────────────────────
|
||||
static uint32_t __attribute__((section(".noinit"))) bootCount;
|
||||
static uint32_t __attribute__((section(".noinit"))) bootMagic;
|
||||
#ifdef FEATURE_BOOT_LOOP_DETECT
|
||||
static uint32_t __attribute__((section(".noinit"))) bootCount;
|
||||
static uint32_t __attribute__((section(".noinit"))) bootMagic;
|
||||
#endif
|
||||
static bool safeMode = false;
|
||||
static bool bootCountCleared = false;
|
||||
|
||||
// ─── BLE Standard Services ────────────────────────────────────────────────────
|
||||
BLEDis bledis;
|
||||
BLEHidAdafruit blehid;
|
||||
BLEBas blebas;
|
||||
#ifdef FEATURE_BATTERY_MONITOR
|
||||
BLEBas blebas;
|
||||
#endif
|
||||
|
||||
// ─── BLE Config Service — 4 characteristics only ─────────────────────────────
|
||||
BLEService cfgService (0x1234);
|
||||
BLECharacteristic cfgBlob (0x1235); // ConfigBlob R/W 16 bytes
|
||||
BLECharacteristic cfgCommand (0x1236); // Command W 1 byte
|
||||
BLECharacteristic cfgTelemetry (0x1237); // Telemetry R/N 24 bytes 1 Hz
|
||||
BLECharacteristic cfgImuStream (0x1238); // ImuStream N 14 bytes ~100 Hz
|
||||
// ─── BLE Config Service ───────────────────────────────────────────────────────
|
||||
#ifdef FEATURE_CONFIG_SERVICE
|
||||
BLEService cfgService (0x1234);
|
||||
BLECharacteristic cfgBlob (0x1235); // ConfigBlob R/W 16 bytes
|
||||
BLECharacteristic cfgCommand (0x1236); // Command W 1 byte
|
||||
#ifdef FEATURE_TELEMETRY
|
||||
BLECharacteristic cfgTelemetry(0x1237); // Telemetry R/N 24 bytes
|
||||
#endif
|
||||
#ifdef FEATURE_IMU_STREAM
|
||||
BLECharacteristic cfgImuStream(0x1238); // ImuStream N 14 bytes
|
||||
#endif
|
||||
#endif
|
||||
|
||||
// ─── IMU ──────────────────────────────────────────────────────────────────────
|
||||
LSM6DS3 imu(I2C_MODE, 0x6A);
|
||||
|
||||
#define LSM6DS3_CTRL1_XL 0x10
|
||||
#define LSM6DS3_TAP_CFG 0x58
|
||||
#define LSM6DS3_TAP_THS_6D 0x59
|
||||
#define LSM6DS3_INT_DUR2 0x5A
|
||||
#define LSM6DS3_WAKE_UP_THS 0x5B
|
||||
#define LSM6DS3_MD1_CFG 0x5E
|
||||
#define LSM6DS3_TAP_SRC 0x1C
|
||||
#define LSM6DS3_OUT_TEMP_L 0x20
|
||||
#define LSM6DS3_OUT_TEMP_H 0x21
|
||||
#define REG_CTRL1_XL 0x10
|
||||
#define REG_TAP_CFG 0x58
|
||||
#define REG_TAP_THS_6D 0x59
|
||||
#define REG_INT_DUR2 0x5A
|
||||
#define REG_WAKE_UP_THS 0x5B
|
||||
#define REG_MD1_CFG 0x5E
|
||||
#define REG_TAP_SRC 0x1C
|
||||
#define REG_OUT_TEMP_L 0x20
|
||||
#define REG_OUT_TEMP_H 0x21
|
||||
|
||||
// ─── Pins ─────────────────────────────────────────────────────────────────────
|
||||
#define PIN_VBAT_ENABLE (14)
|
||||
@@ -96,7 +132,7 @@ LSM6DS3 imu(I2C_MODE, 0x6A);
|
||||
|
||||
// ─── Persistence ──────────────────────────────────────────────────────────────
|
||||
#define CONFIG_FILENAME "/imu_mouse_cfg.bin"
|
||||
#define CONFIG_MAGIC 0xDEAD1238UL // bumped — struct layout unchanged but version tag updated
|
||||
#define CONFIG_MAGIC 0xDEAD1239UL
|
||||
|
||||
using namespace Adafruit_LittleFS_Namespace;
|
||||
File cfgFile(InternalFS);
|
||||
@@ -106,7 +142,7 @@ 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 };
|
||||
|
||||
// ─── Config ───────────────────────────────────────────────────────────────────
|
||||
// ─── Config (stored in flash) ─────────────────────────────────────────────────
|
||||
struct Config {
|
||||
uint32_t magic;
|
||||
float sensitivity;
|
||||
@@ -117,64 +153,78 @@ struct Config {
|
||||
ChargeMode chargeMode;
|
||||
};
|
||||
Config cfg;
|
||||
const Config CFG_DEFAULTS = { CONFIG_MAGIC, 600.0f, 0.060f, 0.08f, CURVE_LINEAR, 0x00, CHARGE_SLOW };
|
||||
const Config CFG_DEFAULTS = {
|
||||
CONFIG_MAGIC, 600.0f, 0.060f, 0.08f, CURVE_LINEAR, 0x00, CHARGE_SLOW
|
||||
};
|
||||
|
||||
// ─── ConfigBlob (what goes over BLE — no magic field) ─────────────────────────
|
||||
// ─── ConfigBlob (over BLE, no magic) ─────────────────────────────────────────
|
||||
struct __attribute__((packed)) ConfigBlob {
|
||||
float sensitivity;
|
||||
float deadZone;
|
||||
float accelStrength;
|
||||
uint8_t curve;
|
||||
uint8_t axisFlip;
|
||||
uint8_t chargeMode;
|
||||
uint8_t _pad;
|
||||
float sensitivity; // [0]
|
||||
float deadZone; // [4]
|
||||
float accelStrength; // [8]
|
||||
uint8_t curve; // [12]
|
||||
uint8_t axisFlip; // [13]
|
||||
uint8_t chargeMode; // [14]
|
||||
uint8_t _pad; // [15]
|
||||
};
|
||||
static_assert(sizeof(ConfigBlob) == 16, "ConfigBlob must be 16 bytes");
|
||||
|
||||
// ─── TelemetryPacket ──────────────────────────────────────────────────────────
|
||||
#ifdef FEATURE_TELEMETRY
|
||||
struct __attribute__((packed)) TelemetryPacket {
|
||||
uint32_t uptimeSeconds;
|
||||
uint32_t leftClicks;
|
||||
uint32_t rightClicks;
|
||||
float tempCelsius;
|
||||
float biasRmsRadS;
|
||||
uint16_t recalCount;
|
||||
uint8_t chargeStatus; // replaces old _pad — no extra characteristic needed
|
||||
uint8_t _pad;
|
||||
uint32_t uptimeSeconds; // [0]
|
||||
uint32_t leftClicks; // [4]
|
||||
uint32_t rightClicks; // [8]
|
||||
float tempCelsius; // [12]
|
||||
float biasRmsRadS; // [16]
|
||||
uint16_t recalCount; // [20]
|
||||
uint8_t chargeStatus; // [22]
|
||||
uint8_t _pad; // [23]
|
||||
};
|
||||
static_assert(sizeof(TelemetryPacket) == 24, "TelemetryPacket must be 24 bytes");
|
||||
TelemetryPacket telem = {};
|
||||
#endif
|
||||
|
||||
// ─── ImuPacket ────────────────────────────────────────────────────────────────
|
||||
#ifdef FEATURE_IMU_STREAM
|
||||
struct __attribute__((packed)) ImuPacket {
|
||||
int16_t gyroY_mDPS;
|
||||
int16_t gyroZ_mDPS;
|
||||
int16_t accelX_mg;
|
||||
int16_t accelY_mg;
|
||||
int16_t accelZ_mg;
|
||||
int8_t moveX;
|
||||
int8_t moveY;
|
||||
uint8_t flags;
|
||||
uint8_t _pad;
|
||||
int16_t gyroY_mDPS; // [0]
|
||||
int16_t gyroZ_mDPS; // [2]
|
||||
int16_t accelX_mg; // [4]
|
||||
int16_t accelY_mg; // [6]
|
||||
int16_t accelZ_mg; // [8]
|
||||
int8_t moveX; // [10]
|
||||
int8_t moveY; // [11]
|
||||
uint8_t flags; // [12] bit0=idle bit1=singleTap bit2=doubleTap
|
||||
uint8_t _pad; // [13]
|
||||
};
|
||||
static_assert(sizeof(ImuPacket) == 14, "ImuPacket must be 14 bytes");
|
||||
#endif
|
||||
|
||||
// ─── Tuning constants ─────────────────────────────────────────────────────────
|
||||
const float ALPHA = 0.96f;
|
||||
const int LOOP_RATE_MS = 10;
|
||||
const int BIAS_SAMPLES = 200;
|
||||
const int IDLE_FRAMES = 150;
|
||||
const float TEMP_COMP_COEFF_DPS_C = 0.004f;
|
||||
const unsigned long AUTO_RECAL_MS = 5UL * 60UL * 1000UL;
|
||||
const unsigned long BATT_REPORT_MS = 10000;
|
||||
const unsigned long TELEMETRY_MS = 1000;
|
||||
const unsigned long HEARTBEAT_MS = 2000;
|
||||
const int HEARTBEAT_DUR = 30;
|
||||
const unsigned long CLICK_HOLD_MS = 40;
|
||||
const unsigned long DOUBLE_TAP_WINDOW_MS = 400;
|
||||
const unsigned long BOOT_SAFE_MS = 5000;
|
||||
const float BATT_FULL = 4.20f;
|
||||
const float BATT_EMPTY = 3.00f;
|
||||
const float BATT_CRITICAL = 3.10f;
|
||||
const float ALPHA = 0.96f;
|
||||
const int LOOP_RATE_MS = 10;
|
||||
const int BIAS_SAMPLES = 200;
|
||||
const int IDLE_FRAMES = 150;
|
||||
const unsigned long BATT_REPORT_MS = 10000;
|
||||
const unsigned long TELEMETRY_MS = 1000;
|
||||
const unsigned long HEARTBEAT_MS = 2000;
|
||||
const int HEARTBEAT_DUR = 30;
|
||||
const unsigned long BOOT_SAFE_MS = 5000;
|
||||
const float BATT_FULL = 4.20f;
|
||||
const float BATT_EMPTY = 3.00f;
|
||||
const float BATT_CRITICAL = 3.10f;
|
||||
|
||||
#ifdef FEATURE_TAP_DETECTION
|
||||
const unsigned long CLICK_HOLD_MS = 40;
|
||||
const unsigned long DOUBLE_TAP_WINDOW_MS = 400;
|
||||
#endif
|
||||
#ifdef FEATURE_TEMP_COMPENSATION
|
||||
const float TEMP_COMP_COEFF_DPS_C = 0.004f;
|
||||
#endif
|
||||
#ifdef FEATURE_AUTO_RECAL
|
||||
const unsigned long AUTO_RECAL_MS = 5UL * 60UL * 1000UL;
|
||||
#endif
|
||||
|
||||
// ─── State ────────────────────────────────────────────────────────────────────
|
||||
float angleX = 0, angleY = 0;
|
||||
@@ -183,18 +233,25 @@ float biasGX = 0, biasGY = 0, biasGZ = 0;
|
||||
float calTempC = 25.0f;
|
||||
float cachedTempC = 25.0f;
|
||||
|
||||
TelemetryPacket telem = {};
|
||||
#ifdef FEATURE_TAP_DETECTION
|
||||
bool tapPending = false;
|
||||
bool clickButtonDown = false;
|
||||
uint8_t clickButton = 0;
|
||||
unsigned long tapSeenMs = 0;
|
||||
unsigned long clickDownMs= 0;
|
||||
uint32_t statLeftClicks = 0;
|
||||
uint32_t statRightClicks = 0;
|
||||
#endif
|
||||
|
||||
bool imuStreamEnabled = false;
|
||||
bool tapPending = false;
|
||||
bool clickButtonDown = false;
|
||||
uint8_t clickButton = 0;
|
||||
unsigned long tapSeenMs = 0;
|
||||
unsigned long clickDownMs = 0;
|
||||
#ifdef FEATURE_IMU_STREAM
|
||||
bool imuStreamEnabled = false;
|
||||
#endif
|
||||
|
||||
bool pendingCal = false;
|
||||
bool pendingReset = false;
|
||||
|
||||
ChargeStatus lastChargeStatus = CHGSTAT_DISCHARGING;
|
||||
|
||||
int idleFrames = 0;
|
||||
unsigned long idleStartMs = 0;
|
||||
unsigned long lastTime = 0;
|
||||
@@ -203,33 +260,73 @@ unsigned long lastHeartbeat = 0;
|
||||
unsigned long lastTelemetry = 0;
|
||||
unsigned long bootStartMs = 0;
|
||||
|
||||
#ifdef FEATURE_TELEMETRY
|
||||
uint16_t statRecalCount = 0;
|
||||
float statBiasRms = 0.0f;
|
||||
#endif
|
||||
|
||||
// ─── I2C helpers ──────────────────────────────────────────────────────────────
|
||||
void imuWriteReg(uint8_t reg, uint8_t val) {
|
||||
Wire.beginTransmission(0x6A); Wire.write(reg); Wire.write(val); Wire.endTransmission();
|
||||
// LSM6DS3 is on Wire1 (internal I2C, SDA=P0.17, SCL=P0.16), NOT Wire (external pins 4/5)
|
||||
Wire1.beginTransmission(0x6A); Wire1.write(reg); Wire1.write(val); Wire1.endTransmission();
|
||||
}
|
||||
uint8_t imuReadReg(uint8_t reg) {
|
||||
Wire.beginTransmission(0x6A); Wire.write(reg); Wire.endTransmission(false);
|
||||
Wire.requestFrom((uint8_t)0x6A, (uint8_t)1);
|
||||
return Wire.available() ? Wire.read() : 0;
|
||||
Wire1.beginTransmission(0x6A); Wire1.write(reg); Wire1.endTransmission(false);
|
||||
Wire1.requestFrom((uint8_t)0x6A, (uint8_t)1);
|
||||
return Wire1.available() ? Wire1.read() : 0;
|
||||
}
|
||||
|
||||
// ─── Temperature ──────────────────────────────────────────────────────────────
|
||||
float readIMUTemp() {
|
||||
int16_t raw = (int16_t)((imuReadReg(LSM6DS3_OUT_TEMP_H) << 8) | imuReadReg(LSM6DS3_OUT_TEMP_L));
|
||||
int16_t raw = (int16_t)((imuReadReg(REG_OUT_TEMP_H) << 8) | imuReadReg(REG_OUT_TEMP_L));
|
||||
return 25.0f + (float)raw / 256.0f;
|
||||
}
|
||||
|
||||
// ─── Tap detection ────────────────────────────────────────────────────────────
|
||||
#ifdef FEATURE_TAP_DETECTION
|
||||
void setupTapDetection() {
|
||||
imuWriteReg(LSM6DS3_CTRL1_XL, 0x60);
|
||||
imuWriteReg(LSM6DS3_TAP_CFG, 0x8E);
|
||||
imuWriteReg(LSM6DS3_TAP_THS_6D, 0x08);
|
||||
imuWriteReg(LSM6DS3_INT_DUR2, 0x77);
|
||||
imuWriteReg(LSM6DS3_WAKE_UP_THS, 0x80);
|
||||
imuWriteReg(LSM6DS3_MD1_CFG, 0x48);
|
||||
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, 0x08); // threshold 500 mg
|
||||
imuWriteReg(REG_INT_DUR2, 0x77); // DUR=7, QUIET=01, SHOCK=11
|
||||
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");
|
||||
}
|
||||
|
||||
void processTaps(unsigned long now) {
|
||||
if (clickButtonDown && (now - clickDownMs >= CLICK_HOLD_MS)) {
|
||||
blehid.mouseButtonPress(clickButton, false);
|
||||
clickButtonDown = false; clickButton = 0;
|
||||
}
|
||||
uint8_t tapSrc = imuReadReg(REG_TAP_SRC);
|
||||
bool singleTap = (tapSrc & 0x20) != 0;
|
||||
bool doubleTap = (tapSrc & 0x10) != 0;
|
||||
bool tapEvent = (tapSrc & 0x40) != 0;
|
||||
if (!tapEvent) {
|
||||
if (tapPending && (now - tapSeenMs >= DOUBLE_TAP_WINDOW_MS)) {
|
||||
tapPending = false;
|
||||
if (!clickButtonDown) {
|
||||
Serial.println("[TAP] Single → LEFT");
|
||||
blehid.mouseButtonPress(MOUSE_BUTTON_LEFT, true);
|
||||
clickButton = MOUSE_BUTTON_LEFT; clickButtonDown = true; clickDownMs = now;
|
||||
statLeftClicks++;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (doubleTap && !clickButtonDown) {
|
||||
tapPending = false;
|
||||
Serial.println("[TAP] Double → RIGHT");
|
||||
blehid.mouseButtonPress(MOUSE_BUTTON_RIGHT, true);
|
||||
clickButton = MOUSE_BUTTON_RIGHT; clickButtonDown = true; clickDownMs = now;
|
||||
statRightClicks++;
|
||||
return;
|
||||
}
|
||||
if (singleTap && !tapPending && !clickButtonDown) { tapPending = true; tapSeenMs = now; }
|
||||
}
|
||||
#endif // FEATURE_TAP_DETECTION
|
||||
|
||||
// ─── Charge mode ──────────────────────────────────────────────────────────────
|
||||
void applyChargeMode(ChargeMode mode) {
|
||||
switch (mode) {
|
||||
@@ -247,8 +344,11 @@ void loadConfig() {
|
||||
cfgFile.open(CONFIG_FILENAME, FILE_O_READ);
|
||||
if (cfgFile) {
|
||||
cfgFile.read(&cfg, sizeof(cfg)); cfgFile.close();
|
||||
if (cfg.magic != CONFIG_MAGIC) { cfg = CFG_DEFAULTS; Serial.println("[CFG] Defaults (bad magic)"); }
|
||||
else { Serial.println("[CFG] Loaded from flash"); }
|
||||
if (cfg.magic != CONFIG_MAGIC) {
|
||||
cfg = CFG_DEFAULTS; Serial.println("[CFG] Defaults (bad magic)");
|
||||
} else {
|
||||
Serial.println("[CFG] Loaded from flash");
|
||||
}
|
||||
} else { cfg = CFG_DEFAULTS; Serial.println("[CFG] Defaults (no file)"); }
|
||||
}
|
||||
|
||||
@@ -256,47 +356,49 @@ void saveConfig() {
|
||||
InternalFS.remove(CONFIG_FILENAME);
|
||||
cfgFile.open(CONFIG_FILENAME, FILE_O_WRITE);
|
||||
if (cfgFile) { cfgFile.write((uint8_t*)&cfg, sizeof(cfg)); cfgFile.close(); Serial.println("[CFG] Saved"); }
|
||||
else { Serial.println("[CFG] ERROR: write failed"); }
|
||||
else { Serial.println("[CFG] ERROR: write failed"); }
|
||||
}
|
||||
|
||||
// Push current config as a ConfigBlob to the BLE characteristic
|
||||
// ─── ConfigBlob push ─────────────────────────────────────────────────────────
|
||||
#ifdef FEATURE_CONFIG_SERVICE
|
||||
void pushConfigBlob() {
|
||||
ConfigBlob blob;
|
||||
blob.sensitivity = cfg.sensitivity;
|
||||
blob.deadZone = cfg.deadZone;
|
||||
blob.accelStrength = cfg.accelStrength;
|
||||
blob.curve = (uint8_t)cfg.curve;
|
||||
blob.axisFlip = cfg.axisFlip;
|
||||
blob.chargeMode = (uint8_t)cfg.chargeMode;
|
||||
blob._pad = 0;
|
||||
cfgBlob.write((uint8_t*)&blob, sizeof(blob));
|
||||
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;
|
||||
cfgBlob.write((uint8_t*)&b, sizeof(b));
|
||||
}
|
||||
#endif
|
||||
|
||||
void factoryReset() {
|
||||
cfg = CFG_DEFAULTS; saveConfig();
|
||||
applyChargeMode(cfg.chargeMode);
|
||||
if (!safeMode) pushConfigBlob();
|
||||
telem = {};
|
||||
#ifdef FEATURE_CONFIG_SERVICE
|
||||
if (!safeMode) pushConfigBlob();
|
||||
#endif
|
||||
#ifdef FEATURE_TELEMETRY
|
||||
telem = {};
|
||||
#endif
|
||||
#ifdef FEATURE_TAP_DETECTION
|
||||
statLeftClicks = statRightClicks = 0;
|
||||
#endif
|
||||
Serial.println("[CFG] Factory reset complete");
|
||||
}
|
||||
|
||||
// ─── BLE callbacks ────────────────────────────────────────────────────────────
|
||||
// Single callback handles the whole config blob
|
||||
#ifdef FEATURE_CONFIG_SERVICE
|
||||
void onConfigBlobWrite(uint16_t h, BLECharacteristic* c, uint8_t* d, uint16_t l) {
|
||||
if (l != sizeof(ConfigBlob)) { Serial.println("[CFG] Bad blob length"); return; }
|
||||
ConfigBlob* b = (ConfigBlob*)d;
|
||||
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);
|
||||
}
|
||||
if (b->chargeMode <= 2) { cfg.chargeMode = (ChargeMode)b->chargeMode; applyChargeMode(cfg.chargeMode); }
|
||||
saveConfig();
|
||||
Serial.print("[CFG] Blob written — sens="); Serial.print(cfg.sensitivity);
|
||||
Serial.print(" dz="); Serial.print(cfg.deadZone, 3);
|
||||
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);
|
||||
}
|
||||
@@ -307,16 +409,19 @@ void onCommandWrite(uint16_t h, BLECharacteristic* c, uint8_t* d, uint16_t l) {
|
||||
if (d[0] == 0xFF) pendingReset = true;
|
||||
}
|
||||
|
||||
#ifdef FEATURE_IMU_STREAM
|
||||
void onImuStreamCccd(uint16_t conn_hdl, BLECharacteristic* chr, uint16_t value) {
|
||||
imuStreamEnabled = (value == BLE_GATT_HVX_NOTIFICATION);
|
||||
Serial.print("[STREAM] "); Serial.println(imuStreamEnabled ? "ON" : "OFF");
|
||||
}
|
||||
#endif
|
||||
#endif // FEATURE_CONFIG_SERVICE
|
||||
|
||||
// ─── BLE service setup ────────────────────────────────────────────────────────
|
||||
#ifdef FEATURE_CONFIG_SERVICE
|
||||
void setupConfigService() {
|
||||
cfgService.begin();
|
||||
|
||||
// ConfigBlob — R/W 16 bytes
|
||||
cfgBlob.setProperties(CHR_PROPS_READ | CHR_PROPS_WRITE);
|
||||
cfgBlob.setPermission(SECMODE_OPEN, SECMODE_OPEN);
|
||||
cfgBlob.setFixedLen(sizeof(ConfigBlob));
|
||||
@@ -324,29 +429,43 @@ void setupConfigService() {
|
||||
cfgBlob.begin();
|
||||
pushConfigBlob();
|
||||
|
||||
// Command — W 1 byte
|
||||
cfgCommand.setProperties(CHR_PROPS_WRITE);
|
||||
cfgCommand.setPermission(SECMODE_OPEN, SECMODE_OPEN);
|
||||
cfgCommand.setFixedLen(1);
|
||||
cfgCommand.setWriteCallback(onCommandWrite);
|
||||
cfgCommand.begin();
|
||||
|
||||
// Telemetry — R/N 24 bytes
|
||||
cfgTelemetry.setProperties(CHR_PROPS_READ | CHR_PROPS_NOTIFY);
|
||||
cfgTelemetry.setPermission(SECMODE_OPEN, SECMODE_NO_ACCESS);
|
||||
cfgTelemetry.setFixedLen(sizeof(TelemetryPacket));
|
||||
cfgTelemetry.begin();
|
||||
cfgTelemetry.write((uint8_t*)&telem, sizeof(telem));
|
||||
#ifdef FEATURE_TELEMETRY
|
||||
cfgTelemetry.setProperties(CHR_PROPS_READ | CHR_PROPS_NOTIFY);
|
||||
cfgTelemetry.setPermission(SECMODE_OPEN, SECMODE_NO_ACCESS);
|
||||
cfgTelemetry.setFixedLen(sizeof(TelemetryPacket));
|
||||
cfgTelemetry.begin();
|
||||
cfgTelemetry.write((uint8_t*)&telem, sizeof(telem));
|
||||
#endif
|
||||
|
||||
// ImuStream — N 14 bytes
|
||||
cfgImuStream.setProperties(CHR_PROPS_NOTIFY);
|
||||
cfgImuStream.setPermission(SECMODE_OPEN, SECMODE_NO_ACCESS);
|
||||
cfgImuStream.setFixedLen(sizeof(ImuPacket));
|
||||
cfgImuStream.setCccdWriteCallback(onImuStreamCccd);
|
||||
cfgImuStream.begin();
|
||||
#ifdef FEATURE_IMU_STREAM
|
||||
cfgImuStream.setProperties(CHR_PROPS_NOTIFY);
|
||||
cfgImuStream.setPermission(SECMODE_OPEN, SECMODE_NO_ACCESS);
|
||||
cfgImuStream.setFixedLen(sizeof(ImuPacket));
|
||||
cfgImuStream.setCccdWriteCallback(onImuStreamCccd);
|
||||
cfgImuStream.begin();
|
||||
#endif
|
||||
|
||||
// Print actual ATT table budget at runtime
|
||||
Serial.print("[BLE] ATT_TABLE_SIZE="); Serial.print(ATT_TABLE_SIZE);
|
||||
Serial.print(" | chars=2");
|
||||
#ifdef FEATURE_TELEMETRY
|
||||
Serial.print("+TELEM");
|
||||
#endif
|
||||
#ifdef FEATURE_IMU_STREAM
|
||||
Serial.print("+STREAM");
|
||||
#endif
|
||||
Serial.println();
|
||||
}
|
||||
#endif // FEATURE_CONFIG_SERVICE
|
||||
|
||||
// ─── Battery ──────────────────────────────────────────────────────────────────
|
||||
#ifdef FEATURE_BATTERY_MONITOR
|
||||
float readBatteryVoltage() {
|
||||
pinMode(PIN_VBAT_ENABLE, OUTPUT); digitalWrite(PIN_VBAT_ENABLE, LOW); delay(1);
|
||||
pinMode(PIN_VBAT_READ, INPUT);
|
||||
@@ -357,21 +476,26 @@ float readBatteryVoltage() {
|
||||
analogReference(AR_DEFAULT); analogReadResolution(10);
|
||||
return (raw / 4096.0f) * 3.0f * 2.0f;
|
||||
}
|
||||
int batteryPercent(float v) { return (int)constrain((v - BATT_EMPTY) / (BATT_FULL - BATT_EMPTY) * 100.f, 0, 100); }
|
||||
int batteryPercent(float v) {
|
||||
return (int)constrain((v - BATT_EMPTY) / (BATT_FULL - BATT_EMPTY) * 100.f, 0, 100);
|
||||
}
|
||||
|
||||
void updateBattery() {
|
||||
float v = readBatteryVoltage(); int pct = batteryPercent(v);
|
||||
bool chg = (digitalRead(PIN_CHG) == LOW);
|
||||
ChargeStatus status = chg ? (pct >= 99 ? CHGSTAT_FULL : CHGSTAT_CHARGING) : CHGSTAT_DISCHARGING;
|
||||
blebas.write(pct);
|
||||
// chargeStatus is now pushed via telemetry packet — no separate characteristic
|
||||
telem.chargeStatus = (uint8_t)status;
|
||||
lastChargeStatus = status;
|
||||
#ifdef FEATURE_TELEMETRY
|
||||
telem.chargeStatus = (uint8_t)status;
|
||||
#endif
|
||||
const char* st[] = {"discharging","charging","full"};
|
||||
Serial.print("[BATT] "); Serial.print(v,2); Serial.print("V ");
|
||||
Serial.print(pct); Serial.print("% "); Serial.println(st[status]);
|
||||
if (status == CHGSTAT_DISCHARGING && v < BATT_CRITICAL)
|
||||
for (int i=0; i<6; i++) { digitalWrite(LED_RED,LOW); delay(80); digitalWrite(LED_RED,HIGH); delay(80); }
|
||||
}
|
||||
#endif // FEATURE_BATTERY_MONITOR
|
||||
|
||||
// ─── Calibration ─────────────────────────────────────────────────────────────
|
||||
void calibrateGyroBias() {
|
||||
@@ -379,14 +503,20 @@ void calibrateGyroBias() {
|
||||
double sx=0, sy=0, sz=0;
|
||||
for (int i=0; i<BIAS_SAMPLES; i++) {
|
||||
sx += imu.readFloatGyroX(); sy += imu.readFloatGyroY(); sz += imu.readFloatGyroZ();
|
||||
digitalWrite(LED_BLUE, (i%20<10)); delay(5);
|
||||
digitalWrite(LED_BLUE, (i%20 < 10)); delay(5);
|
||||
}
|
||||
biasGX = (float)(sx/BIAS_SAMPLES); biasGY = (float)(sy/BIAS_SAMPLES); biasGZ = (float)(sz/BIAS_SAMPLES);
|
||||
biasGX = (float)(sx/BIAS_SAMPLES);
|
||||
biasGY = (float)(sy/BIAS_SAMPLES);
|
||||
biasGZ = (float)(sz/BIAS_SAMPLES);
|
||||
calTempC = readIMUTemp();
|
||||
angleX = angleY = accumX = accumY = 0.0f;
|
||||
telem.recalCount++;
|
||||
float bxr = biasGX*(PI/180.f), byr = biasGY*(PI/180.f), bzr = biasGZ*(PI/180.f);
|
||||
telem.biasRmsRadS = sqrtf((bxr*bxr + byr*byr + bzr*bzr) / 3.0f);
|
||||
|
||||
#ifdef FEATURE_TELEMETRY
|
||||
statRecalCount++;
|
||||
float bxr = biasGX*(PI/180.f), byr = biasGY*(PI/180.f), bzr = biasGZ*(PI/180.f);
|
||||
statBiasRms = sqrtf((bxr*bxr + byr*byr + bzr*bzr) / 3.0f);
|
||||
#endif
|
||||
|
||||
digitalWrite(LED_BLUE, HIGH);
|
||||
Serial.print("[CAL] T="); Serial.print(calTempC,1);
|
||||
Serial.print("C bias="); Serial.print(biasGX,4);
|
||||
@@ -397,54 +527,28 @@ void calibrateGyroBias() {
|
||||
// ─── Motion curve ─────────────────────────────────────────────────────────────
|
||||
float applyCurve(float v) {
|
||||
switch (cfg.curve) {
|
||||
case CURVE_SQUARE: return (v>=0 ? 1.f : -1.f) * v * v;
|
||||
case CURVE_SQRT: return (v>=0 ? 1.f : -1.f) * sqrtf(fabsf(v));
|
||||
case CURVE_SQUARE: return (v >= 0 ? 1.f : -1.f) * v * v;
|
||||
case CURVE_SQRT: return (v >= 0 ? 1.f : -1.f) * sqrtf(fabsf(v));
|
||||
default: return v;
|
||||
}
|
||||
}
|
||||
float applyAcceleration(float d) { return d * (1.0f + fabsf(d) * cfg.accelStrength); }
|
||||
|
||||
// ─── Tap state machine ────────────────────────────────────────────────────────
|
||||
void processTaps(unsigned long now) {
|
||||
if (clickButtonDown && (now - clickDownMs >= CLICK_HOLD_MS)) {
|
||||
blehid.mouseButtonPress(clickButton, false);
|
||||
clickButtonDown = false; clickButton = 0;
|
||||
}
|
||||
uint8_t tapSrc = imuReadReg(LSM6DS3_TAP_SRC);
|
||||
bool singleTap = (tapSrc & 0x20) != 0;
|
||||
bool doubleTap = (tapSrc & 0x10) != 0;
|
||||
bool tapEvent = (tapSrc & 0x40) != 0;
|
||||
if (!tapEvent) {
|
||||
if (tapPending && (now - tapSeenMs >= DOUBLE_TAP_WINDOW_MS)) {
|
||||
tapPending = false;
|
||||
if (!clickButtonDown) {
|
||||
Serial.println("[TAP] Single → LEFT");
|
||||
blehid.mouseButtonPress(MOUSE_BUTTON_LEFT, true);
|
||||
clickButton = MOUSE_BUTTON_LEFT; clickButtonDown = true; clickDownMs = now;
|
||||
telem.leftClicks++;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (doubleTap && !clickButtonDown) {
|
||||
tapPending = false;
|
||||
Serial.println("[TAP] Double → RIGHT");
|
||||
blehid.mouseButtonPress(MOUSE_BUTTON_RIGHT, true);
|
||||
clickButton = MOUSE_BUTTON_RIGHT; clickButtonDown = true; clickDownMs = now;
|
||||
telem.rightClicks++;
|
||||
return;
|
||||
}
|
||||
if (singleTap && !tapPending && !clickButtonDown) { tapPending = true; tapSeenMs = now; }
|
||||
}
|
||||
|
||||
// ─── Telemetry ────────────────────────────────────────────────────────────────
|
||||
// ─── Telemetry push ───────────────────────────────────────────────────────────
|
||||
#ifdef FEATURE_TELEMETRY
|
||||
void pushTelemetry(unsigned long now) {
|
||||
telem.uptimeSeconds = now / 1000;
|
||||
telem.tempCelsius = cachedTempC;
|
||||
// telem.chargeStatus is updated in updateBattery()
|
||||
telem.biasRmsRadS = statBiasRms;
|
||||
telem.recalCount = statRecalCount;
|
||||
#ifdef FEATURE_TAP_DETECTION
|
||||
telem.leftClicks = statLeftClicks;
|
||||
telem.rightClicks = statRightClicks;
|
||||
#endif
|
||||
cfgTelemetry.write ((uint8_t*)&telem, sizeof(telem));
|
||||
cfgTelemetry.notify((uint8_t*)&telem, sizeof(telem));
|
||||
}
|
||||
#endif
|
||||
|
||||
// ─── Advertising ─────────────────────────────────────────────────────────────
|
||||
void startAdvertising() {
|
||||
@@ -452,7 +556,9 @@ void startAdvertising() {
|
||||
Bluefruit.Advertising.addTxPower();
|
||||
Bluefruit.Advertising.addAppearance(BLE_APPEARANCE_HID_MOUSE);
|
||||
Bluefruit.Advertising.addService(blehid);
|
||||
Bluefruit.Advertising.addService(blebas);
|
||||
#ifdef FEATURE_BATTERY_MONITOR
|
||||
Bluefruit.Advertising.addService(blebas);
|
||||
#endif
|
||||
Bluefruit.Advertising.addName();
|
||||
Bluefruit.Advertising.restartOnDisconnect(true);
|
||||
Bluefruit.Advertising.setInterval(32, 244);
|
||||
@@ -471,36 +577,49 @@ void setup() {
|
||||
pinMode(LED_BLUE, OUTPUT); digitalWrite(LED_BLUE, HIGH);
|
||||
|
||||
// ── Boot-loop detection ───────────────────────────────────────────────────
|
||||
if (bootMagic != 0xCAFEBABE) { bootMagic = 0xCAFEBABE; bootCount = 0; }
|
||||
bootCount++;
|
||||
Serial.print("[BOOT] count="); Serial.println(bootCount);
|
||||
if (bootCount >= 3) {
|
||||
bootCount = 0; safeMode = true;
|
||||
Serial.println("[BOOT] Boot loop detected — safe mode");
|
||||
InternalFS.begin(); InternalFS.remove(CONFIG_FILENAME);
|
||||
for (int i=0; i<3; i++) { digitalWrite(LED_RED,LOW); delay(150); digitalWrite(LED_RED,HIGH); delay(150); }
|
||||
}
|
||||
#ifdef FEATURE_BOOT_LOOP_DETECT
|
||||
if (bootMagic != 0xCAFEBABE) { bootMagic = 0xCAFEBABE; bootCount = 0; }
|
||||
bootCount++;
|
||||
Serial.print("[BOOT] count="); Serial.println(bootCount);
|
||||
if (bootCount >= 3) {
|
||||
bootCount = 0; safeMode = true;
|
||||
Serial.println("[BOOT] Boot loop — safe mode (no config service)");
|
||||
InternalFS.begin(); InternalFS.remove(CONFIG_FILENAME);
|
||||
for (int i=0; i<3; i++) { digitalWrite(LED_RED,LOW); delay(150); digitalWrite(LED_RED,HIGH); delay(150); }
|
||||
}
|
||||
#endif
|
||||
|
||||
loadConfig();
|
||||
applyChargeMode(cfg.chargeMode);
|
||||
|
||||
// 1024 is sufficient for 4 characteristics (was 3072/2048 — both overflowed)
|
||||
Bluefruit.configAttrTableSize(1024);
|
||||
// configAttrTableSize MUST be called before Bluefruit.begin().
|
||||
// Value must be >= what HID+DIS+BAS+custom services actually need.
|
||||
// Too small crashes just as hard as too large. Floor is 1536.
|
||||
Serial.print("[BLE] ATT table: "); Serial.print(ATT_TABLE_SIZE); Serial.println(" bytes");
|
||||
Bluefruit.configAttrTableSize(ATT_TABLE_SIZE);
|
||||
|
||||
Bluefruit.begin(1, 0);
|
||||
Bluefruit.setTxPower(4);
|
||||
Bluefruit.setName(safeMode ? "IMU Mouse (safe)" : "IMU Mouse");
|
||||
Bluefruit.Periph.setConnInterval(6, 12);
|
||||
|
||||
Wire1.begin(); // LSM6DS3 is on internal I2C bus (Wire1), must init before imu.begin()
|
||||
if (imu.begin() != 0) {
|
||||
Serial.println("[ERROR] IMU init failed");
|
||||
while(1) { digitalWrite(LED_RED, !digitalRead(LED_RED)); delay(100); }
|
||||
}
|
||||
Serial.println("[OK] IMU ready");
|
||||
|
||||
setupTapDetection();
|
||||
#ifdef FEATURE_TAP_DETECTION
|
||||
setupTapDetection();
|
||||
#endif
|
||||
|
||||
cachedTempC = readIMUTemp();
|
||||
updateBattery();
|
||||
|
||||
#ifdef FEATURE_BATTERY_MONITOR
|
||||
updateBattery();
|
||||
#endif
|
||||
|
||||
calibrateGyroBias();
|
||||
|
||||
bledis.setManufacturer("Seeed Studio");
|
||||
@@ -508,19 +627,47 @@ void setup() {
|
||||
bledis.begin();
|
||||
|
||||
blehid.begin();
|
||||
blebas.begin(); blebas.write(100);
|
||||
|
||||
if (!safeMode) {
|
||||
setupConfigService();
|
||||
Serial.println("[OK] Config service started (4 characteristics)");
|
||||
} else {
|
||||
Serial.println("[SAFE] Config service skipped — basic mouse only");
|
||||
}
|
||||
#ifdef FEATURE_BATTERY_MONITOR
|
||||
blebas.begin(); blebas.write(100);
|
||||
#endif
|
||||
|
||||
#ifdef FEATURE_CONFIG_SERVICE
|
||||
if (!safeMode) {
|
||||
setupConfigService();
|
||||
Serial.println("[OK] Config service started");
|
||||
} else {
|
||||
Serial.println("[SAFE] Config service skipped");
|
||||
}
|
||||
#endif
|
||||
|
||||
startAdvertising();
|
||||
Serial.print("[OK] Advertising as '");
|
||||
Serial.print(safeMode ? "IMU Mouse (safe)" : "IMU Mouse");
|
||||
Serial.println("'");
|
||||
Serial.print("[OK] Advertising — features:");
|
||||
#ifdef FEATURE_CONFIG_SERVICE
|
||||
Serial.print(" CFG");
|
||||
#endif
|
||||
#ifdef FEATURE_TELEMETRY
|
||||
Serial.print(" TELEM");
|
||||
#endif
|
||||
#ifdef FEATURE_IMU_STREAM
|
||||
Serial.print(" STREAM");
|
||||
#endif
|
||||
#ifdef FEATURE_TAP_DETECTION
|
||||
Serial.print(" TAP");
|
||||
#endif
|
||||
#ifdef FEATURE_TEMP_COMPENSATION
|
||||
Serial.print(" TEMPCOMP");
|
||||
#endif
|
||||
#ifdef FEATURE_AUTO_RECAL
|
||||
Serial.print(" AUTORECAL");
|
||||
#endif
|
||||
#ifdef FEATURE_BATTERY_MONITOR
|
||||
Serial.print(" BATT");
|
||||
#endif
|
||||
#ifdef FEATURE_BOOT_LOOP_DETECT
|
||||
Serial.print(" BOOTDET");
|
||||
#endif
|
||||
Serial.println();
|
||||
|
||||
bootStartMs = millis();
|
||||
lastTime = lastBattTime = lastHeartbeat = lastTelemetry = millis();
|
||||
@@ -530,10 +677,13 @@ void setup() {
|
||||
void loop() {
|
||||
unsigned long now = millis();
|
||||
|
||||
if (!bootCountCleared && (now - bootStartMs >= BOOT_SAFE_MS)) {
|
||||
bootCount = 0; bootCountCleared = true;
|
||||
Serial.println("[BOOT] Stable — boot counter cleared");
|
||||
}
|
||||
// Clear boot counter after BOOT_SAFE_MS of stable running
|
||||
#ifdef FEATURE_BOOT_LOOP_DETECT
|
||||
if (!bootCountCleared && (now - bootStartMs >= BOOT_SAFE_MS)) {
|
||||
bootCount = 0; bootCountCleared = true;
|
||||
Serial.println("[BOOT] Stable — counter cleared");
|
||||
}
|
||||
#endif
|
||||
|
||||
if (pendingCal) { pendingCal = false; calibrateGyroBias(); }
|
||||
if (pendingReset) { pendingReset = false; factoryReset(); }
|
||||
@@ -545,9 +695,13 @@ void loop() {
|
||||
digitalWrite(led, LOW); delay(HEARTBEAT_DUR); digitalWrite(led, HIGH);
|
||||
}
|
||||
|
||||
if (now - lastBattTime >= BATT_REPORT_MS) { lastBattTime = now; updateBattery(); }
|
||||
#ifdef FEATURE_BATTERY_MONITOR
|
||||
if (now - lastBattTime >= BATT_REPORT_MS) { lastBattTime = now; updateBattery(); }
|
||||
#endif
|
||||
|
||||
processTaps(now);
|
||||
#ifdef FEATURE_TAP_DETECTION
|
||||
processTaps(now);
|
||||
#endif
|
||||
|
||||
if (now - lastTime < (unsigned long)LOOP_RATE_MS) return;
|
||||
float dt = (now - lastTime) / 1000.0f;
|
||||
@@ -556,21 +710,30 @@ void loop() {
|
||||
|
||||
cachedTempC = readIMUTemp();
|
||||
|
||||
if (!safeMode && (now - lastTelemetry >= TELEMETRY_MS)) {
|
||||
lastTelemetry = now; pushTelemetry(now);
|
||||
}
|
||||
#ifdef FEATURE_TELEMETRY
|
||||
if (!safeMode && (now - lastTelemetry >= TELEMETRY_MS)) {
|
||||
lastTelemetry = now; pushTelemetry(now);
|
||||
}
|
||||
#endif
|
||||
|
||||
float tempDelta = cachedTempC - calTempC;
|
||||
float correction = TEMP_COMP_COEFF_DPS_C * tempDelta;
|
||||
|
||||
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);
|
||||
// Gyro reads with optional temperature compensation
|
||||
float gx, gy, gz;
|
||||
#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);
|
||||
#endif
|
||||
|
||||
float ax = imu.readFloatAccelX();
|
||||
float ay = imu.readFloatAccelY();
|
||||
float az = imu.readFloatAccelZ();
|
||||
|
||||
// Complementary filter
|
||||
angleX = ALPHA*(angleX + gx*dt) + (1.0f - ALPHA)*atan2f(ax, sqrtf(ay*ay + az*az));
|
||||
angleY = ALPHA*(angleY + gy*dt) + (1.0f - ALPHA)*atan2f(ay, sqrtf(ax*ax + az*az));
|
||||
|
||||
@@ -582,10 +745,12 @@ void loop() {
|
||||
else { idleFrames++; if (idleStartMs == 0) idleStartMs = now; }
|
||||
bool idle = (idleFrames >= IDLE_FRAMES);
|
||||
|
||||
if (idle && idleStartMs != 0 && (now - idleStartMs >= AUTO_RECAL_MS)) {
|
||||
Serial.println("[AUTO-CAL] Long idle — recalibrating...");
|
||||
idleStartMs = 0; calibrateGyroBias(); return;
|
||||
}
|
||||
#ifdef FEATURE_AUTO_RECAL
|
||||
if (idle && idleStartMs != 0 && (now - idleStartMs >= AUTO_RECAL_MS)) {
|
||||
Serial.println("[AUTO-CAL] Long idle — recalibrating...");
|
||||
idleStartMs = 0; calibrateGyroBias(); return;
|
||||
}
|
||||
#endif
|
||||
|
||||
int8_t moveX = 0, moveY = 0;
|
||||
uint8_t flags = 0;
|
||||
@@ -605,25 +770,27 @@ void loop() {
|
||||
if (Bluefruit.connected() && (moveX != 0 || moveY != 0)) blehid.mouseMove(moveX, moveY);
|
||||
}
|
||||
|
||||
if (!safeMode && imuStreamEnabled && Bluefruit.connected()) {
|
||||
ImuPacket pkt;
|
||||
pkt.gyroY_mDPS = (int16_t)constrain(gy*(180.f/PI)*1000.f, -32000, 32000);
|
||||
pkt.gyroZ_mDPS = (int16_t)constrain(gz*(180.f/PI)*1000.f, -32000, 32000);
|
||||
pkt.accelX_mg = (int16_t)constrain(ax*1000.f, -32000, 32000);
|
||||
pkt.accelY_mg = (int16_t)constrain(ay*1000.f, -32000, 32000);
|
||||
pkt.accelZ_mg = (int16_t)constrain(az*1000.f, -32000, 32000);
|
||||
pkt.moveX = moveX;
|
||||
pkt.moveY = moveY;
|
||||
pkt.flags = flags;
|
||||
pkt._pad = 0;
|
||||
cfgImuStream.notify((uint8_t*)&pkt, sizeof(pkt));
|
||||
}
|
||||
#ifdef FEATURE_IMU_STREAM
|
||||
if (!safeMode && imuStreamEnabled && Bluefruit.connected()) {
|
||||
ImuPacket pkt;
|
||||
pkt.gyroY_mDPS = (int16_t)constrain(gy*(180.f/PI)*1000.f, -32000, 32000);
|
||||
pkt.gyroZ_mDPS = (int16_t)constrain(gz*(180.f/PI)*1000.f, -32000, 32000);
|
||||
pkt.accelX_mg = (int16_t)constrain(ax*1000.f, -32000, 32000);
|
||||
pkt.accelY_mg = (int16_t)constrain(ay*1000.f, -32000, 32000);
|
||||
pkt.accelZ_mg = (int16_t)constrain(az*1000.f, -32000, 32000);
|
||||
pkt.moveX = moveX;
|
||||
pkt.moveY = moveY;
|
||||
pkt.flags = flags;
|
||||
pkt._pad = 0;
|
||||
cfgImuStream.notify((uint8_t*)&pkt, sizeof(pkt));
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef DEBUG
|
||||
Serial.print("T="); Serial.print(cachedTempC,1);
|
||||
Serial.print(" gy="); Serial.print(gy,3);
|
||||
Serial.print(" gz="); Serial.print(gz,3);
|
||||
Serial.print(" mx="); Serial.print(moveX);
|
||||
Serial.print(" my="); Serial.println(moveY);
|
||||
#endif
|
||||
#ifdef DEBUG
|
||||
Serial.print("T="); Serial.print(cachedTempC,1);
|
||||
Serial.print(" gy="); Serial.print(gy,3);
|
||||
Serial.print(" gz="); Serial.print(gz,3);
|
||||
Serial.print(" mx="); Serial.print(moveX);
|
||||
Serial.print(" my="); Serial.println(moveY);
|
||||
#endif
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user