From aaeb33971c70623e7da8f03f7b37c50e7a14394b Mon Sep 17 00:00:00 2001 From: Valentin Squirelo Date: Sat, 5 Jul 2025 09:01:45 +0200 Subject: [PATCH 1/4] Add imu support for xiao ble sense --- .github/workflows/build-nrf52.yml | 19 +- config-tool-web/code.js | 53 +- config-tool-web/examples.js | 213 ++++++- config-tool-web/index.html | 33 + config-tool-web/usages.js | 17 +- config-tool/common.py | 3 +- config-tool/get_config.py | 7 +- config-tool/set_config.py | 10 +- firmware-bluetooth/CMakeLists.txt | 1 + .../seeed_xiao_nrf52840_sense.conf | 5 + .../seeed_xiao_nrf52840_sense.overlay | 53 ++ firmware-bluetooth/prj.conf | 6 + firmware-bluetooth/src/imu.cc | 584 ++++++++++++++++++ firmware-bluetooth/src/imu.h | 13 + firmware-bluetooth/src/imu_descriptor.h | 50 ++ firmware-bluetooth/src/main.cc | 24 +- firmware/src/config.cc | 40 +- firmware/src/globals.cc | 3 + firmware/src/globals.h | 3 + firmware/src/types.h | 22 +- 20 files changed, 1136 insertions(+), 23 deletions(-) create mode 100644 firmware-bluetooth/boards/arm/seeed_xiao_nrf52840/seeed_xiao_nrf52840_sense.conf create mode 100644 firmware-bluetooth/boards/arm/seeed_xiao_nrf52840/seeed_xiao_nrf52840_sense.overlay create mode 100644 firmware-bluetooth/src/imu.cc create mode 100644 firmware-bluetooth/src/imu.h create mode 100644 firmware-bluetooth/src/imu_descriptor.h diff --git a/.github/workflows/build-nrf52.yml b/.github/workflows/build-nrf52.yml index a313d59d..c4a4d194 100644 --- a/.github/workflows/build-nrf52.yml +++ b/.github/workflows/build-nrf52.yml @@ -13,15 +13,24 @@ jobs: runs-on: ubuntu-22.04 strategy: matrix: - board: ["adafruit_feather_nrf52840", "seeed_xiao_nrf52840"] + include: + - board: "adafruit_feather_nrf52840" + name: "adafruit_feather_nrf52840" + extra_args: "" + - board: "seeed_xiao_nrf52840" + name: "seeed_xiao_nrf52840" + extra_args: "" + - board: "seeed_xiao_nrf52840" + name: "seeed_xiao_nrf52840_sense" + extra_args: "-- \"-DOVERLAY_CONFIG=boards/arm/seeed_xiao_nrf52840/seeed_xiao_nrf52840_sense.conf\" \"-DDTC_OVERLAY_FILE=boards/arm/seeed_xiao_nrf52840/seeed_xiao_nrf52840_sense.overlay\"" steps: - uses: actions/checkout@v4 - name: Build run: | docker run -v $PWD:/workdir/project -w /workdir/project/firmware-bluetooth nordicplayground/nrfconnect-sdk:v2.2-branch \ - west build -b ${{ matrix.board }} - cp firmware-bluetooth/build/zephyr/remapper.uf2 firmware-bluetooth/remapper_${{ matrix.board }}.uf2 + west build -b ${{ matrix.board }} ${{ matrix.extra_args }} + cp firmware-bluetooth/build/zephyr/remapper.uf2 firmware-bluetooth/remapper_${{ matrix.name }}.uf2 - uses: actions/upload-artifact@v4 with: - name: artifact-${{ matrix.board }} - path: firmware-bluetooth/remapper_${{ matrix.board }}.uf2 + name: artifact-${{ matrix.name }} + path: firmware-bluetooth/remapper_${{ matrix.name }}.uf2 diff --git a/config-tool-web/code.js b/config-tool-web/code.js index 01adc610..130b8dfc 100644 --- a/config-tool-web/code.js +++ b/config-tool-web/code.js @@ -8,7 +8,7 @@ const STICKY_FLAG = 1 << 0; const TAP_FLAG = 1 << 1; const HOLD_FLAG = 1 << 2; const CONFIG_SIZE = 32; -const CONFIG_VERSION = 18; +const CONFIG_VERSION = 19; const VENDOR_ID = 0xCAFE; const PRODUCT_ID = 0xBAF2; const DEFAULT_PARTIAL_SCROLL_TIMEOUT = 1000000; @@ -16,6 +16,8 @@ const DEFAULT_TAP_HOLD_THRESHOLD = 200000; const DEFAULT_GPIO_DEBOUNCE_TIME = 5; const DEFAULT_SCALING = 1000; const DEFAULT_MACRO_ENTRY_DURATION = 1; +const DEFAULT_IMU_ANGLE_CLAMP_LIMIT = 45; +const DEFAULT_IMU_FILTER_BUFFER_SIZE = 10; const NLAYERS = 8; const NMACROS = 32; @@ -24,6 +26,7 @@ const MACRO_ITEMS_IN_PACKET = 6; const IGNORE_AUTH_DEV_INPUTS_FLAG = 1 << 4; const GPIO_OUTPUT_MODE_FLAG = 1 << 5; const NORMALIZE_GAMEPAD_INPUTS_FLAG = 1 << 6; +const IMU_ENABLE_FLAG = 1 << 7; const HUB_PORT_NONE = 255; const QUIRK_FLAG_RELATIVE_MASK = 0b10000000; @@ -148,6 +151,9 @@ let config = { 'gpio_output_mode': 0, 'input_labels': 0, 'normalize_gamepad_inputs': true, + 'imu_enabled': false, + 'imu_angle_clamp_limit': DEFAULT_IMU_ANGLE_CLAMP_LIMIT, + 'imu_filter_buffer_size': DEFAULT_IMU_FILTER_BUFFER_SIZE, mappings: [{ 'source_usage': '0x00000000', 'target_usage': '0x00000000', @@ -200,6 +206,8 @@ document.addEventListener("DOMContentLoaded", function () { document.getElementById("tap_hold_threshold_input").addEventListener("change", tap_hold_threshold_onchange); document.getElementById("gpio_debounce_time_input").addEventListener("change", gpio_debounce_time_onchange); document.getElementById("macro_entry_duration_input").addEventListener("change", macro_entry_duration_onchange); + document.getElementById("imu_angle_clamp_limit_input").addEventListener("change", imu_angle_clamp_limit_onchange); + document.getElementById("imu_filter_buffer_size_input").addEventListener("change", imu_filter_buffer_size_onchange); for (let i = 0; i < NLAYERS; i++) { document.getElementById("unmapped_passthrough_checkbox" + i).addEventListener("change", unmapped_passthrough_onchange); } @@ -210,6 +218,9 @@ document.addEventListener("DOMContentLoaded", function () { document.getElementById("input_labels_modal_dropdown").addEventListener("change", input_labels_onchange("input_labels_modal_dropdown")); document.getElementById("ignore_auth_dev_inputs_checkbox").addEventListener("change", ignore_auth_dev_inputs_onchange); document.getElementById("normalize_gamepad_inputs_checkbox").addEventListener("change", normalize_gamepad_inputs_onchange); + document.getElementById("imu_enabled_checkbox").addEventListener("change", imu_enabled_onchange); + document.getElementById("imu_angle_clamp_limit_input").addEventListener("change", imu_angle_clamp_limit_onchange); + document.getElementById("imu_filter_buffer_size_input").addEventListener("change", imu_filter_buffer_size_onchange); document.getElementById("nav-monitor-tab").addEventListener("shown.bs.tab", monitor_tab_shown); document.getElementById("nav-monitor-tab").addEventListener("hide.bs.tab", monitor_tab_hide); @@ -290,8 +301,8 @@ async function load_from_device() { try { await send_feature_command(GET_CONFIG); - const [config_version, flags, unmapped_passthrough_layer_mask, partial_scroll_timeout, mapping_count, our_usage_count, their_usage_count, interval_override, tap_hold_threshold, gpio_debounce_time_ms, our_descriptor_number, macro_entry_duration, quirk_count] = - await read_config_feature([UINT8, UINT8, UINT8, UINT32, UINT16, UINT32, UINT32, UINT8, UINT32, UINT8, UINT8, UINT8, UINT16]); + const [config_version, flags, unmapped_passthrough_layer_mask, partial_scroll_timeout, mapping_count, our_usage_count, their_usage_count, interval_override, tap_hold_threshold, gpio_debounce_time_ms, our_descriptor_number, macro_entry_duration, quirk_count, imu_angle_clamp_limit, imu_filter_buffer_size] = + await read_config_feature([UINT8, UINT8, UINT8, UINT32, UINT16, UINT32, UINT32, UINT8, UINT32, UINT8, UINT8, UINT8, UINT16, UINT8, UINT8]); check_received_version(config_version); config['version'] = config_version; @@ -304,7 +315,10 @@ async function load_from_device() { config['ignore_auth_dev_inputs'] = !!(flags & IGNORE_AUTH_DEV_INPUTS_FLAG); config['gpio_output_mode'] = (flags & GPIO_OUTPUT_MODE_FLAG) ? 1 : 0; config['normalize_gamepad_inputs'] = !!(flags & NORMALIZE_GAMEPAD_INPUTS_FLAG); + config['imu_enabled'] = !!(flags & IMU_ENABLE_FLAG); config['macro_entry_duration'] = macro_entry_duration + 1; + config['imu_angle_clamp_limit'] = imu_angle_clamp_limit; + config['imu_filter_buffer_size'] = imu_filter_buffer_size; config['mappings'] = []; for (let i = 0; i < mapping_count; i++) { @@ -442,7 +456,8 @@ async function save_to_device() { await send_feature_command(SUSPEND); const flags = (config['ignore_auth_dev_inputs'] ? IGNORE_AUTH_DEV_INPUTS_FLAG : 0) | (config['gpio_output_mode'] ? GPIO_OUTPUT_MODE_FLAG : 0) | - (config['normalize_gamepad_inputs'] ? NORMALIZE_GAMEPAD_INPUTS_FLAG : 0); + (config['normalize_gamepad_inputs'] ? NORMALIZE_GAMEPAD_INPUTS_FLAG : 0) | + (config['imu_enabled'] ? IMU_ENABLE_FLAG : 0); await send_feature_command(SET_CONFIG, [ [UINT8, flags], [UINT8, layer_list_to_mask(config['unmapped_passthrough_layers'])], @@ -452,6 +467,8 @@ async function save_to_device() { [UINT8, config['gpio_debounce_time_ms']], [UINT8, config['our_descriptor_number']], [UINT8, config['macro_entry_duration'] - 1], + [UINT8, config['imu_angle_clamp_limit']], + [UINT8, config['imu_filter_buffer_size']], ]); await send_feature_command(CLEAR_MAPPING); @@ -632,6 +649,9 @@ function set_config_ui_state() { document.getElementById('input_labels_dropdown').value = config['input_labels']; document.getElementById('input_labels_modal_dropdown').value = config['input_labels']; document.getElementById('normalize_gamepad_inputs_checkbox').checked = config['normalize_gamepad_inputs']; + document.getElementById('imu_enabled_checkbox').checked = config['imu_enabled']; + document.getElementById('imu_angle_clamp_limit_input').value = config['imu_angle_clamp_limit'] ?? DEFAULT_IMU_ANGLE_CLAMP_LIMIT; + document.getElementById('imu_filter_buffer_size_input').value = config['imu_filter_buffer_size'] ?? DEFAULT_IMU_FILTER_BUFFER_SIZE; } function set_mappings_ui_state() { @@ -764,6 +784,11 @@ function set_ui_state() { // set it to false to preserve previous behavior. config['normalize_gamepad_inputs'] = false; } + if (config['version'] < 19) { + // IMU settings were added in version 19 + config['imu_angle_clamp_limit'] = DEFAULT_IMU_ANGLE_CLAMP_LIMIT; + config['imu_filter_buffer_size'] = DEFAULT_IMU_FILTER_BUFFER_SIZE; + } if (config['version'] < CONFIG_VERSION) { config['version'] = CONFIG_VERSION; } @@ -1427,6 +1452,26 @@ function normalize_gamepad_inputs_onchange() { config['normalize_gamepad_inputs'] = document.getElementById("normalize_gamepad_inputs_checkbox").checked; } +function imu_enabled_onchange() { + config['imu_enabled'] = document.getElementById("imu_enabled_checkbox").checked; +} + +function imu_angle_clamp_limit_onchange() { + let value = parseInt(document.getElementById("imu_angle_clamp_limit_input").value, 10); + if (isNaN(value)) { + value = DEFAULT_IMU_ANGLE_CLAMP_LIMIT; + } + if (value > 90) { + value = 90; + document.getElementById("imu_angle_clamp_limit_input").value = 90; + } + config['imu_angle_clamp_limit'] = value; +} + +function imu_filter_buffer_size_onchange() { + config['imu_filter_buffer_size'] = parseInt(document.getElementById("imu_filter_buffer_size_input").value, 10); +} + function macro_entry_duration_onchange() { let value = parseInt(document.getElementById("macro_entry_duration_input").value, 10); if (isNaN(value)) { diff --git a/config-tool-web/examples.js b/config-tool-web/examples.js index e0a208d5..97cdb032 100644 --- a/config-tool-web/examples.js +++ b/config-tool-web/examples.js @@ -1,4 +1,215 @@ const examples = [ + { + 'description': 'IMU mouse control', + 'config': { + "version": 19, + "unmapped_passthrough_layers": [], + "partial_scroll_timeout": 1000000, + "interval_override": 0, + "tap_hold_threshold": 200000, + "mappings": [ + { + "target_usage": "0x00010030", + "source_usage": "0xfff30001", + "scaling": 1000, + "layers": [ + 0 + ], + "sticky": false, + "tap": false, + "hold": false, + "source_port": 0, + "target_port": 0 + }, + { + "target_usage": "0x00010031", + "source_usage": "0xfff30002", + "scaling": 1000, + "layers": [ + 0 + ], + "sticky": false, + "tap": false, + "hold": false, + "source_port": 0, + "target_port": 0 + }, + { + "target_usage": "0x00090001", + "source_usage": "0xfff30003", + "scaling": 1000, + "layers": [ + 0 + ], + "sticky": false, + "tap": true, + "hold": false, + "source_port": 0, + "target_port": 0 + } + ], + "macros": [ + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [] + ], + "expressions": [ + "0x0020008f input_state dup abs 5000000 gt mul 5000000 div", + "0x0020008e input_state dup abs 5000000 gt mul 5000000 div", + "0x00200073 input_state 100000 gt", + "", + "", + "", + "", + "" + ], + "gpio_debounce_time_ms": 5, + "our_descriptor_number": 0, + "ignore_auth_dev_inputs": false, + "macro_entry_duration": 1, + "gpio_output_mode": 0, + "quirks": [], + "input_labels": 0, + "normalize_gamepad_inputs": false, + "imu_enabled": true, + "imu_angle_clamp_limit": 30, + "imu_filter_buffer_size": 5 + } + }, + { + 'description': 'IMU Switch gamepad', + 'config': { + "version": 19, + "unmapped_passthrough_layers": [], + "partial_scroll_timeout": 1000000, + "interval_override": 0, + "tap_hold_threshold": 200000, + "mappings": [ + { + "target_usage": "0x00010030", + "source_usage": "0x0020008f", + "scaling": 1000, + "layers": [ + 0 + ], + "sticky": false, + "tap": false, + "hold": false, + "source_port": 0, + "target_port": 2 + }, + { + "target_usage": "0x00010031", + "source_usage": "0x0020008e", + "scaling": 1000, + "layers": [ + 0 + ], + "sticky": false, + "tap": false, + "hold": false, + "source_port": 0, + "target_port": 2 + }, + { + "target_usage": "0x00090003", + "source_usage": "0xfff30001", + "scaling": 1000, + "layers": [ + 0 + ], + "sticky": false, + "tap": true, + "hold": false, + "source_port": 0, + "target_port": 0 + } + ], + "macros": [ + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [] + ], + "expressions": [ + "0x00200073 input_state 100000 gt", + "", + "", + "", + "", + "", + "" + ], + "gpio_debounce_time_ms": 5, + "our_descriptor_number": 2, + "ignore_auth_dev_inputs": false, + "macro_entry_duration": 1, + "gpio_output_mode": 0, + "quirks": [], + "input_labels": 0, + "normalize_gamepad_inputs": false, + "imu_enabled": true, + "imu_angle_clamp_limit": 30, + "imu_filter_buffer_size": 5 + } + }, { 'description': 'map caps lock to control', 'config': { @@ -10069,4 +10280,4 @@ const examples = [ } ]; -export default examples; +export default examples; \ No newline at end of file diff --git a/config-tool-web/index.html b/config-tool-web/index.html index 43d396bc..9344f7e4 100644 --- a/config-tool-web/index.html +++ b/config-tool-web/index.html @@ -227,9 +227,42 @@

HID Remapper Configuration

+
+
+ +
+
+ +
+
+
+
+ +
+
+
+ + degrees +
+
1 to 90 degrees
+
+
+
+
+ +
+
+
+ + samples +
+
1(slow) to 16(fast)
+
+

Changes to the emulated device type become active after disconnecting and reconnecting HID Remapper.

Changes to gamepad input normalization are applied after re-plugging the device or HID Remapper.

+

Changes to IMU enable setting are applied after re-plugging the device or HID Remapper.

Custom usages
diff --git a/config-tool-web/usages.js b/config-tool-web/usages.js index 3bce62d8..38e760c5 100644 --- a/config-tool-web/usages.js +++ b/config-tool-web/usages.js @@ -12,6 +12,11 @@ const usages = { "0x00010031": { 'name': 'Cursor Y', 'class': 'mouse' }, "0x00010038": { 'name': 'V scroll', 'class': 'mouse' }, "0x000c0238": { 'name': 'H scroll', 'class': 'mouse' }, + + "0x0020008d": { 'name': 'Yaw', 'class': 'mouse' }, + "0x0020008e": { 'name': 'Pitch', 'class': 'mouse' }, + "0x0020008f": { 'name': 'Roll', 'class': 'mouse' }, + "0x00200073": { 'name': 'Magnitude', 'class': 'mouse' }, }, 'source_1': { "0x00010030": { 'name': 'Left stick X', 'class': 'gamepad' }, @@ -199,6 +204,11 @@ const usages = { "0x000c00b6": { 'name': 'Previous track', 'class': 'media' }, "0x00000000": { 'name': 'Nothing', 'class': 'other' }, + + "0x0020008d": { 'name': 'Yaw', 'class': 'mouse' }, + "0x0020008e": { 'name': 'Pitch', 'class': 'mouse' }, + "0x0020008f": { 'name': 'Roll', 'class': 'mouse' }, + "0x00200073": { 'name': 'Magnitude', 'class': 'mouse' }, "0xfff30001": { 'name': 'Expression 1', 'class': 'other' }, "0xfff30002": { 'name': 'Expression 2', 'class': 'other' }, "0xfff30003": { 'name': 'Expression 3', 'class': 'other' }, @@ -425,6 +435,11 @@ const usages = { "0x000c00b7": { 'name': 'Stop', 'class': 'media' }, "0x000c00b5": { 'name': 'Next track', 'class': 'media' }, "0x000c00b6": { 'name': 'Previous track', 'class': 'media' }, + + "0x0020008d": { 'name': 'Yaw', 'class': 'mouse' }, + "0x0020008e": { 'name': 'Pitch', 'class': 'mouse' }, + "0x0020008f": { 'name': 'Roll', 'class': 'mouse' }, + "0x00200073": { 'name': 'Magnitude', 'class': 'mouse' }, }, 2: { "0x00090001": { 'name': 'Y', 'class': 'mouse' }, @@ -647,4 +662,4 @@ Object.assign(usages[4], common_target_usages); Object.assign(usages[5], common_target_usages); usages[1] = usages[0]; // absolute mouse & keyboard is the same as regular mouse & keyboard -export default usages; +export default usages; \ No newline at end of file diff --git a/config-tool/common.py b/config-tool/common.py index 1dcbcb91..6e767bcd 100644 --- a/config-tool/common.py +++ b/config-tool/common.py @@ -11,7 +11,7 @@ CONFIG_USAGE_PAGE = 0xFF00 CONFIG_USAGE = 0x0020 -CONFIG_VERSION = 18 +CONFIG_VERSION = 19 CONFIG_SIZE = 32 REPORT_ID_CONFIG = 100 @@ -61,6 +61,7 @@ IGNORE_AUTH_DEV_INPUTS_FLAG = 1 << 4 GPIO_OUTPUT_MODE_FLAG = 1 << 5 NORMALIZE_GAMEPAD_INPUTS_FLAG = 1 << 6 +IMU_ENABLE_FLAG = 1 << 7 NMACROS = 32 NEXPRESSIONS = 8 diff --git a/config-tool/get_config.py b/config-tool/get_config.py index 376cc88a..63b83cf3 100755 --- a/config-tool/get_config.py +++ b/config-tool/get_config.py @@ -27,9 +27,11 @@ our_descriptor_number, macro_entry_duration, quirk_count, + imu_angle_clamp_limit, + imu_filter_buffer_size, *_, crc, -) = struct.unpack("= 18 else False ) +imu_enabled = config.get("imu_enabled", False) +imu_angle_clamp_limit = config.get("imu_angle_clamp_limit", 90) +imu_filter_buffer_size = config.get("imu_filter_buffer_size", 10) flags = 0 flags |= IGNORE_AUTH_DEV_INPUTS_FLAG if ignore_auth_dev_inputs else 0 flags |= GPIO_OUTPUT_MODE_FLAG if gpio_output_mode == 1 else 0 flags |= NORMALIZE_GAMEPAD_INPUTS_FLAG if normalize_gamepad_inputs else 0 +flags |= IMU_ENABLE_FLAG if imu_enabled else 0 data = struct.pack( - " +#include + +/ { + aliases { + accel0 = &lsm6ds3tr_c; + }; + + lsm6ds3tr-c-en { + compatible = "regulator-fixed-sync", "regulator-fixed"; + enable-gpios = <&gpio1 8 (NRF_GPIO_DRIVE_S0H1 | GPIO_ACTIVE_HIGH)>; + regulator-name = "LSM6DS3TR_C_EN"; + regulator-boot-on; + regulator-always-on; + startup-delay-us = <10000>; + }; +}; + +&pinctrl { + i2c0_default: i2c0_default { + group1 { + psels = , + ; + }; + }; + + i2c0_sleep: i2c0_sleep { + group1 { + psels = , + ; + low-power-enable; + }; + }; +}; + +&i2c0 { + compatible = "nordic,nrf-twim"; + /* Cannot be used together with spi0. */ + status = "okay"; + pinctrl-0 = <&i2c0_default>; + pinctrl-1 = <&i2c0_sleep>; + pinctrl-names = "default", "sleep"; + clock-frequency = ; + + zephyr,concat-buf-size = <48>; + + lsm6ds3tr_c: lsm6ds3tr-c@6a { + compatible = "st,lsm6dsl"; + reg = <0x6a>; + irq-gpios = <&gpio0 11 GPIO_ACTIVE_HIGH>; + status = "okay"; + }; +}; \ No newline at end of file diff --git a/firmware-bluetooth/prj.conf b/firmware-bluetooth/prj.conf index 99e32201..6a79a379 100644 --- a/firmware-bluetooth/prj.conf +++ b/firmware-bluetooth/prj.conf @@ -23,6 +23,12 @@ CONFIG_SETTINGS=y CONFIG_GPIO=y +CONFIG_SENSOR=y +CONFIG_I2C=y +CONFIG_LSM6DSL_TRIGGER_GLOBAL_THREAD=y +CONFIG_LSM6DSL=y +CONFIG_CBPRINTF_FP_SUPPORT=y + CONFIG_BT=y CONFIG_BT_DEBUG_LOG=y CONFIG_BT_CENTRAL=y diff --git a/firmware-bluetooth/src/imu.cc b/firmware-bluetooth/src/imu.cc new file mode 100644 index 00000000..fbd95fa4 --- /dev/null +++ b/firmware-bluetooth/src/imu.cc @@ -0,0 +1,584 @@ +#include "imu.h" +#include "imu_descriptor.h" +#include "config.h" +#include "descriptor_parser.h" +#include "globals.h" +#include "platform.h" +#include "remapper.h" + +#include +#include +#include +#include +#include +#include + +#if DT_NODE_EXISTS(DT_NODELABEL(lsm6ds3tr_c)) + +#define IMU_VIRTUAL_INTERFACE 0x1000 +#define CALIBRATION_SAMPLES 200 + +#define IMU_SAMPLE_RATE_MS 15 +#define MAX_ERROR_COUNT_BEFORE_BACKOFF 5 +#define ERROR_BACKOFF_MULTIPLIER 4 +#define CALIBRATION_RETRY_DELAY_MS 500 + +#define PI 3.14159265359f +#define DEG_TO_RAD (PI / 180.0f) +#define RAD_TO_DEG (180.0f / PI) +#define GRAVITY 9.81f + +#define LED_ACTIVITY_DURATION_MS 50 + +#define MIN_DT_SECONDS 0.005f +#define MAX_DT_SECONDS 0.050f +#define EXPECTED_DT_SECONDS 0.015f +#define CALIBRATION_SAMPLE_DELAY_MS 5 + +#define IMU_ODR_FREQUENCY 52 +#define ACCEL_SCALE_RANGE 2 +#define GYRO_SCALE_RANGE 125 + +#define MAX_FILTER_BUFFER_SIZE 16 + +typedef struct { + float beta_base; + float beta_min; + float beta_max; + float stationary_threshold; + float accel_trust_threshold_high; + float accel_trust_threshold_low; + float bias_update_rate; + float gyro_deadzone; + float angle_clamp_limit; + float magnitude_filter_alpha; +} imu_config_t; + +static imu_config_t imu_config = { + .beta_base = 0.1f, + .beta_min = 0.01f, + .beta_max = 0.3f, + .stationary_threshold = 0.01f, + .accel_trust_threshold_high = 2.0f, + .accel_trust_threshold_low = 0.5f, + .bias_update_rate = 0.001f, + .gyro_deadzone = 0.001f, + .angle_clamp_limit = 45.0f, + .magnitude_filter_alpha = 0.9f +}; + +typedef struct { + float y, alpha; +} iir_t; + +typedef struct { + float buffer[MAX_FILTER_BUFFER_SIZE]; + int index; + int count; + int size; + bool initialized; +} moving_avg_filter_t; + +static volatile float madgwick_q0 = 1.0f; +static volatile float madgwick_q1 = 0.0f; +static volatile float madgwick_q2 = 0.0f; +static volatile float madgwick_q3 = 0.0f; + +static const struct device* imu_dev; +static void imu_work_fn(struct k_work* work); +static K_WORK_DELAYABLE_DEFINE(imu_work, imu_work_fn); + +static volatile float pitch_offset = 0.0f; +static volatile float roll_offset = 0.0f; +static int64_t last_timestamp = 0; + +static volatile float gyro_bias_x = 0.0f; +static volatile float gyro_bias_y = 0.0f; +static volatile float gyro_bias_z = 0.0f; +static float accel_bias_x = 0.0f; +static float accel_bias_y = 0.0f; +static float accel_bias_z = 0.0f; +static bool is_calibrated = false; + +static iir_t magnitude_filter = {.y = 9.81f, .alpha = 0.9f}; +static uint32_t error_count = 0; + +static uint8_t last_known_angle_clamp_limit = 90; + +static moving_avg_filter_t pitch_filter = { + .index = 0, + .count = 0, + .initialized = false +}; + +static moving_avg_filter_t roll_filter = { + .index = 0, + .count = 0, + .initialized = false +}; + +extern const struct gpio_dt_spec led0; +extern struct k_work_delayable activity_led_off_work; + +static float inv_sqrt(float x) { + float halfx = 0.5f * x; + float y = x; + long i = *(long*)&y; + i = 0x5f3759df - (i >> 1); + y = *(float*)&i; + y = y * (1.5f - (halfx * y * y)); + return y; +} + +static float compute_dynamic_beta(float hp_magnitude) { + if (hp_magnitude < imu_config.accel_trust_threshold_low) { + return imu_config.beta_max; + } else if (hp_magnitude > imu_config.accel_trust_threshold_high) { + return imu_config.beta_min; + } else { + float ratio = (hp_magnitude - imu_config.accel_trust_threshold_low) / + (imu_config.accel_trust_threshold_high - imu_config.accel_trust_threshold_low); + return imu_config.beta_max - ratio * (imu_config.beta_max - imu_config.beta_min); + } +} + +static void madgwick_update_imu(float gx, float gy, float gz, float ax, float ay, float az, float dt, float beta) { + float recipNorm; + float s0, s1, s2, s3; + float qDot1, qDot2, qDot3, qDot4; + float _2q0, _2q1, _2q2, _2q3, _4q0, _4q1, _4q2, _8q1, _8q2, q0q0, q1q1, q2q2, q3q3; + + qDot1 = 0.5f * (-madgwick_q1 * gx - madgwick_q2 * gy - madgwick_q3 * gz); + qDot2 = 0.5f * (madgwick_q0 * gx + madgwick_q2 * gz - madgwick_q3 * gy); + qDot3 = 0.5f * (madgwick_q0 * gy - madgwick_q1 * gz + madgwick_q3 * gx); + qDot4 = 0.5f * (madgwick_q0 * gz + madgwick_q1 * gy - madgwick_q2 * gx); + + if (!((ax == 0.0f) && (ay == 0.0f) && (az == 0.0f))) { + + recipNorm = inv_sqrt(ax * ax + ay * ay + az * az); + ax *= recipNorm; + ay *= recipNorm; + az *= recipNorm; + + _2q0 = 2.0f * madgwick_q0; + _2q1 = 2.0f * madgwick_q1; + _2q2 = 2.0f * madgwick_q2; + _2q3 = 2.0f * madgwick_q3; + _4q0 = 4.0f * madgwick_q0; + _4q1 = 4.0f * madgwick_q1; + _4q2 = 4.0f * madgwick_q2; + _8q1 = 8.0f * madgwick_q1; + _8q2 = 8.0f * madgwick_q2; + q0q0 = madgwick_q0 * madgwick_q0; + q1q1 = madgwick_q1 * madgwick_q1; + q2q2 = madgwick_q2 * madgwick_q2; + q3q3 = madgwick_q3 * madgwick_q3; + + s0 = _4q0 * q2q2 + _2q2 * ax + _4q0 * q1q1 - _2q1 * ay; + s1 = _4q1 * q3q3 - _2q3 * ax + 4.0f * q0q0 * madgwick_q1 - _2q0 * ay - _4q1 + _8q1 * q1q1 + _8q1 * q2q2 + _4q1 * az; + s2 = 4.0f * q0q0 * madgwick_q2 + _2q0 * ax + _4q2 * q3q3 - _2q3 * ay - _4q2 + _8q2 * q1q1 + _8q2 * q2q2 + _4q2 * az; + s3 = 4.0f * q1q1 * madgwick_q3 - _2q1 * ax + 4.0f * q2q2 * madgwick_q3 - _2q2 * ay; + recipNorm = inv_sqrt(s0 * s0 + s1 * s1 + s2 * s2 + s3 * s3); + s0 *= recipNorm; + s1 *= recipNorm; + s2 *= recipNorm; + s3 *= recipNorm; + + qDot1 -= beta * s0; + qDot2 -= beta * s1; + qDot3 -= beta * s2; + qDot4 -= beta * s3; + } + + madgwick_q0 += qDot1 * dt; + madgwick_q1 += qDot2 * dt; + madgwick_q2 += qDot3 * dt; + madgwick_q3 += qDot4 * dt; + + recipNorm = inv_sqrt(madgwick_q0 * madgwick_q0 + madgwick_q1 * madgwick_q1 + madgwick_q2 * madgwick_q2 + madgwick_q3 * madgwick_q3); + madgwick_q0 *= recipNorm; + madgwick_q1 *= recipNorm; + madgwick_q2 *= recipNorm; + madgwick_q3 *= recipNorm; +} + +static float madgwick_get_pitch(void) { + return asinf(-2.0f * (madgwick_q1 * madgwick_q3 - madgwick_q0 * madgwick_q2)) * RAD_TO_DEG; +} + +static float madgwick_get_roll(void) { + return atan2f(2.0f * (madgwick_q0 * madgwick_q1 + madgwick_q2 * madgwick_q3), + 1.0f - 2.0f * (madgwick_q1 * madgwick_q1 + madgwick_q2 * madgwick_q2)) * RAD_TO_DEG; +} + +static bool read_imu_raw(float *ax, float *ay, float *az, float *gx, float *gy, float *gz) { + struct sensor_value accel[3], gyro[3]; + + if (sensor_sample_fetch(imu_dev) < 0) return false; + + if (sensor_channel_get(imu_dev, SENSOR_CHAN_ACCEL_X, &accel[0]) < 0 || + sensor_channel_get(imu_dev, SENSOR_CHAN_ACCEL_Y, &accel[1]) < 0 || + sensor_channel_get(imu_dev, SENSOR_CHAN_ACCEL_Z, &accel[2]) < 0) { + return false; + } + + if (sensor_channel_get(imu_dev, SENSOR_CHAN_GYRO_X, &gyro[0]) < 0 || + sensor_channel_get(imu_dev, SENSOR_CHAN_GYRO_Y, &gyro[1]) < 0 || + sensor_channel_get(imu_dev, SENSOR_CHAN_GYRO_Z, &gyro[2]) < 0) { + return false; + } + + *ax = (float)sensor_value_to_double(&accel[0]); + *ay = (float)sensor_value_to_double(&accel[1]); + *az = (float)sensor_value_to_double(&accel[2]); + *gx = (float)sensor_value_to_double(&gyro[0]); + *gy = (float)sensor_value_to_double(&gyro[1]); + *gz = (float)sensor_value_to_double(&gyro[2]); + + return true; +} + +static int16_t scale_angle_to_int16(float angle, float min_angle, float max_angle) { + angle = fmaxf(min_angle, fminf(max_angle, angle)); + float normalized = (angle - min_angle) / (max_angle - min_angle); + + int scaled = (int)((normalized - 0.5f) * 65535.0f); + return (int16_t)fmaxf(-32768.0f, fminf(32767.0f, (float)scaled)); +} + +static uint16_t scale_magnitude_to_uint16(float magnitude, float max_magnitude) { + magnitude = fmaxf(0.0f, fminf(max_magnitude, magnitude)); + float normalized = magnitude / max_magnitude; + int scaled = (int)(normalized * 255.0f); + return (uint16_t)fmaxf(0.0f, fminf(255.0f, (float)scaled)); +} + +static void clamp_angle_to_limit(float* angle) { + float current_clamp_limit = (float)imu_angle_clamp_limit; + *angle = fmaxf(-current_clamp_limit, fminf(current_clamp_limit, *angle)); +} + +static float apply_deadzone(float value, float deadzone) { + if (fabsf(value) < deadzone) { + return 0.0f; + } + return value > 0 ? value - deadzone : value + deadzone; +} + +static void calibrate_orientation(float pitch, float roll) { + pitch_offset = pitch; + roll_offset = roll; +} + +static bool calibrate_sensors(void) { + float sum_accel_x = 0.0f, sum_accel_y = 0.0f, sum_accel_z = 0.0f; + float sum_gyro_x = 0.0f, sum_gyro_y = 0.0f, sum_gyro_z = 0.0f; + + for (int i = 0; i < CALIBRATION_SAMPLES; i++) { + float ax, ay, az, gx, gy, gz; + if (!read_imu_raw(&ax, &ay, &az, &gx, &gy, &gz)) { + return false; + } + + sum_accel_x += ax; + sum_accel_y += ay; + sum_accel_z += az; + sum_gyro_x += gx; + sum_gyro_y += gy; + sum_gyro_z += gz; + + k_msleep(CALIBRATION_SAMPLE_DELAY_MS); + } + + accel_bias_x = sum_accel_x / CALIBRATION_SAMPLES; + accel_bias_y = sum_accel_y / CALIBRATION_SAMPLES; + accel_bias_z = sum_accel_z / CALIBRATION_SAMPLES - GRAVITY; + + gyro_bias_x = sum_gyro_x / CALIBRATION_SAMPLES; + gyro_bias_y = sum_gyro_y / CALIBRATION_SAMPLES; + gyro_bias_z = sum_gyro_z / CALIBRATION_SAMPLES; + + return true; +} + +static float iir_update_magnitude(iir_t *filter, float input) { + filter->y = filter->alpha * filter->y + (1.0f - filter->alpha) * input; + return filter->y; +} + +static float moving_avg_filter_update(moving_avg_filter_t *filter, float input) { + int bufsize = filter->size; + if (bufsize < 1) bufsize = 1; + if (bufsize > MAX_FILTER_BUFFER_SIZE) bufsize = MAX_FILTER_BUFFER_SIZE; + filter->buffer[filter->index] = input; + filter->index = (filter->index + 1) % bufsize; + + if (!filter->initialized) { + filter->initialized = true; + filter->count = 1; + return input; + } + + if (filter->count < bufsize) { + filter->count++; + } + + float sum = 0.0f; + for (int i = 0; i < filter->count; i++) { + sum += filter->buffer[i]; + } + + return sum / filter->count; +} + +static void update_gyro_bias_if_stationary(float gx_raw, float gy_raw, float gz_raw, float hp_magnitude) { + if (hp_magnitude < imu_config.stationary_threshold) { + gyro_bias_x += imu_config.bias_update_rate * (gx_raw - gyro_bias_x); + gyro_bias_y += imu_config.bias_update_rate * (gy_raw - gyro_bias_y); + gyro_bias_z += imu_config.bias_update_rate * (gz_raw - gyro_bias_z); + } +} + +static void imu_work_fn(struct k_work* work) { + if (!imu_dev) { + error_count++; + if (error_count > MAX_ERROR_COUNT_BEFORE_BACKOFF) { + + k_work_reschedule(&imu_work, K_MSEC(IMU_SAMPLE_RATE_MS * ERROR_BACKOFF_MULTIPLIER)); + error_count = 0; + } else { + k_work_reschedule(&imu_work, K_MSEC(IMU_SAMPLE_RATE_MS)); + } + return; + } + + int64_t now = k_uptime_get(); + float dt = last_timestamp ? (now - last_timestamp) / 1000.0f : EXPECTED_DT_SECONDS; + last_timestamp = now; + + + if (dt < MIN_DT_SECONDS || dt > MAX_DT_SECONDS) { + dt = EXPECTED_DT_SECONDS; + } + + if (!is_calibrated) { + if (calibrate_sensors()) { + is_calibrated = true; + magnitude_filter.alpha = imu_config.magnitude_filter_alpha; + + float pitch = madgwick_get_pitch(); + float roll = madgwick_get_roll(); + calibrate_orientation(pitch, roll); + } else { + error_count++; + + k_work_reschedule(&imu_work, K_MSEC(CALIBRATION_RETRY_DELAY_MS)); + return; + } + } + + float ax_raw, ay_raw, az_raw, gx_raw, gy_raw, gz_raw; + if (!read_imu_raw(&ax_raw, &ay_raw, &az_raw, &gx_raw, &gy_raw, &gz_raw)) { + error_count++; + gpio_pin_set_dt(&led0, false); + + + uint32_t delay_ms = (error_count > MAX_ERROR_COUNT_BEFORE_BACKOFF) ? + IMU_SAMPLE_RATE_MS * ERROR_BACKOFF_MULTIPLIER : + IMU_SAMPLE_RATE_MS; + k_work_reschedule(&imu_work, K_MSEC(delay_ms)); + return; + } + + error_count = 0; + + if (last_known_angle_clamp_limit != imu_angle_clamp_limit) { + last_known_angle_clamp_limit = imu_angle_clamp_limit; + imu_config.angle_clamp_limit = (float)imu_angle_clamp_limit; + + int adaptive_buffer_size = imu_filter_buffer_size; + + pitch_filter.size = adaptive_buffer_size; + roll_filter.size = adaptive_buffer_size; + pitch_filter.initialized = false; + roll_filter.initialized = false; + pitch_filter.count = 0; + roll_filter.count = 0; + pitch_filter.index = 0; + roll_filter.index = 0; + } + + float ax = ax_raw - accel_bias_x; + float ay = ay_raw - accel_bias_y; + float az = az_raw - accel_bias_z; + float gx = gx_raw - gyro_bias_x; + float gy = gy_raw - gyro_bias_y; + float gz = gz_raw - gyro_bias_z; + + gx = apply_deadzone(gx, imu_config.gyro_deadzone); + gy = apply_deadzone(gy, imu_config.gyro_deadzone); + gz = apply_deadzone(gz, imu_config.gyro_deadzone); + + float accel_mag = sqrtf(ax * ax + ay * ay + az * az); + float filtered_mag = iir_update_magnitude(&magnitude_filter, accel_mag); + float hp_magnitude = accel_mag - filtered_mag; + + update_gyro_bias_if_stationary(gx_raw, gy_raw, gz_raw, fabsf(hp_magnitude)); + + float beta = compute_dynamic_beta(fabsf(hp_magnitude)); + + madgwick_update_imu(gx, gy, gz, ax, ay, az, dt, beta); + + float pitch = madgwick_get_pitch(); + float roll = madgwick_get_roll(); + + float pitch_corrected = -(pitch - pitch_offset); + float roll_corrected = roll - roll_offset; + + pitch_corrected = moving_avg_filter_update(&pitch_filter, pitch_corrected); + roll_corrected = moving_avg_filter_update(&roll_filter, roll_corrected); + + clamp_angle_to_limit(&pitch_corrected); + clamp_angle_to_limit(&roll_corrected); + + float current_clamp_limit = (float)imu_angle_clamp_limit; + int16_t pitch_scaled = scale_angle_to_int16(pitch_corrected, -current_clamp_limit, current_clamp_limit); + int16_t roll_scaled = scale_angle_to_int16(roll_corrected, -current_clamp_limit, current_clamp_limit); + uint16_t magnitude_scaled = scale_magnitude_to_uint16(hp_magnitude, 25.0f); + + imu_report_t imu_report = { + .pitch = pitch_scaled, + .roll = roll_scaled, + .magnitude = magnitude_scaled + }; + + handle_received_report((uint8_t*)&imu_report, (int)sizeof(imu_report), IMU_VIRTUAL_INTERFACE); + + k_work_cancel_delayable(&activity_led_off_work); + gpio_pin_set_dt(&led0, true); + k_work_reschedule(&activity_led_off_work, K_MSEC(LED_ACTIVITY_DURATION_MS)); + + + k_work_reschedule(&imu_work, K_MSEC(IMU_SAMPLE_RATE_MS)); +} + +bool imu_init() { + imu_dev = DEVICE_DT_GET(DT_NODELABEL(lsm6ds3tr_c)); + + if (!device_is_ready(imu_dev)) { + return false; + } + + imu_config.angle_clamp_limit = (float)imu_angle_clamp_limit; + + int adaptive_buffer_size = imu_filter_buffer_size; + + pitch_filter.size = adaptive_buffer_size; + roll_filter.size = adaptive_buffer_size; + pitch_filter.initialized = false; + roll_filter.initialized = false; + pitch_filter.count = 0; + roll_filter.count = 0; + pitch_filter.index = 0; + roll_filter.index = 0; + + struct sensor_value odr_attr; + odr_attr.val1 = IMU_ODR_FREQUENCY; + odr_attr.val2 = 0; + + if (sensor_attr_set(imu_dev, SENSOR_CHAN_ACCEL_XYZ, + SENSOR_ATTR_SAMPLING_FREQUENCY, &odr_attr) < 0) { + return false; + } + + if (sensor_attr_set(imu_dev, SENSOR_CHAN_GYRO_XYZ, + SENSOR_ATTR_SAMPLING_FREQUENCY, &odr_attr) < 0) { + return false; + } + + struct sensor_value accel_scale_attr; + accel_scale_attr.val1 = ACCEL_SCALE_RANGE; + accel_scale_attr.val2 = 0; + + sensor_attr_set(imu_dev, SENSOR_CHAN_ACCEL_XYZ, + SENSOR_ATTR_FULL_SCALE, &accel_scale_attr); + + struct sensor_value angular_scale_attr; + angular_scale_attr.val1 = GYRO_SCALE_RANGE; + angular_scale_attr.val2 = 0; + + sensor_attr_set(imu_dev, SENSOR_CHAN_GYRO_XYZ, + SENSOR_ATTR_FULL_SCALE, &angular_scale_attr); + + + float ax, ay, az, gx, gy, gz; + if (!read_imu_raw(&ax, &ay, &az, &gx, &gy, &gz)) { + return false; + } + + parse_descriptor(0x0F0D, 0x00C1, imu_hid_report_desc, IMU_HID_REPORT_DESC_SIZE, IMU_VIRTUAL_INTERFACE, 0); + device_connected_callback(IMU_VIRTUAL_INTERFACE, 0x0F0D, 0x00C1, 0); + + their_descriptor_updated = true; + + + k_work_schedule(&imu_work, K_MSEC(IMU_SAMPLE_RATE_MS)); + + return true; +} + +void imu_recalibrate_orientation() { + if (is_calibrated) { + float pitch = madgwick_get_pitch(); + float roll = madgwick_get_roll(); + calibrate_orientation(pitch, roll); + + int adaptive_buffer_size = imu_filter_buffer_size; + + pitch_filter.size = adaptive_buffer_size; + roll_filter.size = adaptive_buffer_size; + pitch_filter.initialized = false; + roll_filter.initialized = false; + pitch_filter.count = 0; + roll_filter.count = 0; + pitch_filter.index = 0; + roll_filter.index = 0; + + + } +} + +void imu_recalibrate_sensors() { + if (is_calibrated) { + is_calibrated = false; + error_count = 0; + + madgwick_q0 = 1.0f; + madgwick_q1 = 0.0f; + madgwick_q2 = 0.0f; + madgwick_q3 = 0.0f; + + magnitude_filter = (iir_t){.y = 9.81f, .alpha = imu_config.magnitude_filter_alpha}; + + int adaptive_buffer_size = imu_filter_buffer_size; + + pitch_filter.size = adaptive_buffer_size; + roll_filter.size = adaptive_buffer_size; + pitch_filter.initialized = false; + roll_filter.initialized = false; + pitch_filter.count = 0; + roll_filter.count = 0; + pitch_filter.index = 0; + roll_filter.index = 0; + + + } +} + +#else + +bool imu_init() { + return true; +} + +#endif \ No newline at end of file diff --git a/firmware-bluetooth/src/imu.h b/firmware-bluetooth/src/imu.h new file mode 100644 index 00000000..90404d26 --- /dev/null +++ b/firmware-bluetooth/src/imu.h @@ -0,0 +1,13 @@ +#pragma once + +#include +#include +#include +#include "globals.h" + +extern const struct gpio_dt_spec led0; +extern struct k_work_delayable activity_led_off_work; + +bool imu_init(); +void imu_recalibrate_orientation(); +void imu_recalibrate_sensors(); \ No newline at end of file diff --git a/firmware-bluetooth/src/imu_descriptor.h b/firmware-bluetooth/src/imu_descriptor.h new file mode 100644 index 00000000..a1d7dd46 --- /dev/null +++ b/firmware-bluetooth/src/imu_descriptor.h @@ -0,0 +1,50 @@ +#ifndef _IMU_DESCRIPTOR_H_ +#define _IMU_DESCRIPTOR_H_ + +#include + +// Custom HID Report Descriptor for orientation data (pitch, roll) and acceleration magnitude +static const uint8_t imu_hid_report_desc[] = { + 0x05, 0x20, // Usage Page (Sensor) + 0x09, 0x8A, // Usage (Motion: Orientation) + 0xA1, 0x01, // Collection (Application) + + // Pitch (rotation around X-axis) + 0x05, 0x20, // Usage Page (Sensor) + 0x09, 0x8E, // Usage (Orientation: Pitch) + 0x16, 0x00, 0x80, // Logical Minimum (-32768) + 0x26, 0xFF, 0x7F, // Logical Maximum (+32767) + 0x75, 0x10, // Report Size (16 bits) + 0x95, 0x01, // Report Count (1) + 0x55, 0x00, // Unit Exponent (0) + 0x65, 0x14, // Unit (Degrees) + 0x81, 0x02, // Input (Data,Var,Abs) + + // Roll (rotation around Y-axis) + 0x09, 0x8F, // Usage (Orientation: Roll) + 0x81, 0x02, // Input (Data,Var,Abs) + + // Acceleration Magnitude + 0x05, 0x20, // Usage Page (Sensor) + 0x09, 0x73, // Usage (Motion: Acceleration) + 0x15, 0x00, // Logical Minimum (0) + 0x26, 0xFF, 0x00, // Logical Maximum (255) + 0x75, 0x10, // Report Size (16 bits) + 0x95, 0x01, // Report Count (1) + 0x55, 0x00, // Unit Exponent (0) + 0x66, 0x14, 0xF0, // Unit (m/s²) + 0x81, 0x02, // Input (Data,Var,Abs) + + 0xC0, // End Collection +}; + +#define IMU_HID_REPORT_DESC_SIZE sizeof(imu_hid_report_desc) + +// Orientation and magnitude report structure +typedef struct { + int16_t pitch; // Pitch angle (-32768 to +32767 representing -90 to +90 degrees) + int16_t roll; // Roll angle (-32768 to +32767 representing -90 to +90 degrees) + uint16_t magnitude; // High-pass filtered acceleration magnitude (0 to 255 representing dynamic motion intensity, always positive) +} __attribute__((packed)) imu_report_t; + +#endif // _IMU_DESCRIPTOR_H_ \ No newline at end of file diff --git a/firmware-bluetooth/src/main.cc b/firmware-bluetooth/src/main.cc index 913618aa..dda8dc21 100644 --- a/firmware-bluetooth/src/main.cc +++ b/firmware-bluetooth/src/main.cc @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -21,6 +22,7 @@ #include #include "config.h" +#include "imu.h" #include "descriptor_parser.h" #include "globals.h" #include "our_descriptor.h" @@ -53,6 +55,9 @@ static bool get_report_response_ready = false; static const struct device* hid_dev0; static const struct device* hid_dev1; // config interface +// Forward declarations +static bool do_send_report(uint8_t interface, const uint8_t* report_with_id, uint8_t len); + struct report_type { uint16_t interface; uint8_t len; @@ -106,7 +111,7 @@ static const struct gpio_dt_spec button = GPIO_DT_SPEC_GET(SW0_NODE, gpios); static struct gpio_callback button_cb_data; -static const struct gpio_dt_spec led0 = GPIO_DT_SPEC_GET(LED0_NODE, gpios); +const struct gpio_dt_spec led0 = GPIO_DT_SPEC_GET(LED0_NODE, gpios); static const struct gpio_dt_spec led1 = GPIO_DT_SPEC_GET(LED1_NODE, gpios); static bool scanning = false; @@ -117,7 +122,7 @@ static struct bt_le_conn_param* conn_param = BT_LE_CONN_PARAM(6, 6, 44, 400); static void activity_led_off_work_fn(struct k_work* work) { gpio_pin_set_dt(&led0, false); } -static K_WORK_DELAYABLE_DEFINE(activity_led_off_work, activity_led_off_work_fn); +K_WORK_DELAYABLE_DEFINE(activity_led_off_work, activity_led_off_work_fn); enum class LedMode { OFF = 0, @@ -718,6 +723,7 @@ static bool do_send_report(uint8_t interface, const uint8_t* report_with_id, uin if (interface == 1) { return CHK(hid_int_ep_write(hid_dev1, report_with_id, len, NULL)); } + return false; } static void button_init() { @@ -905,6 +911,7 @@ int main() { my_mutexes_init(); button_init(); leds_init(); + bt_init(); CHK(settings_subsys_init()); CHK(settings_register(&our_settings_handlers)); @@ -914,6 +921,19 @@ int main() { scan_init(); parse_our_descriptor(); set_mapping_from_config(); + + // Initialize 6-axis IMU AFTER mapping system is ready +#if DT_NODE_EXISTS(DT_NODELABEL(lsm6ds3tr_c)) + if (imu_enabled) { + if (!imu_init()) { + LOG_ERR("Failed to initialize 6-axis IMU"); + } + } else { + LOG_INF("IMU disabled in configuration - skipping IMU initialization"); + } +#else + LOG_INF("IMU not available on this board - skipping IMU initialization"); +#endif k_work_reschedule(&scan_start_work, K_MSEC(SCAN_DELAY_MS)); diff --git a/firmware/src/config.cc b/firmware/src/config.cc index 18764e54..5265c338 100644 --- a/firmware/src/config.cc +++ b/firmware/src/config.cc @@ -10,7 +10,7 @@ #include "platform.h" #include "remapper.h" -const uint8_t CONFIG_VERSION = 18; +const uint8_t CONFIG_VERSION = 19; const uint8_t CONFIG_FLAG_UNMAPPED_PASSTHROUGH = 0x01; const uint8_t CONFIG_FLAG_UNMAPPED_PASSTHROUGH_MASK = 0b00001111; @@ -18,6 +18,7 @@ const uint8_t CONFIG_FLAG_UNMAPPED_PASSTHROUGH_BIT = 0; const uint8_t CONFIG_FLAG_IGNORE_AUTH_DEV_INPUTS_BIT = 4; const uint8_t CONFIG_FLAG_GPIO_OUTPUT_MODE_BIT = 5; const uint8_t CONFIG_FLAG_NORMALIZE_GAMEPAD_INPUTS_BIT = 6; +const uint8_t CONFIG_FLAG_IMU_ENABLE_BIT = 7; ConfigCommand last_config_command = ConfigCommand::NO_COMMAND; uint32_t requested_index = 0; @@ -549,6 +550,8 @@ void load_config_v13(const uint8_t* persisted_config) { my_mutex_exit(MutexId::QUIRKS); } + + void load_config(const uint8_t* persisted_config) { if (!checksum_ok(persisted_config, PERSISTED_CONFIG_SIZE) || !persisted_version_ok(persisted_config)) { return; @@ -562,6 +565,12 @@ void load_config(const uint8_t* persisted_config) { normalize_gamepad_inputs = false; } + if (version < 19) { + imu_angle_clamp_limit = 45; + imu_filter_buffer_size = 10; + imu_enabled = false; + } + if ((version == 3) || (version == 4)) { load_config_v3_v4(persisted_config); return; @@ -616,11 +625,13 @@ void load_config(const uint8_t* persisted_config) { return; } - persist_config_v18_t* config = (persist_config_v18_t*) persisted_config; + + persist_config_v19_t* config = (persist_config_v19_t*) persisted_config; unmapped_passthrough_layer_mask = config->unmapped_passthrough_layer_mask; ignore_auth_dev_inputs = config->flags & (1 << CONFIG_FLAG_IGNORE_AUTH_DEV_INPUTS_BIT); gpio_output_mode = !!(config->flags & (1 << CONFIG_FLAG_GPIO_OUTPUT_MODE_BIT)); normalize_gamepad_inputs = !!(config->flags & (1 << CONFIG_FLAG_NORMALIZE_GAMEPAD_INPUTS_BIT)); + imu_enabled = !!(config->flags & (1 << CONFIG_FLAG_IMU_ENABLE_BIT)); partial_scroll_timeout = config->partial_scroll_timeout; tap_hold_threshold = config->tap_hold_threshold; gpio_debounce_time = config->gpio_debounce_time_ms * 1000; @@ -630,12 +641,19 @@ void load_config(const uint8_t* persisted_config) { our_descriptor_number = 0; } macro_entry_duration = config->macro_entry_duration; - mapping_config11_t* buffer_mappings = (mapping_config11_t*) (persisted_config + sizeof(persist_config_v18_t)); + if (version >= 19) { + imu_angle_clamp_limit = config->imu_angle_clamp_limit; + if (imu_angle_clamp_limit > 90) { + imu_angle_clamp_limit = 90; + } + imu_filter_buffer_size = config->imu_filter_buffer_size; + } + mapping_config11_t* buffer_mappings = (mapping_config11_t*) (persisted_config + sizeof(persist_config_v19_t)); for (uint32_t i = 0; i < config->mapping_count; i++) { config_mappings.push_back(buffer_mappings[i]); } - const uint8_t* macros_config_ptr = (persisted_config + sizeof(persist_config_v18_t) + config->mapping_count * sizeof(mapping_config11_t)); + const uint8_t* macros_config_ptr = (persisted_config + sizeof(persist_config_v19_t) + config->mapping_count * sizeof(mapping_config11_t)); my_mutex_enter(MutexId::MACROS); for (int i = 0; i < NMACROS; i++) { macros[i].clear(); @@ -690,6 +708,7 @@ void fill_get_config(get_config_t* config) { config->flags |= ignore_auth_dev_inputs << CONFIG_FLAG_IGNORE_AUTH_DEV_INPUTS_BIT; config->flags |= gpio_output_mode << CONFIG_FLAG_GPIO_OUTPUT_MODE_BIT; config->flags |= normalize_gamepad_inputs << CONFIG_FLAG_NORMALIZE_GAMEPAD_INPUTS_BIT; + config->flags |= imu_enabled << CONFIG_FLAG_IMU_ENABLE_BIT; config->unmapped_passthrough_layer_mask = unmapped_passthrough_layer_mask; config->partial_scroll_timeout = partial_scroll_timeout; config->tap_hold_threshold = tap_hold_threshold; @@ -700,6 +719,8 @@ void fill_get_config(get_config_t* config) { config->interval_override = interval_override; config->our_descriptor_number = our_descriptor_number; config->macro_entry_duration = macro_entry_duration; + config->imu_angle_clamp_limit = imu_angle_clamp_limit; + config->imu_filter_buffer_size = imu_filter_buffer_size; my_mutex_enter(MutexId::QUIRKS); config->quirk_count = quirks.size(); my_mutex_exit(MutexId::QUIRKS); @@ -711,6 +732,7 @@ void fill_persist_config(persist_config_t* config) { config->flags |= ignore_auth_dev_inputs << CONFIG_FLAG_IGNORE_AUTH_DEV_INPUTS_BIT; config->flags |= gpio_output_mode << CONFIG_FLAG_GPIO_OUTPUT_MODE_BIT; config->flags |= normalize_gamepad_inputs << CONFIG_FLAG_NORMALIZE_GAMEPAD_INPUTS_BIT; + config->flags |= imu_enabled << CONFIG_FLAG_IMU_ENABLE_BIT; config->unmapped_passthrough_layer_mask = unmapped_passthrough_layer_mask; config->partial_scroll_timeout = partial_scroll_timeout; config->tap_hold_threshold = tap_hold_threshold; @@ -719,6 +741,8 @@ void fill_persist_config(persist_config_t* config) { config->interval_override = interval_override; config->our_descriptor_number = our_descriptor_number; config->macro_entry_duration = macro_entry_duration; + config->imu_angle_clamp_limit = imu_angle_clamp_limit; + config->imu_filter_buffer_size = imu_filter_buffer_size; my_mutex_enter(MutexId::QUIRKS); config->quirk_count = quirks.size(); my_mutex_exit(MutexId::QUIRKS); @@ -803,7 +827,7 @@ PersistConfigReturnCode persist_config() { my_mutex_enter(MutexId::QUIRKS); quirk_t* quirk_config_ptr = (quirk_t*) expr_config_ptr; - for (uint16_t i = 0; i < quirks.size(); i++) { + for (size_t i = 0; i < quirks.size(); i++) { *quirk_config_ptr = quirks[i]; quirk_config_ptr++; } @@ -971,6 +995,7 @@ void handle_set_report1(uint8_t report_id, uint8_t const* buffer, uint16_t bufsi ignore_auth_dev_inputs = config->flags & (1 << CONFIG_FLAG_IGNORE_AUTH_DEV_INPUTS_BIT); gpio_output_mode = !!(config->flags & (1 << CONFIG_FLAG_GPIO_OUTPUT_MODE_BIT)); normalize_gamepad_inputs = !!(config->flags & (1 << CONFIG_FLAG_NORMALIZE_GAMEPAD_INPUTS_BIT)); + imu_enabled = !!(config->flags & (1 << CONFIG_FLAG_IMU_ENABLE_BIT)); partial_scroll_timeout = config->partial_scroll_timeout; tap_hold_threshold = config->tap_hold_threshold; gpio_debounce_time = config->gpio_debounce_time_ms * 1000; @@ -984,6 +1009,11 @@ void handle_set_report1(uint8_t report_id, uint8_t const* buffer, uint16_t bufsi our_descriptor_number = 0; } macro_entry_duration = config->macro_entry_duration; + imu_angle_clamp_limit = config->imu_angle_clamp_limit; + if (imu_angle_clamp_limit > 90) { + imu_angle_clamp_limit = 90; + } + imu_filter_buffer_size = config->imu_filter_buffer_size; break; } case ConfigCommand::GET_CONFIG: diff --git a/firmware/src/globals.cc b/firmware/src/globals.cc index c0372823..8a29112e 100644 --- a/firmware/src/globals.cc +++ b/firmware/src/globals.cc @@ -32,6 +32,9 @@ bool ignore_auth_dev_inputs = false; uint8_t macro_entry_duration = 0; // 0 means 1ms uint8_t gpio_output_mode = 0; bool normalize_gamepad_inputs = true; +bool imu_enabled = true; +uint8_t imu_angle_clamp_limit = 45; +uint8_t imu_filter_buffer_size = 10; std::vector config_mappings; diff --git a/firmware/src/globals.h b/firmware/src/globals.h index 5e9e3ad2..c85660d9 100644 --- a/firmware/src/globals.h +++ b/firmware/src/globals.h @@ -39,6 +39,9 @@ extern bool ignore_auth_dev_inputs; extern uint8_t macro_entry_duration; extern uint8_t gpio_output_mode; extern bool normalize_gamepad_inputs; +extern bool imu_enabled; +extern uint8_t imu_angle_clamp_limit; +extern uint8_t imu_filter_buffer_size; extern std::vector config_mappings; diff --git a/firmware/src/types.h b/firmware/src/types.h index 3bfa1be6..654e816c 100644 --- a/firmware/src/types.h +++ b/firmware/src/types.h @@ -310,7 +310,23 @@ struct __attribute__((packed)) persist_config_v12_t { typedef persist_config_v12_t persist_config_v13_t; -typedef persist_config_v13_t persist_config_v18_t; +struct __attribute__((packed)) persist_config_v19_t { + uint8_t version; + uint8_t flags; + uint8_t unmapped_passthrough_layer_mask; + uint32_t partial_scroll_timeout; + uint16_t mapping_count; + uint8_t interval_override; + uint32_t tap_hold_threshold; + uint8_t gpio_debounce_time_ms; + uint8_t our_descriptor_number; + uint8_t macro_entry_duration; + uint16_t quirk_count; + uint8_t imu_angle_clamp_limit; + uint8_t imu_filter_buffer_size; +}; + +typedef persist_config_v19_t persist_config_v18_t; typedef persist_config_v18_t persist_config_t; @@ -328,6 +344,8 @@ struct __attribute__((packed)) get_config_t { uint8_t our_descriptor_number; uint8_t macro_entry_duration; uint16_t quirk_count; + uint8_t imu_angle_clamp_limit; + uint8_t imu_filter_buffer_size; }; struct __attribute__((packed)) set_config_t { @@ -339,6 +357,8 @@ struct __attribute__((packed)) set_config_t { uint8_t gpio_debounce_time_ms; uint8_t our_descriptor_number; uint8_t macro_entry_duration; + uint8_t imu_angle_clamp_limit; + uint8_t imu_filter_buffer_size; }; struct __attribute__((packed)) get_indexed_t { From bd2066936c349683952debe44e6d4af5b8dc87ce Mon Sep 17 00:00:00 2001 From: Valentin Squirelo Date: Sat, 5 Jul 2025 09:03:11 +0200 Subject: [PATCH 2/4] imu_enabled false by default --- firmware/src/globals.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/firmware/src/globals.cc b/firmware/src/globals.cc index 8a29112e..f3866790 100644 --- a/firmware/src/globals.cc +++ b/firmware/src/globals.cc @@ -32,7 +32,7 @@ bool ignore_auth_dev_inputs = false; uint8_t macro_entry_duration = 0; // 0 means 1ms uint8_t gpio_output_mode = 0; bool normalize_gamepad_inputs = true; -bool imu_enabled = true; +bool imu_enabled = false; uint8_t imu_angle_clamp_limit = 45; uint8_t imu_filter_buffer_size = 10; From bf62b73b6b0e0c484c97e0a1dd38a61b6e32bb15 Mon Sep 17 00:00:00 2001 From: Valentin Squirelo Date: Sun, 13 Jul 2025 09:32:12 +0200 Subject: [PATCH 3/4] Add axis inversion for roll and pitch + fix config size --- config-tool-web/code.js | 22 +++++++++++++++++++--- config-tool-web/index.html | 24 ++++++++++++++++++++---- config-tool/common.py | 6 +++--- config-tool/get_config.py | 6 +++++- config-tool/set_config.py | 8 ++++++-- firmware-bluetooth/src/imu.cc | 7 +++++++ firmware/src/config.cc | 22 ++++++++++++++++++++++ firmware/src/globals.cc | 2 ++ firmware/src/globals.h | 2 ++ firmware/src/our_descriptor.h | 2 +- firmware/src/types.h | 8 +++++++- 11 files changed, 94 insertions(+), 15 deletions(-) diff --git a/config-tool-web/code.js b/config-tool-web/code.js index 130b8dfc..6c7f049c 100644 --- a/config-tool-web/code.js +++ b/config-tool-web/code.js @@ -7,7 +7,7 @@ const REPORT_ID_MONITOR = 101; const STICKY_FLAG = 1 << 0; const TAP_FLAG = 1 << 1; const HOLD_FLAG = 1 << 2; -const CONFIG_SIZE = 32; +const CONFIG_SIZE = 36; const CONFIG_VERSION = 19; const VENDOR_ID = 0xCAFE; const PRODUCT_ID = 0xBAF2; @@ -221,6 +221,8 @@ document.addEventListener("DOMContentLoaded", function () { document.getElementById("imu_enabled_checkbox").addEventListener("change", imu_enabled_onchange); document.getElementById("imu_angle_clamp_limit_input").addEventListener("change", imu_angle_clamp_limit_onchange); document.getElementById("imu_filter_buffer_size_input").addEventListener("change", imu_filter_buffer_size_onchange); + document.getElementById("imu_roll_inverted_checkbox").addEventListener("change", imu_roll_inverted_onchange); + document.getElementById("imu_pitch_inverted_checkbox").addEventListener("change", imu_pitch_inverted_onchange); document.getElementById("nav-monitor-tab").addEventListener("shown.bs.tab", monitor_tab_shown); document.getElementById("nav-monitor-tab").addEventListener("hide.bs.tab", monitor_tab_hide); @@ -301,8 +303,8 @@ async function load_from_device() { try { await send_feature_command(GET_CONFIG); - const [config_version, flags, unmapped_passthrough_layer_mask, partial_scroll_timeout, mapping_count, our_usage_count, their_usage_count, interval_override, tap_hold_threshold, gpio_debounce_time_ms, our_descriptor_number, macro_entry_duration, quirk_count, imu_angle_clamp_limit, imu_filter_buffer_size] = - await read_config_feature([UINT8, UINT8, UINT8, UINT32, UINT16, UINT32, UINT32, UINT8, UINT32, UINT8, UINT8, UINT8, UINT16, UINT8, UINT8]); + const [config_version, flags, unmapped_passthrough_layer_mask, partial_scroll_timeout, mapping_count, our_usage_count, their_usage_count, interval_override, tap_hold_threshold, gpio_debounce_time_ms, our_descriptor_number, macro_entry_duration, quirk_count, imu_angle_clamp_limit, imu_filter_buffer_size, imu_roll_inverted, imu_pitch_inverted] = + await read_config_feature([UINT8, UINT8, UINT8, UINT32, UINT16, UINT32, UINT32, UINT8, UINT32, UINT8, UINT8, UINT8, UINT16, UINT8, UINT8, UINT8, UINT8]); check_received_version(config_version); config['version'] = config_version; @@ -319,6 +321,8 @@ async function load_from_device() { config['macro_entry_duration'] = macro_entry_duration + 1; config['imu_angle_clamp_limit'] = imu_angle_clamp_limit; config['imu_filter_buffer_size'] = imu_filter_buffer_size; + config['imu_roll_inverted'] = !!imu_roll_inverted; + config['imu_pitch_inverted'] = !!imu_pitch_inverted; config['mappings'] = []; for (let i = 0; i < mapping_count; i++) { @@ -469,6 +473,8 @@ async function save_to_device() { [UINT8, config['macro_entry_duration'] - 1], [UINT8, config['imu_angle_clamp_limit']], [UINT8, config['imu_filter_buffer_size']], + [UINT8, config['imu_roll_inverted'] ? 1 : 0], + [UINT8, config['imu_pitch_inverted'] ? 1 : 0], ]); await send_feature_command(CLEAR_MAPPING); @@ -652,6 +658,8 @@ function set_config_ui_state() { document.getElementById('imu_enabled_checkbox').checked = config['imu_enabled']; document.getElementById('imu_angle_clamp_limit_input').value = config['imu_angle_clamp_limit'] ?? DEFAULT_IMU_ANGLE_CLAMP_LIMIT; document.getElementById('imu_filter_buffer_size_input').value = config['imu_filter_buffer_size'] ?? DEFAULT_IMU_FILTER_BUFFER_SIZE; + document.getElementById('imu_roll_inverted_checkbox').checked = config['imu_roll_inverted'] ?? false; + document.getElementById('imu_pitch_inverted_checkbox').checked = config['imu_pitch_inverted'] ?? false; } function set_mappings_ui_state() { @@ -1472,6 +1480,14 @@ function imu_filter_buffer_size_onchange() { config['imu_filter_buffer_size'] = parseInt(document.getElementById("imu_filter_buffer_size_input").value, 10); } +function imu_roll_inverted_onchange() { + config['imu_roll_inverted'] = document.getElementById("imu_roll_inverted_checkbox").checked; +} + +function imu_pitch_inverted_onchange() { + config['imu_pitch_inverted'] = document.getElementById("imu_pitch_inverted_checkbox").checked; +} + function macro_entry_duration_onchange() { let value = parseInt(document.getElementById("macro_entry_duration_input").value, 10); if (isNaN(value)) { diff --git a/config-tool-web/index.html b/config-tool-web/index.html index 9344f7e4..8249f6fb 100644 --- a/config-tool-web/index.html +++ b/config-tool-web/index.html @@ -237,26 +237,42 @@

HID Remapper Configuration

- +
- degrees + °
1 to 90 degrees
- +
samples
-
1(slow) to 16(fast)
+
1(reactive) to 16(stable)
+
+
+
+
+ +
+
+ +
+
+
+
+ +
+
+
diff --git a/config-tool/common.py b/config-tool/common.py index 6e767bcd..62ebe554 100644 --- a/config-tool/common.py +++ b/config-tool/common.py @@ -12,7 +12,7 @@ CONFIG_USAGE = 0x0020 CONFIG_VERSION = 19 -CONFIG_SIZE = 32 +CONFIG_SIZE = 36 REPORT_ID_CONFIG = 100 DEFAULT_PARTIAL_SCROLL_TIMEOUT = 1000000 @@ -133,12 +133,12 @@ def check_crc(buf, crc_): - if binascii.crc32(buf[1:29]) != crc_: + if binascii.crc32(buf[1:CONFIG_SIZE-3]) != crc_: raise Exception("CRC mismatch") def add_crc(buf): - return buf + struct.pack("imu_filter_buffer_size; + if (imu_filter_buffer_size < 1) { + imu_filter_buffer_size = 1; + } + if (imu_filter_buffer_size > 16) { + imu_filter_buffer_size = 16; + } + imu_roll_inverted = config->imu_roll_inverted; + imu_pitch_inverted = config->imu_pitch_inverted; } mapping_config11_t* buffer_mappings = (mapping_config11_t*) (persisted_config + sizeof(persist_config_v19_t)); for (uint32_t i = 0; i < config->mapping_count; i++) { @@ -721,6 +731,8 @@ void fill_get_config(get_config_t* config) { config->macro_entry_duration = macro_entry_duration; config->imu_angle_clamp_limit = imu_angle_clamp_limit; config->imu_filter_buffer_size = imu_filter_buffer_size; + config->imu_roll_inverted = imu_roll_inverted; + config->imu_pitch_inverted = imu_pitch_inverted; my_mutex_enter(MutexId::QUIRKS); config->quirk_count = quirks.size(); my_mutex_exit(MutexId::QUIRKS); @@ -743,6 +755,8 @@ void fill_persist_config(persist_config_t* config) { config->macro_entry_duration = macro_entry_duration; config->imu_angle_clamp_limit = imu_angle_clamp_limit; config->imu_filter_buffer_size = imu_filter_buffer_size; + config->imu_roll_inverted = imu_roll_inverted; + config->imu_pitch_inverted = imu_pitch_inverted; my_mutex_enter(MutexId::QUIRKS); config->quirk_count = quirks.size(); my_mutex_exit(MutexId::QUIRKS); @@ -1014,6 +1028,14 @@ void handle_set_report1(uint8_t report_id, uint8_t const* buffer, uint16_t bufsi imu_angle_clamp_limit = 90; } imu_filter_buffer_size = config->imu_filter_buffer_size; + if (imu_filter_buffer_size < 1) { + imu_filter_buffer_size = 1; + } + if (imu_filter_buffer_size > 16) { + imu_filter_buffer_size = 16; + } + imu_roll_inverted = config->imu_roll_inverted; + imu_pitch_inverted = config->imu_pitch_inverted; break; } case ConfigCommand::GET_CONFIG: diff --git a/firmware/src/globals.cc b/firmware/src/globals.cc index f3866790..00015c96 100644 --- a/firmware/src/globals.cc +++ b/firmware/src/globals.cc @@ -35,6 +35,8 @@ bool normalize_gamepad_inputs = true; bool imu_enabled = false; uint8_t imu_angle_clamp_limit = 45; uint8_t imu_filter_buffer_size = 10; +bool imu_roll_inverted = false; +bool imu_pitch_inverted = false; std::vector config_mappings; diff --git a/firmware/src/globals.h b/firmware/src/globals.h index c85660d9..9cdc3fcc 100644 --- a/firmware/src/globals.h +++ b/firmware/src/globals.h @@ -42,6 +42,8 @@ extern bool normalize_gamepad_inputs; extern bool imu_enabled; extern uint8_t imu_angle_clamp_limit; extern uint8_t imu_filter_buffer_size; +extern bool imu_roll_inverted; +extern bool imu_pitch_inverted; extern std::vector config_mappings; diff --git a/firmware/src/our_descriptor.h b/firmware/src/our_descriptor.h index 0bdbc8e0..1a77014a 100644 --- a/firmware/src/our_descriptor.h +++ b/firmware/src/our_descriptor.h @@ -3,7 +3,7 @@ #include -#define CONFIG_SIZE 32 +#define CONFIG_SIZE 36 #define RESOLUTION_MULTIPLIER 120 #define REPORT_ID_LEDS 98 diff --git a/firmware/src/types.h b/firmware/src/types.h index 654e816c..f912abfa 100644 --- a/firmware/src/types.h +++ b/firmware/src/types.h @@ -201,7 +201,7 @@ struct __attribute__((packed)) set_feature_t { }; struct __attribute__((packed)) get_feature_t { - uint8_t data[28]; + uint8_t data[32]; uint32_t crc32; }; @@ -324,6 +324,8 @@ struct __attribute__((packed)) persist_config_v19_t { uint16_t quirk_count; uint8_t imu_angle_clamp_limit; uint8_t imu_filter_buffer_size; + uint8_t imu_roll_inverted; + uint8_t imu_pitch_inverted; }; typedef persist_config_v19_t persist_config_v18_t; @@ -346,6 +348,8 @@ struct __attribute__((packed)) get_config_t { uint16_t quirk_count; uint8_t imu_angle_clamp_limit; uint8_t imu_filter_buffer_size; + uint8_t imu_roll_inverted; + uint8_t imu_pitch_inverted; }; struct __attribute__((packed)) set_config_t { @@ -359,6 +363,8 @@ struct __attribute__((packed)) set_config_t { uint8_t macro_entry_duration; uint8_t imu_angle_clamp_limit; uint8_t imu_filter_buffer_size; + uint8_t imu_roll_inverted; + uint8_t imu_pitch_inverted; }; struct __attribute__((packed)) get_indexed_t { From 6b66e26286e184912f7da0a4f30bd01fa87be676 Mon Sep 17 00:00:00 2001 From: Valentin Squirelo Date: Sun, 13 Jul 2025 09:43:33 +0200 Subject: [PATCH 4/4] rename magnitude to shake to be more user friendly --- config-tool-web/usages.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/config-tool-web/usages.js b/config-tool-web/usages.js index 38e760c5..c396dfca 100644 --- a/config-tool-web/usages.js +++ b/config-tool-web/usages.js @@ -16,7 +16,7 @@ const usages = { "0x0020008d": { 'name': 'Yaw', 'class': 'mouse' }, "0x0020008e": { 'name': 'Pitch', 'class': 'mouse' }, "0x0020008f": { 'name': 'Roll', 'class': 'mouse' }, - "0x00200073": { 'name': 'Magnitude', 'class': 'mouse' }, + "0x00200073": { 'name': 'Shake', 'class': 'mouse' }, }, 'source_1': { "0x00010030": { 'name': 'Left stick X', 'class': 'gamepad' }, @@ -208,7 +208,7 @@ const usages = { "0x0020008d": { 'name': 'Yaw', 'class': 'mouse' }, "0x0020008e": { 'name': 'Pitch', 'class': 'mouse' }, "0x0020008f": { 'name': 'Roll', 'class': 'mouse' }, - "0x00200073": { 'name': 'Magnitude', 'class': 'mouse' }, + "0x00200073": { 'name': 'Shake', 'class': 'mouse' }, "0xfff30001": { 'name': 'Expression 1', 'class': 'other' }, "0xfff30002": { 'name': 'Expression 2', 'class': 'other' }, "0xfff30003": { 'name': 'Expression 3', 'class': 'other' }, @@ -439,7 +439,7 @@ const usages = { "0x0020008d": { 'name': 'Yaw', 'class': 'mouse' }, "0x0020008e": { 'name': 'Pitch', 'class': 'mouse' }, "0x0020008f": { 'name': 'Roll', 'class': 'mouse' }, - "0x00200073": { 'name': 'Magnitude', 'class': 'mouse' }, + "0x00200073": { 'name': 'Shake', 'class': 'mouse' }, }, 2: { "0x00090001": { 'name': 'Y', 'class': 'mouse' },