From 9852d574d4df0d128c820d2bfea7043610fb3df8 Mon Sep 17 00:00:00 2001 From: Shadowtrance Date: Wed, 1 Apr 2026 23:41:57 +1000 Subject: [PATCH 01/12] Bluetooth LE addition --- .../service/bluetooth/settings.properties | 1 + Devices/m5stack-tab5/device.properties | 3 + Devices/m5stack-tab5/m5stack,tab5.dts | 5 + Modules/lvgl-module/assets/generate-all.py | 8 +- .../include/tactility/lvgl_icon_shared.h | 1 + .../include/tactility/lvgl_icon_statusbar.h | 4 + .../source-fonts/material_symbols_shared_12.c | 102 +- .../source-fonts/material_symbols_shared_16.c | 103 +- .../source-fonts/material_symbols_shared_20.c | 105 +- .../source-fonts/material_symbols_shared_24.c | 108 +- .../source-fonts/material_symbols_shared_32.c | 113 +- .../material_symbols_statusbar_12.c | 79 +- .../material_symbols_statusbar_16.c | 86 +- .../material_symbols_statusbar_20.c | 96 +- .../material_symbols_statusbar_30.c | 131 ++- Platforms/platform-esp32/CMakeLists.txt | 2 + .../bindings/esp32,ble-nimble.yaml | 8 + .../tactility/bindings/esp32_ble_nimble.h | 7 + .../tactility/drivers/esp32_ble_nimble.h | 15 + .../private/bluetooth/esp32_ble_internal.h | 154 +++ .../source/drivers/bluetooth/README.md | 48 + .../source/drivers/bluetooth/bluetooth.puml | 45 + .../source/drivers/bluetooth/esp32_ble.cpp | 1010 +++++++++++++++++ .../bluetooth/esp32_ble_hid_device.cpp | 515 +++++++++ .../drivers/bluetooth/esp32_ble_midi.cpp | 177 +++ .../drivers/bluetooth/esp32_ble_scan.cpp | 219 ++++ .../drivers/bluetooth/esp32_ble_spp.cpp | 173 +++ Platforms/platform-esp32/source/module.cpp | 14 + Tactility/CMakeLists.txt | 2 + .../Include/Tactility/app/btmanage/BtManage.h | 9 + .../Include/Tactility/bluetooth/Bluetooth.h | 128 +++ .../bluetooth/BluetoothPairedDevice.h | 30 + .../Tactility/bluetooth/BluetoothSettings.h | 14 + .../Private/Tactility/app/btmanage/Bindings.h | 24 + .../Tactility/app/btmanage/BtManagePrivate.h | 40 + .../Private/Tactility/app/btmanage/State.h | 41 + .../Private/Tactility/app/btmanage/View.h | 41 + .../app/btpeersettings/BtPeerSettings.h | 9 + .../Tactility/bluetooth/BluetoothPrivate.h | 29 + Tactility/Source/Tactility.cpp | 7 + Tactility/Source/app/btmanage/BtManage.cpp | 181 +++ Tactility/Source/app/btmanage/State.cpp | 44 + Tactility/Source/app/btmanage/View.cpp | 246 ++++ .../app/btpeersettings/BtPeerSettings.cpp | 227 ++++ Tactility/Source/bluetooth/Bluetooth.cpp | 375 ++++++ .../Source/bluetooth/BluetoothHidDevice.cpp | 34 + .../Source/bluetooth/BluetoothHidHost.cpp | 885 +++++++++++++++ Tactility/Source/bluetooth/BluetoothMidi.cpp | 31 + Tactility/Source/bluetooth/BluetoothMock.cpp | 53 + .../bluetooth/BluetoothPairedDevice.cpp | 117 ++ .../Source/bluetooth/BluetoothSettings.cpp | 103 ++ Tactility/Source/bluetooth/BluetoothSpp.cpp | 31 + Tactility/Source/bluetooth/README.md | 52 + Tactility/Source/lvgl/Keyboard.cpp | 8 + .../Source/service/statusbar/Statusbar.cpp | 44 + TactilityC/Source/tt_init.cpp | 1 + .../include/tactility/drivers/bluetooth.h | 276 +++++ .../tactility/drivers/bluetooth_hid_device.h | 124 ++ .../tactility/drivers/bluetooth_midi.h | 65 ++ .../tactility/drivers/bluetooth_serial.h | 77 ++ TactilityKernel/source/drivers/bluetooth.cpp | 91 ++ .../source/drivers/bluetooth_hid_device.cpp | 57 + .../source/drivers/bluetooth_midi.cpp | 41 + .../source/drivers/bluetooth_serial.cpp | 45 + TactilityKernel/source/kernel_symbols.c | 46 + device.py | 15 + 66 files changed, 6630 insertions(+), 345 deletions(-) create mode 100644 Data/data/service/bluetooth/settings.properties create mode 100644 Platforms/platform-esp32/bindings/esp32,ble-nimble.yaml create mode 100644 Platforms/platform-esp32/include/tactility/bindings/esp32_ble_nimble.h create mode 100644 Platforms/platform-esp32/include/tactility/drivers/esp32_ble_nimble.h create mode 100644 Platforms/platform-esp32/private/bluetooth/esp32_ble_internal.h create mode 100644 Platforms/platform-esp32/source/drivers/bluetooth/README.md create mode 100644 Platforms/platform-esp32/source/drivers/bluetooth/bluetooth.puml create mode 100644 Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble.cpp create mode 100644 Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_hid_device.cpp create mode 100644 Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_midi.cpp create mode 100644 Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_scan.cpp create mode 100644 Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_spp.cpp create mode 100644 Tactility/Include/Tactility/app/btmanage/BtManage.h create mode 100644 Tactility/Include/Tactility/bluetooth/Bluetooth.h create mode 100644 Tactility/Include/Tactility/bluetooth/BluetoothPairedDevice.h create mode 100644 Tactility/Include/Tactility/bluetooth/BluetoothSettings.h create mode 100644 Tactility/Private/Tactility/app/btmanage/Bindings.h create mode 100644 Tactility/Private/Tactility/app/btmanage/BtManagePrivate.h create mode 100644 Tactility/Private/Tactility/app/btmanage/State.h create mode 100644 Tactility/Private/Tactility/app/btmanage/View.h create mode 100644 Tactility/Private/Tactility/app/btpeersettings/BtPeerSettings.h create mode 100644 Tactility/Private/Tactility/bluetooth/BluetoothPrivate.h create mode 100644 Tactility/Source/app/btmanage/BtManage.cpp create mode 100644 Tactility/Source/app/btmanage/State.cpp create mode 100644 Tactility/Source/app/btmanage/View.cpp create mode 100644 Tactility/Source/app/btpeersettings/BtPeerSettings.cpp create mode 100644 Tactility/Source/bluetooth/Bluetooth.cpp create mode 100644 Tactility/Source/bluetooth/BluetoothHidDevice.cpp create mode 100644 Tactility/Source/bluetooth/BluetoothHidHost.cpp create mode 100644 Tactility/Source/bluetooth/BluetoothMidi.cpp create mode 100644 Tactility/Source/bluetooth/BluetoothMock.cpp create mode 100644 Tactility/Source/bluetooth/BluetoothPairedDevice.cpp create mode 100644 Tactility/Source/bluetooth/BluetoothSettings.cpp create mode 100644 Tactility/Source/bluetooth/BluetoothSpp.cpp create mode 100644 Tactility/Source/bluetooth/README.md create mode 100644 TactilityKernel/include/tactility/drivers/bluetooth.h create mode 100644 TactilityKernel/include/tactility/drivers/bluetooth_hid_device.h create mode 100644 TactilityKernel/include/tactility/drivers/bluetooth_midi.h create mode 100644 TactilityKernel/include/tactility/drivers/bluetooth_serial.h create mode 100644 TactilityKernel/source/drivers/bluetooth.cpp create mode 100644 TactilityKernel/source/drivers/bluetooth_hid_device.cpp create mode 100644 TactilityKernel/source/drivers/bluetooth_midi.cpp create mode 100644 TactilityKernel/source/drivers/bluetooth_serial.cpp diff --git a/Data/data/service/bluetooth/settings.properties b/Data/data/service/bluetooth/settings.properties new file mode 100644 index 000000000..583f5735a --- /dev/null +++ b/Data/data/service/bluetooth/settings.properties @@ -0,0 +1 @@ +enableOnBoot=false \ No newline at end of file diff --git a/Devices/m5stack-tab5/device.properties b/Devices/m5stack-tab5/device.properties index 541092dce..ba5416e4c 100644 --- a/Devices/m5stack-tab5/device.properties +++ b/Devices/m5stack-tab5/device.properties @@ -12,6 +12,7 @@ spiRam=true spiRamMode=OCT spiRamSpeed=200M esptoolFlashFreq=80M +bluetooth=true [display] size=5" @@ -41,3 +42,5 @@ CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_D1_4BIT_BUS_SLOT_1=10 CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_D2_4BIT_BUS_SLOT_1=9 CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_D3_4BIT_BUS_SLOT_1=8 CONFIG_ESP_HOSTED_SDIO_GPIO_RESET_SLAVE=15 +CONFIG_BT_NIMBLE_TRANSPORT_UART=n +CONFIG_ESP_HOSTED_ENABLE_BT_NIMBLE=y diff --git a/Devices/m5stack-tab5/m5stack,tab5.dts b/Devices/m5stack-tab5/m5stack,tab5.dts index 93fc843dc..254b84322 100644 --- a/Devices/m5stack-tab5/m5stack,tab5.dts +++ b/Devices/m5stack-tab5/m5stack,tab5.dts @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -13,6 +14,10 @@ compatible = "root"; model = "Tab5"; + ble0 { + compatible = "esp32,ble-nimble"; + }; + gpio0 { compatible = "espressif,esp32-gpio"; gpio-count = <57>; diff --git a/Modules/lvgl-module/assets/generate-all.py b/Modules/lvgl-module/assets/generate-all.py index 483a55c50..cff67bcb6 100644 --- a/Modules/lvgl-module/assets/generate-all.py +++ b/Modules/lvgl-module/assets/generate-all.py @@ -109,6 +109,7 @@ def generate_icon_names(codepoint_map: dict, codepoint_names: list, variable_nam "keyboard_alt", # Keyboard (settings) app "usb", # Power (settings) app "wifi", # WiFi (settings) app + "bluetooth", # Bluetooth (settings) app ] # Get more from https://fonts.google.com/icons?icon.set=Material+Symbols&icon.style=Rounded @@ -138,7 +139,12 @@ def generate_icon_names(codepoint_map: dict, codepoint_names: list, variable_nam "battery_android_frame_5", "battery_android_frame_6", "battery_android_frame_full", - "battery_android_frame_bolt" + "battery_android_frame_bolt", + # Bluetooth + "bluetooth", + "bluetooth_searching", + "bluetooth_connected", + "bluetooth_disabled", ] # Get more from https://fonts.google.com/icons?icon.set=Material+Symbols&icon.style=Rounded diff --git a/Modules/lvgl-module/include/tactility/lvgl_icon_shared.h b/Modules/lvgl-module/include/tactility/lvgl_icon_shared.h index 41dd1bf9f..142367c3c 100644 --- a/Modules/lvgl-module/include/tactility/lvgl_icon_shared.h +++ b/Modules/lvgl-module/include/tactility/lvgl_icon_shared.h @@ -42,3 +42,4 @@ #define LVGL_ICON_SHARED_KEYBOARD_ALT "\xEF\x80\xA8" #define LVGL_ICON_SHARED_USB "\xEE\x87\xA0" #define LVGL_ICON_SHARED_WIFI "\xEE\x98\xBE" +#define LVGL_ICON_SHARED_BLUETOOTH "\xEE\x86\xA7" diff --git a/Modules/lvgl-module/include/tactility/lvgl_icon_statusbar.h b/Modules/lvgl-module/include/tactility/lvgl_icon_statusbar.h index bf82eedfc..a4f7967f0 100644 --- a/Modules/lvgl-module/include/tactility/lvgl_icon_statusbar.h +++ b/Modules/lvgl-module/include/tactility/lvgl_icon_statusbar.h @@ -20,3 +20,7 @@ #define LVGL_ICON_STATUSBAR_BATTERY_ANDROID_FRAME_6 "\xEF\x89\x92" #define LVGL_ICON_STATUSBAR_BATTERY_ANDROID_FRAME_FULL "\xEF\x89\x8F" #define LVGL_ICON_STATUSBAR_BATTERY_ANDROID_FRAME_BOLT "\xEF\x89\x90" +#define LVGL_ICON_STATUSBAR_BLUETOOTH "\xEE\x86\xA7" +#define LVGL_ICON_STATUSBAR_BLUETOOTH_SEARCHING "\xEE\x98\x8F" +#define LVGL_ICON_STATUSBAR_BLUETOOTH_CONNECTED "\xEE\x86\xA8" +#define LVGL_ICON_STATUSBAR_BLUETOOTH_DISABLED "\xEE\x86\xA9" diff --git a/Modules/lvgl-module/source-fonts/material_symbols_shared_12.c b/Modules/lvgl-module/source-fonts/material_symbols_shared_12.c index ba6d5efac..3389b6f2c 100644 --- a/Modules/lvgl-module/source-fonts/material_symbols_shared_12.c +++ b/Modules/lvgl-module/source-fonts/material_symbols_shared_12.c @@ -1,7 +1,7 @@ /******************************************************************************* * Size: 12 px * Bpp: 2 - * Opts: --no-compress --no-prefilter --bpp 2 --size 12 --font MaterialSymbolsRounded.ttf -r 0xE145,0xE5C3,0xE770,0xEF40,0xEBCC,0xEFE6,0xEF4A,0xE5CD,0xF15C,0xE5CA,0xE92E,0xE326,0xEB97,0xE745,0xEC1C,0xE2C7,0xF720,0xF090,0xE8AF,0xE30F,0xE8FD,0xE9F4,0xE3F4,0xE316,0xE90F,0xE894,0xE9B9,0xE159,0xE5D2,0xE28D,0xE5D4,0xE405,0xE89C,0xF8C7,0xE5D5,0xE8B6,0xE8B8,0xE9F7,0xE55D,0xF028,0xE1E0,0xE63E --format lvgl -o ../source-fonts/material_symbols_shared_12.c --force-fast-kern-format + * Opts: --no-compress --no-prefilter --bpp 2 --size 12 --font MaterialSymbolsRounded.ttf -r 0xE145,0xE5C3,0xE770,0xEF40,0xEBCC,0xEFE6,0xEF4A,0xE5CD,0xF15C,0xE5CA,0xE92E,0xE326,0xEB97,0xE745,0xEC1C,0xE2C7,0xF720,0xF090,0xE8AF,0xE30F,0xE8FD,0xE9F4,0xE3F4,0xE316,0xE90F,0xE894,0xE9B9,0xE159,0xE5D2,0xE28D,0xE5D4,0xE405,0xE89C,0xF8C7,0xE5D5,0xE8B6,0xE8B8,0xE9F7,0xE55D,0xF028,0xE1E0,0xE63E,0xE1A7 --format lvgl -o ../source-fonts/material_symbols_shared_12.c --force-fast-kern-format ******************************************************************************/ #ifdef LV_LVGL_H_INCLUDE_SIMPLE @@ -31,6 +31,11 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = { 0x2f, 0x83, 0xc0, 0x0, 0x3c, 0x0, 0x3, 0xc0, 0x0, 0x3f, 0xff, 0xff, + /* U+E1A7 "" */ + 0x1, 0x40, 0xb, 0x47, 0x2b, 0x7, 0xec, 0x7, + 0xc0, 0x1f, 0x1, 0xfb, 0x1c, 0x9d, 0x2, 0xd0, + 0x9, 0x0, + /* U+E1E0 "" */ 0x2, 0x0, 0x7, 0x80, 0x3, 0x0, 0x73, 0x3c, 0xb3, 0x3c, 0x33, 0x18, 0x3f, 0xf4, 0x3, 0x0, @@ -248,46 +253,47 @@ static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = { {.bitmap_index = 0, .adv_w = 0, .box_w = 0, .box_h = 0, .ofs_x = 0, .ofs_y = 0} /* id = 0 reserved */, {.bitmap_index = 0, .adv_w = 192, .box_w = 8, .box_h = 8, .ofs_x = 2, .ofs_y = 2}, {.bitmap_index = 16, .adv_w = 192, .box_w = 10, .box_h = 8, .ofs_x = 1, .ofs_y = 2}, - {.bitmap_index = 36, .adv_w = 192, .box_w = 8, .box_h = 10, .ofs_x = 2, .ofs_y = 1}, - {.bitmap_index = 56, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = 0}, - {.bitmap_index = 89, .adv_w = 192, .box_w = 10, .box_h = 8, .ofs_x = 1, .ofs_y = 2}, - {.bitmap_index = 109, .adv_w = 192, .box_w = 10, .box_h = 10, .ofs_x = 1, .ofs_y = 1}, - {.bitmap_index = 134, .adv_w = 192, .box_w = 6, .box_h = 4, .ofs_x = 3, .ofs_y = 4}, - {.bitmap_index = 140, .adv_w = 192, .box_w = 10, .box_h = 8, .ofs_x = 1, .ofs_y = 2}, - {.bitmap_index = 160, .adv_w = 192, .box_w = 10, .box_h = 10, .ofs_x = 1, .ofs_y = 1}, - {.bitmap_index = 185, .adv_w = 192, .box_w = 6, .box_h = 10, .ofs_x = 3, .ofs_y = 1}, - {.bitmap_index = 200, .adv_w = 192, .box_w = 8, .box_h = 10, .ofs_x = 2, .ofs_y = 1}, - {.bitmap_index = 220, .adv_w = 192, .box_w = 8, .box_h = 8, .ofs_x = 2, .ofs_y = 2}, - {.bitmap_index = 236, .adv_w = 192, .box_w = 8, .box_h = 6, .ofs_x = 2, .ofs_y = 3}, - {.bitmap_index = 248, .adv_w = 192, .box_w = 8, .box_h = 8, .ofs_x = 2, .ofs_y = 2}, - {.bitmap_index = 264, .adv_w = 192, .box_w = 10, .box_h = 6, .ofs_x = 1, .ofs_y = 3}, - {.bitmap_index = 279, .adv_w = 192, .box_w = 2, .box_h = 8, .ofs_x = 5, .ofs_y = 2}, - {.bitmap_index = 283, .adv_w = 192, .box_w = 8, .box_h = 8, .ofs_x = 2, .ofs_y = 2}, - {.bitmap_index = 299, .adv_w = 192, .box_w = 12, .box_h = 9, .ofs_x = 0, .ofs_y = 1}, - {.bitmap_index = 326, .adv_w = 192, .box_w = 9, .box_h = 8, .ofs_x = 2, .ofs_y = 2}, - {.bitmap_index = 344, .adv_w = 192, .box_w = 10, .box_h = 9, .ofs_x = 1, .ofs_y = 1}, - {.bitmap_index = 367, .adv_w = 192, .box_w = 10, .box_h = 10, .ofs_x = 1, .ofs_y = 1}, - {.bitmap_index = 392, .adv_w = 192, .box_w = 8, .box_h = 10, .ofs_x = 2, .ofs_y = 1}, - {.bitmap_index = 412, .adv_w = 192, .box_w = 10, .box_h = 10, .ofs_x = 1, .ofs_y = 1}, - {.bitmap_index = 437, .adv_w = 192, .box_w = 10, .box_h = 10, .ofs_x = 1, .ofs_y = 1}, - {.bitmap_index = 462, .adv_w = 192, .box_w = 10, .box_h = 10, .ofs_x = 1, .ofs_y = 1}, - {.bitmap_index = 487, .adv_w = 192, .box_w = 10, .box_h = 10, .ofs_x = 1, .ofs_y = 1}, - {.bitmap_index = 512, .adv_w = 192, .box_w = 8, .box_h = 10, .ofs_x = 2, .ofs_y = 1}, - {.bitmap_index = 532, .adv_w = 192, .box_w = 8, .box_h = 10, .ofs_x = 2, .ofs_y = 1}, - {.bitmap_index = 552, .adv_w = 192, .box_w = 10, .box_h = 8, .ofs_x = 1, .ofs_y = 2}, - {.bitmap_index = 572, .adv_w = 192, .box_w = 12, .box_h = 12, .ofs_x = 0, .ofs_y = 0}, - {.bitmap_index = 608, .adv_w = 192, .box_w = 10, .box_h = 10, .ofs_x = 1, .ofs_y = 1}, - {.bitmap_index = 633, .adv_w = 192, .box_w = 10, .box_h = 10, .ofs_x = 1, .ofs_y = 1}, - {.bitmap_index = 658, .adv_w = 192, .box_w = 10, .box_h = 10, .ofs_x = 1, .ofs_y = 1}, - {.bitmap_index = 683, .adv_w = 192, .box_w = 8, .box_h = 10, .ofs_x = 2, .ofs_y = 1}, - {.bitmap_index = 703, .adv_w = 192, .box_w = 9, .box_h = 8, .ofs_x = 2, .ofs_y = 2}, - {.bitmap_index = 721, .adv_w = 192, .box_w = 10, .box_h = 10, .ofs_x = 1, .ofs_y = 1}, - {.bitmap_index = 746, .adv_w = 192, .box_w = 10, .box_h = 10, .ofs_x = 1, .ofs_y = 1}, - {.bitmap_index = 771, .adv_w = 192, .box_w = 12, .box_h = 8, .ofs_x = 0, .ofs_y = 2}, - {.bitmap_index = 795, .adv_w = 192, .box_w = 8, .box_h = 8, .ofs_x = 2, .ofs_y = 2}, - {.bitmap_index = 811, .adv_w = 192, .box_w = 12, .box_h = 8, .ofs_x = 0, .ofs_y = 2}, - {.bitmap_index = 835, .adv_w = 192, .box_w = 9, .box_h = 10, .ofs_x = 1, .ofs_y = 1}, - {.bitmap_index = 858, .adv_w = 192, .box_w = 10, .box_h = 10, .ofs_x = 1, .ofs_y = 1} + {.bitmap_index = 36, .adv_w = 192, .box_w = 7, .box_h = 10, .ofs_x = 2, .ofs_y = 1}, + {.bitmap_index = 54, .adv_w = 192, .box_w = 8, .box_h = 10, .ofs_x = 2, .ofs_y = 1}, + {.bitmap_index = 74, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = 0}, + {.bitmap_index = 107, .adv_w = 192, .box_w = 10, .box_h = 8, .ofs_x = 1, .ofs_y = 2}, + {.bitmap_index = 127, .adv_w = 192, .box_w = 10, .box_h = 10, .ofs_x = 1, .ofs_y = 1}, + {.bitmap_index = 152, .adv_w = 192, .box_w = 6, .box_h = 4, .ofs_x = 3, .ofs_y = 4}, + {.bitmap_index = 158, .adv_w = 192, .box_w = 10, .box_h = 8, .ofs_x = 1, .ofs_y = 2}, + {.bitmap_index = 178, .adv_w = 192, .box_w = 10, .box_h = 10, .ofs_x = 1, .ofs_y = 1}, + {.bitmap_index = 203, .adv_w = 192, .box_w = 6, .box_h = 10, .ofs_x = 3, .ofs_y = 1}, + {.bitmap_index = 218, .adv_w = 192, .box_w = 8, .box_h = 10, .ofs_x = 2, .ofs_y = 1}, + {.bitmap_index = 238, .adv_w = 192, .box_w = 8, .box_h = 8, .ofs_x = 2, .ofs_y = 2}, + {.bitmap_index = 254, .adv_w = 192, .box_w = 8, .box_h = 6, .ofs_x = 2, .ofs_y = 3}, + {.bitmap_index = 266, .adv_w = 192, .box_w = 8, .box_h = 8, .ofs_x = 2, .ofs_y = 2}, + {.bitmap_index = 282, .adv_w = 192, .box_w = 10, .box_h = 6, .ofs_x = 1, .ofs_y = 3}, + {.bitmap_index = 297, .adv_w = 192, .box_w = 2, .box_h = 8, .ofs_x = 5, .ofs_y = 2}, + {.bitmap_index = 301, .adv_w = 192, .box_w = 8, .box_h = 8, .ofs_x = 2, .ofs_y = 2}, + {.bitmap_index = 317, .adv_w = 192, .box_w = 12, .box_h = 9, .ofs_x = 0, .ofs_y = 1}, + {.bitmap_index = 344, .adv_w = 192, .box_w = 9, .box_h = 8, .ofs_x = 2, .ofs_y = 2}, + {.bitmap_index = 362, .adv_w = 192, .box_w = 10, .box_h = 9, .ofs_x = 1, .ofs_y = 1}, + {.bitmap_index = 385, .adv_w = 192, .box_w = 10, .box_h = 10, .ofs_x = 1, .ofs_y = 1}, + {.bitmap_index = 410, .adv_w = 192, .box_w = 8, .box_h = 10, .ofs_x = 2, .ofs_y = 1}, + {.bitmap_index = 430, .adv_w = 192, .box_w = 10, .box_h = 10, .ofs_x = 1, .ofs_y = 1}, + {.bitmap_index = 455, .adv_w = 192, .box_w = 10, .box_h = 10, .ofs_x = 1, .ofs_y = 1}, + {.bitmap_index = 480, .adv_w = 192, .box_w = 10, .box_h = 10, .ofs_x = 1, .ofs_y = 1}, + {.bitmap_index = 505, .adv_w = 192, .box_w = 10, .box_h = 10, .ofs_x = 1, .ofs_y = 1}, + {.bitmap_index = 530, .adv_w = 192, .box_w = 8, .box_h = 10, .ofs_x = 2, .ofs_y = 1}, + {.bitmap_index = 550, .adv_w = 192, .box_w = 8, .box_h = 10, .ofs_x = 2, .ofs_y = 1}, + {.bitmap_index = 570, .adv_w = 192, .box_w = 10, .box_h = 8, .ofs_x = 1, .ofs_y = 2}, + {.bitmap_index = 590, .adv_w = 192, .box_w = 12, .box_h = 12, .ofs_x = 0, .ofs_y = 0}, + {.bitmap_index = 626, .adv_w = 192, .box_w = 10, .box_h = 10, .ofs_x = 1, .ofs_y = 1}, + {.bitmap_index = 651, .adv_w = 192, .box_w = 10, .box_h = 10, .ofs_x = 1, .ofs_y = 1}, + {.bitmap_index = 676, .adv_w = 192, .box_w = 10, .box_h = 10, .ofs_x = 1, .ofs_y = 1}, + {.bitmap_index = 701, .adv_w = 192, .box_w = 8, .box_h = 10, .ofs_x = 2, .ofs_y = 1}, + {.bitmap_index = 721, .adv_w = 192, .box_w = 9, .box_h = 8, .ofs_x = 2, .ofs_y = 2}, + {.bitmap_index = 739, .adv_w = 192, .box_w = 10, .box_h = 10, .ofs_x = 1, .ofs_y = 1}, + {.bitmap_index = 764, .adv_w = 192, .box_w = 10, .box_h = 10, .ofs_x = 1, .ofs_y = 1}, + {.bitmap_index = 789, .adv_w = 192, .box_w = 12, .box_h = 8, .ofs_x = 0, .ofs_y = 2}, + {.bitmap_index = 813, .adv_w = 192, .box_w = 8, .box_h = 8, .ofs_x = 2, .ofs_y = 2}, + {.bitmap_index = 829, .adv_w = 192, .box_w = 12, .box_h = 8, .ofs_x = 0, .ofs_y = 2}, + {.bitmap_index = 853, .adv_w = 192, .box_w = 9, .box_h = 10, .ofs_x = 1, .ofs_y = 1}, + {.bitmap_index = 876, .adv_w = 192, .box_w = 10, .box_h = 10, .ofs_x = 1, .ofs_y = 1} }; /*--------------------- @@ -295,12 +301,12 @@ static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = { *--------------------*/ static const uint16_t unicode_list_0[] = { - 0x0, 0x14, 0x9b, 0x148, 0x182, 0x1ca, 0x1d1, 0x1e1, - 0x2af, 0x2c0, 0x418, 0x47e, 0x485, 0x488, 0x48d, 0x48f, - 0x490, 0x4f9, 0x600, 0x62b, 0x74f, 0x757, 0x76a, 0x771, - 0x773, 0x7b8, 0x7ca, 0x7e9, 0x874, 0x8af, 0x8b2, 0xa52, - 0xa87, 0xad7, 0xdfb, 0xe05, 0xea1, 0xee3, 0xf4b, 0x1017, - 0x15db, 0x1782 + 0x0, 0x14, 0x62, 0x9b, 0x148, 0x182, 0x1ca, 0x1d1, + 0x1e1, 0x2af, 0x2c0, 0x418, 0x47e, 0x485, 0x488, 0x48d, + 0x48f, 0x490, 0x4f9, 0x600, 0x62b, 0x74f, 0x757, 0x76a, + 0x771, 0x773, 0x7b8, 0x7ca, 0x7e9, 0x874, 0x8af, 0x8b2, + 0xa52, 0xa87, 0xad7, 0xdfb, 0xe05, 0xea1, 0xee3, 0xf4b, + 0x1017, 0x15db, 0x1782 }; /*Collect the unicode lists and glyph_id offsets*/ @@ -308,7 +314,7 @@ static const lv_font_fmt_txt_cmap_t cmaps[] = { { .range_start = 57669, .range_length = 6019, .glyph_id_start = 1, - .unicode_list = unicode_list_0, .glyph_id_ofs_list = NULL, .list_length = 42, .type = LV_FONT_FMT_TXT_CMAP_SPARSE_TINY + .unicode_list = unicode_list_0, .glyph_id_ofs_list = NULL, .list_length = 43, .type = LV_FONT_FMT_TXT_CMAP_SPARSE_TINY } }; diff --git a/Modules/lvgl-module/source-fonts/material_symbols_shared_16.c b/Modules/lvgl-module/source-fonts/material_symbols_shared_16.c index 129816e83..784560262 100644 --- a/Modules/lvgl-module/source-fonts/material_symbols_shared_16.c +++ b/Modules/lvgl-module/source-fonts/material_symbols_shared_16.c @@ -1,7 +1,7 @@ /******************************************************************************* * Size: 16 px * Bpp: 2 - * Opts: --no-compress --no-prefilter --bpp 2 --size 16 --font MaterialSymbolsRounded.ttf -r 0xE145,0xE5C3,0xE770,0xEF40,0xEBCC,0xEFE6,0xEF4A,0xE5CD,0xF15C,0xE5CA,0xE92E,0xE326,0xEB97,0xE745,0xEC1C,0xE2C7,0xF720,0xF090,0xE8AF,0xE30F,0xE8FD,0xE9F4,0xE3F4,0xE316,0xE90F,0xE894,0xE9B9,0xE159,0xE5D2,0xE28D,0xE5D4,0xE405,0xE89C,0xF8C7,0xE5D5,0xE8B6,0xE8B8,0xE9F7,0xE55D,0xF028,0xE1E0,0xE63E --format lvgl -o ../source-fonts/material_symbols_shared_16.c --force-fast-kern-format + * Opts: --no-compress --no-prefilter --bpp 2 --size 16 --font MaterialSymbolsRounded.ttf -r 0xE145,0xE5C3,0xE770,0xEF40,0xEBCC,0xEFE6,0xEF4A,0xE5CD,0xF15C,0xE5CA,0xE92E,0xE326,0xEB97,0xE745,0xEC1C,0xE2C7,0xF720,0xF090,0xE8AF,0xE30F,0xE8FD,0xE9F4,0xE3F4,0xE316,0xE90F,0xE894,0xE9B9,0xE159,0xE5D2,0xE28D,0xE5D4,0xE405,0xE89C,0xF8C7,0xE5D5,0xE8B6,0xE8B8,0xE9F7,0xE55D,0xF028,0xE1E0,0xE63E,0xE1A7 --format lvgl -o ../source-fonts/material_symbols_shared_16.c --force-fast-kern-format ******************************************************************************/ #ifdef LV_LVGL_H_INCLUDE_SIMPLE @@ -36,6 +36,12 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = { 0x0, 0x0, 0xd, 0xbf, 0xff, 0xff, 0xd1, 0x55, 0x55, 0x50, + /* U+E1A7 "" */ + 0x0, 0x40, 0x0, 0x3c, 0x1, 0xf, 0xc1, 0xd3, + 0x6c, 0x1d, 0xef, 0x1, 0xff, 0x0, 0x1f, 0x0, + 0xf, 0xd0, 0xf, 0xfd, 0xf, 0x36, 0xcb, 0xe, + 0xe0, 0x3, 0xe0, 0x0, 0xe0, 0x0, 0x0, 0x0, + /* U+E1E0 "" */ 0x0, 0x0, 0x0, 0xe, 0x0, 0x2, 0xf0, 0x0, 0xe, 0x0, 0x10, 0xd1, 0x5b, 0xcd, 0x3e, 0x78, @@ -343,46 +349,47 @@ static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = { {.bitmap_index = 0, .adv_w = 0, .box_w = 0, .box_h = 0, .ofs_x = 0, .ofs_y = 0} /* id = 0 reserved */, {.bitmap_index = 0, .adv_w = 256, .box_w = 10, .box_h = 10, .ofs_x = 3, .ofs_y = 3}, {.bitmap_index = 25, .adv_w = 256, .box_w = 14, .box_h = 12, .ofs_x = 1, .ofs_y = 2}, - {.bitmap_index = 67, .adv_w = 256, .box_w = 10, .box_h = 14, .ofs_x = 3, .ofs_y = 1}, - {.bitmap_index = 102, .adv_w = 256, .box_w = 16, .box_h = 16, .ofs_x = 0, .ofs_y = 0}, - {.bitmap_index = 166, .adv_w = 256, .box_w = 14, .box_h = 12, .ofs_x = 1, .ofs_y = 2}, - {.bitmap_index = 208, .adv_w = 256, .box_w = 14, .box_h = 14, .ofs_x = 1, .ofs_y = 1}, - {.bitmap_index = 257, .adv_w = 256, .box_w = 8, .box_h = 5, .ofs_x = 4, .ofs_y = 6}, - {.bitmap_index = 267, .adv_w = 256, .box_w = 14, .box_h = 12, .ofs_x = 1, .ofs_y = 2}, - {.bitmap_index = 309, .adv_w = 256, .box_w = 12, .box_h = 12, .ofs_x = 2, .ofs_y = 2}, - {.bitmap_index = 345, .adv_w = 256, .box_w = 8, .box_h = 12, .ofs_x = 4, .ofs_y = 2}, - {.bitmap_index = 369, .adv_w = 256, .box_w = 12, .box_h = 12, .ofs_x = 2, .ofs_y = 2}, - {.bitmap_index = 405, .adv_w = 256, .box_w = 12, .box_h = 12, .ofs_x = 2, .ofs_y = 2}, - {.bitmap_index = 441, .adv_w = 256, .box_w = 12, .box_h = 8, .ofs_x = 2, .ofs_y = 4}, - {.bitmap_index = 465, .adv_w = 256, .box_w = 10, .box_h = 10, .ofs_x = 3, .ofs_y = 3}, - {.bitmap_index = 490, .adv_w = 256, .box_w = 12, .box_h = 8, .ofs_x = 2, .ofs_y = 4}, - {.bitmap_index = 514, .adv_w = 256, .box_w = 4, .box_h = 12, .ofs_x = 6, .ofs_y = 2}, - {.bitmap_index = 526, .adv_w = 256, .box_w = 12, .box_h = 12, .ofs_x = 2, .ofs_y = 2}, - {.bitmap_index = 562, .adv_w = 256, .box_w = 16, .box_h = 12, .ofs_x = 0, .ofs_y = 2}, - {.bitmap_index = 610, .adv_w = 256, .box_w = 13, .box_h = 12, .ofs_x = 2, .ofs_y = 2}, - {.bitmap_index = 649, .adv_w = 256, .box_w = 12, .box_h = 12, .ofs_x = 2, .ofs_y = 2}, - {.bitmap_index = 685, .adv_w = 256, .box_w = 14, .box_h = 14, .ofs_x = 1, .ofs_y = 1}, - {.bitmap_index = 734, .adv_w = 256, .box_w = 12, .box_h = 14, .ofs_x = 2, .ofs_y = 1}, - {.bitmap_index = 776, .adv_w = 256, .box_w = 14, .box_h = 13, .ofs_x = 1, .ofs_y = 2}, - {.bitmap_index = 822, .adv_w = 256, .box_w = 12, .box_h = 12, .ofs_x = 2, .ofs_y = 2}, - {.bitmap_index = 858, .adv_w = 256, .box_w = 14, .box_h = 14, .ofs_x = 1, .ofs_y = 1}, - {.bitmap_index = 907, .adv_w = 256, .box_w = 14, .box_h = 14, .ofs_x = 1, .ofs_y = 1}, - {.bitmap_index = 956, .adv_w = 256, .box_w = 10, .box_h = 14, .ofs_x = 3, .ofs_y = 1}, - {.bitmap_index = 991, .adv_w = 256, .box_w = 12, .box_h = 12, .ofs_x = 2, .ofs_y = 2}, - {.bitmap_index = 1027, .adv_w = 256, .box_w = 14, .box_h = 12, .ofs_x = 1, .ofs_y = 2}, - {.bitmap_index = 1069, .adv_w = 256, .box_w = 16, .box_h = 16, .ofs_x = 0, .ofs_y = 0}, - {.bitmap_index = 1133, .adv_w = 256, .box_w = 12, .box_h = 12, .ofs_x = 2, .ofs_y = 2}, - {.bitmap_index = 1169, .adv_w = 256, .box_w = 14, .box_h = 13, .ofs_x = 1, .ofs_y = 1}, - {.bitmap_index = 1215, .adv_w = 256, .box_w = 12, .box_h = 14, .ofs_x = 2, .ofs_y = 1}, - {.bitmap_index = 1257, .adv_w = 256, .box_w = 12, .box_h = 14, .ofs_x = 2, .ofs_y = 1}, - {.bitmap_index = 1299, .adv_w = 256, .box_w = 13, .box_h = 12, .ofs_x = 2, .ofs_y = 2}, - {.bitmap_index = 1338, .adv_w = 256, .box_w = 14, .box_h = 14, .ofs_x = 1, .ofs_y = 1}, - {.bitmap_index = 1387, .adv_w = 256, .box_w = 12, .box_h = 12, .ofs_x = 2, .ofs_y = 2}, - {.bitmap_index = 1423, .adv_w = 256, .box_w = 16, .box_h = 12, .ofs_x = 0, .ofs_y = 2}, - {.bitmap_index = 1471, .adv_w = 256, .box_w = 12, .box_h = 12, .ofs_x = 2, .ofs_y = 2}, - {.bitmap_index = 1507, .adv_w = 256, .box_w = 16, .box_h = 12, .ofs_x = 0, .ofs_y = 2}, - {.bitmap_index = 1555, .adv_w = 256, .box_w = 12, .box_h = 14, .ofs_x = 2, .ofs_y = 1}, - {.bitmap_index = 1597, .adv_w = 256, .box_w = 14, .box_h = 14, .ofs_x = 1, .ofs_y = 1} + {.bitmap_index = 67, .adv_w = 256, .box_w = 9, .box_h = 14, .ofs_x = 3, .ofs_y = 1}, + {.bitmap_index = 99, .adv_w = 256, .box_w = 10, .box_h = 14, .ofs_x = 3, .ofs_y = 1}, + {.bitmap_index = 134, .adv_w = 256, .box_w = 16, .box_h = 16, .ofs_x = 0, .ofs_y = 0}, + {.bitmap_index = 198, .adv_w = 256, .box_w = 14, .box_h = 12, .ofs_x = 1, .ofs_y = 2}, + {.bitmap_index = 240, .adv_w = 256, .box_w = 14, .box_h = 14, .ofs_x = 1, .ofs_y = 1}, + {.bitmap_index = 289, .adv_w = 256, .box_w = 8, .box_h = 5, .ofs_x = 4, .ofs_y = 6}, + {.bitmap_index = 299, .adv_w = 256, .box_w = 14, .box_h = 12, .ofs_x = 1, .ofs_y = 2}, + {.bitmap_index = 341, .adv_w = 256, .box_w = 12, .box_h = 12, .ofs_x = 2, .ofs_y = 2}, + {.bitmap_index = 377, .adv_w = 256, .box_w = 8, .box_h = 12, .ofs_x = 4, .ofs_y = 2}, + {.bitmap_index = 401, .adv_w = 256, .box_w = 12, .box_h = 12, .ofs_x = 2, .ofs_y = 2}, + {.bitmap_index = 437, .adv_w = 256, .box_w = 12, .box_h = 12, .ofs_x = 2, .ofs_y = 2}, + {.bitmap_index = 473, .adv_w = 256, .box_w = 12, .box_h = 8, .ofs_x = 2, .ofs_y = 4}, + {.bitmap_index = 497, .adv_w = 256, .box_w = 10, .box_h = 10, .ofs_x = 3, .ofs_y = 3}, + {.bitmap_index = 522, .adv_w = 256, .box_w = 12, .box_h = 8, .ofs_x = 2, .ofs_y = 4}, + {.bitmap_index = 546, .adv_w = 256, .box_w = 4, .box_h = 12, .ofs_x = 6, .ofs_y = 2}, + {.bitmap_index = 558, .adv_w = 256, .box_w = 12, .box_h = 12, .ofs_x = 2, .ofs_y = 2}, + {.bitmap_index = 594, .adv_w = 256, .box_w = 16, .box_h = 12, .ofs_x = 0, .ofs_y = 2}, + {.bitmap_index = 642, .adv_w = 256, .box_w = 13, .box_h = 12, .ofs_x = 2, .ofs_y = 2}, + {.bitmap_index = 681, .adv_w = 256, .box_w = 12, .box_h = 12, .ofs_x = 2, .ofs_y = 2}, + {.bitmap_index = 717, .adv_w = 256, .box_w = 14, .box_h = 14, .ofs_x = 1, .ofs_y = 1}, + {.bitmap_index = 766, .adv_w = 256, .box_w = 12, .box_h = 14, .ofs_x = 2, .ofs_y = 1}, + {.bitmap_index = 808, .adv_w = 256, .box_w = 14, .box_h = 13, .ofs_x = 1, .ofs_y = 2}, + {.bitmap_index = 854, .adv_w = 256, .box_w = 12, .box_h = 12, .ofs_x = 2, .ofs_y = 2}, + {.bitmap_index = 890, .adv_w = 256, .box_w = 14, .box_h = 14, .ofs_x = 1, .ofs_y = 1}, + {.bitmap_index = 939, .adv_w = 256, .box_w = 14, .box_h = 14, .ofs_x = 1, .ofs_y = 1}, + {.bitmap_index = 988, .adv_w = 256, .box_w = 10, .box_h = 14, .ofs_x = 3, .ofs_y = 1}, + {.bitmap_index = 1023, .adv_w = 256, .box_w = 12, .box_h = 12, .ofs_x = 2, .ofs_y = 2}, + {.bitmap_index = 1059, .adv_w = 256, .box_w = 14, .box_h = 12, .ofs_x = 1, .ofs_y = 2}, + {.bitmap_index = 1101, .adv_w = 256, .box_w = 16, .box_h = 16, .ofs_x = 0, .ofs_y = 0}, + {.bitmap_index = 1165, .adv_w = 256, .box_w = 12, .box_h = 12, .ofs_x = 2, .ofs_y = 2}, + {.bitmap_index = 1201, .adv_w = 256, .box_w = 14, .box_h = 13, .ofs_x = 1, .ofs_y = 1}, + {.bitmap_index = 1247, .adv_w = 256, .box_w = 12, .box_h = 14, .ofs_x = 2, .ofs_y = 1}, + {.bitmap_index = 1289, .adv_w = 256, .box_w = 12, .box_h = 14, .ofs_x = 2, .ofs_y = 1}, + {.bitmap_index = 1331, .adv_w = 256, .box_w = 13, .box_h = 12, .ofs_x = 2, .ofs_y = 2}, + {.bitmap_index = 1370, .adv_w = 256, .box_w = 14, .box_h = 14, .ofs_x = 1, .ofs_y = 1}, + {.bitmap_index = 1419, .adv_w = 256, .box_w = 12, .box_h = 12, .ofs_x = 2, .ofs_y = 2}, + {.bitmap_index = 1455, .adv_w = 256, .box_w = 16, .box_h = 12, .ofs_x = 0, .ofs_y = 2}, + {.bitmap_index = 1503, .adv_w = 256, .box_w = 12, .box_h = 12, .ofs_x = 2, .ofs_y = 2}, + {.bitmap_index = 1539, .adv_w = 256, .box_w = 16, .box_h = 12, .ofs_x = 0, .ofs_y = 2}, + {.bitmap_index = 1587, .adv_w = 256, .box_w = 12, .box_h = 14, .ofs_x = 2, .ofs_y = 1}, + {.bitmap_index = 1629, .adv_w = 256, .box_w = 14, .box_h = 14, .ofs_x = 1, .ofs_y = 1} }; /*--------------------- @@ -390,12 +397,12 @@ static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = { *--------------------*/ static const uint16_t unicode_list_0[] = { - 0x0, 0x14, 0x9b, 0x148, 0x182, 0x1ca, 0x1d1, 0x1e1, - 0x2af, 0x2c0, 0x418, 0x47e, 0x485, 0x488, 0x48d, 0x48f, - 0x490, 0x4f9, 0x600, 0x62b, 0x74f, 0x757, 0x76a, 0x771, - 0x773, 0x7b8, 0x7ca, 0x7e9, 0x874, 0x8af, 0x8b2, 0xa52, - 0xa87, 0xad7, 0xdfb, 0xe05, 0xea1, 0xee3, 0xf4b, 0x1017, - 0x15db, 0x1782 + 0x0, 0x14, 0x62, 0x9b, 0x148, 0x182, 0x1ca, 0x1d1, + 0x1e1, 0x2af, 0x2c0, 0x418, 0x47e, 0x485, 0x488, 0x48d, + 0x48f, 0x490, 0x4f9, 0x600, 0x62b, 0x74f, 0x757, 0x76a, + 0x771, 0x773, 0x7b8, 0x7ca, 0x7e9, 0x874, 0x8af, 0x8b2, + 0xa52, 0xa87, 0xad7, 0xdfb, 0xe05, 0xea1, 0xee3, 0xf4b, + 0x1017, 0x15db, 0x1782 }; /*Collect the unicode lists and glyph_id offsets*/ @@ -403,7 +410,7 @@ static const lv_font_fmt_txt_cmap_t cmaps[] = { { .range_start = 57669, .range_length = 6019, .glyph_id_start = 1, - .unicode_list = unicode_list_0, .glyph_id_ofs_list = NULL, .list_length = 42, .type = LV_FONT_FMT_TXT_CMAP_SPARSE_TINY + .unicode_list = unicode_list_0, .glyph_id_ofs_list = NULL, .list_length = 43, .type = LV_FONT_FMT_TXT_CMAP_SPARSE_TINY } }; diff --git a/Modules/lvgl-module/source-fonts/material_symbols_shared_20.c b/Modules/lvgl-module/source-fonts/material_symbols_shared_20.c index 0b7870555..b5408c5d2 100644 --- a/Modules/lvgl-module/source-fonts/material_symbols_shared_20.c +++ b/Modules/lvgl-module/source-fonts/material_symbols_shared_20.c @@ -1,7 +1,7 @@ /******************************************************************************* * Size: 20 px * Bpp: 2 - * Opts: --no-compress --no-prefilter --bpp 2 --size 20 --font MaterialSymbolsRounded.ttf -r 0xE145,0xE5C3,0xE770,0xEF40,0xEBCC,0xEFE6,0xEF4A,0xE5CD,0xF15C,0xE5CA,0xE92E,0xE326,0xEB97,0xE745,0xEC1C,0xE2C7,0xF720,0xF090,0xE8AF,0xE30F,0xE8FD,0xE9F4,0xE3F4,0xE316,0xE90F,0xE894,0xE9B9,0xE159,0xE5D2,0xE28D,0xE5D4,0xE405,0xE89C,0xF8C7,0xE5D5,0xE8B6,0xE8B8,0xE9F7,0xE55D,0xF028,0xE1E0,0xE63E --format lvgl -o ../source-fonts/material_symbols_shared_20.c --force-fast-kern-format + * Opts: --no-compress --no-prefilter --bpp 2 --size 20 --font MaterialSymbolsRounded.ttf -r 0xE145,0xE5C3,0xE770,0xEF40,0xEBCC,0xEFE6,0xEF4A,0xE5CD,0xF15C,0xE5CA,0xE92E,0xE326,0xEB97,0xE745,0xEC1C,0xE2C7,0xF720,0xF090,0xE8AF,0xE30F,0xE8FD,0xE9F4,0xE3F4,0xE316,0xE90F,0xE894,0xE9B9,0xE159,0xE5D2,0xE28D,0xE5D4,0xE405,0xE89C,0xF8C7,0xE5D5,0xE8B6,0xE8B8,0xE9F7,0xE55D,0xF028,0xE1E0,0xE63E,0xE1A7 --format lvgl -o ../source-fonts/material_symbols_shared_20.c --force-fast-kern-format ******************************************************************************/ #ifdef LV_LVGL_H_INCLUDE_SIMPLE @@ -39,6 +39,14 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = { 0x0, 0xe3, 0x80, 0x0, 0x0, 0xe, 0x3f, 0xff, 0xff, 0xff, 0xd1, 0xaa, 0xaa, 0xaa, 0xa8, + /* U+E1A7 "" */ + 0x0, 0x28, 0x0, 0x0, 0xf8, 0x1, 0x3, 0xf8, + 0x1e, 0xe, 0xb8, 0x2e, 0x38, 0xf0, 0x2e, 0xfb, + 0x80, 0x2f, 0xf8, 0x0, 0x2f, 0x80, 0x0, 0xbe, + 0x0, 0xb, 0xfe, 0x0, 0xbb, 0xee, 0xb, 0x8e, + 0x3c, 0x78, 0x3a, 0xe0, 0x40, 0xfe, 0x0, 0x3, + 0xe0, 0x0, 0xa, 0x0, + /* U+E1E0 "" */ 0x0, 0x0, 0x0, 0x0, 0x28, 0x0, 0x0, 0x7d, 0x0, 0x0, 0xff, 0x0, 0x0, 0x38, 0x0, 0x10, @@ -451,46 +459,47 @@ static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = { {.bitmap_index = 0, .adv_w = 0, .box_w = 0, .box_h = 0, .ofs_x = 0, .ofs_y = 0} /* id = 0 reserved */, {.bitmap_index = 0, .adv_w = 320, .box_w = 12, .box_h = 12, .ofs_x = 4, .ofs_y = 4}, {.bitmap_index = 36, .adv_w = 320, .box_w = 18, .box_h = 14, .ofs_x = 1, .ofs_y = 3}, - {.bitmap_index = 99, .adv_w = 320, .box_w = 12, .box_h = 18, .ofs_x = 4, .ofs_y = 1}, - {.bitmap_index = 153, .adv_w = 320, .box_w = 19, .box_h = 20, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 248, .adv_w = 320, .box_w = 18, .box_h = 14, .ofs_x = 1, .ofs_y = 3}, - {.bitmap_index = 311, .adv_w = 320, .box_w = 18, .box_h = 18, .ofs_x = 1, .ofs_y = 1}, - {.bitmap_index = 392, .adv_w = 320, .box_w = 10, .box_h = 6, .ofs_x = 5, .ofs_y = 7}, - {.bitmap_index = 407, .adv_w = 320, .box_w = 18, .box_h = 14, .ofs_x = 1, .ofs_y = 3}, - {.bitmap_index = 470, .adv_w = 320, .box_w = 16, .box_h = 16, .ofs_x = 2, .ofs_y = 2}, - {.bitmap_index = 534, .adv_w = 320, .box_w = 10, .box_h = 16, .ofs_x = 5, .ofs_y = 2}, - {.bitmap_index = 574, .adv_w = 320, .box_w = 14, .box_h = 16, .ofs_x = 3, .ofs_y = 2}, - {.bitmap_index = 630, .adv_w = 320, .box_w = 14, .box_h = 14, .ofs_x = 3, .ofs_y = 3}, - {.bitmap_index = 679, .adv_w = 320, .box_w = 14, .box_h = 10, .ofs_x = 3, .ofs_y = 5}, - {.bitmap_index = 714, .adv_w = 320, .box_w = 12, .box_h = 12, .ofs_x = 4, .ofs_y = 4}, - {.bitmap_index = 750, .adv_w = 320, .box_w = 16, .box_h = 10, .ofs_x = 2, .ofs_y = 5}, - {.bitmap_index = 790, .adv_w = 320, .box_w = 4, .box_h = 14, .ofs_x = 8, .ofs_y = 3}, - {.bitmap_index = 804, .adv_w = 320, .box_w = 14, .box_h = 14, .ofs_x = 3, .ofs_y = 3}, - {.bitmap_index = 853, .adv_w = 320, .box_w = 20, .box_h = 15, .ofs_x = 0, .ofs_y = 2}, - {.bitmap_index = 928, .adv_w = 320, .box_w = 16, .box_h = 14, .ofs_x = 3, .ofs_y = 3}, - {.bitmap_index = 984, .adv_w = 320, .box_w = 16, .box_h = 14, .ofs_x = 2, .ofs_y = 3}, - {.bitmap_index = 1040, .adv_w = 320, .box_w = 18, .box_h = 18, .ofs_x = 1, .ofs_y = 1}, - {.bitmap_index = 1121, .adv_w = 320, .box_w = 14, .box_h = 18, .ofs_x = 3, .ofs_y = 1}, - {.bitmap_index = 1184, .adv_w = 320, .box_w = 18, .box_h = 17, .ofs_x = 1, .ofs_y = 2}, - {.bitmap_index = 1261, .adv_w = 320, .box_w = 16, .box_h = 15, .ofs_x = 2, .ofs_y = 3}, - {.bitmap_index = 1321, .adv_w = 320, .box_w = 17, .box_h = 18, .ofs_x = 2, .ofs_y = 1}, - {.bitmap_index = 1398, .adv_w = 320, .box_w = 18, .box_h = 18, .ofs_x = 1, .ofs_y = 1}, - {.bitmap_index = 1479, .adv_w = 320, .box_w = 14, .box_h = 18, .ofs_x = 3, .ofs_y = 1}, - {.bitmap_index = 1542, .adv_w = 320, .box_w = 14, .box_h = 16, .ofs_x = 3, .ofs_y = 2}, - {.bitmap_index = 1598, .adv_w = 320, .box_w = 18, .box_h = 14, .ofs_x = 1, .ofs_y = 3}, - {.bitmap_index = 1661, .adv_w = 320, .box_w = 20, .box_h = 20, .ofs_x = 0, .ofs_y = 0}, - {.bitmap_index = 1761, .adv_w = 320, .box_w = 16, .box_h = 16, .ofs_x = 2, .ofs_y = 2}, - {.bitmap_index = 1825, .adv_w = 320, .box_w = 18, .box_h = 16, .ofs_x = 1, .ofs_y = 2}, - {.bitmap_index = 1897, .adv_w = 320, .box_w = 16, .box_h = 18, .ofs_x = 2, .ofs_y = 1}, - {.bitmap_index = 1969, .adv_w = 320, .box_w = 14, .box_h = 18, .ofs_x = 3, .ofs_y = 1}, - {.bitmap_index = 2032, .adv_w = 320, .box_w = 16, .box_h = 14, .ofs_x = 3, .ofs_y = 3}, - {.bitmap_index = 2088, .adv_w = 320, .box_w = 18, .box_h = 18, .ofs_x = 1, .ofs_y = 1}, - {.bitmap_index = 2169, .adv_w = 320, .box_w = 16, .box_h = 16, .ofs_x = 2, .ofs_y = 2}, - {.bitmap_index = 2233, .adv_w = 320, .box_w = 20, .box_h = 14, .ofs_x = 0, .ofs_y = 3}, - {.bitmap_index = 2303, .adv_w = 320, .box_w = 14, .box_h = 14, .ofs_x = 3, .ofs_y = 3}, - {.bitmap_index = 2352, .adv_w = 320, .box_w = 20, .box_h = 14, .ofs_x = 0, .ofs_y = 3}, - {.bitmap_index = 2422, .adv_w = 320, .box_w = 16, .box_h = 18, .ofs_x = 2, .ofs_y = 1}, - {.bitmap_index = 2494, .adv_w = 320, .box_w = 18, .box_h = 18, .ofs_x = 1, .ofs_y = 1} + {.bitmap_index = 99, .adv_w = 320, .box_w = 11, .box_h = 16, .ofs_x = 4, .ofs_y = 2}, + {.bitmap_index = 143, .adv_w = 320, .box_w = 12, .box_h = 18, .ofs_x = 4, .ofs_y = 1}, + {.bitmap_index = 197, .adv_w = 320, .box_w = 19, .box_h = 20, .ofs_x = 1, .ofs_y = 0}, + {.bitmap_index = 292, .adv_w = 320, .box_w = 18, .box_h = 14, .ofs_x = 1, .ofs_y = 3}, + {.bitmap_index = 355, .adv_w = 320, .box_w = 18, .box_h = 18, .ofs_x = 1, .ofs_y = 1}, + {.bitmap_index = 436, .adv_w = 320, .box_w = 10, .box_h = 6, .ofs_x = 5, .ofs_y = 7}, + {.bitmap_index = 451, .adv_w = 320, .box_w = 18, .box_h = 14, .ofs_x = 1, .ofs_y = 3}, + {.bitmap_index = 514, .adv_w = 320, .box_w = 16, .box_h = 16, .ofs_x = 2, .ofs_y = 2}, + {.bitmap_index = 578, .adv_w = 320, .box_w = 10, .box_h = 16, .ofs_x = 5, .ofs_y = 2}, + {.bitmap_index = 618, .adv_w = 320, .box_w = 14, .box_h = 16, .ofs_x = 3, .ofs_y = 2}, + {.bitmap_index = 674, .adv_w = 320, .box_w = 14, .box_h = 14, .ofs_x = 3, .ofs_y = 3}, + {.bitmap_index = 723, .adv_w = 320, .box_w = 14, .box_h = 10, .ofs_x = 3, .ofs_y = 5}, + {.bitmap_index = 758, .adv_w = 320, .box_w = 12, .box_h = 12, .ofs_x = 4, .ofs_y = 4}, + {.bitmap_index = 794, .adv_w = 320, .box_w = 16, .box_h = 10, .ofs_x = 2, .ofs_y = 5}, + {.bitmap_index = 834, .adv_w = 320, .box_w = 4, .box_h = 14, .ofs_x = 8, .ofs_y = 3}, + {.bitmap_index = 848, .adv_w = 320, .box_w = 14, .box_h = 14, .ofs_x = 3, .ofs_y = 3}, + {.bitmap_index = 897, .adv_w = 320, .box_w = 20, .box_h = 15, .ofs_x = 0, .ofs_y = 2}, + {.bitmap_index = 972, .adv_w = 320, .box_w = 16, .box_h = 14, .ofs_x = 3, .ofs_y = 3}, + {.bitmap_index = 1028, .adv_w = 320, .box_w = 16, .box_h = 14, .ofs_x = 2, .ofs_y = 3}, + {.bitmap_index = 1084, .adv_w = 320, .box_w = 18, .box_h = 18, .ofs_x = 1, .ofs_y = 1}, + {.bitmap_index = 1165, .adv_w = 320, .box_w = 14, .box_h = 18, .ofs_x = 3, .ofs_y = 1}, + {.bitmap_index = 1228, .adv_w = 320, .box_w = 18, .box_h = 17, .ofs_x = 1, .ofs_y = 2}, + {.bitmap_index = 1305, .adv_w = 320, .box_w = 16, .box_h = 15, .ofs_x = 2, .ofs_y = 3}, + {.bitmap_index = 1365, .adv_w = 320, .box_w = 17, .box_h = 18, .ofs_x = 2, .ofs_y = 1}, + {.bitmap_index = 1442, .adv_w = 320, .box_w = 18, .box_h = 18, .ofs_x = 1, .ofs_y = 1}, + {.bitmap_index = 1523, .adv_w = 320, .box_w = 14, .box_h = 18, .ofs_x = 3, .ofs_y = 1}, + {.bitmap_index = 1586, .adv_w = 320, .box_w = 14, .box_h = 16, .ofs_x = 3, .ofs_y = 2}, + {.bitmap_index = 1642, .adv_w = 320, .box_w = 18, .box_h = 14, .ofs_x = 1, .ofs_y = 3}, + {.bitmap_index = 1705, .adv_w = 320, .box_w = 20, .box_h = 20, .ofs_x = 0, .ofs_y = 0}, + {.bitmap_index = 1805, .adv_w = 320, .box_w = 16, .box_h = 16, .ofs_x = 2, .ofs_y = 2}, + {.bitmap_index = 1869, .adv_w = 320, .box_w = 18, .box_h = 16, .ofs_x = 1, .ofs_y = 2}, + {.bitmap_index = 1941, .adv_w = 320, .box_w = 16, .box_h = 18, .ofs_x = 2, .ofs_y = 1}, + {.bitmap_index = 2013, .adv_w = 320, .box_w = 14, .box_h = 18, .ofs_x = 3, .ofs_y = 1}, + {.bitmap_index = 2076, .adv_w = 320, .box_w = 16, .box_h = 14, .ofs_x = 3, .ofs_y = 3}, + {.bitmap_index = 2132, .adv_w = 320, .box_w = 18, .box_h = 18, .ofs_x = 1, .ofs_y = 1}, + {.bitmap_index = 2213, .adv_w = 320, .box_w = 16, .box_h = 16, .ofs_x = 2, .ofs_y = 2}, + {.bitmap_index = 2277, .adv_w = 320, .box_w = 20, .box_h = 14, .ofs_x = 0, .ofs_y = 3}, + {.bitmap_index = 2347, .adv_w = 320, .box_w = 14, .box_h = 14, .ofs_x = 3, .ofs_y = 3}, + {.bitmap_index = 2396, .adv_w = 320, .box_w = 20, .box_h = 14, .ofs_x = 0, .ofs_y = 3}, + {.bitmap_index = 2466, .adv_w = 320, .box_w = 16, .box_h = 18, .ofs_x = 2, .ofs_y = 1}, + {.bitmap_index = 2538, .adv_w = 320, .box_w = 18, .box_h = 18, .ofs_x = 1, .ofs_y = 1} }; /*--------------------- @@ -498,12 +507,12 @@ static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = { *--------------------*/ static const uint16_t unicode_list_0[] = { - 0x0, 0x14, 0x9b, 0x148, 0x182, 0x1ca, 0x1d1, 0x1e1, - 0x2af, 0x2c0, 0x418, 0x47e, 0x485, 0x488, 0x48d, 0x48f, - 0x490, 0x4f9, 0x600, 0x62b, 0x74f, 0x757, 0x76a, 0x771, - 0x773, 0x7b8, 0x7ca, 0x7e9, 0x874, 0x8af, 0x8b2, 0xa52, - 0xa87, 0xad7, 0xdfb, 0xe05, 0xea1, 0xee3, 0xf4b, 0x1017, - 0x15db, 0x1782 + 0x0, 0x14, 0x62, 0x9b, 0x148, 0x182, 0x1ca, 0x1d1, + 0x1e1, 0x2af, 0x2c0, 0x418, 0x47e, 0x485, 0x488, 0x48d, + 0x48f, 0x490, 0x4f9, 0x600, 0x62b, 0x74f, 0x757, 0x76a, + 0x771, 0x773, 0x7b8, 0x7ca, 0x7e9, 0x874, 0x8af, 0x8b2, + 0xa52, 0xa87, 0xad7, 0xdfb, 0xe05, 0xea1, 0xee3, 0xf4b, + 0x1017, 0x15db, 0x1782 }; /*Collect the unicode lists and glyph_id offsets*/ @@ -511,7 +520,7 @@ static const lv_font_fmt_txt_cmap_t cmaps[] = { { .range_start = 57669, .range_length = 6019, .glyph_id_start = 1, - .unicode_list = unicode_list_0, .glyph_id_ofs_list = NULL, .list_length = 42, .type = LV_FONT_FMT_TXT_CMAP_SPARSE_TINY + .unicode_list = unicode_list_0, .glyph_id_ofs_list = NULL, .list_length = 43, .type = LV_FONT_FMT_TXT_CMAP_SPARSE_TINY } }; diff --git a/Modules/lvgl-module/source-fonts/material_symbols_shared_24.c b/Modules/lvgl-module/source-fonts/material_symbols_shared_24.c index fb9c2ff92..852fe1168 100644 --- a/Modules/lvgl-module/source-fonts/material_symbols_shared_24.c +++ b/Modules/lvgl-module/source-fonts/material_symbols_shared_24.c @@ -1,7 +1,7 @@ /******************************************************************************* * Size: 24 px * Bpp: 2 - * Opts: --no-compress --no-prefilter --bpp 2 --size 24 --font MaterialSymbolsRounded.ttf -r 0xE145,0xE5C3,0xE770,0xEF40,0xEBCC,0xEFE6,0xEF4A,0xE5CD,0xF15C,0xE5CA,0xE92E,0xE326,0xEB97,0xE745,0xEC1C,0xE2C7,0xF720,0xF090,0xE8AF,0xE30F,0xE8FD,0xE9F4,0xE3F4,0xE316,0xE90F,0xE894,0xE9B9,0xE159,0xE5D2,0xE28D,0xE5D4,0xE405,0xE89C,0xF8C7,0xE5D5,0xE8B6,0xE8B8,0xE9F7,0xE55D,0xF028,0xE1E0,0xE63E --format lvgl -o ../source-fonts/material_symbols_shared_24.c --force-fast-kern-format + * Opts: --no-compress --no-prefilter --bpp 2 --size 24 --font MaterialSymbolsRounded.ttf -r 0xE145,0xE5C3,0xE770,0xEF40,0xEBCC,0xEFE6,0xEF4A,0xE5CD,0xF15C,0xE5CA,0xE92E,0xE326,0xEB97,0xE745,0xEC1C,0xE2C7,0xF720,0xF090,0xE8AF,0xE30F,0xE8FD,0xE9F4,0xE3F4,0xE316,0xE90F,0xE894,0xE9B9,0xE159,0xE5D2,0xE28D,0xE5D4,0xE405,0xE89C,0xF8C7,0xE5D5,0xE8B6,0xE8B8,0xE9F7,0xE55D,0xF028,0xE1E0,0xE63E,0xE1A7 --format lvgl -o ../source-fonts/material_symbols_shared_24.c --force-fast-kern-format ******************************************************************************/ #ifdef LV_LVGL_H_INCLUDE_SIMPLE @@ -43,6 +43,17 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = { 0xf, 0xf0, 0x0, 0x0, 0x0, 0xf, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f, 0xff, 0xff, 0xff, 0xfd, + /* U+E1A7 "" */ + 0x0, 0x0, 0x0, 0x0, 0x3, 0xd0, 0x0, 0x0, + 0xfd, 0x0, 0x40, 0x3f, 0xd0, 0xb8, 0xf, 0xbd, + 0xf, 0x83, 0xcb, 0xc0, 0xf8, 0xf2, 0xf0, 0xf, + 0xbe, 0xf0, 0x0, 0xff, 0xf0, 0x0, 0xf, 0xf0, + 0x0, 0x2, 0xf8, 0x0, 0x2, 0xff, 0x80, 0x2, + 0xff, 0xf8, 0x2, 0xf3, 0xcf, 0x82, 0xf0, 0xf1, + 0xf1, 0xf0, 0x3c, 0xf8, 0x60, 0xf, 0xf8, 0x0, + 0x3, 0xf8, 0x0, 0x0, 0xf8, 0x0, 0x0, 0x28, + 0x0, + /* U+E1E0 "" */ 0x0, 0x5, 0x0, 0x0, 0x0, 0xf0, 0x0, 0x0, 0x3f, 0xc0, 0x0, 0x7, 0xfd, 0x0, 0x0, 0xf, @@ -543,46 +554,47 @@ static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = { {.bitmap_index = 0, .adv_w = 0, .box_w = 0, .box_h = 0, .ofs_x = 0, .ofs_y = 0} /* id = 0 reserved */, {.bitmap_index = 0, .adv_w = 384, .box_w = 14, .box_h = 14, .ofs_x = 5, .ofs_y = 5}, {.bitmap_index = 49, .adv_w = 384, .box_w = 20, .box_h = 16, .ofs_x = 2, .ofs_y = 4}, - {.bitmap_index = 129, .adv_w = 384, .box_w = 14, .box_h = 20, .ofs_x = 5, .ofs_y = 2}, - {.bitmap_index = 199, .adv_w = 384, .box_w = 22, .box_h = 22, .ofs_x = 1, .ofs_y = 1}, - {.bitmap_index = 320, .adv_w = 384, .box_w = 20, .box_h = 16, .ofs_x = 2, .ofs_y = 4}, - {.bitmap_index = 400, .adv_w = 384, .box_w = 20, .box_h = 20, .ofs_x = 2, .ofs_y = 2}, - {.bitmap_index = 500, .adv_w = 384, .box_w = 12, .box_h = 7, .ofs_x = 6, .ofs_y = 9}, - {.bitmap_index = 521, .adv_w = 384, .box_w = 20, .box_h = 16, .ofs_x = 2, .ofs_y = 4}, - {.bitmap_index = 601, .adv_w = 384, .box_w = 18, .box_h = 18, .ofs_x = 3, .ofs_y = 3}, - {.bitmap_index = 682, .adv_w = 384, .box_w = 12, .box_h = 18, .ofs_x = 6, .ofs_y = 3}, - {.bitmap_index = 736, .adv_w = 384, .box_w = 16, .box_h = 18, .ofs_x = 4, .ofs_y = 3}, - {.bitmap_index = 808, .adv_w = 384, .box_w = 16, .box_h = 16, .ofs_x = 4, .ofs_y = 4}, - {.bitmap_index = 872, .adv_w = 384, .box_w = 16, .box_h = 12, .ofs_x = 4, .ofs_y = 6}, - {.bitmap_index = 920, .adv_w = 384, .box_w = 14, .box_h = 14, .ofs_x = 5, .ofs_y = 5}, - {.bitmap_index = 969, .adv_w = 384, .box_w = 18, .box_h = 12, .ofs_x = 3, .ofs_y = 6}, - {.bitmap_index = 1023, .adv_w = 384, .box_w = 4, .box_h = 16, .ofs_x = 10, .ofs_y = 4}, - {.bitmap_index = 1039, .adv_w = 384, .box_w = 16, .box_h = 16, .ofs_x = 4, .ofs_y = 4}, - {.bitmap_index = 1103, .adv_w = 384, .box_w = 24, .box_h = 17, .ofs_x = 0, .ofs_y = 3}, - {.bitmap_index = 1205, .adv_w = 384, .box_w = 18, .box_h = 16, .ofs_x = 4, .ofs_y = 4}, - {.bitmap_index = 1277, .adv_w = 384, .box_w = 18, .box_h = 17, .ofs_x = 3, .ofs_y = 4}, - {.bitmap_index = 1354, .adv_w = 384, .box_w = 20, .box_h = 20, .ofs_x = 2, .ofs_y = 2}, - {.bitmap_index = 1454, .adv_w = 384, .box_w = 16, .box_h = 20, .ofs_x = 4, .ofs_y = 2}, - {.bitmap_index = 1534, .adv_w = 384, .box_w = 20, .box_h = 19, .ofs_x = 2, .ofs_y = 3}, - {.bitmap_index = 1629, .adv_w = 384, .box_w = 18, .box_h = 18, .ofs_x = 3, .ofs_y = 3}, - {.bitmap_index = 1710, .adv_w = 384, .box_w = 20, .box_h = 20, .ofs_x = 2, .ofs_y = 2}, - {.bitmap_index = 1810, .adv_w = 384, .box_w = 20, .box_h = 20, .ofs_x = 2, .ofs_y = 2}, - {.bitmap_index = 1910, .adv_w = 384, .box_w = 16, .box_h = 20, .ofs_x = 4, .ofs_y = 2}, - {.bitmap_index = 1990, .adv_w = 384, .box_w = 16, .box_h = 18, .ofs_x = 4, .ofs_y = 3}, - {.bitmap_index = 2062, .adv_w = 384, .box_w = 20, .box_h = 16, .ofs_x = 2, .ofs_y = 4}, - {.bitmap_index = 2142, .adv_w = 384, .box_w = 24, .box_h = 23, .ofs_x = 0, .ofs_y = 1}, - {.bitmap_index = 2280, .adv_w = 384, .box_w = 18, .box_h = 18, .ofs_x = 3, .ofs_y = 3}, - {.bitmap_index = 2361, .adv_w = 384, .box_w = 20, .box_h = 18, .ofs_x = 2, .ofs_y = 3}, - {.bitmap_index = 2451, .adv_w = 384, .box_w = 18, .box_h = 20, .ofs_x = 3, .ofs_y = 2}, - {.bitmap_index = 2541, .adv_w = 384, .box_w = 16, .box_h = 20, .ofs_x = 4, .ofs_y = 2}, - {.bitmap_index = 2621, .adv_w = 384, .box_w = 18, .box_h = 16, .ofs_x = 4, .ofs_y = 4}, - {.bitmap_index = 2693, .adv_w = 384, .box_w = 20, .box_h = 20, .ofs_x = 2, .ofs_y = 2}, - {.bitmap_index = 2793, .adv_w = 384, .box_w = 18, .box_h = 18, .ofs_x = 3, .ofs_y = 3}, - {.bitmap_index = 2874, .adv_w = 384, .box_w = 22, .box_h = 16, .ofs_x = 1, .ofs_y = 4}, - {.bitmap_index = 2962, .adv_w = 384, .box_w = 16, .box_h = 16, .ofs_x = 4, .ofs_y = 4}, - {.bitmap_index = 3026, .adv_w = 384, .box_w = 22, .box_h = 16, .ofs_x = 1, .ofs_y = 4}, - {.bitmap_index = 3114, .adv_w = 384, .box_w = 18, .box_h = 20, .ofs_x = 3, .ofs_y = 2}, - {.bitmap_index = 3204, .adv_w = 384, .box_w = 20, .box_h = 20, .ofs_x = 2, .ofs_y = 2} + {.bitmap_index = 129, .adv_w = 384, .box_w = 13, .box_h = 20, .ofs_x = 5, .ofs_y = 2}, + {.bitmap_index = 194, .adv_w = 384, .box_w = 14, .box_h = 20, .ofs_x = 5, .ofs_y = 2}, + {.bitmap_index = 264, .adv_w = 384, .box_w = 22, .box_h = 22, .ofs_x = 1, .ofs_y = 1}, + {.bitmap_index = 385, .adv_w = 384, .box_w = 20, .box_h = 16, .ofs_x = 2, .ofs_y = 4}, + {.bitmap_index = 465, .adv_w = 384, .box_w = 20, .box_h = 20, .ofs_x = 2, .ofs_y = 2}, + {.bitmap_index = 565, .adv_w = 384, .box_w = 12, .box_h = 7, .ofs_x = 6, .ofs_y = 9}, + {.bitmap_index = 586, .adv_w = 384, .box_w = 20, .box_h = 16, .ofs_x = 2, .ofs_y = 4}, + {.bitmap_index = 666, .adv_w = 384, .box_w = 18, .box_h = 18, .ofs_x = 3, .ofs_y = 3}, + {.bitmap_index = 747, .adv_w = 384, .box_w = 12, .box_h = 18, .ofs_x = 6, .ofs_y = 3}, + {.bitmap_index = 801, .adv_w = 384, .box_w = 16, .box_h = 18, .ofs_x = 4, .ofs_y = 3}, + {.bitmap_index = 873, .adv_w = 384, .box_w = 16, .box_h = 16, .ofs_x = 4, .ofs_y = 4}, + {.bitmap_index = 937, .adv_w = 384, .box_w = 16, .box_h = 12, .ofs_x = 4, .ofs_y = 6}, + {.bitmap_index = 985, .adv_w = 384, .box_w = 14, .box_h = 14, .ofs_x = 5, .ofs_y = 5}, + {.bitmap_index = 1034, .adv_w = 384, .box_w = 18, .box_h = 12, .ofs_x = 3, .ofs_y = 6}, + {.bitmap_index = 1088, .adv_w = 384, .box_w = 4, .box_h = 16, .ofs_x = 10, .ofs_y = 4}, + {.bitmap_index = 1104, .adv_w = 384, .box_w = 16, .box_h = 16, .ofs_x = 4, .ofs_y = 4}, + {.bitmap_index = 1168, .adv_w = 384, .box_w = 24, .box_h = 17, .ofs_x = 0, .ofs_y = 3}, + {.bitmap_index = 1270, .adv_w = 384, .box_w = 18, .box_h = 16, .ofs_x = 4, .ofs_y = 4}, + {.bitmap_index = 1342, .adv_w = 384, .box_w = 18, .box_h = 17, .ofs_x = 3, .ofs_y = 4}, + {.bitmap_index = 1419, .adv_w = 384, .box_w = 20, .box_h = 20, .ofs_x = 2, .ofs_y = 2}, + {.bitmap_index = 1519, .adv_w = 384, .box_w = 16, .box_h = 20, .ofs_x = 4, .ofs_y = 2}, + {.bitmap_index = 1599, .adv_w = 384, .box_w = 20, .box_h = 19, .ofs_x = 2, .ofs_y = 3}, + {.bitmap_index = 1694, .adv_w = 384, .box_w = 18, .box_h = 18, .ofs_x = 3, .ofs_y = 3}, + {.bitmap_index = 1775, .adv_w = 384, .box_w = 20, .box_h = 20, .ofs_x = 2, .ofs_y = 2}, + {.bitmap_index = 1875, .adv_w = 384, .box_w = 20, .box_h = 20, .ofs_x = 2, .ofs_y = 2}, + {.bitmap_index = 1975, .adv_w = 384, .box_w = 16, .box_h = 20, .ofs_x = 4, .ofs_y = 2}, + {.bitmap_index = 2055, .adv_w = 384, .box_w = 16, .box_h = 18, .ofs_x = 4, .ofs_y = 3}, + {.bitmap_index = 2127, .adv_w = 384, .box_w = 20, .box_h = 16, .ofs_x = 2, .ofs_y = 4}, + {.bitmap_index = 2207, .adv_w = 384, .box_w = 24, .box_h = 23, .ofs_x = 0, .ofs_y = 1}, + {.bitmap_index = 2345, .adv_w = 384, .box_w = 18, .box_h = 18, .ofs_x = 3, .ofs_y = 3}, + {.bitmap_index = 2426, .adv_w = 384, .box_w = 20, .box_h = 18, .ofs_x = 2, .ofs_y = 3}, + {.bitmap_index = 2516, .adv_w = 384, .box_w = 18, .box_h = 20, .ofs_x = 3, .ofs_y = 2}, + {.bitmap_index = 2606, .adv_w = 384, .box_w = 16, .box_h = 20, .ofs_x = 4, .ofs_y = 2}, + {.bitmap_index = 2686, .adv_w = 384, .box_w = 18, .box_h = 16, .ofs_x = 4, .ofs_y = 4}, + {.bitmap_index = 2758, .adv_w = 384, .box_w = 20, .box_h = 20, .ofs_x = 2, .ofs_y = 2}, + {.bitmap_index = 2858, .adv_w = 384, .box_w = 18, .box_h = 18, .ofs_x = 3, .ofs_y = 3}, + {.bitmap_index = 2939, .adv_w = 384, .box_w = 22, .box_h = 16, .ofs_x = 1, .ofs_y = 4}, + {.bitmap_index = 3027, .adv_w = 384, .box_w = 16, .box_h = 16, .ofs_x = 4, .ofs_y = 4}, + {.bitmap_index = 3091, .adv_w = 384, .box_w = 22, .box_h = 16, .ofs_x = 1, .ofs_y = 4}, + {.bitmap_index = 3179, .adv_w = 384, .box_w = 18, .box_h = 20, .ofs_x = 3, .ofs_y = 2}, + {.bitmap_index = 3269, .adv_w = 384, .box_w = 20, .box_h = 20, .ofs_x = 2, .ofs_y = 2} }; /*--------------------- @@ -590,12 +602,12 @@ static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = { *--------------------*/ static const uint16_t unicode_list_0[] = { - 0x0, 0x14, 0x9b, 0x148, 0x182, 0x1ca, 0x1d1, 0x1e1, - 0x2af, 0x2c0, 0x418, 0x47e, 0x485, 0x488, 0x48d, 0x48f, - 0x490, 0x4f9, 0x600, 0x62b, 0x74f, 0x757, 0x76a, 0x771, - 0x773, 0x7b8, 0x7ca, 0x7e9, 0x874, 0x8af, 0x8b2, 0xa52, - 0xa87, 0xad7, 0xdfb, 0xe05, 0xea1, 0xee3, 0xf4b, 0x1017, - 0x15db, 0x1782 + 0x0, 0x14, 0x62, 0x9b, 0x148, 0x182, 0x1ca, 0x1d1, + 0x1e1, 0x2af, 0x2c0, 0x418, 0x47e, 0x485, 0x488, 0x48d, + 0x48f, 0x490, 0x4f9, 0x600, 0x62b, 0x74f, 0x757, 0x76a, + 0x771, 0x773, 0x7b8, 0x7ca, 0x7e9, 0x874, 0x8af, 0x8b2, + 0xa52, 0xa87, 0xad7, 0xdfb, 0xe05, 0xea1, 0xee3, 0xf4b, + 0x1017, 0x15db, 0x1782 }; /*Collect the unicode lists and glyph_id offsets*/ @@ -603,7 +615,7 @@ static const lv_font_fmt_txt_cmap_t cmaps[] = { { .range_start = 57669, .range_length = 6019, .glyph_id_start = 1, - .unicode_list = unicode_list_0, .glyph_id_ofs_list = NULL, .list_length = 42, .type = LV_FONT_FMT_TXT_CMAP_SPARSE_TINY + .unicode_list = unicode_list_0, .glyph_id_ofs_list = NULL, .list_length = 43, .type = LV_FONT_FMT_TXT_CMAP_SPARSE_TINY } }; diff --git a/Modules/lvgl-module/source-fonts/material_symbols_shared_32.c b/Modules/lvgl-module/source-fonts/material_symbols_shared_32.c index 25319892c..9e3d422d7 100644 --- a/Modules/lvgl-module/source-fonts/material_symbols_shared_32.c +++ b/Modules/lvgl-module/source-fonts/material_symbols_shared_32.c @@ -1,7 +1,7 @@ /******************************************************************************* * Size: 32 px * Bpp: 2 - * Opts: --no-compress --no-prefilter --bpp 2 --size 32 --font MaterialSymbolsRounded.ttf -r 0xE145,0xE5C3,0xE770,0xEF40,0xEBCC,0xEFE6,0xEF4A,0xE5CD,0xF15C,0xE5CA,0xE92E,0xE326,0xEB97,0xE745,0xEC1C,0xE2C7,0xF720,0xF090,0xE8AF,0xE30F,0xE8FD,0xE9F4,0xE3F4,0xE316,0xE90F,0xE894,0xE9B9,0xE159,0xE5D2,0xE28D,0xE5D4,0xE405,0xE89C,0xF8C7,0xE5D5,0xE8B6,0xE8B8,0xE9F7,0xE55D,0xF028,0xE1E0,0xE63E --format lvgl -o ../source-fonts/material_symbols_shared_32.c --force-fast-kern-format + * Opts: --no-compress --no-prefilter --bpp 2 --size 32 --font MaterialSymbolsRounded.ttf -r 0xE145,0xE5C3,0xE770,0xEF40,0xEBCC,0xEFE6,0xEF4A,0xE5CD,0xF15C,0xE5CA,0xE92E,0xE326,0xEB97,0xE745,0xEC1C,0xE2C7,0xF720,0xF090,0xE8AF,0xE30F,0xE8FD,0xE9F4,0xE3F4,0xE316,0xE90F,0xE894,0xE9B9,0xE159,0xE5D2,0xE28D,0xE5D4,0xE405,0xE89C,0xF8C7,0xE5D5,0xE8B6,0xE8B8,0xE9F7,0xE55D,0xF028,0xE1E0,0xE63E,0xE1A7 --format lvgl -o ../source-fonts/material_symbols_shared_32.c --force-fast-kern-format ******************************************************************************/ #ifdef LV_LVGL_H_INCLUDE_SIMPLE @@ -59,6 +59,22 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = { 0xff, 0xff, 0xfc, 0x6, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xa0, + /* U+E1A7 "" */ + 0x0, 0x0, 0x60, 0x0, 0x0, 0x0, 0x3f, 0x0, + 0x0, 0x0, 0xf, 0xf0, 0x0, 0x0, 0x3, 0xff, + 0x0, 0x24, 0x0, 0xff, 0xf0, 0x2f, 0x80, 0x3e, + 0xff, 0x3, 0xf8, 0xf, 0x8b, 0xf0, 0x7f, 0x83, + 0xe0, 0xfc, 0x7, 0xf8, 0xf8, 0xfe, 0x0, 0x7f, + 0xbe, 0xfe, 0x0, 0x7, 0xff, 0xfe, 0x0, 0x0, + 0x7f, 0xfe, 0x0, 0x0, 0x3, 0xfe, 0x0, 0x0, + 0x0, 0xff, 0x80, 0x0, 0x0, 0xff, 0xf8, 0x0, + 0x0, 0xff, 0xff, 0x80, 0x0, 0xfe, 0xff, 0xf8, + 0x0, 0xfe, 0x3e, 0x7f, 0x80, 0xfe, 0xf, 0x87, + 0xf0, 0xfe, 0x3, 0xe2, 0xfc, 0xbe, 0x0, 0xfa, + 0xfc, 0xe, 0x0, 0x3f, 0xfc, 0x0, 0x0, 0xf, + 0xfc, 0x0, 0x0, 0x3, 0xfc, 0x0, 0x0, 0x0, + 0xfc, 0x0, 0x0, 0x0, 0x2c, 0x0, 0x0, + /* U+E1E0 "" */ 0x0, 0x0, 0x18, 0x0, 0x0, 0x0, 0x0, 0x3e, 0x0, 0x0, 0x0, 0x0, 0xff, 0x40, 0x0, 0x0, @@ -907,46 +923,47 @@ static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = { {.bitmap_index = 0, .adv_w = 0, .box_w = 0, .box_h = 0, .ofs_x = 0, .ofs_y = 0} /* id = 0 reserved */, {.bitmap_index = 0, .adv_w = 512, .box_w = 20, .box_h = 20, .ofs_x = 6, .ofs_y = 6}, {.bitmap_index = 100, .adv_w = 512, .box_w = 28, .box_h = 22, .ofs_x = 2, .ofs_y = 5}, - {.bitmap_index = 254, .adv_w = 512, .box_w = 20, .box_h = 27, .ofs_x = 6, .ofs_y = 2}, - {.bitmap_index = 389, .adv_w = 512, .box_w = 30, .box_h = 30, .ofs_x = 1, .ofs_y = 1}, - {.bitmap_index = 614, .adv_w = 512, .box_w = 28, .box_h = 22, .ofs_x = 2, .ofs_y = 5}, - {.bitmap_index = 768, .adv_w = 512, .box_w = 28, .box_h = 28, .ofs_x = 2, .ofs_y = 2}, - {.bitmap_index = 964, .adv_w = 512, .box_w = 16, .box_h = 9, .ofs_x = 8, .ofs_y = 12}, - {.bitmap_index = 1000, .adv_w = 512, .box_w = 28, .box_h = 22, .ofs_x = 2, .ofs_y = 5}, - {.bitmap_index = 1154, .adv_w = 512, .box_w = 24, .box_h = 24, .ofs_x = 4, .ofs_y = 4}, - {.bitmap_index = 1298, .adv_w = 512, .box_w = 16, .box_h = 24, .ofs_x = 8, .ofs_y = 4}, - {.bitmap_index = 1394, .adv_w = 512, .box_w = 22, .box_h = 24, .ofs_x = 5, .ofs_y = 4}, - {.bitmap_index = 1526, .adv_w = 512, .box_w = 22, .box_h = 22, .ofs_x = 5, .ofs_y = 5}, - {.bitmap_index = 1647, .adv_w = 512, .box_w = 22, .box_h = 16, .ofs_x = 5, .ofs_y = 8}, - {.bitmap_index = 1735, .adv_w = 512, .box_w = 18, .box_h = 18, .ofs_x = 7, .ofs_y = 7}, - {.bitmap_index = 1816, .adv_w = 512, .box_w = 24, .box_h = 16, .ofs_x = 4, .ofs_y = 8}, - {.bitmap_index = 1912, .adv_w = 512, .box_w = 6, .box_h = 22, .ofs_x = 13, .ofs_y = 5}, - {.bitmap_index = 1945, .adv_w = 512, .box_w = 22, .box_h = 22, .ofs_x = 5, .ofs_y = 5}, - {.bitmap_index = 2066, .adv_w = 512, .box_w = 32, .box_h = 23, .ofs_x = 0, .ofs_y = 4}, - {.bitmap_index = 2250, .adv_w = 512, .box_w = 25, .box_h = 22, .ofs_x = 5, .ofs_y = 5}, - {.bitmap_index = 2388, .adv_w = 512, .box_w = 24, .box_h = 22, .ofs_x = 4, .ofs_y = 5}, - {.bitmap_index = 2520, .adv_w = 512, .box_w = 28, .box_h = 28, .ofs_x = 2, .ofs_y = 2}, - {.bitmap_index = 2716, .adv_w = 512, .box_w = 22, .box_h = 28, .ofs_x = 5, .ofs_y = 2}, - {.bitmap_index = 2870, .adv_w = 512, .box_w = 28, .box_h = 26, .ofs_x = 2, .ofs_y = 4}, - {.bitmap_index = 3052, .adv_w = 512, .box_w = 24, .box_h = 24, .ofs_x = 4, .ofs_y = 4}, - {.bitmap_index = 3196, .adv_w = 512, .box_w = 26, .box_h = 28, .ofs_x = 3, .ofs_y = 2}, - {.bitmap_index = 3378, .adv_w = 512, .box_w = 28, .box_h = 28, .ofs_x = 2, .ofs_y = 2}, - {.bitmap_index = 3574, .adv_w = 512, .box_w = 20, .box_h = 28, .ofs_x = 6, .ofs_y = 2}, - {.bitmap_index = 3714, .adv_w = 512, .box_w = 22, .box_h = 24, .ofs_x = 5, .ofs_y = 4}, - {.bitmap_index = 3846, .adv_w = 512, .box_w = 28, .box_h = 22, .ofs_x = 2, .ofs_y = 5}, - {.bitmap_index = 4000, .adv_w = 512, .box_w = 32, .box_h = 31, .ofs_x = 0, .ofs_y = 1}, - {.bitmap_index = 4248, .adv_w = 512, .box_w = 24, .box_h = 24, .ofs_x = 4, .ofs_y = 4}, - {.bitmap_index = 4392, .adv_w = 512, .box_w = 28, .box_h = 24, .ofs_x = 2, .ofs_y = 4}, - {.bitmap_index = 4560, .adv_w = 512, .box_w = 24, .box_h = 28, .ofs_x = 4, .ofs_y = 2}, - {.bitmap_index = 4728, .adv_w = 512, .box_w = 22, .box_h = 28, .ofs_x = 5, .ofs_y = 2}, - {.bitmap_index = 4882, .adv_w = 512, .box_w = 25, .box_h = 22, .ofs_x = 5, .ofs_y = 5}, - {.bitmap_index = 5020, .adv_w = 512, .box_w = 28, .box_h = 28, .ofs_x = 2, .ofs_y = 2}, - {.bitmap_index = 5216, .adv_w = 512, .box_w = 24, .box_h = 24, .ofs_x = 4, .ofs_y = 4}, - {.bitmap_index = 5360, .adv_w = 512, .box_w = 30, .box_h = 22, .ofs_x = 1, .ofs_y = 5}, - {.bitmap_index = 5525, .adv_w = 512, .box_w = 22, .box_h = 22, .ofs_x = 5, .ofs_y = 5}, - {.bitmap_index = 5646, .adv_w = 512, .box_w = 30, .box_h = 22, .ofs_x = 1, .ofs_y = 5}, - {.bitmap_index = 5811, .adv_w = 512, .box_w = 25, .box_h = 28, .ofs_x = 4, .ofs_y = 2}, - {.bitmap_index = 5986, .adv_w = 512, .box_w = 28, .box_h = 28, .ofs_x = 2, .ofs_y = 2} + {.bitmap_index = 254, .adv_w = 512, .box_w = 17, .box_h = 26, .ofs_x = 7, .ofs_y = 3}, + {.bitmap_index = 365, .adv_w = 512, .box_w = 20, .box_h = 27, .ofs_x = 6, .ofs_y = 2}, + {.bitmap_index = 500, .adv_w = 512, .box_w = 30, .box_h = 30, .ofs_x = 1, .ofs_y = 1}, + {.bitmap_index = 725, .adv_w = 512, .box_w = 28, .box_h = 22, .ofs_x = 2, .ofs_y = 5}, + {.bitmap_index = 879, .adv_w = 512, .box_w = 28, .box_h = 28, .ofs_x = 2, .ofs_y = 2}, + {.bitmap_index = 1075, .adv_w = 512, .box_w = 16, .box_h = 9, .ofs_x = 8, .ofs_y = 12}, + {.bitmap_index = 1111, .adv_w = 512, .box_w = 28, .box_h = 22, .ofs_x = 2, .ofs_y = 5}, + {.bitmap_index = 1265, .adv_w = 512, .box_w = 24, .box_h = 24, .ofs_x = 4, .ofs_y = 4}, + {.bitmap_index = 1409, .adv_w = 512, .box_w = 16, .box_h = 24, .ofs_x = 8, .ofs_y = 4}, + {.bitmap_index = 1505, .adv_w = 512, .box_w = 22, .box_h = 24, .ofs_x = 5, .ofs_y = 4}, + {.bitmap_index = 1637, .adv_w = 512, .box_w = 22, .box_h = 22, .ofs_x = 5, .ofs_y = 5}, + {.bitmap_index = 1758, .adv_w = 512, .box_w = 22, .box_h = 16, .ofs_x = 5, .ofs_y = 8}, + {.bitmap_index = 1846, .adv_w = 512, .box_w = 18, .box_h = 18, .ofs_x = 7, .ofs_y = 7}, + {.bitmap_index = 1927, .adv_w = 512, .box_w = 24, .box_h = 16, .ofs_x = 4, .ofs_y = 8}, + {.bitmap_index = 2023, .adv_w = 512, .box_w = 6, .box_h = 22, .ofs_x = 13, .ofs_y = 5}, + {.bitmap_index = 2056, .adv_w = 512, .box_w = 22, .box_h = 22, .ofs_x = 5, .ofs_y = 5}, + {.bitmap_index = 2177, .adv_w = 512, .box_w = 32, .box_h = 23, .ofs_x = 0, .ofs_y = 4}, + {.bitmap_index = 2361, .adv_w = 512, .box_w = 25, .box_h = 22, .ofs_x = 5, .ofs_y = 5}, + {.bitmap_index = 2499, .adv_w = 512, .box_w = 24, .box_h = 22, .ofs_x = 4, .ofs_y = 5}, + {.bitmap_index = 2631, .adv_w = 512, .box_w = 28, .box_h = 28, .ofs_x = 2, .ofs_y = 2}, + {.bitmap_index = 2827, .adv_w = 512, .box_w = 22, .box_h = 28, .ofs_x = 5, .ofs_y = 2}, + {.bitmap_index = 2981, .adv_w = 512, .box_w = 28, .box_h = 26, .ofs_x = 2, .ofs_y = 4}, + {.bitmap_index = 3163, .adv_w = 512, .box_w = 24, .box_h = 24, .ofs_x = 4, .ofs_y = 4}, + {.bitmap_index = 3307, .adv_w = 512, .box_w = 26, .box_h = 28, .ofs_x = 3, .ofs_y = 2}, + {.bitmap_index = 3489, .adv_w = 512, .box_w = 28, .box_h = 28, .ofs_x = 2, .ofs_y = 2}, + {.bitmap_index = 3685, .adv_w = 512, .box_w = 20, .box_h = 28, .ofs_x = 6, .ofs_y = 2}, + {.bitmap_index = 3825, .adv_w = 512, .box_w = 22, .box_h = 24, .ofs_x = 5, .ofs_y = 4}, + {.bitmap_index = 3957, .adv_w = 512, .box_w = 28, .box_h = 22, .ofs_x = 2, .ofs_y = 5}, + {.bitmap_index = 4111, .adv_w = 512, .box_w = 32, .box_h = 31, .ofs_x = 0, .ofs_y = 1}, + {.bitmap_index = 4359, .adv_w = 512, .box_w = 24, .box_h = 24, .ofs_x = 4, .ofs_y = 4}, + {.bitmap_index = 4503, .adv_w = 512, .box_w = 28, .box_h = 24, .ofs_x = 2, .ofs_y = 4}, + {.bitmap_index = 4671, .adv_w = 512, .box_w = 24, .box_h = 28, .ofs_x = 4, .ofs_y = 2}, + {.bitmap_index = 4839, .adv_w = 512, .box_w = 22, .box_h = 28, .ofs_x = 5, .ofs_y = 2}, + {.bitmap_index = 4993, .adv_w = 512, .box_w = 25, .box_h = 22, .ofs_x = 5, .ofs_y = 5}, + {.bitmap_index = 5131, .adv_w = 512, .box_w = 28, .box_h = 28, .ofs_x = 2, .ofs_y = 2}, + {.bitmap_index = 5327, .adv_w = 512, .box_w = 24, .box_h = 24, .ofs_x = 4, .ofs_y = 4}, + {.bitmap_index = 5471, .adv_w = 512, .box_w = 30, .box_h = 22, .ofs_x = 1, .ofs_y = 5}, + {.bitmap_index = 5636, .adv_w = 512, .box_w = 22, .box_h = 22, .ofs_x = 5, .ofs_y = 5}, + {.bitmap_index = 5757, .adv_w = 512, .box_w = 30, .box_h = 22, .ofs_x = 1, .ofs_y = 5}, + {.bitmap_index = 5922, .adv_w = 512, .box_w = 25, .box_h = 28, .ofs_x = 4, .ofs_y = 2}, + {.bitmap_index = 6097, .adv_w = 512, .box_w = 28, .box_h = 28, .ofs_x = 2, .ofs_y = 2} }; /*--------------------- @@ -954,12 +971,12 @@ static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = { *--------------------*/ static const uint16_t unicode_list_0[] = { - 0x0, 0x14, 0x9b, 0x148, 0x182, 0x1ca, 0x1d1, 0x1e1, - 0x2af, 0x2c0, 0x418, 0x47e, 0x485, 0x488, 0x48d, 0x48f, - 0x490, 0x4f9, 0x600, 0x62b, 0x74f, 0x757, 0x76a, 0x771, - 0x773, 0x7b8, 0x7ca, 0x7e9, 0x874, 0x8af, 0x8b2, 0xa52, - 0xa87, 0xad7, 0xdfb, 0xe05, 0xea1, 0xee3, 0xf4b, 0x1017, - 0x15db, 0x1782 + 0x0, 0x14, 0x62, 0x9b, 0x148, 0x182, 0x1ca, 0x1d1, + 0x1e1, 0x2af, 0x2c0, 0x418, 0x47e, 0x485, 0x488, 0x48d, + 0x48f, 0x490, 0x4f9, 0x600, 0x62b, 0x74f, 0x757, 0x76a, + 0x771, 0x773, 0x7b8, 0x7ca, 0x7e9, 0x874, 0x8af, 0x8b2, + 0xa52, 0xa87, 0xad7, 0xdfb, 0xe05, 0xea1, 0xee3, 0xf4b, + 0x1017, 0x15db, 0x1782 }; /*Collect the unicode lists and glyph_id offsets*/ @@ -967,7 +984,7 @@ static const lv_font_fmt_txt_cmap_t cmaps[] = { { .range_start = 57669, .range_length = 6019, .glyph_id_start = 1, - .unicode_list = unicode_list_0, .glyph_id_ofs_list = NULL, .list_length = 42, .type = LV_FONT_FMT_TXT_CMAP_SPARSE_TINY + .unicode_list = unicode_list_0, .glyph_id_ofs_list = NULL, .list_length = 43, .type = LV_FONT_FMT_TXT_CMAP_SPARSE_TINY } }; diff --git a/Modules/lvgl-module/source-fonts/material_symbols_statusbar_12.c b/Modules/lvgl-module/source-fonts/material_symbols_statusbar_12.c index b69558322..880815480 100644 --- a/Modules/lvgl-module/source-fonts/material_symbols_statusbar_12.c +++ b/Modules/lvgl-module/source-fonts/material_symbols_statusbar_12.c @@ -1,7 +1,7 @@ /******************************************************************************* * Size: 12 px * Bpp: 2 - * Opts: --no-compress --no-prefilter --bpp 2 --size 12 --font MaterialSymbolsRounded.ttf -r 0xF1DB,0xF15C,0xE322,0xE623,0xF057,0xF0B0,0xEBE4,0xEBD6,0xEBE1,0xF065,0xE1DA,0xF064,0xF257,0xF256,0xF255,0xF254,0xF253,0xF252,0xF24F,0xF250 --format lvgl -o ../source-fonts/material_symbols_statusbar_12.c --force-fast-kern-format + * Opts: --no-compress --no-prefilter --bpp 2 --size 12 --font MaterialSymbolsRounded.ttf -r 0xF1DB,0xF15C,0xE322,0xE623,0xF057,0xF0B0,0xEBE4,0xEBD6,0xEBE1,0xF065,0xE1DA,0xF064,0xF257,0xF256,0xF255,0xF254,0xF253,0xF252,0xF24F,0xF250,0xE1A7,0xE60F,0xE1A8,0xE1A9 --format lvgl -o ../source-fonts/material_symbols_statusbar_12.c --force-fast-kern-format ******************************************************************************/ #ifdef LV_LVGL_H_INCLUDE_SIMPLE @@ -22,6 +22,23 @@ /*Store the image of the glyphs*/ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = { + /* U+E1A7 "" */ + 0x1, 0x40, 0xb, 0x47, 0x2b, 0x7, 0xec, 0x7, + 0xc0, 0x1f, 0x1, 0xfb, 0x1c, 0x9d, 0x2, 0xd0, + 0x9, 0x0, + + /* U+E1A8 "" */ + 0x0, 0x20, 0x0, 0x7, 0x80, 0xd, 0x7a, 0x0, + 0x3b, 0xd0, 0x20, 0xf4, 0x82, 0xf, 0x48, 0x3, + 0xbd, 0x0, 0xd6, 0xa0, 0x0, 0x7c, 0x0, 0x2, + 0x0, + + /* U+E1A9 "" */ + 0x0, 0x14, 0x0, 0xd0, 0xb4, 0x0, 0xd2, 0xb0, + 0x0, 0xd6, 0xc0, 0x0, 0xd4, 0x0, 0x2, 0xd0, + 0x0, 0x1f, 0xd0, 0x1, 0xca, 0xd0, 0x0, 0x2e, + 0xd0, 0x0, 0x90, 0xc0, 0x0, 0x0, 0x0, + /* U+E1DA "" */ 0x50, 0x0, 0x0, 0x38, 0xff, 0x80, 0x2e, 0x0, 0x78, 0xa3, 0x80, 0xe, 0x34, 0xe0, 0x1c, 0xd, @@ -35,6 +52,12 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = { 0xa3, 0x82, 0x55, 0x70, 0x1b, 0xfe, 0x0, 0x11, 0x0, + /* U+E60F "" */ + 0x1, 0x40, 0x0, 0x2d, 0x0, 0x72, 0xb0, 0x1, + 0xfb, 0xc, 0x7, 0xc8, 0x80, 0x7c, 0x88, 0x1f, + 0xb0, 0xc7, 0x27, 0x40, 0x2, 0xd0, 0x0, 0x24, + 0x0, + /* U+E623 "" */ 0x7, 0xff, 0x1c, 0x3, 0x70, 0x43, 0xc5, 0xd7, 0xc0, 0x43, 0xc0, 0x3, 0xc0, 0x3, 0xc0, 0x3, @@ -139,26 +162,30 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = { static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = { {.bitmap_index = 0, .adv_w = 0, .box_w = 0, .box_h = 0, .ofs_x = 0, .ofs_y = 0} /* id = 0 reserved */, - {.bitmap_index = 0, .adv_w = 192, .box_w = 12, .box_h = 11, .ofs_x = 0, .ofs_y = 0}, - {.bitmap_index = 33, .adv_w = 192, .box_w = 10, .box_h = 10, .ofs_x = 1, .ofs_y = 1}, - {.bitmap_index = 58, .adv_w = 192, .box_w = 8, .box_h = 10, .ofs_x = 2, .ofs_y = 1}, - {.bitmap_index = 78, .adv_w = 192, .box_w = 12, .box_h = 9, .ofs_x = 0, .ofs_y = 1}, - {.bitmap_index = 105, .adv_w = 192, .box_w = 12, .box_h = 9, .ofs_x = 0, .ofs_y = 1}, - {.bitmap_index = 132, .adv_w = 192, .box_w = 12, .box_h = 9, .ofs_x = 0, .ofs_y = 1}, - {.bitmap_index = 159, .adv_w = 192, .box_w = 8, .box_h = 10, .ofs_x = 2, .ofs_y = 1}, - {.bitmap_index = 179, .adv_w = 192, .box_w = 12, .box_h = 9, .ofs_x = 0, .ofs_y = 1}, - {.bitmap_index = 206, .adv_w = 192, .box_w = 12, .box_h = 9, .ofs_x = 0, .ofs_y = 1}, - {.bitmap_index = 233, .adv_w = 192, .box_w = 12, .box_h = 9, .ofs_x = 0, .ofs_y = 1}, - {.bitmap_index = 260, .adv_w = 192, .box_w = 12, .box_h = 8, .ofs_x = 0, .ofs_y = 2}, - {.bitmap_index = 284, .adv_w = 192, .box_w = 8, .box_h = 10, .ofs_x = 2, .ofs_y = 1}, - {.bitmap_index = 304, .adv_w = 192, .box_w = 12, .box_h = 6, .ofs_x = 0, .ofs_y = 3}, - {.bitmap_index = 322, .adv_w = 192, .box_w = 12, .box_h = 6, .ofs_x = 0, .ofs_y = 3}, - {.bitmap_index = 340, .adv_w = 192, .box_w = 12, .box_h = 6, .ofs_x = 0, .ofs_y = 3}, - {.bitmap_index = 358, .adv_w = 192, .box_w = 12, .box_h = 6, .ofs_x = 0, .ofs_y = 3}, - {.bitmap_index = 376, .adv_w = 192, .box_w = 12, .box_h = 6, .ofs_x = 0, .ofs_y = 3}, - {.bitmap_index = 394, .adv_w = 192, .box_w = 12, .box_h = 6, .ofs_x = 0, .ofs_y = 3}, - {.bitmap_index = 412, .adv_w = 192, .box_w = 12, .box_h = 6, .ofs_x = 0, .ofs_y = 3}, - {.bitmap_index = 430, .adv_w = 192, .box_w = 12, .box_h = 6, .ofs_x = 0, .ofs_y = 3} + {.bitmap_index = 0, .adv_w = 192, .box_w = 7, .box_h = 10, .ofs_x = 2, .ofs_y = 1}, + {.bitmap_index = 18, .adv_w = 192, .box_w = 10, .box_h = 10, .ofs_x = 1, .ofs_y = 1}, + {.bitmap_index = 43, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = 0}, + {.bitmap_index = 74, .adv_w = 192, .box_w = 12, .box_h = 11, .ofs_x = 0, .ofs_y = 0}, + {.bitmap_index = 107, .adv_w = 192, .box_w = 10, .box_h = 10, .ofs_x = 1, .ofs_y = 1}, + {.bitmap_index = 132, .adv_w = 192, .box_w = 10, .box_h = 10, .ofs_x = 1, .ofs_y = 1}, + {.bitmap_index = 157, .adv_w = 192, .box_w = 8, .box_h = 10, .ofs_x = 2, .ofs_y = 1}, + {.bitmap_index = 177, .adv_w = 192, .box_w = 12, .box_h = 9, .ofs_x = 0, .ofs_y = 1}, + {.bitmap_index = 204, .adv_w = 192, .box_w = 12, .box_h = 9, .ofs_x = 0, .ofs_y = 1}, + {.bitmap_index = 231, .adv_w = 192, .box_w = 12, .box_h = 9, .ofs_x = 0, .ofs_y = 1}, + {.bitmap_index = 258, .adv_w = 192, .box_w = 8, .box_h = 10, .ofs_x = 2, .ofs_y = 1}, + {.bitmap_index = 278, .adv_w = 192, .box_w = 12, .box_h = 9, .ofs_x = 0, .ofs_y = 1}, + {.bitmap_index = 305, .adv_w = 192, .box_w = 12, .box_h = 9, .ofs_x = 0, .ofs_y = 1}, + {.bitmap_index = 332, .adv_w = 192, .box_w = 12, .box_h = 9, .ofs_x = 0, .ofs_y = 1}, + {.bitmap_index = 359, .adv_w = 192, .box_w = 12, .box_h = 8, .ofs_x = 0, .ofs_y = 2}, + {.bitmap_index = 383, .adv_w = 192, .box_w = 8, .box_h = 10, .ofs_x = 2, .ofs_y = 1}, + {.bitmap_index = 403, .adv_w = 192, .box_w = 12, .box_h = 6, .ofs_x = 0, .ofs_y = 3}, + {.bitmap_index = 421, .adv_w = 192, .box_w = 12, .box_h = 6, .ofs_x = 0, .ofs_y = 3}, + {.bitmap_index = 439, .adv_w = 192, .box_w = 12, .box_h = 6, .ofs_x = 0, .ofs_y = 3}, + {.bitmap_index = 457, .adv_w = 192, .box_w = 12, .box_h = 6, .ofs_x = 0, .ofs_y = 3}, + {.bitmap_index = 475, .adv_w = 192, .box_w = 12, .box_h = 6, .ofs_x = 0, .ofs_y = 3}, + {.bitmap_index = 493, .adv_w = 192, .box_w = 12, .box_h = 6, .ofs_x = 0, .ofs_y = 3}, + {.bitmap_index = 511, .adv_w = 192, .box_w = 12, .box_h = 6, .ofs_x = 0, .ofs_y = 3}, + {.bitmap_index = 529, .adv_w = 192, .box_w = 12, .box_h = 6, .ofs_x = 0, .ofs_y = 3} }; /*--------------------- @@ -166,17 +193,17 @@ static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = { *--------------------*/ static const uint16_t unicode_list_0[] = { - 0x0, 0x148, 0x449, 0x9fc, 0xa07, 0xa0a, 0xe7d, 0xe8a, - 0xe8b, 0xed6, 0xf82, 0x1001, 0x1075, 0x1076, 0x1078, 0x1079, - 0x107a, 0x107b, 0x107c, 0x107d + 0x0, 0x1, 0x2, 0x33, 0x17b, 0x468, 0x47c, 0xa2f, + 0xa3a, 0xa3d, 0xeb0, 0xebd, 0xebe, 0xf09, 0xfb5, 0x1034, + 0x10a8, 0x10a9, 0x10ab, 0x10ac, 0x10ad, 0x10ae, 0x10af, 0x10b0 }; /*Collect the unicode lists and glyph_id offsets*/ static const lv_font_fmt_txt_cmap_t cmaps[] = { { - .range_start = 57818, .range_length = 4222, .glyph_id_start = 1, - .unicode_list = unicode_list_0, .glyph_id_ofs_list = NULL, .list_length = 20, .type = LV_FONT_FMT_TXT_CMAP_SPARSE_TINY + .range_start = 57767, .range_length = 4273, .glyph_id_start = 1, + .unicode_list = unicode_list_0, .glyph_id_ofs_list = NULL, .list_length = 24, .type = LV_FONT_FMT_TXT_CMAP_SPARSE_TINY } }; diff --git a/Modules/lvgl-module/source-fonts/material_symbols_statusbar_16.c b/Modules/lvgl-module/source-fonts/material_symbols_statusbar_16.c index a0daf9090..1cdbf1a3c 100644 --- a/Modules/lvgl-module/source-fonts/material_symbols_statusbar_16.c +++ b/Modules/lvgl-module/source-fonts/material_symbols_statusbar_16.c @@ -1,7 +1,7 @@ /******************************************************************************* * Size: 16 px * Bpp: 2 - * Opts: --no-compress --no-prefilter --bpp 2 --size 16 --font MaterialSymbolsRounded.ttf -r 0xF1DB,0xF15C,0xE322,0xE623,0xF057,0xF0B0,0xEBE4,0xEBD6,0xEBE1,0xF065,0xE1DA,0xF064,0xF257,0xF256,0xF255,0xF254,0xF253,0xF252,0xF24F,0xF250 --format lvgl -o ../source-fonts/material_symbols_statusbar_16.c --force-fast-kern-format + * Opts: --no-compress --no-prefilter --bpp 2 --size 16 --font MaterialSymbolsRounded.ttf -r 0xF1DB,0xF15C,0xE322,0xE623,0xF057,0xF0B0,0xEBE4,0xEBD6,0xEBE1,0xF065,0xE1DA,0xF064,0xF257,0xF256,0xF255,0xF254,0xF253,0xF252,0xF24F,0xF250,0xE1A7,0xE60F,0xE1A8,0xE1A9 --format lvgl -o ../source-fonts/material_symbols_statusbar_16.c --force-fast-kern-format ******************************************************************************/ #ifdef LV_LVGL_H_INCLUDE_SIMPLE @@ -22,6 +22,28 @@ /*Store the image of the glyphs*/ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = { + /* U+E1A7 "" */ + 0x0, 0x40, 0x0, 0x3c, 0x1, 0xf, 0xc1, 0xd3, + 0x6c, 0x1d, 0xef, 0x1, 0xff, 0x0, 0x1f, 0x0, + 0xf, 0xd0, 0xf, 0xfd, 0xf, 0x36, 0xcb, 0xe, + 0xe0, 0x3, 0xe0, 0x0, 0xe0, 0x0, 0x0, 0x0, + + /* U+E1A8 "" */ + 0x0, 0x10, 0x0, 0x0, 0x3c, 0x0, 0x14, 0x3f, + 0x0, 0x1d, 0x36, 0xc0, 0x7, 0x77, 0xc0, 0x1, + 0xff, 0x0, 0xb0, 0xbc, 0xe, 0x60, 0xfd, 0x9, + 0x3, 0xff, 0x40, 0xf, 0x36, 0xc0, 0x2c, 0x3b, + 0x80, 0x0, 0x3e, 0x0, 0x0, 0x38, 0x0, 0x0, + 0x0, 0x0, + + /* U+E1A9 "" */ + 0x0, 0x4, 0x0, 0x38, 0x3, 0xc0, 0x7, 0x80, + 0xfc, 0x0, 0x78, 0x36, 0xc0, 0x7, 0x8a, 0xf0, + 0x0, 0x78, 0xb0, 0x0, 0x7, 0x80, 0x0, 0x0, + 0xf8, 0x0, 0x0, 0xff, 0x80, 0x0, 0xf3, 0xf8, + 0x0, 0xb0, 0xef, 0x80, 0x0, 0x3e, 0x78, 0x0, + 0xe, 0x7, 0x40, 0x0, 0x0, 0x40, + /* U+E1DA "" */ 0x0, 0x0, 0x0, 0x0, 0x34, 0x1, 0x40, 0x0, 0x1d, 0x2f, 0xfe, 0x40, 0x1f, 0x40, 0x6, 0xf0, @@ -40,6 +62,14 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = { 0x40, 0x7f, 0xff, 0xc0, 0x0, 0xca, 0x0, 0x0, 0x0, 0x0, 0x0, + /* U+E60F "" */ + 0x0, 0x40, 0x0, 0x0, 0xf0, 0x0, 0x50, 0xfc, + 0x0, 0x74, 0xdb, 0x0, 0x2d, 0xdf, 0x9, 0xb, + 0xfc, 0x7, 0x2, 0xf0, 0xd3, 0x3, 0xf4, 0xd3, + 0xf, 0xfd, 0x7, 0x3c, 0xdb, 0x9, 0xb0, 0xee, + 0x0, 0x0, 0xf8, 0x0, 0x0, 0xe0, 0x0, 0x0, + 0x0, 0x0, + /* U+E623 "" */ 0x0, 0x15, 0x54, 0x1, 0xff, 0xfc, 0x7, 0x80, 0xd, 0x1e, 0x0, 0xd, 0x38, 0xdd, 0xdd, 0x70, @@ -176,26 +206,30 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = { static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = { {.bitmap_index = 0, .adv_w = 0, .box_w = 0, .box_h = 0, .ofs_x = 0, .ofs_y = 0} /* id = 0 reserved */, - {.bitmap_index = 0, .adv_w = 256, .box_w = 16, .box_h = 15, .ofs_x = 0, .ofs_y = 0}, - {.bitmap_index = 60, .adv_w = 256, .box_w = 13, .box_h = 13, .ofs_x = 1, .ofs_y = 1}, - {.bitmap_index = 103, .adv_w = 256, .box_w = 12, .box_h = 14, .ofs_x = 2, .ofs_y = 1}, - {.bitmap_index = 145, .adv_w = 256, .box_w = 16, .box_h = 12, .ofs_x = 0, .ofs_y = 2}, - {.bitmap_index = 193, .adv_w = 256, .box_w = 16, .box_h = 12, .ofs_x = 0, .ofs_y = 2}, - {.bitmap_index = 241, .adv_w = 256, .box_w = 16, .box_h = 12, .ofs_x = 0, .ofs_y = 2}, - {.bitmap_index = 289, .adv_w = 256, .box_w = 12, .box_h = 14, .ofs_x = 2, .ofs_y = 1}, - {.bitmap_index = 331, .adv_w = 256, .box_w = 16, .box_h = 12, .ofs_x = 0, .ofs_y = 2}, - {.bitmap_index = 379, .adv_w = 256, .box_w = 16, .box_h = 12, .ofs_x = 0, .ofs_y = 2}, - {.bitmap_index = 427, .adv_w = 256, .box_w = 16, .box_h = 12, .ofs_x = 0, .ofs_y = 2}, - {.bitmap_index = 475, .adv_w = 256, .box_w = 16, .box_h = 12, .ofs_x = 0, .ofs_y = 2}, - {.bitmap_index = 523, .adv_w = 256, .box_w = 12, .box_h = 14, .ofs_x = 2, .ofs_y = 1}, - {.bitmap_index = 565, .adv_w = 256, .box_w = 16, .box_h = 8, .ofs_x = 0, .ofs_y = 4}, - {.bitmap_index = 597, .adv_w = 256, .box_w = 16, .box_h = 8, .ofs_x = 0, .ofs_y = 4}, - {.bitmap_index = 629, .adv_w = 256, .box_w = 16, .box_h = 8, .ofs_x = 0, .ofs_y = 4}, - {.bitmap_index = 661, .adv_w = 256, .box_w = 16, .box_h = 8, .ofs_x = 0, .ofs_y = 4}, - {.bitmap_index = 693, .adv_w = 256, .box_w = 16, .box_h = 8, .ofs_x = 0, .ofs_y = 4}, - {.bitmap_index = 725, .adv_w = 256, .box_w = 16, .box_h = 8, .ofs_x = 0, .ofs_y = 4}, - {.bitmap_index = 757, .adv_w = 256, .box_w = 16, .box_h = 8, .ofs_x = 0, .ofs_y = 4}, - {.bitmap_index = 789, .adv_w = 256, .box_w = 16, .box_h = 8, .ofs_x = 0, .ofs_y = 4} + {.bitmap_index = 0, .adv_w = 256, .box_w = 9, .box_h = 14, .ofs_x = 3, .ofs_y = 1}, + {.bitmap_index = 32, .adv_w = 256, .box_w = 12, .box_h = 14, .ofs_x = 2, .ofs_y = 1}, + {.bitmap_index = 74, .adv_w = 256, .box_w = 13, .box_h = 14, .ofs_x = 1, .ofs_y = 1}, + {.bitmap_index = 120, .adv_w = 256, .box_w = 16, .box_h = 15, .ofs_x = 0, .ofs_y = 0}, + {.bitmap_index = 180, .adv_w = 256, .box_w = 13, .box_h = 13, .ofs_x = 1, .ofs_y = 1}, + {.bitmap_index = 223, .adv_w = 256, .box_w = 12, .box_h = 14, .ofs_x = 2, .ofs_y = 1}, + {.bitmap_index = 265, .adv_w = 256, .box_w = 12, .box_h = 14, .ofs_x = 2, .ofs_y = 1}, + {.bitmap_index = 307, .adv_w = 256, .box_w = 16, .box_h = 12, .ofs_x = 0, .ofs_y = 2}, + {.bitmap_index = 355, .adv_w = 256, .box_w = 16, .box_h = 12, .ofs_x = 0, .ofs_y = 2}, + {.bitmap_index = 403, .adv_w = 256, .box_w = 16, .box_h = 12, .ofs_x = 0, .ofs_y = 2}, + {.bitmap_index = 451, .adv_w = 256, .box_w = 12, .box_h = 14, .ofs_x = 2, .ofs_y = 1}, + {.bitmap_index = 493, .adv_w = 256, .box_w = 16, .box_h = 12, .ofs_x = 0, .ofs_y = 2}, + {.bitmap_index = 541, .adv_w = 256, .box_w = 16, .box_h = 12, .ofs_x = 0, .ofs_y = 2}, + {.bitmap_index = 589, .adv_w = 256, .box_w = 16, .box_h = 12, .ofs_x = 0, .ofs_y = 2}, + {.bitmap_index = 637, .adv_w = 256, .box_w = 16, .box_h = 12, .ofs_x = 0, .ofs_y = 2}, + {.bitmap_index = 685, .adv_w = 256, .box_w = 12, .box_h = 14, .ofs_x = 2, .ofs_y = 1}, + {.bitmap_index = 727, .adv_w = 256, .box_w = 16, .box_h = 8, .ofs_x = 0, .ofs_y = 4}, + {.bitmap_index = 759, .adv_w = 256, .box_w = 16, .box_h = 8, .ofs_x = 0, .ofs_y = 4}, + {.bitmap_index = 791, .adv_w = 256, .box_w = 16, .box_h = 8, .ofs_x = 0, .ofs_y = 4}, + {.bitmap_index = 823, .adv_w = 256, .box_w = 16, .box_h = 8, .ofs_x = 0, .ofs_y = 4}, + {.bitmap_index = 855, .adv_w = 256, .box_w = 16, .box_h = 8, .ofs_x = 0, .ofs_y = 4}, + {.bitmap_index = 887, .adv_w = 256, .box_w = 16, .box_h = 8, .ofs_x = 0, .ofs_y = 4}, + {.bitmap_index = 919, .adv_w = 256, .box_w = 16, .box_h = 8, .ofs_x = 0, .ofs_y = 4}, + {.bitmap_index = 951, .adv_w = 256, .box_w = 16, .box_h = 8, .ofs_x = 0, .ofs_y = 4} }; /*--------------------- @@ -203,17 +237,17 @@ static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = { *--------------------*/ static const uint16_t unicode_list_0[] = { - 0x0, 0x148, 0x449, 0x9fc, 0xa07, 0xa0a, 0xe7d, 0xe8a, - 0xe8b, 0xed6, 0xf82, 0x1001, 0x1075, 0x1076, 0x1078, 0x1079, - 0x107a, 0x107b, 0x107c, 0x107d + 0x0, 0x1, 0x2, 0x33, 0x17b, 0x468, 0x47c, 0xa2f, + 0xa3a, 0xa3d, 0xeb0, 0xebd, 0xebe, 0xf09, 0xfb5, 0x1034, + 0x10a8, 0x10a9, 0x10ab, 0x10ac, 0x10ad, 0x10ae, 0x10af, 0x10b0 }; /*Collect the unicode lists and glyph_id offsets*/ static const lv_font_fmt_txt_cmap_t cmaps[] = { { - .range_start = 57818, .range_length = 4222, .glyph_id_start = 1, - .unicode_list = unicode_list_0, .glyph_id_ofs_list = NULL, .list_length = 20, .type = LV_FONT_FMT_TXT_CMAP_SPARSE_TINY + .range_start = 57767, .range_length = 4273, .glyph_id_start = 1, + .unicode_list = unicode_list_0, .glyph_id_ofs_list = NULL, .list_length = 24, .type = LV_FONT_FMT_TXT_CMAP_SPARSE_TINY } }; diff --git a/Modules/lvgl-module/source-fonts/material_symbols_statusbar_20.c b/Modules/lvgl-module/source-fonts/material_symbols_statusbar_20.c index 83b9e0f01..6ce319c58 100644 --- a/Modules/lvgl-module/source-fonts/material_symbols_statusbar_20.c +++ b/Modules/lvgl-module/source-fonts/material_symbols_statusbar_20.c @@ -1,7 +1,7 @@ /******************************************************************************* * Size: 20 px * Bpp: 2 - * Opts: --no-compress --no-prefilter --bpp 2 --size 20 --font MaterialSymbolsRounded.ttf -r 0xF1DB,0xF15C,0xE322,0xE623,0xF057,0xF0B0,0xEBE4,0xEBD6,0xEBE1,0xF065,0xE1DA,0xF064,0xF257,0xF256,0xF255,0xF254,0xF253,0xF252,0xF24F,0xF250 --format lvgl -o ../source-fonts/material_symbols_statusbar_20.c --force-fast-kern-format + * Opts: --no-compress --no-prefilter --bpp 2 --size 20 --font MaterialSymbolsRounded.ttf -r 0xF1DB,0xF15C,0xE322,0xE623,0xF057,0xF0B0,0xEBE4,0xEBD6,0xEBE1,0xF065,0xE1DA,0xF064,0xF257,0xF256,0xF255,0xF254,0xF253,0xF252,0xF24F,0xF250,0xE1A7,0xE60F,0xE1A8,0xE1A9 --format lvgl -o ../source-fonts/material_symbols_statusbar_20.c --force-fast-kern-format ******************************************************************************/ #ifdef LV_LVGL_H_INCLUDE_SIMPLE @@ -22,6 +22,36 @@ /*Store the image of the glyphs*/ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = { + /* U+E1A7 "" */ + 0x0, 0x28, 0x0, 0x0, 0xf8, 0x1, 0x3, 0xf8, + 0x1e, 0xe, 0xb8, 0x2e, 0x38, 0xf0, 0x2e, 0xfb, + 0x80, 0x2f, 0xf8, 0x0, 0x2f, 0x80, 0x0, 0xbe, + 0x0, 0xb, 0xfe, 0x0, 0xbb, 0xee, 0xb, 0x8e, + 0x3c, 0x78, 0x3a, 0xe0, 0x40, 0xfe, 0x0, 0x3, + 0xe0, 0x0, 0xa, 0x0, + + /* U+E1A8 "" */ + 0x0, 0x2, 0x80, 0x0, 0x0, 0x3, 0xe0, 0x0, + 0x1, 0x3, 0xf8, 0x0, 0x7, 0x83, 0xae, 0x0, + 0x2, 0xe3, 0x8f, 0x0, 0x0, 0xbb, 0xee, 0x0, + 0x0, 0x2f, 0xf8, 0x0, 0x3d, 0xb, 0xe0, 0x7c, + 0x3d, 0xb, 0xe0, 0x7c, 0x0, 0x2f, 0xf8, 0x0, + 0x0, 0xbb, 0xee, 0x0, 0x2, 0xe3, 0x8f, 0x0, + 0x7, 0x83, 0xae, 0x0, 0x1, 0x3, 0xf8, 0x0, + 0x0, 0x3, 0xe0, 0x0, 0x0, 0x2, 0x80, 0x0, + + /* U+E1A9 "" */ + 0x0, 0x0, 0xa0, 0x0, 0x1d, 0x0, 0x3e, 0x0, + 0x3, 0xd0, 0xf, 0xe0, 0x0, 0x3d, 0x3, 0xae, + 0x0, 0x3, 0xd0, 0xe3, 0xc0, 0x0, 0x3d, 0x1a, + 0xe0, 0x0, 0x3, 0xd1, 0xe0, 0x0, 0x0, 0x3d, + 0x0, 0x0, 0x0, 0x3, 0xd0, 0x0, 0x0, 0x2, + 0xfd, 0x0, 0x0, 0x2, 0xef, 0xd0, 0x0, 0x2, + 0xe3, 0xfd, 0x0, 0x1, 0xe0, 0xeb, 0xd0, 0x0, + 0x10, 0x3f, 0xbd, 0x0, 0x0, 0xf, 0x83, 0xd0, + 0x0, 0x2, 0x80, 0x3c, 0x0, 0x0, 0x0, 0x1, + 0x0, + /* U+E1DA "" */ 0x34, 0x0, 0x0, 0x0, 0x0, 0x3d, 0x1, 0xaa, 0x40, 0x0, 0xf, 0x47, 0xff, 0xfd, 0x0, 0xb, @@ -46,6 +76,16 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = { 0xf, 0xaa, 0xaa, 0xe0, 0x7, 0xff, 0xff, 0xc0, 0x0, 0x2c, 0x34, 0x0, 0x0, 0x4, 0x20, 0x0, + /* U+E60F "" */ + 0x0, 0x1c, 0x0, 0x0, 0x0, 0x2f, 0x0, 0x0, + 0x10, 0x2f, 0xc0, 0x0, 0x3c, 0x2d, 0xf0, 0x0, + 0x1f, 0x2c, 0xb8, 0x28, 0x7, 0xee, 0xf0, 0x2c, + 0x1, 0xff, 0xc1, 0xc, 0x0, 0x7f, 0xb, 0xd, + 0x0, 0x7f, 0xb, 0xd, 0x1, 0xff, 0xc1, 0xc, + 0x7, 0xee, 0xf0, 0x2c, 0x1f, 0x2c, 0xb8, 0x28, + 0x3c, 0x2d, 0xf0, 0x0, 0x10, 0x2f, 0xc0, 0x0, + 0x0, 0x2f, 0x0, 0x0, 0x0, 0x1c, 0x0, 0x0, + /* U+E623 "" */ 0x0, 0x1a, 0xaa, 0x40, 0xb, 0xff, 0xfd, 0x2, 0xe0, 0x0, 0xe0, 0xb8, 0x0, 0xe, 0x2e, 0x10, @@ -232,26 +272,30 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = { static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = { {.bitmap_index = 0, .adv_w = 0, .box_w = 0, .box_h = 0, .ofs_x = 0, .ofs_y = 0} /* id = 0 reserved */, - {.bitmap_index = 0, .adv_w = 320, .box_w = 20, .box_h = 18, .ofs_x = 0, .ofs_y = 0}, - {.bitmap_index = 90, .adv_w = 320, .box_w = 16, .box_h = 16, .ofs_x = 2, .ofs_y = 2}, - {.bitmap_index = 154, .adv_w = 320, .box_w = 14, .box_h = 18, .ofs_x = 3, .ofs_y = 1}, - {.bitmap_index = 217, .adv_w = 320, .box_w = 20, .box_h = 14, .ofs_x = 0, .ofs_y = 3}, - {.bitmap_index = 287, .adv_w = 320, .box_w = 20, .box_h = 14, .ofs_x = 0, .ofs_y = 3}, - {.bitmap_index = 357, .adv_w = 320, .box_w = 20, .box_h = 14, .ofs_x = 0, .ofs_y = 3}, - {.bitmap_index = 427, .adv_w = 320, .box_w = 14, .box_h = 18, .ofs_x = 3, .ofs_y = 1}, - {.bitmap_index = 490, .adv_w = 320, .box_w = 20, .box_h = 14, .ofs_x = 0, .ofs_y = 3}, - {.bitmap_index = 560, .adv_w = 320, .box_w = 20, .box_h = 14, .ofs_x = 0, .ofs_y = 3}, - {.bitmap_index = 630, .adv_w = 320, .box_w = 20, .box_h = 14, .ofs_x = 0, .ofs_y = 3}, - {.bitmap_index = 700, .adv_w = 320, .box_w = 20, .box_h = 14, .ofs_x = 0, .ofs_y = 3}, - {.bitmap_index = 770, .adv_w = 320, .box_w = 14, .box_h = 17, .ofs_x = 3, .ofs_y = 2}, - {.bitmap_index = 830, .adv_w = 320, .box_w = 20, .box_h = 10, .ofs_x = 0, .ofs_y = 5}, - {.bitmap_index = 880, .adv_w = 320, .box_w = 19, .box_h = 10, .ofs_x = 0, .ofs_y = 5}, - {.bitmap_index = 928, .adv_w = 320, .box_w = 20, .box_h = 10, .ofs_x = 0, .ofs_y = 5}, - {.bitmap_index = 978, .adv_w = 320, .box_w = 20, .box_h = 10, .ofs_x = 0, .ofs_y = 5}, - {.bitmap_index = 1028, .adv_w = 320, .box_w = 20, .box_h = 10, .ofs_x = 0, .ofs_y = 5}, - {.bitmap_index = 1078, .adv_w = 320, .box_w = 20, .box_h = 10, .ofs_x = 0, .ofs_y = 5}, - {.bitmap_index = 1128, .adv_w = 320, .box_w = 20, .box_h = 10, .ofs_x = 0, .ofs_y = 5}, - {.bitmap_index = 1178, .adv_w = 320, .box_w = 20, .box_h = 10, .ofs_x = 0, .ofs_y = 5} + {.bitmap_index = 0, .adv_w = 320, .box_w = 11, .box_h = 16, .ofs_x = 4, .ofs_y = 2}, + {.bitmap_index = 44, .adv_w = 320, .box_w = 16, .box_h = 16, .ofs_x = 2, .ofs_y = 2}, + {.bitmap_index = 108, .adv_w = 320, .box_w = 17, .box_h = 17, .ofs_x = 1, .ofs_y = 1}, + {.bitmap_index = 181, .adv_w = 320, .box_w = 20, .box_h = 18, .ofs_x = 0, .ofs_y = 0}, + {.bitmap_index = 271, .adv_w = 320, .box_w = 16, .box_h = 16, .ofs_x = 2, .ofs_y = 2}, + {.bitmap_index = 335, .adv_w = 320, .box_w = 16, .box_h = 16, .ofs_x = 2, .ofs_y = 2}, + {.bitmap_index = 399, .adv_w = 320, .box_w = 14, .box_h = 18, .ofs_x = 3, .ofs_y = 1}, + {.bitmap_index = 462, .adv_w = 320, .box_w = 20, .box_h = 14, .ofs_x = 0, .ofs_y = 3}, + {.bitmap_index = 532, .adv_w = 320, .box_w = 20, .box_h = 14, .ofs_x = 0, .ofs_y = 3}, + {.bitmap_index = 602, .adv_w = 320, .box_w = 20, .box_h = 14, .ofs_x = 0, .ofs_y = 3}, + {.bitmap_index = 672, .adv_w = 320, .box_w = 14, .box_h = 18, .ofs_x = 3, .ofs_y = 1}, + {.bitmap_index = 735, .adv_w = 320, .box_w = 20, .box_h = 14, .ofs_x = 0, .ofs_y = 3}, + {.bitmap_index = 805, .adv_w = 320, .box_w = 20, .box_h = 14, .ofs_x = 0, .ofs_y = 3}, + {.bitmap_index = 875, .adv_w = 320, .box_w = 20, .box_h = 14, .ofs_x = 0, .ofs_y = 3}, + {.bitmap_index = 945, .adv_w = 320, .box_w = 20, .box_h = 14, .ofs_x = 0, .ofs_y = 3}, + {.bitmap_index = 1015, .adv_w = 320, .box_w = 14, .box_h = 17, .ofs_x = 3, .ofs_y = 2}, + {.bitmap_index = 1075, .adv_w = 320, .box_w = 20, .box_h = 10, .ofs_x = 0, .ofs_y = 5}, + {.bitmap_index = 1125, .adv_w = 320, .box_w = 19, .box_h = 10, .ofs_x = 0, .ofs_y = 5}, + {.bitmap_index = 1173, .adv_w = 320, .box_w = 20, .box_h = 10, .ofs_x = 0, .ofs_y = 5}, + {.bitmap_index = 1223, .adv_w = 320, .box_w = 20, .box_h = 10, .ofs_x = 0, .ofs_y = 5}, + {.bitmap_index = 1273, .adv_w = 320, .box_w = 20, .box_h = 10, .ofs_x = 0, .ofs_y = 5}, + {.bitmap_index = 1323, .adv_w = 320, .box_w = 20, .box_h = 10, .ofs_x = 0, .ofs_y = 5}, + {.bitmap_index = 1373, .adv_w = 320, .box_w = 20, .box_h = 10, .ofs_x = 0, .ofs_y = 5}, + {.bitmap_index = 1423, .adv_w = 320, .box_w = 20, .box_h = 10, .ofs_x = 0, .ofs_y = 5} }; /*--------------------- @@ -259,17 +303,17 @@ static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = { *--------------------*/ static const uint16_t unicode_list_0[] = { - 0x0, 0x148, 0x449, 0x9fc, 0xa07, 0xa0a, 0xe7d, 0xe8a, - 0xe8b, 0xed6, 0xf82, 0x1001, 0x1075, 0x1076, 0x1078, 0x1079, - 0x107a, 0x107b, 0x107c, 0x107d + 0x0, 0x1, 0x2, 0x33, 0x17b, 0x468, 0x47c, 0xa2f, + 0xa3a, 0xa3d, 0xeb0, 0xebd, 0xebe, 0xf09, 0xfb5, 0x1034, + 0x10a8, 0x10a9, 0x10ab, 0x10ac, 0x10ad, 0x10ae, 0x10af, 0x10b0 }; /*Collect the unicode lists and glyph_id offsets*/ static const lv_font_fmt_txt_cmap_t cmaps[] = { { - .range_start = 57818, .range_length = 4222, .glyph_id_start = 1, - .unicode_list = unicode_list_0, .glyph_id_ofs_list = NULL, .list_length = 20, .type = LV_FONT_FMT_TXT_CMAP_SPARSE_TINY + .range_start = 57767, .range_length = 4273, .glyph_id_start = 1, + .unicode_list = unicode_list_0, .glyph_id_ofs_list = NULL, .list_length = 24, .type = LV_FONT_FMT_TXT_CMAP_SPARSE_TINY } }; diff --git a/Modules/lvgl-module/source-fonts/material_symbols_statusbar_30.c b/Modules/lvgl-module/source-fonts/material_symbols_statusbar_30.c index bd24080f5..fd9675db2 100644 --- a/Modules/lvgl-module/source-fonts/material_symbols_statusbar_30.c +++ b/Modules/lvgl-module/source-fonts/material_symbols_statusbar_30.c @@ -1,7 +1,7 @@ /******************************************************************************* * Size: 30 px * Bpp: 2 - * Opts: --no-compress --no-prefilter --bpp 2 --size 30 --font MaterialSymbolsRounded.ttf -r 0xF1DB,0xF15C,0xE322,0xE623,0xF057,0xF0B0,0xEBE4,0xEBD6,0xEBE1,0xF065,0xE1DA,0xF064,0xF257,0xF256,0xF255,0xF254,0xF253,0xF252,0xF24F,0xF250 --format lvgl -o ../source-fonts/material_symbols_statusbar_30.c --force-fast-kern-format + * Opts: --no-compress --no-prefilter --bpp 2 --size 30 --font MaterialSymbolsRounded.ttf -r 0xF1DB,0xF15C,0xE322,0xE623,0xF057,0xF0B0,0xEBE4,0xEBD6,0xEBE1,0xF065,0xE1DA,0xF064,0xF257,0xF256,0xF255,0xF254,0xF253,0xF252,0xF24F,0xF250,0xE1A7,0xE60F,0xE1A8,0xE1A9 --format lvgl -o ../source-fonts/material_symbols_statusbar_30.c --force-fast-kern-format ******************************************************************************/ #ifdef LV_LVGL_H_INCLUDE_SIMPLE @@ -22,6 +22,61 @@ /*Store the image of the glyphs*/ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = { + /* U+E1A7 "" */ + 0x0, 0x0, 0xb0, 0x0, 0x0, 0x0, 0xfc, 0x0, + 0x0, 0x0, 0xff, 0x0, 0x0, 0x0, 0xff, 0xc0, + 0x3d, 0x0, 0xff, 0xf0, 0x3f, 0x40, 0xf5, 0xfc, + 0xf, 0xd0, 0xf4, 0x7e, 0x3, 0xf4, 0xf4, 0xfd, + 0x0, 0xfe, 0xfb, 0xf4, 0x0, 0x3f, 0xff, 0xd0, + 0x0, 0xf, 0xff, 0x40, 0x0, 0x3, 0xfd, 0x0, + 0x0, 0x3, 0xfd, 0x0, 0x0, 0xf, 0xff, 0x40, + 0x0, 0x3f, 0xff, 0xd0, 0x0, 0xfe, 0xfb, 0xf4, + 0x3, 0xf4, 0xf4, 0xfd, 0xf, 0xd0, 0xf4, 0x7e, + 0x3f, 0x40, 0xf5, 0xfc, 0x3d, 0x0, 0xff, 0xf0, + 0x0, 0x0, 0xff, 0xc0, 0x0, 0x0, 0xff, 0x0, + 0x0, 0x0, 0xfc, 0x0, 0x0, 0x0, 0xb0, 0x0, + + /* U+E1A8 "" */ + 0x0, 0x0, 0xb, 0x0, 0x0, 0x0, 0x0, 0x0, + 0xfc, 0x0, 0x0, 0x0, 0x0, 0xf, 0xf0, 0x0, + 0x0, 0x0, 0x0, 0xff, 0xc0, 0x0, 0x3, 0xd0, + 0xf, 0xff, 0x0, 0x0, 0x3f, 0x40, 0xf5, 0xfc, + 0x0, 0x0, 0xfd, 0xf, 0x47, 0xe0, 0x0, 0x3, + 0xf4, 0xf4, 0xfd, 0x0, 0x0, 0xf, 0xef, 0xbf, + 0x40, 0x0, 0x0, 0x3f, 0xff, 0xd0, 0x0, 0x2d, + 0x0, 0xff, 0xf4, 0x7, 0x8b, 0xf0, 0x3, 0xfd, + 0x0, 0xfe, 0xbf, 0x0, 0x3f, 0xd0, 0xf, 0xe2, + 0xd0, 0xf, 0xff, 0x40, 0x78, 0x0, 0x3, 0xff, + 0xfd, 0x0, 0x0, 0x0, 0xfe, 0xfb, 0xf4, 0x0, + 0x0, 0x3f, 0x4f, 0x4f, 0xd0, 0x0, 0xf, 0xd0, + 0xf4, 0x7e, 0x0, 0x3, 0xf4, 0xf, 0x5f, 0xc0, + 0x0, 0x3d, 0x0, 0xff, 0xf0, 0x0, 0x0, 0x0, + 0xf, 0xfc, 0x0, 0x0, 0x0, 0x0, 0xff, 0x0, + 0x0, 0x0, 0x0, 0xf, 0xc0, 0x0, 0x0, 0x0, + 0x0, 0xb0, 0x0, 0x0, + + /* U+E1A9 "" */ + 0x0, 0x0, 0x0, 0xb0, 0x0, 0x0, 0x1e, 0x0, + 0x0, 0x3f, 0x0, 0x0, 0x7, 0xe0, 0x0, 0xf, + 0xf0, 0x0, 0x0, 0xfe, 0x0, 0x3, 0xff, 0x0, + 0x0, 0xf, 0xe0, 0x0, 0xff, 0xf0, 0x0, 0x0, + 0xfe, 0x0, 0x3d, 0x7f, 0x0, 0x0, 0xf, 0xe0, + 0xf, 0x47, 0xe0, 0x0, 0x0, 0xfe, 0x3, 0xd3, + 0xf4, 0x0, 0x0, 0xf, 0xe0, 0x3b, 0xf4, 0x0, + 0x0, 0x0, 0xfe, 0x3, 0xf4, 0x0, 0x0, 0x0, + 0xf, 0xe0, 0x34, 0x0, 0x0, 0x0, 0x0, 0xfe, + 0x0, 0x0, 0x0, 0x0, 0x0, 0xf, 0xe0, 0x0, + 0x0, 0x0, 0x0, 0x3, 0xfe, 0x0, 0x0, 0x0, + 0x0, 0x3, 0xff, 0xe0, 0x0, 0x0, 0x0, 0x3, + 0xfb, 0xfe, 0x0, 0x0, 0x0, 0x3, 0xf4, 0xff, + 0xe0, 0x0, 0x0, 0x3, 0xf4, 0x3d, 0xfe, 0x0, + 0x0, 0x3, 0xf4, 0xf, 0x5f, 0xe0, 0x0, 0x0, + 0xf4, 0x3, 0xff, 0xfe, 0x0, 0x0, 0x0, 0x0, + 0xff, 0xdf, 0xe0, 0x0, 0x0, 0x0, 0x3f, 0xc0, + 0xfe, 0x0, 0x0, 0x0, 0xf, 0xc0, 0xf, 0xe0, + 0x0, 0x0, 0x2, 0xc0, 0x0, 0xfc, 0x0, 0x0, + 0x0, 0x0, 0x0, 0xa, 0x0, + /* U+E1DA "" */ 0x1d, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3, 0xf4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xf, @@ -69,6 +124,26 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = { 0xf8, 0x3e, 0x0, 0x0, 0x0, 0x0, 0xf4, 0x3d, 0x0, 0x0, 0x0, 0x0, 0x10, 0x4, 0x0, 0x0, + /* U+E60F "" */ + 0x0, 0x2, 0xd0, 0x0, 0x0, 0x0, 0x0, 0xf, + 0xd0, 0x0, 0x0, 0x0, 0x0, 0x3f, 0xd0, 0x0, + 0x0, 0x0, 0x0, 0xff, 0xd0, 0x0, 0x0, 0xb8, + 0x3, 0xff, 0xd0, 0x0, 0x2, 0xf8, 0xf, 0x9f, + 0xd0, 0x0, 0x3, 0xf8, 0x3e, 0x1f, 0xc0, 0x28, + 0x3, 0xf8, 0xf8, 0xfe, 0x0, 0xf0, 0x3, 0xfb, + 0xef, 0xe0, 0x2, 0xe0, 0x3, 0xff, 0xfe, 0x0, + 0x7, 0xc0, 0x3, 0xff, 0xe0, 0x3c, 0xf, 0x0, + 0x3, 0xfe, 0x3, 0xf0, 0x3c, 0x0, 0xf, 0xf8, + 0xf, 0xc0, 0xf0, 0x0, 0xff, 0xf8, 0xf, 0x3, + 0xc0, 0xf, 0xff, 0xf8, 0x0, 0x1f, 0x0, 0xfe, + 0xfb, 0xf8, 0x0, 0xb4, 0xf, 0xe3, 0xe3, 0xf8, + 0x3, 0xc0, 0xfe, 0xf, 0x87, 0xf0, 0xa, 0xb, + 0xe0, 0x3e, 0x3f, 0x40, 0x0, 0x2e, 0x0, 0xff, + 0xf4, 0x0, 0x0, 0x0, 0x3, 0xff, 0x40, 0x0, + 0x0, 0x0, 0xf, 0xf4, 0x0, 0x0, 0x0, 0x0, + 0x3f, 0x40, 0x0, 0x0, 0x0, 0x0, 0xb4, 0x0, + 0x0, 0x0, + /* U+E623 "" */ 0x0, 0x0, 0x6a, 0xaa, 0x90, 0x0, 0x7, 0xff, 0xff, 0xfd, 0x0, 0x1f, 0xff, 0xff, 0xff, 0x0, @@ -407,26 +482,30 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = { static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = { {.bitmap_index = 0, .adv_w = 0, .box_w = 0, .box_h = 0, .ofs_x = 0, .ofs_y = 0} /* id = 0 reserved */, - {.bitmap_index = 0, .adv_w = 480, .box_w = 30, .box_h = 26, .ofs_x = 0, .ofs_y = 1}, - {.bitmap_index = 195, .adv_w = 480, .box_w = 24, .box_h = 24, .ofs_x = 3, .ofs_y = 3}, - {.bitmap_index = 339, .adv_w = 480, .box_w = 20, .box_h = 26, .ofs_x = 5, .ofs_y = 2}, - {.bitmap_index = 469, .adv_w = 480, .box_w = 28, .box_h = 21, .ofs_x = 1, .ofs_y = 4}, - {.bitmap_index = 616, .adv_w = 480, .box_w = 28, .box_h = 21, .ofs_x = 1, .ofs_y = 4}, - {.bitmap_index = 763, .adv_w = 480, .box_w = 28, .box_h = 21, .ofs_x = 1, .ofs_y = 4}, - {.bitmap_index = 910, .adv_w = 480, .box_w = 20, .box_h = 26, .ofs_x = 5, .ofs_y = 2}, - {.bitmap_index = 1040, .adv_w = 480, .box_w = 28, .box_h = 21, .ofs_x = 1, .ofs_y = 4}, - {.bitmap_index = 1187, .adv_w = 480, .box_w = 28, .box_h = 21, .ofs_x = 1, .ofs_y = 4}, - {.bitmap_index = 1334, .adv_w = 480, .box_w = 28, .box_h = 21, .ofs_x = 1, .ofs_y = 4}, - {.bitmap_index = 1481, .adv_w = 480, .box_w = 28, .box_h = 20, .ofs_x = 1, .ofs_y = 5}, - {.bitmap_index = 1621, .adv_w = 480, .box_w = 20, .box_h = 25, .ofs_x = 5, .ofs_y = 3}, - {.bitmap_index = 1746, .adv_w = 480, .box_w = 28, .box_h = 16, .ofs_x = 1, .ofs_y = 7}, - {.bitmap_index = 1858, .adv_w = 480, .box_w = 28, .box_h = 16, .ofs_x = 1, .ofs_y = 7}, - {.bitmap_index = 1970, .adv_w = 480, .box_w = 28, .box_h = 16, .ofs_x = 1, .ofs_y = 7}, - {.bitmap_index = 2082, .adv_w = 480, .box_w = 28, .box_h = 16, .ofs_x = 1, .ofs_y = 7}, - {.bitmap_index = 2194, .adv_w = 480, .box_w = 28, .box_h = 16, .ofs_x = 1, .ofs_y = 7}, - {.bitmap_index = 2306, .adv_w = 480, .box_w = 28, .box_h = 16, .ofs_x = 1, .ofs_y = 7}, - {.bitmap_index = 2418, .adv_w = 480, .box_w = 28, .box_h = 16, .ofs_x = 1, .ofs_y = 7}, - {.bitmap_index = 2530, .adv_w = 480, .box_w = 28, .box_h = 16, .ofs_x = 1, .ofs_y = 7} + {.bitmap_index = 0, .adv_w = 480, .box_w = 16, .box_h = 24, .ofs_x = 6, .ofs_y = 3}, + {.bitmap_index = 96, .adv_w = 480, .box_w = 22, .box_h = 24, .ofs_x = 4, .ofs_y = 3}, + {.bitmap_index = 228, .adv_w = 480, .box_w = 25, .box_h = 25, .ofs_x = 2, .ofs_y = 2}, + {.bitmap_index = 385, .adv_w = 480, .box_w = 30, .box_h = 26, .ofs_x = 0, .ofs_y = 1}, + {.bitmap_index = 580, .adv_w = 480, .box_w = 24, .box_h = 24, .ofs_x = 3, .ofs_y = 3}, + {.bitmap_index = 724, .adv_w = 480, .box_w = 23, .box_h = 24, .ofs_x = 4, .ofs_y = 3}, + {.bitmap_index = 862, .adv_w = 480, .box_w = 20, .box_h = 26, .ofs_x = 5, .ofs_y = 2}, + {.bitmap_index = 992, .adv_w = 480, .box_w = 28, .box_h = 21, .ofs_x = 1, .ofs_y = 4}, + {.bitmap_index = 1139, .adv_w = 480, .box_w = 28, .box_h = 21, .ofs_x = 1, .ofs_y = 4}, + {.bitmap_index = 1286, .adv_w = 480, .box_w = 28, .box_h = 21, .ofs_x = 1, .ofs_y = 4}, + {.bitmap_index = 1433, .adv_w = 480, .box_w = 20, .box_h = 26, .ofs_x = 5, .ofs_y = 2}, + {.bitmap_index = 1563, .adv_w = 480, .box_w = 28, .box_h = 21, .ofs_x = 1, .ofs_y = 4}, + {.bitmap_index = 1710, .adv_w = 480, .box_w = 28, .box_h = 21, .ofs_x = 1, .ofs_y = 4}, + {.bitmap_index = 1857, .adv_w = 480, .box_w = 28, .box_h = 21, .ofs_x = 1, .ofs_y = 4}, + {.bitmap_index = 2004, .adv_w = 480, .box_w = 28, .box_h = 20, .ofs_x = 1, .ofs_y = 5}, + {.bitmap_index = 2144, .adv_w = 480, .box_w = 20, .box_h = 25, .ofs_x = 5, .ofs_y = 3}, + {.bitmap_index = 2269, .adv_w = 480, .box_w = 28, .box_h = 16, .ofs_x = 1, .ofs_y = 7}, + {.bitmap_index = 2381, .adv_w = 480, .box_w = 28, .box_h = 16, .ofs_x = 1, .ofs_y = 7}, + {.bitmap_index = 2493, .adv_w = 480, .box_w = 28, .box_h = 16, .ofs_x = 1, .ofs_y = 7}, + {.bitmap_index = 2605, .adv_w = 480, .box_w = 28, .box_h = 16, .ofs_x = 1, .ofs_y = 7}, + {.bitmap_index = 2717, .adv_w = 480, .box_w = 28, .box_h = 16, .ofs_x = 1, .ofs_y = 7}, + {.bitmap_index = 2829, .adv_w = 480, .box_w = 28, .box_h = 16, .ofs_x = 1, .ofs_y = 7}, + {.bitmap_index = 2941, .adv_w = 480, .box_w = 28, .box_h = 16, .ofs_x = 1, .ofs_y = 7}, + {.bitmap_index = 3053, .adv_w = 480, .box_w = 28, .box_h = 16, .ofs_x = 1, .ofs_y = 7} }; /*--------------------- @@ -434,17 +513,17 @@ static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = { *--------------------*/ static const uint16_t unicode_list_0[] = { - 0x0, 0x148, 0x449, 0x9fc, 0xa07, 0xa0a, 0xe7d, 0xe8a, - 0xe8b, 0xed6, 0xf82, 0x1001, 0x1075, 0x1076, 0x1078, 0x1079, - 0x107a, 0x107b, 0x107c, 0x107d + 0x0, 0x1, 0x2, 0x33, 0x17b, 0x468, 0x47c, 0xa2f, + 0xa3a, 0xa3d, 0xeb0, 0xebd, 0xebe, 0xf09, 0xfb5, 0x1034, + 0x10a8, 0x10a9, 0x10ab, 0x10ac, 0x10ad, 0x10ae, 0x10af, 0x10b0 }; /*Collect the unicode lists and glyph_id offsets*/ static const lv_font_fmt_txt_cmap_t cmaps[] = { { - .range_start = 57818, .range_length = 4222, .glyph_id_start = 1, - .unicode_list = unicode_list_0, .glyph_id_ofs_list = NULL, .list_length = 20, .type = LV_FONT_FMT_TXT_CMAP_SPARSE_TINY + .range_start = 57767, .range_length = 4273, .glyph_id_start = 1, + .unicode_list = unicode_list_0, .glyph_id_ofs_list = NULL, .list_length = 24, .type = LV_FONT_FMT_TXT_CMAP_SPARSE_TINY } }; diff --git a/Platforms/platform-esp32/CMakeLists.txt b/Platforms/platform-esp32/CMakeLists.txt index ac049c589..80f20f8d1 100644 --- a/Platforms/platform-esp32/CMakeLists.txt +++ b/Platforms/platform-esp32/CMakeLists.txt @@ -8,3 +8,5 @@ idf_component_register( PRIV_INCLUDE_DIRS "private/" REQUIRES TactilityKernel driver vfs fatfs ) + +idf_component_optional_requires(PRIVATE bt) diff --git a/Platforms/platform-esp32/bindings/esp32,ble-nimble.yaml b/Platforms/platform-esp32/bindings/esp32,ble-nimble.yaml new file mode 100644 index 000000000..f0a09bf61 --- /dev/null +++ b/Platforms/platform-esp32/bindings/esp32,ble-nimble.yaml @@ -0,0 +1,8 @@ +description: ESP32 BLE driver (NimBLE) + +compatible: "esp32,ble-nimble" + +properties: + _unused: + type: int + default: 0 diff --git a/Platforms/platform-esp32/include/tactility/bindings/esp32_ble_nimble.h b/Platforms/platform-esp32/include/tactility/bindings/esp32_ble_nimble.h new file mode 100644 index 000000000..9ae79c184 --- /dev/null +++ b/Platforms/platform-esp32/include/tactility/bindings/esp32_ble_nimble.h @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include + +DEFINE_DEVICETREE(ble_nimble, struct Esp32BleNimbleConfig) diff --git a/Platforms/platform-esp32/include/tactility/drivers/esp32_ble_nimble.h b/Platforms/platform-esp32/include/tactility/drivers/esp32_ble_nimble.h new file mode 100644 index 000000000..c38833e63 --- /dev/null +++ b/Platforms/platform-esp32/include/tactility/drivers/esp32_ble_nimble.h @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +/** No device-tree configuration required for the NimBLE BLE driver. */ +struct Esp32BleNimbleConfig { + int _unused; /**< Placeholder — driver reads all config from NimBLE Kconfig. */ +}; + +#ifdef __cplusplus +} +#endif diff --git a/Platforms/platform-esp32/private/bluetooth/esp32_ble_internal.h b/Platforms/platform-esp32/private/bluetooth/esp32_ble_internal.h new file mode 100644 index 000000000..700126474 --- /dev/null +++ b/Platforms/platform-esp32/private/bluetooth/esp32_ble_internal.h @@ -0,0 +1,154 @@ +#pragma once + +#ifdef ESP_PLATFORM +#include +#endif + +#if defined(CONFIG_BT_NIMBLE_ENABLED) + +#include +#include +#include +#include +#include +// Must be included before any NimBLE header: log_common.h (pulled in by ble_hs.h) +// defines LOG_LEVEL_* as macros with the same names as tactility/log.h's LogLevel enum. +// Including tactility/log.h first ensures the enum is compiled before the macros shadow it. +#include + +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include + +// ---- HID profile selection ---- + +enum class BleHidProfile { None, KbConsumer, Mouse, KbMouse, Gamepad }; + +// ---- BleCtx ---- + +#define BLE_MAX_CALLBACKS 8 +#define BLE_SCAN_MAX 64 + +struct BleCallbackEntry { + BtEventCallback fn; + void* ctx; +}; + +struct BleCtx { + // Mutexes + SemaphoreHandle_t radio_mutex; // guards radio state transitions + SemaphoreHandle_t data_mutex; // guards scan results + RX queues + SemaphoreHandle_t cb_mutex; // guards callbacks array + + // Radio / scan state (atomic — read from multiple tasks) + std::atomic radio_state; + std::atomic scan_active; + // Set by Tactility HID host to prevent simultaneous central connection during name resolution + std::atomic hid_host_active; + + // Event callbacks (guarded by cb_mutex) + BleCallbackEntry callbacks[BLE_MAX_CALLBACKS]; + size_t callback_count; + + // Scan results (guarded by data_mutex) + BtPeerRecord scan_results[BLE_SCAN_MAX]; + ble_addr_t scan_addrs[BLE_SCAN_MAX]; // parallel: full ble_addr_t (type+val) for connections + size_t scan_count; + + // RX queues (guarded by data_mutex, capped at 16 packets each) + std::deque> spp_rx_queue; + std::deque> midi_rx_queue; + + // Connection handles + active flags (atomic — accessed from multiple tasks) + std::atomic spp_conn_handle; + std::atomic spp_active; + std::atomic midi_conn_handle; + std::atomic midi_active; + std::atomic midi_use_indicate; // true when client subscribed for INDICATE (e.g. Windows) + std::atomic hid_conn_handle; + std::atomic hid_active; + std::atomic link_encrypted; + std::atomic pending_reset_count; + + // Timers + esp_timer_handle_t midi_keepalive_timer; // 2-second periodic Active Sensing + esp_timer_handle_t adv_restart_timer; // one-shot after connect failure (500 ms) + // One-shot timer used to dispatch dispatchDisable off the NimBLE host task. + // nimble_port_stop() must not be called from the NimBLE host task itself. + esp_timer_handle_t disable_timer; + + // Device reference (passed to BtEventCallback) + struct Device* device; + + // Child devices (created by esp32_ble_start_device, destroyed by stop_device) + struct Device* serial_child; + struct Device* midi_child; + struct Device* hid_device_child; +}; + +// ---- Global context pointer ---- +// Set in start_device; used by NimBLE callbacks that cannot receive a Device* argument. +extern BleCtx* g_ctx; + +// ---- Event publishing ---- +void ble_publish_event(BleCtx* ctx, struct BtEvent event); + +// ---- Advertising helpers (defined in esp32_ble.cpp) ---- +void ble_start_advertising(const ble_uuid128_t* svc_uuid); // svc_uuid=nullptr → name-only +void ble_start_advertising_hid(uint16_t appearance); +void ble_schedule_adv_restart(BleCtx* ctx, uint64_t delay_us); + +// ---- GAP scan callback (defined in esp32_ble_scan.cpp) ---- +int ble_gap_disc_event_handler(struct ble_gap_event* event, void* arg); +void ble_resolve_next_unnamed_peer(BleCtx* ctx, size_t start_idx); + +// ---- SPP GATT (defined in esp32_ble_spp.cpp) ---- +void ble_spp_init_gatt_handles(BleCtx* ctx); +error_t ble_spp_start_internal(BleCtx* ctx); + +// ---- MIDI GATT (defined in esp32_ble_midi.cpp) ---- +void ble_midi_init_gatt_handles(BleCtx* ctx); +error_t ble_midi_start_internal(BleCtx* ctx); + +// ---- HID device GATT (defined in esp32_ble_hid_device.cpp) ---- +void ble_hid_device_init_gatt(); +void ble_hid_device_init_gatt_handles(); +void ble_hid_device_switch_profile(BleCtx* ctx, BleHidProfile profile); + +// ---- Cross-module GATT char / service arrays ---- +extern const struct ble_gatt_chr_def nus_chars_with_handle[]; // esp32_ble_spp.cpp +extern const struct ble_gatt_chr_def midi_chars[]; // esp32_ble_midi.cpp + +// ---- Cross-module service UUIDs ---- +extern const ble_uuid128_t NUS_SVC_UUID; // esp32_ble_spp.cpp +extern const ble_uuid128_t MIDI_SVC_UUID; // esp32_ble_midi.cpp + +// ---- Cross-module GATT handle variables ---- +extern uint16_t nus_tx_handle; // esp32_ble_spp.cpp +extern uint16_t midi_io_handle; // esp32_ble_midi.cpp +extern uint16_t hid_kb_input_handle; // esp32_ble_hid_device.cpp +extern uint16_t hid_consumer_input_handle; // esp32_ble_hid_device.cpp +extern uint16_t hid_mouse_input_handle; // esp32_ble_hid_device.cpp +extern uint16_t hid_gamepad_input_handle; // esp32_ble_hid_device.cpp + +// ---- HID active report map / appearance ---- +extern const uint8_t* active_hid_rpt_map; // esp32_ble_hid_device.cpp +extern size_t active_hid_rpt_map_len; // esp32_ble_hid_device.cpp +extern uint16_t hid_appearance; // esp32_ble_hid_device.cpp +extern BleHidProfile current_hid_profile; // esp32_ble_hid_device.cpp + +// ---- Cross-module sub-API structs ---- +extern const BtHidDeviceApi nimble_hid_device_api; // esp32_ble_hid_device.cpp +extern const BtSerialApi nimble_serial_api; // esp32_ble_spp.cpp +extern const BtMidiApi nimble_midi_api; // esp32_ble_midi.cpp + +#endif // CONFIG_BT_NIMBLE_ENABLED diff --git a/Platforms/platform-esp32/source/drivers/bluetooth/README.md b/Platforms/platform-esp32/source/drivers/bluetooth/README.md new file mode 100644 index 000000000..d821bfdfc --- /dev/null +++ b/Platforms/platform-esp32/source/drivers/bluetooth/README.md @@ -0,0 +1,48 @@ +# ESP32 Bluetooth Driver + +Implements the `BluetoothApi` kernel driver interface on top of the NimBLE host stack. +Only compiled when `CONFIG_BT_NIMBLE_ENABLED=y`. + +## Source Files + +| File | Purpose | +|------|---------| +| `esp32_ble.cpp` | Core driver: NimBLE lifecycle, GAP event handler, advertising, radio enable/disable | +| `esp32_ble_scan.cpp` | GAP discovery, name resolution, auto-connect dispatch | +| `esp32_ble_spp.cpp` | NUS (Nordic UART Service) GATT server — `BtSerialApi` | +| `esp32_ble_midi.cpp` | BLE MIDI GATT server — `BtMidiApi` | +| `esp32_ble_hid_device.cpp` | HID peripheral (keyboard / mouse / gamepad) — `BtHidDeviceApi` | + +Internal shared state is defined in +`Platforms/platform-esp32/private/bluetooth/esp32_ble_internal.h` (`BleCtx`). + +The public C API (`bluetooth_scan_start()` etc.) is implemented in +`TactilityKernel/source/drivers/bluetooth.cpp` as thin wrappers over the `BluetoothApi` +function-pointer struct. External code must only call those public functions — never touch +`BluetoothApi` directly. + +SPP, MIDI, and HID Device profiles are exposed as **child devices** created by +`esp32_ble_start_device()`. Each gets its own `DeviceType` and is found at runtime via +`bluetooth_serial_get_device()`, `bluetooth_midi_get_device()`, or +`bluetooth_hid_device_get_device()`. Their drivers have `start_device=nullptr` since +initialization is handled by the parent driver; they share the global `BleCtx` (`g_ctx`). + +## Profiles + +| Profile | Role | API struct | +|---------|------|------------| +| HID Host | Central | Managed in Tactility layer (`BluetoothHidHost.cpp`) | +| HID Device | Peripheral | `BtHidDeviceApi` (child device: `BLUETOOTH_HID_DEVICE_TYPE`) | +| SPP (NUS) | Peripheral | `BtSerialApi` (child device: `BLUETOOTH_SERIAL_TYPE`) | +| MIDI | Peripheral | `BtMidiApi` (child device: `BLUETOOTH_MIDI_TYPE`) | + +Only one peripheral profile (HID Device, SPP, or MIDI) can advertise at a time. + +## Locking Rules + +- `BleCtx::radio_mutex` — guards radio enable/disable state transitions. +- `BleCtx::data_mutex` — guards scan results and RX queues. Released before any NimBLE call. +- `std::atomic<>` — connection handles and flag bools are atomics (read/written cross-task). +- File I/O must **never** run on the NimBLE host task — dispatch via `getMainDispatcher()`. + +See `bluetooth.puml` for the full thread model diagram. diff --git a/Platforms/platform-esp32/source/drivers/bluetooth/bluetooth.puml b/Platforms/platform-esp32/source/drivers/bluetooth/bluetooth.puml new file mode 100644 index 000000000..e34976c8e --- /dev/null +++ b/Platforms/platform-esp32/source/drivers/bluetooth/bluetooth.puml @@ -0,0 +1,45 @@ +@startuml Bluetooth Thread Model + +box "nimble_host task\n(NimBLE internal, 4 KB stack)" #LightBlue + participant "gap_event_handler\n(esp32_ble.cpp)" as GAP + participant "ble_gatt\ncallbacks" as GATT + participant "bt_event_bridge\n(Bluetooth.cpp)" as Bridge + participant "hidHostGapCb\n& discovery\n(BluetoothHidHost.cpp)" as HID +end box + +box "main_dispatcher task\n(Tactility main task)" #LightGreen + participant "settings I/O\n& peer updates" as Dispatch +end box + +box "App tasks" #LightYellow + participant "BtManage\nBtPeerSettings" as Apps +end box + +box "esp_timer task" #LightGray + participant "advRestart\nmidiKeepalive\nhidEncRetry" as Timers +end box + +box "LVGL task\n(GuiService)" #MistyRose + participant "hidHostKeyboard\nReadCb / mouseReadCb" as LVGL +end box + +GAP -> Bridge : ble_publish_event() →\nBtEventCallback (bt_event_bridge) +Bridge -> Dispatch : getMainDispatcher().dispatch()\n(settings::load/save, autoConnect) +Dispatch -> Apps : publishEventCpp → PubSub callbacks +GATT -> GAP : GATT access within\nnimble_host task +HID -> Dispatch : getMainDispatcher().dispatch()\n(indev cleanup, ProfileStateChanged) +Timers -> GAP : advRestartCallback\n(calls ble_start_advertising) +LVGL -> HID : lv_indev read callbacks\n(LVGL tick) + +note over GAP, GATT + NO file I/O on NimBLE host task — + stringstream blows the 4 KB stack +end note + +note over GAP, Bridge + Driver fires BtEvent via callback array. + Bridge (Tactility layer) translates to + C++ PubSub and dispatches I/O. +end note + +@enduml diff --git a/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble.cpp b/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble.cpp new file mode 100644 index 000000000..66134e514 --- /dev/null +++ b/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble.cpp @@ -0,0 +1,1010 @@ +#ifdef ESP_PLATFORM +#include +#endif + +#if defined(CONFIG_BT_NIMBLE_ENABLED) + +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#define TAG "esp32_ble" +#include +#include + +// ble_store_config_init() is not declared in the public header in some IDF versions. +extern "C" void ble_store_config_init(void); + +// ---- Global context pointer ---- +// Set in start_device(); used by NimBLE callbacks that cannot receive a Device*. +BleCtx* g_ctx = nullptr; + +// ---- Forward declarations ---- +static void ble_host_task(void* param); +static void on_sync(); +static void on_reset(int reason); +static void dispatch_enable(BleCtx* ctx); +static void dispatch_disable(BleCtx* ctx); +static int gap_event_handler(struct ble_gap_event* event, void* arg); + +// ---- Event publishing ---- + +void ble_publish_event(BleCtx* ctx, struct BtEvent event) { + // Copy under mutex so callbacks can safely call add/remove_event_callback + BleCallbackEntry local[BLE_MAX_CALLBACKS]; + size_t count; + xSemaphoreTake(ctx->cb_mutex, portMAX_DELAY); + count = ctx->callback_count; + memcpy(local, ctx->callbacks, count * sizeof(BleCallbackEntry)); + xSemaphoreGive(ctx->cb_mutex); + for (size_t i = 0; i < count; i++) { + local[i].fn(ctx->device, local[i].ctx, event); + } +} + +// ---- Advertising restart helper ---- + +static void adv_restart_callback(void* /*arg*/) { + BleCtx* ctx = g_ctx; + if (ctx == nullptr || ctx->radio_state.load() != BT_RADIO_STATE_ON) return; + if (ctx->midi_active.load() && ctx->midi_conn_handle.load() == BLE_HS_CONN_HANDLE_NONE) { + ble_start_advertising(&MIDI_SVC_UUID); + } else if (ctx->spp_active.load() && ctx->spp_conn_handle.load() == BLE_HS_CONN_HANDLE_NONE) { + ble_start_advertising(&NUS_SVC_UUID); + } else if (ctx->hid_active.load() && ctx->hid_conn_handle.load() == BLE_HS_CONN_HANDLE_NONE) { + ble_start_advertising_hid(hid_appearance); + } +} + +void ble_schedule_adv_restart(BleCtx* ctx, uint64_t delay_us) { + if (delay_us == 0) { + adv_restart_callback(nullptr); + return; + } + if (ctx->adv_restart_timer == nullptr) { + esp_timer_create_args_t args = {}; + args.callback = adv_restart_callback; + args.dispatch_method = ESP_TIMER_TASK; + args.name = "ble_adv_restart"; + int rc = esp_timer_create(&args, &ctx->adv_restart_timer); + if (rc != ESP_OK) { + LOG_E(TAG, "adv_restart timer create failed (rc=%d)", rc); + return; + } + } + esp_timer_stop(ctx->adv_restart_timer); + int rc = esp_timer_start_once(ctx->adv_restart_timer, delay_us); + if (rc != ESP_OK) { + LOG_E(TAG, "adv_restart timer start failed (rc=%d)", rc); + } +} + +// ---- GAP connection event handler ---- + +static int gap_event_handler(struct ble_gap_event* event, void* arg) { + BleCtx* ctx = g_ctx; + if (ctx == nullptr) return 0; + + switch (event->type) { + case BLE_GAP_EVENT_CONNECT: + if (event->connect.status == 0) { + LOG_I(TAG, "Connected (handle=%u hid_active=%d hid_conn=%u)", + event->connect.conn_handle, + (int)ctx->hid_active.load(), + (unsigned)ctx->hid_conn_handle.load()); + // Do NOT call ble_gap_security_initiate() here. + // Windows BLE MIDI initiates encryption itself; calling here creates a race + // with REPEAT_PAIRING+RETRY → two concurrent SM procedures → disconnect. + // On esp_hosted, ENC_CHANGE can arrive BEFORE CONNECT, so re-initiating + // on an already-encrypted link triggers unwanted re-pairing. + } else { + LOG_W(TAG, "Connection failed (status=%d)", event->connect.status); + // Delay restart so NimBLE can clean up SMP/connection state before peer retries. + ble_schedule_adv_restart(ctx, 500'000); + } + break; + + case BLE_GAP_EVENT_DISCONNECT: { + LOG_I(TAG, "Disconnected (reason=%d)", event->disconnect.reason); + uint16_t hdl = event->disconnect.conn.conn_handle; + bool was_spp = (ctx->spp_conn_handle.load() == hdl); + bool was_midi = (ctx->midi_conn_handle.load() == hdl); + bool was_hid = (ctx->hid_conn_handle.load() == hdl); + if (was_spp) ctx->spp_conn_handle.store(BLE_HS_CONN_HANDLE_NONE); + if (was_midi) { ctx->midi_conn_handle.store(BLE_HS_CONN_HANDLE_NONE); ctx->midi_use_indicate.store(false); } + if (was_hid) ctx->hid_conn_handle.store(BLE_HS_CONN_HANDLE_NONE); + ctx->link_encrypted.store(false); + // If HID was stopped while connected, switch profile to None now that the + // connection is gone. ble_gatts_mutable() is true here (no active connection, + // advertising stopped by hid_device_stop), so switch_profile is safe. + if (was_hid && !ctx->hid_active.load() && current_hid_profile != BleHidProfile::None) { + ble_hid_device_switch_profile(ctx, BleHidProfile::None); + } + // Restart advertising whenever a service is active without a live connection. + // Covers both normal disconnect and Windows discovery-only connections. + if (ctx->midi_active.load() && ctx->midi_conn_handle.load() == BLE_HS_CONN_HANDLE_NONE) { + ble_start_advertising(&MIDI_SVC_UUID); + } else if (ctx->spp_active.load() && ctx->spp_conn_handle.load() == BLE_HS_CONN_HANDLE_NONE) { + ble_start_advertising(&NUS_SVC_UUID); + } else if (ctx->hid_active.load() && ctx->hid_conn_handle.load() == BLE_HS_CONN_HANDLE_NONE) { + ble_start_advertising_hid(hid_appearance); + } + break; + } + + case BLE_GAP_EVENT_SUBSCRIBE: + LOG_I(TAG, "Subscribe attr=%u cur_notify=%u cur_indicate=%u (nus_tx=%u midi_io=%u)", + event->subscribe.attr_handle, + (unsigned)event->subscribe.cur_notify, + (unsigned)event->subscribe.cur_indicate, + nus_tx_handle, midi_io_handle); + if (event->subscribe.attr_handle != nus_tx_handle && + event->subscribe.attr_handle != midi_io_handle && + event->subscribe.cur_indicate) { + // Service Changed subscription — ignore (Windows discovers on its own) + LOG_I(TAG, "Service Changed subscription (attr=%u) — ignoring", + event->subscribe.attr_handle); + } else if (event->subscribe.attr_handle == nus_tx_handle) { + if (!ctx->spp_active.load()) { + LOG_I(TAG, "SPP CCCD subscribed but spp_active=false — ignoring"); + break; + } + ctx->spp_conn_handle.store(event->subscribe.conn_handle); + LOG_I(TAG, "SPP client subscribed (nus_tx=%u)", nus_tx_handle); + // Fire PROFILE_STATE_CHANGED so Tactility bridge can persist the profile + // off the NimBLE host task (file I/O → stack overflow on nimble_host). + { + struct ble_gap_conn_desc sub_desc = {}; + if (ble_gap_conn_find(event->subscribe.conn_handle, &sub_desc) == 0) { + struct BtEvent e = {}; + e.type = BT_EVENT_PROFILE_STATE_CHANGED; + memcpy(e.profile_state.addr, sub_desc.peer_id_addr.val, 6); + e.profile_state.profile = BT_PROFILE_SPP; + e.profile_state.state = BT_PROFILE_STATE_CONNECTED; + ble_publish_event(ctx, e); + } + } + } else if (event->subscribe.attr_handle == midi_io_handle) { + if ((event->subscribe.cur_notify || event->subscribe.cur_indicate) && !ctx->midi_active.load()) { + LOG_I(TAG, "MIDI CCCD subscribed but midi_active=false — ignoring"); + break; + } + if (event->subscribe.cur_notify || event->subscribe.cur_indicate) { + ctx->midi_conn_handle.store(event->subscribe.conn_handle); + ctx->midi_use_indicate.store(event->subscribe.cur_indicate != 0); + LOG_I(TAG, "MIDI client subscribed (midi_io=%u indicate=%d)", + midi_io_handle, (int)ctx->midi_use_indicate.load()); + // Fire PROFILE_STATE_CHANGED so Tactility bridge persists the profile. + { + struct ble_gap_conn_desc sub_desc = {}; + if (ble_gap_conn_find(event->subscribe.conn_handle, &sub_desc) == 0) { + struct BtEvent e = {}; + e.type = BT_EVENT_PROFILE_STATE_CHANGED; + memcpy(e.profile_state.addr, sub_desc.peer_id_addr.val, 6); + e.profile_state.profile = BT_PROFILE_MIDI; + e.profile_state.state = BT_PROFILE_STATE_CONNECTED; + ble_publish_event(ctx, e); + } + } + // Send MIDI Active Sensing immediately after subscription. + static const uint8_t active_sensing_pkt[3] = { 0x80, 0x80, 0xFE }; + struct os_mbuf* as_om = ble_hs_mbuf_from_flat(active_sensing_pkt, 3); + if (as_om != nullptr) { + int as_rc = ctx->midi_use_indicate.load() + ? ble_gatts_indicate_custom(ctx->midi_conn_handle.load(), midi_io_handle, as_om) + : ble_gatts_notify_custom(ctx->midi_conn_handle.load(), midi_io_handle, as_om); + if (as_rc != 0) os_mbuf_free_chain(as_om); + LOG_I(TAG, "Active Sensing (subscribe) rc=%d", as_rc); + } + } else { + ctx->midi_conn_handle.store(BLE_HS_CONN_HANDLE_NONE); + ctx->midi_use_indicate.store(false); + LOG_I(TAG, "MIDI client unsubscribed"); + } + } else if (event->subscribe.cur_notify && + (event->subscribe.attr_handle == hid_kb_input_handle || + event->subscribe.attr_handle == hid_consumer_input_handle || + event->subscribe.attr_handle == hid_mouse_input_handle || + event->subscribe.attr_handle == hid_gamepad_input_handle)) { + const char* rpt_name = + (event->subscribe.attr_handle == hid_kb_input_handle) ? "keyboard" : + (event->subscribe.attr_handle == hid_consumer_input_handle) ? "consumer" : + (event->subscribe.attr_handle == hid_mouse_input_handle) ? "mouse" : + (event->subscribe.attr_handle == hid_gamepad_input_handle) ? "gamepad" : "unknown"; + if (!ctx->hid_active.load()) { + LOG_I(TAG, "HID CCCD subscribed (%s) but hid_active=false — ignoring", rpt_name); + break; + } + LOG_I(TAG, "HID CCCD subscribed: %s (attr=%u conn=%u)", + rpt_name, event->subscribe.attr_handle, event->subscribe.conn_handle); + if (ctx->hid_conn_handle.load() == BLE_HS_CONN_HANDLE_NONE) { + ctx->hid_conn_handle.store(event->subscribe.conn_handle); + } + } + break; + + case BLE_GAP_EVENT_MTU: + LOG_I(TAG, "MTU updated (conn=%u mtu=%u)", + event->mtu.conn_handle, event->mtu.value); + break; + + case BLE_GAP_EVENT_CONN_UPDATE: { + struct ble_gap_conn_desc desc = {}; + if (ble_gap_conn_find(event->conn_update.conn_handle, &desc) == 0) { + LOG_I(TAG, "Conn params updated (status=%d itvl=%u latency=%u timeout=%u)", + event->conn_update.status, desc.conn_itvl, + desc.conn_latency, desc.supervision_timeout); + } + break; + } + + case BLE_GAP_EVENT_CONN_UPDATE_REQ: + *event->conn_update_req.self_params = *event->conn_update_req.peer_params; + return 0; + + case BLE_GAP_EVENT_ENC_CHANGE: + LOG_I(TAG, "Encryption changed (conn=%u status=%d)", + event->enc_change.conn_handle, event->enc_change.status); + if (event->enc_change.status == 0) { + ctx->link_encrypted.store(true); + // For HID device: set hid_conn_handle at encryption time so Phase 2 bonded + // reconnects are tracked even when BLE_GAP_EVENT_SUBSCRIBE doesn't fire. + // Windows only writes HID CCCDs in Phase 2; NimBLE may restore them from NVS + // silently (no SUBSCRIBE event). Without this, hid_conn_handle stays NONE + // and hid_device_is_connected() returns false for the entire Phase 2 session. + if (ctx->hid_active.load() && + ctx->hid_conn_handle.load() == BLE_HS_CONN_HANDLE_NONE) { + ctx->hid_conn_handle.store(event->enc_change.conn_handle); + LOG_I(TAG, "HID conn handle set on ENC_CHANGE (conn=%u)", + event->enc_change.conn_handle); + } + // Fire PAIR_RESULT so Tactility bridge can persist the paired device + // off the NimBLE host task (file I/O → stack overflow on nimble_host). + struct ble_gap_conn_desc desc = {}; + if (ble_gap_conn_find(event->enc_change.conn_handle, &desc) == 0) { + struct BtEvent e = {}; + e.type = BT_EVENT_PAIR_RESULT; + memcpy(e.pair_result.addr, desc.peer_id_addr.val, 6); + e.pair_result.result = BT_PAIR_RESULT_SUCCESS; + e.pair_result.profile = ctx->midi_active.load() ? BT_PROFILE_MIDI + : ctx->spp_active.load() ? BT_PROFILE_SPP + : ctx->hid_active.load() ? BT_PROFILE_HID_DEVICE + : BT_PROFILE_HID_HOST; + ble_publish_event(ctx, e); + } + } + // Re-send Active Sensing now that the link is encrypted. + if (event->enc_change.status == 0 && + ctx->midi_conn_handle.load() == event->enc_change.conn_handle) { + static const uint8_t as_pkt[3] = { 0x80, 0x80, 0xFE }; + struct os_mbuf* om = ble_hs_mbuf_from_flat(as_pkt, 3); + if (om != nullptr) { + int rc = ctx->midi_use_indicate.load() + ? ble_gatts_indicate_custom(ctx->midi_conn_handle.load(), midi_io_handle, om) + : ble_gatts_notify_custom(ctx->midi_conn_handle.load(), midi_io_handle, om); + if (rc != 0) os_mbuf_free_chain(om); + LOG_I(TAG, "Active Sensing (post-enc) rc=%d", rc); + } + } + break; + + case BLE_GAP_EVENT_PASSKEY_ACTION: { + LOG_I(TAG, "Passkey action (conn=%u action=%u)", + event->passkey.conn_handle, event->passkey.params.action); + struct ble_sm_io pkey = {}; + pkey.action = event->passkey.params.action; + if (pkey.action == BLE_SM_IOACT_NONE) { + ble_sm_inject_io(event->passkey.conn_handle, &pkey); + } + break; + } + + case BLE_GAP_EVENT_REPEAT_PAIRING: { + LOG_I(TAG, "Repeat pairing (conn=%u encrypted=%d)", + event->repeat_pairing.conn_handle, (int)ctx->link_encrypted.load()); + struct ble_gap_conn_desc desc; + if (ble_gap_conn_find(event->repeat_pairing.conn_handle, &desc) == 0) { + ble_store_util_delete_peer(&desc.peer_id_addr); + if (!ctx->link_encrypted.load()) { + // Fire BOND_LOST so Tactility bridge removes the stored device record. + struct BtEvent e = {}; + e.type = BT_EVENT_PAIR_RESULT; + memcpy(e.pair_result.addr, desc.peer_id_addr.val, 6); + e.pair_result.result = BT_PAIR_RESULT_BOND_LOST; + ble_publish_event(ctx, e); + } + } + // If already encrypted, IGNORE so the current session continues. + // RETRY would start a 30-second SMP timer that always expires (peer won't + // respond to a new Pair Request on an encrypted link) → forced disconnect. + if (ctx->link_encrypted.load()) { + LOG_I(TAG, "Repeat pairing: link encrypted — ignoring"); + return BLE_GAP_REPEAT_PAIRING_IGNORE; + } + return BLE_GAP_REPEAT_PAIRING_RETRY; + } + + default: + break; + } + return 0; +} + +// ---- NimBLE host sync / reset callbacks ---- + +static void on_sync() { + LOG_I(TAG, "NimBLE synced (nus_tx=%u midi_io=%u hid_kb=%u hid_cs=%u hid_ms=%u hid_gp=%u)", + nus_tx_handle, midi_io_handle, + hid_kb_input_handle, hid_consumer_input_handle, + hid_mouse_input_handle, hid_gamepad_input_handle); + BleCtx* ctx = g_ctx; + if (ctx == nullptr) return; + + ctx->pending_reset_count.store(0); + + uint8_t own_addr_type; + int rc = ble_hs_util_ensure_addr(0); + if (rc != 0) LOG_E(TAG, "ensure_addr failed (rc=%d)", rc); + rc = ble_hs_id_infer_auto(0, &own_addr_type); + if (rc != 0) LOG_E(TAG, "infer addr type failed (rc=%d)", rc); + + // Sync GATT handle values + ble_spp_init_gatt_handles(ctx); + ble_midi_init_gatt_handles(ctx); + ble_hid_device_init_gatt_handles(); + + ctx->radio_state.store(BT_RADIO_STATE_ON); + struct BtEvent e = {}; + e.type = BT_EVENT_RADIO_STATE_CHANGED; + e.radio_state = BT_RADIO_STATE_ON; + ble_publish_event(ctx, e); + + // The Tactility bridge handles auto-start (SPP/MIDI) in response to + // BT_EVENT_RADIO_STATE_CHANGED(ON). We just start name-only advertising + // so the device is visible immediately. + ble_start_advertising(nullptr); +} + +static void dispatch_disable_timer_cb(void* arg) { + BleCtx* ctx = (BleCtx*)arg; + dispatch_disable(ctx); +} + +static void on_reset(int reason) { + LOG_W(TAG, "NimBLE host reset (reason=%d)", reason); + BleCtx* ctx = g_ctx; + if (ctx == nullptr) return; + + if (ctx->radio_state.load() == BT_RADIO_STATE_ON_PENDING) { + int count = ctx->pending_reset_count.fetch_add(1) + 1; + if (count == 3) { + LOG_E(TAG, "BT controller unresponsive after 3 resets — giving up"); + // Can't call nimble_port_stop() from the NimBLE host task. + // Fire a one-shot esp_timer (delay=0 → fires on esp_timer task immediately). + if (ctx->disable_timer != nullptr) { + esp_timer_start_once(ctx->disable_timer, 0); + } + } + } +} + +static void ble_host_task(void* param) { + LOG_I(TAG, "BLE host task started"); + nimble_port_run(); + // nimble_port_deinit() is called by dispatch_disable() after nimble_port_stop() returns. + nimble_port_freertos_deinit(); +} + +// ---- Advertising helpers ---- + +void ble_start_advertising(const ble_uuid128_t* svc_uuid) { + ble_gap_adv_stop(); + + int rc; + if (svc_uuid != nullptr) { + const char* name = ble_svc_gap_device_name(); + uint8_t name_len = (uint8_t)strlen(name); + uint8_t short_len = (name_len > 8) ? 8 : name_len; + + ble_uuid128_t uuid_copy = *svc_uuid; + struct ble_hs_adv_fields fields; + memset(&fields, 0, sizeof(fields)); + fields.flags = BLE_HS_ADV_F_DISC_GEN | BLE_HS_ADV_F_BREDR_UNSUP; + fields.uuids128 = &uuid_copy; + fields.num_uuids128 = 1; + fields.uuids128_is_complete = 1; + fields.name = (const uint8_t*)name; + fields.name_len = short_len; + fields.name_is_complete = 0; // AD type 0x08 = Shortened Local Name + + rc = ble_gap_adv_set_fields(&fields); + if (rc != 0) { + LOG_W(TAG, "startAdvertising: set_fields with name failed rc=%d, retrying", rc); + fields.name = nullptr; + fields.name_len = 0; + rc = ble_gap_adv_set_fields(&fields); + if (rc != 0) { + LOG_E(TAG, "startAdvertising: set_fields failed rc=%d", rc); + return; + } + } + struct ble_hs_adv_fields rsp; + memset(&rsp, 0, sizeof(rsp)); + rsp.name = (const uint8_t*)name; + rsp.name_len = name_len; + rsp.name_is_complete = 1; + rc = ble_gap_adv_rsp_set_fields(&rsp); + if (rc != 0) { + LOG_W(TAG, "startAdvertising: rsp_set_fields rc=%d (non-fatal)", rc); + } + LOG_I(TAG, "startAdvertising: UUID mode (uuid[0..3]=%02x%02x%02x%02x)", + svc_uuid->value[0], svc_uuid->value[1], + svc_uuid->value[2], svc_uuid->value[3]); + } else { + struct ble_hs_adv_fields fields; + memset(&fields, 0, sizeof(fields)); + fields.flags = BLE_HS_ADV_F_DISC_GEN | BLE_HS_ADV_F_BREDR_UNSUP; + const char* name = ble_svc_gap_device_name(); + fields.name = (const uint8_t*)name; + fields.name_len = (uint8_t)strlen(name); + fields.name_is_complete = 1; + rc = ble_gap_adv_set_fields(&fields); + if (rc != 0) { + LOG_E(TAG, "startAdvertising: set_fields failed rc=%d", rc); + return; + } + LOG_I(TAG, "startAdvertising: name-only mode"); + } + + struct ble_gap_adv_params adv_params; + memset(&adv_params, 0, sizeof(adv_params)); + adv_params.conn_mode = BLE_GAP_CONN_MODE_UND; + adv_params.disc_mode = BLE_GAP_DISC_MODE_GEN; + adv_params.itvl_min = 160; // 100 ms + adv_params.itvl_max = 240; // 150 ms + + rc = ble_gap_adv_start(BLE_OWN_ADDR_PUBLIC, nullptr, BLE_HS_FOREVER, + &adv_params, gap_event_handler, nullptr); + if (rc != 0 && rc != BLE_HS_EALREADY) { + LOG_E(TAG, "startAdvertising: adv_start failed rc=%d", rc); + } else { + LOG_I(TAG, "startAdvertising: OK"); + } +} + +void ble_start_advertising_hid(uint16_t appearance) { + ble_gap_adv_stop(); + + const char* name = ble_svc_gap_device_name(); + uint8_t name_len = (uint8_t)strlen(name); + uint8_t short_len = (name_len > 8) ? 8 : name_len; + + static const ble_uuid16_t hid_uuid16 = BLE_UUID16_INIT(0x1812); + + struct ble_hs_adv_fields fields; + memset(&fields, 0, sizeof(fields)); + fields.flags = BLE_HS_ADV_F_DISC_GEN | BLE_HS_ADV_F_BREDR_UNSUP; + fields.appearance = appearance; + fields.appearance_is_present = 1; + fields.uuids16 = &hid_uuid16; + fields.num_uuids16 = 1; + fields.uuids16_is_complete = 1; + fields.name = (const uint8_t*)name; + fields.name_len = short_len; + fields.name_is_complete = 0; + + int rc = ble_gap_adv_set_fields(&fields); + if (rc != 0) { + fields.name = nullptr; + fields.name_len = 0; + rc = ble_gap_adv_set_fields(&fields); + if (rc != 0) { + LOG_E(TAG, "startAdvertisingHid: set_fields failed rc=%d", rc); + return; + } + } + + struct ble_hs_adv_fields rsp; + memset(&rsp, 0, sizeof(rsp)); + rsp.name = (const uint8_t*)name; + rsp.name_len = name_len; + rsp.name_is_complete = 1; + rc = ble_gap_adv_rsp_set_fields(&rsp); + if (rc != 0) { + LOG_W(TAG, "startAdvertisingHid: rsp_set_fields rc=%d (non-fatal)", rc); + } + + struct ble_gap_adv_params adv_params; + memset(&adv_params, 0, sizeof(adv_params)); + adv_params.conn_mode = BLE_GAP_CONN_MODE_UND; + adv_params.disc_mode = BLE_GAP_DISC_MODE_GEN; + adv_params.itvl_min = 160; + adv_params.itvl_max = 240; + + rc = ble_gap_adv_start(BLE_OWN_ADDR_PUBLIC, nullptr, BLE_HS_FOREVER, + &adv_params, gap_event_handler, nullptr); + if (rc != 0 && rc != BLE_HS_EALREADY) { + LOG_E(TAG, "startAdvertisingHid: adv_start failed rc=%d", rc); + } else { + LOG_I(TAG, "startAdvertisingHid: OK"); + } +} + +// ---- Dispatch helpers ---- + +static void dispatch_enable(BleCtx* ctx) { + LOG_I(TAG, "dispatch_enable()"); + + if (ctx->radio_state.load() != BT_RADIO_STATE_OFF) { + LOG_W(TAG, "Cannot enable from current state"); + return; + } + + ctx->radio_state.store(BT_RADIO_STATE_ON_PENDING); + { + struct BtEvent e = {}; + e.type = BT_EVENT_RADIO_STATE_CHANGED; + e.radio_state = BT_RADIO_STATE_ON_PENDING; + ble_publish_event(ctx, e); + } + + int rc = nimble_port_init(); + if (rc != 0) { + LOG_E(TAG, "nimble_port_init failed (rc=%d)", rc); + ctx->radio_state.store(BT_RADIO_STATE_OFF); + struct BtEvent e = {}; + e.type = BT_EVENT_RADIO_STATE_CHANGED; + e.radio_state = BT_RADIO_STATE_OFF; + ble_publish_event(ctx, e); + return; + } + + ble_hs_cfg.sync_cb = on_sync; + ble_hs_cfg.reset_cb = on_reset; + ble_hs_cfg.store_status_cb = ble_store_util_status_rr; + + ble_hs_cfg.sm_io_cap = BLE_SM_IO_CAP_NO_IO; + ble_hs_cfg.sm_bonding = 1; + ble_hs_cfg.sm_mitm = 1; + ble_hs_cfg.sm_sc = 1; + ble_hs_cfg.sm_our_key_dist = BLE_SM_PAIR_KEY_DIST_ENC | BLE_SM_PAIR_KEY_DIST_ID; + ble_hs_cfg.sm_their_key_dist = BLE_SM_PAIR_KEY_DIST_ENC | BLE_SM_PAIR_KEY_DIST_ID; + + ble_store_config_init(); + ble_hs_cfg.store_read_cb = ble_store_config_read; + ble_hs_cfg.store_write_cb = ble_store_config_write; + ble_hs_cfg.store_delete_cb = ble_store_config_delete; + + ble_svc_gap_init(); + ble_svc_gatt_init(); + + // Register base GATT services (NUS + MIDI; HID added by switch_profile when started) + ble_hid_device_init_gatt(); + + ble_svc_gap_device_name_set(CONFIG_TT_DEVICE_NAME); + ble_att_set_preferred_mtu(BLE_ATT_MTU_MAX); + + // Start NimBLE host task (on_sync will fire when ready) + nimble_port_freertos_init(ble_host_task); +} + +static void dispatch_disable(BleCtx* ctx) { + LOG_I(TAG, "dispatch_disable()"); + + if (ctx->radio_state.load() == BT_RADIO_STATE_OFF) { + LOG_W(TAG, "Already off"); + return; + } + + ctx->radio_state.store(BT_RADIO_STATE_OFF_PENDING); + { + struct BtEvent e = {}; + e.type = BT_EVENT_RADIO_STATE_CHANGED; + e.radio_state = BT_RADIO_STATE_OFF_PENDING; + ble_publish_event(ctx, e); + } + + // Blocking: waits for nimble_port_run() to exit. + // Do NOT call ble_gap_adv_stop()/disc_cancel() before — if controller is + // unresponsive they generate more HCI timeouts before the stop takes effect. + nimble_port_stop(); + nimble_port_deinit(); + + ctx->spp_conn_handle.store(BLE_HS_CONN_HANDLE_NONE); + ctx->spp_active.store(false); + ctx->midi_conn_handle.store(BLE_HS_CONN_HANDLE_NONE); + ctx->midi_active.store(false); + ctx->hid_conn_handle.store(BLE_HS_CONN_HANDLE_NONE); + ctx->hid_active.store(false); + ctx->link_encrypted.store(false); + ctx->pending_reset_count.store(0); + current_hid_profile = BleHidProfile::None; + active_hid_rpt_map = nullptr; + active_hid_rpt_map_len = 0; + + if (ctx->midi_keepalive_timer != nullptr) { + esp_timer_stop(ctx->midi_keepalive_timer); + esp_timer_delete(ctx->midi_keepalive_timer); + ctx->midi_keepalive_timer = nullptr; + } + if (ctx->adv_restart_timer != nullptr) { + esp_timer_stop(ctx->adv_restart_timer); + esp_timer_delete(ctx->adv_restart_timer); + ctx->adv_restart_timer = nullptr; + } + + ctx->radio_state.store(BT_RADIO_STATE_OFF); + { + struct BtEvent e = {}; + e.type = BT_EVENT_RADIO_STATE_CHANGED; + e.radio_state = BT_RADIO_STATE_OFF; + ble_publish_event(ctx, e); + } +} + +// ---- BluetoothApi implementations ---- + +static error_t api_get_radio_state(struct Device* device, enum BtRadioState* state) { + BleCtx* ctx = (BleCtx*)device_get_driver_data(device); + if (!ctx || !state) return ERROR_INVALID_ARGUMENT; + *state = ctx->radio_state.load(); + return ERROR_NONE; +} + +static error_t api_set_radio_enabled(struct Device* device, bool enabled) { + BleCtx* ctx = (BleCtx*)device_get_driver_data(device); + if (!ctx) return ERROR_INVALID_STATE; + xSemaphoreTake(ctx->radio_mutex, portMAX_DELAY); + if (enabled) { + dispatch_enable(ctx); + } else { + dispatch_disable(ctx); + } + xSemaphoreGive(ctx->radio_mutex); + return ERROR_NONE; +} + +static error_t api_scan_start(struct Device* device) { + BleCtx* ctx = (BleCtx*)device_get_driver_data(device); + if (!ctx) return ERROR_INVALID_STATE; + + if (ctx->radio_state.load() != BT_RADIO_STATE_ON) { + LOG_W(TAG, "scan_start: radio not on"); + return ERROR_INVALID_STATE; + } + if (ctx->scan_active.load()) { + LOG_W(TAG, "scan_start: already scanning"); + return ERROR_INVALID_STATE; + } + + { + xSemaphoreTake(ctx->data_mutex, portMAX_DELAY); + ctx->scan_count = 0; + memset(ctx->scan_results, 0, sizeof(ctx->scan_results)); + xSemaphoreGive(ctx->data_mutex); + } + + struct ble_gap_disc_params disc_params = {}; + disc_params.passive = 0; + disc_params.filter_duplicates = 1; + + uint8_t own_addr_type; + ble_hs_id_infer_auto(0, &own_addr_type); + + int rc = ble_gap_disc(own_addr_type, 5000, &disc_params, ble_gap_disc_event_handler, nullptr); + if (rc != 0 && rc != BLE_HS_EALREADY) { + LOG_E(TAG, "ble_gap_disc failed (rc=%d)", rc); + return ERROR_UNDEFINED; + } + + ctx->scan_active.store(true); + { + struct BtEvent e = {}; + e.type = BT_EVENT_SCAN_STARTED; + ble_publish_event(ctx, e); + } + return ERROR_NONE; +} + +static error_t api_scan_stop(struct Device* device) { + BleCtx* ctx = (BleCtx*)device_get_driver_data(device); + if (!ctx) return ERROR_INVALID_STATE; + ble_gap_disc_cancel(); + ctx->scan_active.store(false); + struct BtEvent e = {}; + e.type = BT_EVENT_SCAN_FINISHED; + ble_publish_event(ctx, e); + return ERROR_NONE; +} + +static bool api_is_scanning(struct Device* device) { + BleCtx* ctx = (BleCtx*)device_get_driver_data(device); + return ctx != nullptr && ctx->scan_active.load(); +} + +static error_t api_pair(struct Device* /*device*/, const BtAddr /*addr*/) { + // Pairing handled automatically by NimBLE SM during connection. + // ENC_CHANGE fires when encryption is established (PAIR_RESULT event). + return ERROR_NONE; +} + +static error_t api_unpair(struct Device* device, const BtAddr addr) { + // Remove from NimBLE bond store. Settings file removal is handled by + // the Tactility bluetooth::unpair() wrapper which calls settings::remove(). + ble_addr_t ble_addr = {}; + ble_addr.type = BLE_ADDR_PUBLIC; + memcpy(ble_addr.val, addr, BT_ADDR_LEN); + ble_store_util_delete_peer(&ble_addr); + return ERROR_NONE; +} + +static error_t api_get_paired_peers(struct Device* /*device*/, struct BtPeerRecord* /*out*/, size_t* /*count*/) { + // Paired peer enumeration reads filesystem — handled in Tactility layer. + return ERROR_NOT_SUPPORTED; +} + +static error_t api_connect(struct Device* device, const BtAddr addr, enum BtProfileId profile) { + BleCtx* ctx = (BleCtx*)device_get_driver_data(device); + if (!ctx) return ERROR_INVALID_STATE; + if (profile == BT_PROFILE_HID_DEVICE) { + return nimble_hid_device_api.start(device, BT_HID_DEVICE_MODE_KEYBOARD); + } else if (profile == BT_PROFILE_SPP) { + return ble_spp_start_internal(ctx); + } else if (profile == BT_PROFILE_MIDI) { + return ble_midi_start_internal(ctx); + } + // BT_PROFILE_HID_HOST is handled entirely in the Tactility layer. + return ERROR_NOT_SUPPORTED; +} + +static error_t api_disconnect(struct Device* device, const BtAddr addr, enum BtProfileId profile) { + BleCtx* ctx = (BleCtx*)device_get_driver_data(device); + if (!ctx) return ERROR_INVALID_STATE; + if (profile == BT_PROFILE_HID_DEVICE) { + return nimble_hid_device_api.stop(device); + } else if (profile == BT_PROFILE_SPP) { + return nimble_serial_api.stop(device); + } else if (profile == BT_PROFILE_MIDI) { + return nimble_midi_api.stop(device); + } + return ERROR_NOT_SUPPORTED; +} + +static error_t api_add_event_callback(struct Device* device, void* cb_ctx, BtEventCallback fn) { + BleCtx* ctx = (BleCtx*)device_get_driver_data(device); + if (!ctx || !fn) return ERROR_INVALID_ARGUMENT; + xSemaphoreTake(ctx->cb_mutex, portMAX_DELAY); + if (ctx->callback_count >= BLE_MAX_CALLBACKS) { + xSemaphoreGive(ctx->cb_mutex); + return ERROR_OUT_OF_MEMORY; + } + ctx->callbacks[ctx->callback_count].fn = fn; + ctx->callbacks[ctx->callback_count].ctx = cb_ctx; + ctx->callback_count++; + xSemaphoreGive(ctx->cb_mutex); + return ERROR_NONE; +} + +static error_t api_remove_event_callback(struct Device* device, BtEventCallback fn) { + BleCtx* ctx = (BleCtx*)device_get_driver_data(device); + if (!ctx || !fn) return ERROR_INVALID_ARGUMENT; + xSemaphoreTake(ctx->cb_mutex, portMAX_DELAY); + for (size_t i = 0; i < ctx->callback_count; i++) { + if (ctx->callbacks[i].fn == fn) { + // Shift remaining entries down + for (size_t j = i; j + 1 < ctx->callback_count; j++) { + ctx->callbacks[j] = ctx->callbacks[j + 1]; + } + ctx->callback_count--; + xSemaphoreGive(ctx->cb_mutex); + return ERROR_NONE; + } + } + xSemaphoreGive(ctx->cb_mutex); + return ERROR_NOT_FOUND; +} + +static void api_set_hid_host_active(struct Device* device, bool active) { + BleCtx* ctx = (BleCtx*)device_get_driver_data(device); + if (ctx) ctx->hid_host_active.store(active); +} + +static void api_fire_event(struct Device* device, struct BtEvent event) { + BleCtx* ctx = (BleCtx*)device_get_driver_data(device); + if (ctx) ble_publish_event(ctx, event); +} + +// ---- BluetoothApi struct ---- + +const BluetoothApi nimble_bluetooth_api = { + .get_radio_state = api_get_radio_state, + .set_radio_enabled = api_set_radio_enabled, + .scan_start = api_scan_start, + .scan_stop = api_scan_stop, + .is_scanning = api_is_scanning, + .pair = api_pair, + .unpair = api_unpair, + .get_paired_peers = api_get_paired_peers, + .connect = api_connect, + .disconnect = api_disconnect, + .add_event_callback = api_add_event_callback, + .remove_event_callback = api_remove_event_callback, + .set_hid_host_active = api_set_hid_host_active, + .fire_event = api_fire_event, +}; + +// ---- Child device drivers ---- +// Serial, MIDI and HID device are child devices of the Bluetooth parent device. +// Their drivers have no start/stop of their own (lifecycle is tied to the parent BleCtx). +// driver_construct is called once at first start; the static DriverInternal persists. + +static Driver esp32_ble_serial_driver = { + .name = "esp32-ble-serial", + .compatible = nullptr, + .start_device = nullptr, + .stop_device = nullptr, + .api = &nimble_serial_api, + .device_type = &BLUETOOTH_SERIAL_TYPE, + .owner = nullptr, + .internal = nullptr, +}; + +static Driver esp32_ble_midi_driver = { + .name = "esp32-ble-midi", + .compatible = nullptr, + .start_device = nullptr, + .stop_device = nullptr, + .api = &nimble_midi_api, + .device_type = &BLUETOOTH_MIDI_TYPE, + .owner = nullptr, + .internal = nullptr, +}; + +static Driver esp32_ble_hid_device_driver = { + .name = "esp32-ble-hid-device", + .compatible = nullptr, + .start_device = nullptr, + .stop_device = nullptr, + .api = &nimble_hid_device_api, + .device_type = &BLUETOOTH_HID_DEVICE_TYPE, + .owner = nullptr, + .internal = nullptr, +}; + +// ---- Driver lifecycle ---- + +static void create_child_device(struct Device* parent, const char* name, + Driver* drv, struct Device*& out) { + out = new Device { .name = name, .config = nullptr, .parent = nullptr, .internal = nullptr }; + device_construct(out); + device_set_parent(out, parent); + device_set_driver(out, drv); + device_add(out); + device_start(out); +} + +static void destroy_child_device(struct Device*& child) { + if (child == nullptr) return; + device_stop(child); + device_remove(child); + device_destruct(child); + delete child; + child = nullptr; +} + +static error_t esp32_ble_start_device(struct Device* device) { + // Construct child drivers once (they are static; DriverInternal persists for process lifetime). + static bool child_drivers_constructed = false; + if (!child_drivers_constructed) { + driver_construct(&esp32_ble_serial_driver); + driver_construct(&esp32_ble_midi_driver); + driver_construct(&esp32_ble_hid_device_driver); + child_drivers_constructed = true; + } + + BleCtx* ctx = new BleCtx(); + ctx->radio_mutex = xSemaphoreCreateRecursiveMutex(); + ctx->data_mutex = xSemaphoreCreateMutex(); + ctx->cb_mutex = xSemaphoreCreateMutex(); + ctx->radio_state.store(BT_RADIO_STATE_OFF); + ctx->scan_active.store(false); + ctx->hid_host_active.store(false); + ctx->callback_count = 0; + ctx->scan_count = 0; + ctx->spp_conn_handle.store(BLE_HS_CONN_HANDLE_NONE); + ctx->spp_active.store(false); + ctx->midi_conn_handle.store(BLE_HS_CONN_HANDLE_NONE); + ctx->midi_active.store(false); + ctx->midi_use_indicate.store(false); + ctx->hid_conn_handle.store(BLE_HS_CONN_HANDLE_NONE); + ctx->hid_active.store(false); + ctx->link_encrypted.store(false); + ctx->pending_reset_count.store(0); + ctx->midi_keepalive_timer = nullptr; + ctx->adv_restart_timer = nullptr; + ctx->disable_timer = nullptr; + ctx->device = device; + ctx->serial_child = nullptr; + ctx->midi_child = nullptr; + ctx->hid_device_child = nullptr; + + // Create the disable timer used to dispatch dispatchDisable off the NimBLE host task. + esp_timer_create_args_t disable_args = {}; + disable_args.callback = dispatch_disable_timer_cb; + disable_args.arg = ctx; + disable_args.dispatch_method = ESP_TIMER_TASK; + disable_args.name = "ble_disable"; + int rc = esp_timer_create(&disable_args, &ctx->disable_timer); + if (rc != ESP_OK) { + LOG_E(TAG, "start_device: disable timer create failed (rc=%d)", rc); + } + + device_set_driver_data(device, ctx); + g_ctx = ctx; + + // Create child devices for the serial, MIDI and HID device profiles. + create_child_device(device, "ble-serial", &esp32_ble_serial_driver, ctx->serial_child); + create_child_device(device, "ble-midi", &esp32_ble_midi_driver, ctx->midi_child); + create_child_device(device, "ble-hid-device", &esp32_ble_hid_device_driver, ctx->hid_device_child); + + return ERROR_NONE; +} + +static error_t esp32_ble_stop_device(struct Device* device) { + BleCtx* ctx = (BleCtx*)device_get_driver_data(device); + if (!ctx) return ERROR_NONE; + + // Destroy child devices before stopping the radio and freeing the context. + destroy_child_device(ctx->hid_device_child); + destroy_child_device(ctx->midi_child); + destroy_child_device(ctx->serial_child); + + if (ctx->radio_state.load() != BT_RADIO_STATE_OFF) { + dispatch_disable(ctx); + } + + if (ctx->disable_timer != nullptr) { + esp_timer_stop(ctx->disable_timer); + esp_timer_delete(ctx->disable_timer); + ctx->disable_timer = nullptr; + } + + g_ctx = nullptr; + device_set_driver_data(device, nullptr); + delete ctx; + return ERROR_NONE; +} + +// ---- Driver registration ---- + +static const char* esp32_bt_compatible[] = { "esp32,ble-nimble", nullptr }; + +Driver esp32_bluetooth_driver = { + .name = "esp32-bluetooth", + .compatible = esp32_bt_compatible, + .start_device = esp32_ble_start_device, + .stop_device = esp32_ble_stop_device, + .api = &nimble_bluetooth_api, + .device_type = &BLUETOOTH_TYPE, + .owner = nullptr, + .internal = nullptr, +}; + +#endif // CONFIG_BT_NIMBLE_ENABLED diff --git a/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_hid_device.cpp b/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_hid_device.cpp new file mode 100644 index 000000000..9607c391d --- /dev/null +++ b/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_hid_device.cpp @@ -0,0 +1,515 @@ +#ifdef ESP_PLATFORM +#include +#endif + +#if defined(CONFIG_BT_NIMBLE_ENABLED) + +#include + +#include +#include +#include +#include +#include + +#include + +#define TAG "esp32_ble_hid" +#include + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wmissing-field-initializers" + +// ---- Module globals ---- + +BleHidProfile current_hid_profile = BleHidProfile::None; +uint16_t hid_appearance = 0x03C1; // Keyboard (default) +const uint8_t* active_hid_rpt_map = nullptr; +size_t active_hid_rpt_map_len = 0; + +// GATT attribute handles — written by NimBLE via val_handle pointers below. +uint16_t hid_kb_input_handle; +uint16_t hid_consumer_input_handle; +uint16_t hid_mouse_input_handle; +uint16_t hid_gamepad_input_handle; + +// ---- Static UUID objects for HID 16-bit UUIDs ---- + +static const ble_uuid16_t UUID16_RPT_REF = BLE_UUID16_INIT(0x2908); +static const ble_uuid16_t UUID16_HID_INFO = BLE_UUID16_INIT(0x2A4A); +static const ble_uuid16_t UUID16_RPT_MAP = BLE_UUID16_INIT(0x2A4B); +static const ble_uuid16_t UUID16_HID_CTRL = BLE_UUID16_INIT(0x2A4C); +static const ble_uuid16_t UUID16_HID_REPORT = BLE_UUID16_INIT(0x2A4D); +static const ble_uuid16_t UUID16_PROTO_MODE = BLE_UUID16_INIT(0x2A4E); +static const ble_uuid16_t UUID16_HID_SVC = BLE_UUID16_INIT(0x1812); + +static uint8_t hid_protocol_mode = 0x01; // 0x00=Boot, 0x01=Report + +// ============================================================================ +// Per-profile HID Report Maps +// ============================================================================ + +// Keyboard + Consumer (IDs 1 and 2) +static const uint8_t hid_rpt_map_kb_consumer[] = { + 0x05, 0x01, 0x09, 0x06, 0xA1, 0x01, + 0x85, 0x01, + 0x05, 0x07, 0x19, 0xE0, 0x29, 0xE7, 0x15, 0x00, 0x25, 0x01, + 0x75, 0x01, 0x95, 0x08, 0x81, 0x02, + 0x75, 0x08, 0x95, 0x01, 0x81, 0x01, + 0x05, 0x08, 0x19, 0x01, 0x29, 0x05, 0x75, 0x01, 0x95, 0x05, 0x91, 0x02, + 0x75, 0x03, 0x95, 0x01, 0x91, 0x01, + 0x15, 0x00, 0x25, 0x73, 0x05, 0x07, 0x19, 0x00, 0x29, 0x73, + 0x75, 0x08, 0x95, 0x06, 0x81, 0x00, + 0xC0, + 0x05, 0x0C, 0x09, 0x01, 0xA1, 0x01, + 0x85, 0x02, + 0x15, 0x00, 0x26, 0xFF, 0x03, 0x19, 0x00, 0x2A, 0xFF, 0x03, + 0x75, 0x10, 0x95, 0x01, 0x81, 0x00, + 0xC0, +}; + +// Mouse only (ID 1, 4 bytes) +static const uint8_t hid_rpt_map_mouse[] = { + 0x05, 0x01, 0x09, 0x02, 0xA1, 0x01, + 0x85, 0x01, + 0x09, 0x01, 0xA1, 0x00, + 0x05, 0x09, 0x19, 0x01, 0x29, 0x05, + 0x15, 0x00, 0x25, 0x01, 0x95, 0x05, 0x75, 0x01, 0x81, 0x02, + 0x95, 0x01, 0x75, 0x03, 0x81, 0x01, + 0x05, 0x01, 0x09, 0x30, 0x09, 0x31, 0x09, 0x38, + 0x15, 0x81, 0x25, 0x7F, 0x75, 0x08, 0x95, 0x03, 0x81, 0x06, + 0xC0, 0xC0, +}; + +// Keyboard + Consumer + Mouse (IDs 1, 2, 3) +static const uint8_t hid_rpt_map_kb_mouse[] = { + 0x05, 0x01, 0x09, 0x06, 0xA1, 0x01, + 0x85, 0x01, + 0x05, 0x07, 0x19, 0xE0, 0x29, 0xE7, 0x15, 0x00, 0x25, 0x01, + 0x75, 0x01, 0x95, 0x08, 0x81, 0x02, + 0x75, 0x08, 0x95, 0x01, 0x81, 0x01, + 0x05, 0x08, 0x19, 0x01, 0x29, 0x05, 0x75, 0x01, 0x95, 0x05, 0x91, 0x02, + 0x75, 0x03, 0x95, 0x01, 0x91, 0x01, + 0x15, 0x00, 0x25, 0x73, 0x05, 0x07, 0x19, 0x00, 0x29, 0x73, + 0x75, 0x08, 0x95, 0x06, 0x81, 0x00, + 0xC0, + 0x05, 0x0C, 0x09, 0x01, 0xA1, 0x01, + 0x85, 0x02, + 0x15, 0x00, 0x26, 0xFF, 0x03, 0x19, 0x00, 0x2A, 0xFF, 0x03, + 0x75, 0x10, 0x95, 0x01, 0x81, 0x00, + 0xC0, + 0x05, 0x01, 0x09, 0x02, 0xA1, 0x01, + 0x85, 0x03, + 0x09, 0x01, 0xA1, 0x00, + 0x05, 0x09, 0x19, 0x01, 0x29, 0x05, + 0x15, 0x00, 0x25, 0x01, 0x95, 0x05, 0x75, 0x01, 0x81, 0x02, + 0x95, 0x01, 0x75, 0x03, 0x81, 0x01, + 0x05, 0x01, 0x09, 0x30, 0x09, 0x31, 0x09, 0x38, + 0x15, 0x81, 0x25, 0x7F, 0x75, 0x08, 0x95, 0x03, 0x81, 0x06, + 0xC0, 0xC0, +}; + +// Gamepad only (ID 1, 8 bytes) +static const uint8_t hid_rpt_map_gamepad[] = { + 0x05, 0x01, 0x09, 0x05, 0xA1, 0x01, + 0x85, 0x01, + 0x05, 0x09, 0x19, 0x01, 0x29, 0x10, + 0x15, 0x00, 0x25, 0x01, 0x75, 0x01, 0x95, 0x10, 0x81, 0x02, + 0x05, 0x01, 0x09, 0x30, 0x09, 0x31, 0x09, 0x32, 0x09, 0x35, 0x09, 0x33, 0x09, 0x34, + 0x15, 0x81, 0x25, 0x7F, 0x75, 0x08, 0x95, 0x06, 0x81, 0x02, + 0xC0, +}; + +// ---- Per-profile Report Reference descriptor data ---- +// Format: {Report ID, Report Type} — Type: 1=Input, 2=Output + +static const uint8_t rpt_ref_kbc_kb_in[2] = {1, 1}; +static const uint8_t rpt_ref_kbc_cs_in[2] = {2, 1}; +static const uint8_t rpt_ref_kbc_kb_out[2] = {1, 2}; +static const uint8_t rpt_ref_ms_in[2] = {1, 1}; +static const uint8_t rpt_ref_kbm_kb_in[2] = {1, 1}; +static const uint8_t rpt_ref_kbm_cs_in[2] = {2, 1}; +static const uint8_t rpt_ref_kbm_ms_in[2] = {3, 1}; +static const uint8_t rpt_ref_kbm_kb_out[2] = {1, 2}; +static const uint8_t rpt_ref_gp_in[2] = {1, 1}; + +// ---- HID GATT callbacks ---- + +static int hid_dsc_access(uint16_t /*conn_handle*/, uint16_t /*attr_handle*/, + struct ble_gatt_access_ctxt* ctxt, void* arg) { + if (ctxt->op == BLE_GATT_ACCESS_OP_READ_DSC) { + const uint8_t* data = (const uint8_t*)arg; + int rc = os_mbuf_append(ctxt->om, data, 2); + return (rc == 0) ? 0 : BLE_ATT_ERR_INSUFFICIENT_RES; + } + return BLE_ATT_ERR_UNLIKELY; +} + +static int hid_chr_access(uint16_t /*conn_handle*/, uint16_t attr_handle, + struct ble_gatt_access_ctxt* ctxt, void* /*arg*/) { + uint16_t uuid16 = ble_uuid_u16(ctxt->chr->uuid); + + switch (ctxt->op) { + case BLE_GATT_ACCESS_OP_READ_CHR: { + if (uuid16 == 0x2A4A) { + static const uint8_t hid_info[4] = { 0x11, 0x01, 0x00, 0x02 }; + int rc = os_mbuf_append(ctxt->om, hid_info, sizeof(hid_info)); + return (rc == 0) ? 0 : BLE_ATT_ERR_INSUFFICIENT_RES; + } + if (uuid16 == 0x2A4B) { + if (active_hid_rpt_map == nullptr || active_hid_rpt_map_len == 0) + return BLE_ATT_ERR_UNLIKELY; + int rc = os_mbuf_append(ctxt->om, active_hid_rpt_map, active_hid_rpt_map_len); + return (rc == 0) ? 0 : BLE_ATT_ERR_INSUFFICIENT_RES; + } + if (uuid16 == 0x2A4E) { + int rc = os_mbuf_append(ctxt->om, &hid_protocol_mode, 1); + return (rc == 0) ? 0 : BLE_ATT_ERR_INSUFFICIENT_RES; + } + if (uuid16 == 0x2A4D) { + static const uint8_t zeros[8] = {}; + size_t report_len = 8; + if (attr_handle == hid_consumer_input_handle) report_len = 2; + else if (attr_handle == hid_mouse_input_handle) report_len = 4; + int rc = os_mbuf_append(ctxt->om, zeros, report_len); + return (rc == 0) ? 0 : BLE_ATT_ERR_INSUFFICIENT_RES; + } + return BLE_ATT_ERR_UNLIKELY; + } + + case BLE_GATT_ACCESS_OP_WRITE_CHR: { + if (uuid16 == 0x2A4C) return 0; // HID Control Point — no action + if (uuid16 == 0x2A4E) { + if (OS_MBUF_PKTLEN(ctxt->om) >= 1) { + os_mbuf_copydata(ctxt->om, 0, 1, &hid_protocol_mode); + LOG_I(TAG, "Protocol Mode -> %u", hid_protocol_mode); + } + return 0; + } + if (uuid16 == 0x2A4D) { + if (OS_MBUF_PKTLEN(ctxt->om) >= 1) { + uint8_t leds = 0; + os_mbuf_copydata(ctxt->om, 0, 1, &leds); + LOG_I(TAG, "KB LED state: 0x%02x", leds); + } + return 0; + } + return BLE_ATT_ERR_UNLIKELY; + } + + default: + return BLE_ATT_ERR_UNLIKELY; + } +} + +// ---- Per-profile HID descriptor arrays ---- + +static struct ble_gatt_dsc_def hid_kbc_kb_dscs[] = { { .uuid = &UUID16_RPT_REF.u, .att_flags = BLE_ATT_F_READ, .access_cb = hid_dsc_access, .arg = (void*)rpt_ref_kbc_kb_in }, { 0 } }; +static struct ble_gatt_dsc_def hid_kbc_cs_dscs[] = { { .uuid = &UUID16_RPT_REF.u, .att_flags = BLE_ATT_F_READ, .access_cb = hid_dsc_access, .arg = (void*)rpt_ref_kbc_cs_in }, { 0 } }; +static struct ble_gatt_dsc_def hid_kbc_out_dscs[] = { { .uuid = &UUID16_RPT_REF.u, .att_flags = BLE_ATT_F_READ, .access_cb = hid_dsc_access, .arg = (void*)rpt_ref_kbc_kb_out }, { 0 } }; +static struct ble_gatt_dsc_def hid_ms_dscs[] = { { .uuid = &UUID16_RPT_REF.u, .att_flags = BLE_ATT_F_READ, .access_cb = hid_dsc_access, .arg = (void*)rpt_ref_ms_in }, { 0 } }; +static struct ble_gatt_dsc_def hid_kbm_kb_dscs[] = { { .uuid = &UUID16_RPT_REF.u, .att_flags = BLE_ATT_F_READ, .access_cb = hid_dsc_access, .arg = (void*)rpt_ref_kbm_kb_in }, { 0 } }; +static struct ble_gatt_dsc_def hid_kbm_cs_dscs[] = { { .uuid = &UUID16_RPT_REF.u, .att_flags = BLE_ATT_F_READ, .access_cb = hid_dsc_access, .arg = (void*)rpt_ref_kbm_cs_in }, { 0 } }; +static struct ble_gatt_dsc_def hid_kbm_ms_dscs[] = { { .uuid = &UUID16_RPT_REF.u, .att_flags = BLE_ATT_F_READ, .access_cb = hid_dsc_access, .arg = (void*)rpt_ref_kbm_ms_in }, { 0 } }; +static struct ble_gatt_dsc_def hid_kbm_out_dscs[] = { { .uuid = &UUID16_RPT_REF.u, .att_flags = BLE_ATT_F_READ, .access_cb = hid_dsc_access, .arg = (void*)rpt_ref_kbm_kb_out }, { 0 } }; +static struct ble_gatt_dsc_def hid_gp_dscs[] = { { .uuid = &UUID16_RPT_REF.u, .att_flags = BLE_ATT_F_READ, .access_cb = hid_dsc_access, .arg = (void*)rpt_ref_gp_in }, { 0 } }; + +// ---- Per-profile HID characteristic arrays ---- + +static struct ble_gatt_chr_def hid_chars_kb_consumer[] = { + { .uuid = &UUID16_HID_INFO.u, .access_cb = hid_chr_access, .flags = BLE_GATT_CHR_F_READ }, + { .uuid = &UUID16_RPT_MAP.u, .access_cb = hid_chr_access, .flags = BLE_GATT_CHR_F_READ }, + { .uuid = &UUID16_HID_CTRL.u, .access_cb = hid_chr_access, .flags = BLE_GATT_CHR_F_WRITE_NO_RSP }, + { .uuid = &UUID16_PROTO_MODE.u, .access_cb = hid_chr_access, .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_WRITE_NO_RSP }, + { .uuid = &UUID16_HID_REPORT.u, .access_cb = hid_chr_access, .descriptors = hid_kbc_kb_dscs, + .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY, .val_handle = &hid_kb_input_handle }, + { .uuid = &UUID16_HID_REPORT.u, .access_cb = hid_chr_access, .descriptors = hid_kbc_cs_dscs, + .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY, .val_handle = &hid_consumer_input_handle }, + { .uuid = &UUID16_HID_REPORT.u, .access_cb = hid_chr_access, .descriptors = hid_kbc_out_dscs, + .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_WRITE | BLE_GATT_CHR_F_WRITE_NO_RSP }, + { 0 } +}; + +static struct ble_gatt_chr_def hid_chars_mouse[] = { + { .uuid = &UUID16_HID_INFO.u, .access_cb = hid_chr_access, .flags = BLE_GATT_CHR_F_READ }, + { .uuid = &UUID16_RPT_MAP.u, .access_cb = hid_chr_access, .flags = BLE_GATT_CHR_F_READ }, + { .uuid = &UUID16_HID_CTRL.u, .access_cb = hid_chr_access, .flags = BLE_GATT_CHR_F_WRITE_NO_RSP }, + { .uuid = &UUID16_PROTO_MODE.u, .access_cb = hid_chr_access, .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_WRITE_NO_RSP }, + { .uuid = &UUID16_HID_REPORT.u, .access_cb = hid_chr_access, .descriptors = hid_ms_dscs, + .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY, .val_handle = &hid_mouse_input_handle }, + { 0 } +}; + +static struct ble_gatt_chr_def hid_chars_kb_mouse[] = { + { .uuid = &UUID16_HID_INFO.u, .access_cb = hid_chr_access, .flags = BLE_GATT_CHR_F_READ }, + { .uuid = &UUID16_RPT_MAP.u, .access_cb = hid_chr_access, .flags = BLE_GATT_CHR_F_READ }, + { .uuid = &UUID16_HID_CTRL.u, .access_cb = hid_chr_access, .flags = BLE_GATT_CHR_F_WRITE_NO_RSP }, + { .uuid = &UUID16_PROTO_MODE.u, .access_cb = hid_chr_access, .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_WRITE_NO_RSP }, + { .uuid = &UUID16_HID_REPORT.u, .access_cb = hid_chr_access, .descriptors = hid_kbm_kb_dscs, + .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY, .val_handle = &hid_kb_input_handle }, + { .uuid = &UUID16_HID_REPORT.u, .access_cb = hid_chr_access, .descriptors = hid_kbm_cs_dscs, + .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY, .val_handle = &hid_consumer_input_handle }, + { .uuid = &UUID16_HID_REPORT.u, .access_cb = hid_chr_access, .descriptors = hid_kbm_ms_dscs, + .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY, .val_handle = &hid_mouse_input_handle }, + { .uuid = &UUID16_HID_REPORT.u, .access_cb = hid_chr_access, .descriptors = hid_kbm_out_dscs, + .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_WRITE | BLE_GATT_CHR_F_WRITE_NO_RSP }, + { 0 } +}; + +static struct ble_gatt_chr_def hid_chars_gamepad[] = { + { .uuid = &UUID16_HID_INFO.u, .access_cb = hid_chr_access, .flags = BLE_GATT_CHR_F_READ }, + { .uuid = &UUID16_RPT_MAP.u, .access_cb = hid_chr_access, .flags = BLE_GATT_CHR_F_READ }, + { .uuid = &UUID16_HID_CTRL.u, .access_cb = hid_chr_access, .flags = BLE_GATT_CHR_F_WRITE_NO_RSP }, + { .uuid = &UUID16_PROTO_MODE.u, .access_cb = hid_chr_access, .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_WRITE_NO_RSP }, + { .uuid = &UUID16_HID_REPORT.u, .access_cb = hid_chr_access, .descriptors = hid_gp_dscs, + .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY, .val_handle = &hid_gamepad_input_handle }, + { 0 } +}; + +// ---- Per-profile GATT service arrays ---- + +static const struct ble_gatt_svc_def gatt_svcs_none[] = { + { .type = BLE_GATT_SVC_TYPE_PRIMARY, .uuid = &NUS_SVC_UUID.u, .characteristics = nus_chars_with_handle }, + { .type = BLE_GATT_SVC_TYPE_PRIMARY, .uuid = &MIDI_SVC_UUID.u, .characteristics = midi_chars }, + { 0 } +}; +static const struct ble_gatt_svc_def gatt_svcs_kb_consumer[] = { + { .type = BLE_GATT_SVC_TYPE_PRIMARY, .uuid = &NUS_SVC_UUID.u, .characteristics = nus_chars_with_handle }, + { .type = BLE_GATT_SVC_TYPE_PRIMARY, .uuid = &MIDI_SVC_UUID.u, .characteristics = midi_chars }, + { .type = BLE_GATT_SVC_TYPE_PRIMARY, .uuid = &UUID16_HID_SVC.u, .characteristics = hid_chars_kb_consumer }, + { 0 } +}; +static const struct ble_gatt_svc_def gatt_svcs_mouse[] = { + { .type = BLE_GATT_SVC_TYPE_PRIMARY, .uuid = &NUS_SVC_UUID.u, .characteristics = nus_chars_with_handle }, + { .type = BLE_GATT_SVC_TYPE_PRIMARY, .uuid = &MIDI_SVC_UUID.u, .characteristics = midi_chars }, + { .type = BLE_GATT_SVC_TYPE_PRIMARY, .uuid = &UUID16_HID_SVC.u, .characteristics = hid_chars_mouse }, + { 0 } +}; +static const struct ble_gatt_svc_def gatt_svcs_kb_mouse[] = { + { .type = BLE_GATT_SVC_TYPE_PRIMARY, .uuid = &NUS_SVC_UUID.u, .characteristics = nus_chars_with_handle }, + { .type = BLE_GATT_SVC_TYPE_PRIMARY, .uuid = &MIDI_SVC_UUID.u, .characteristics = midi_chars }, + { .type = BLE_GATT_SVC_TYPE_PRIMARY, .uuid = &UUID16_HID_SVC.u, .characteristics = hid_chars_kb_mouse }, + { 0 } +}; +static const struct ble_gatt_svc_def gatt_svcs_gamepad[] = { + { .type = BLE_GATT_SVC_TYPE_PRIMARY, .uuid = &NUS_SVC_UUID.u, .characteristics = nus_chars_with_handle }, + { .type = BLE_GATT_SVC_TYPE_PRIMARY, .uuid = &MIDI_SVC_UUID.u, .characteristics = midi_chars }, + { .type = BLE_GATT_SVC_TYPE_PRIMARY, .uuid = &UUID16_HID_SVC.u, .characteristics = hid_chars_gamepad }, + { 0 } +}; + +// ---- GATT profile switch ---- + +void ble_hid_device_switch_profile(BleCtx* ctx, BleHidProfile profile) { + if (profile == current_hid_profile) return; + LOG_I(TAG, "switchGattProfile: %d -> %d", (int)current_hid_profile, (int)profile); + + ble_gap_adv_stop(); + + if (ctx && ctx->hid_conn_handle.load() != BLE_HS_CONN_HANDLE_NONE) { + ble_gap_terminate(ctx->hid_conn_handle.load(), BLE_ERR_REM_USER_CONN_TERM); + ctx->hid_conn_handle.store(BLE_HS_CONN_HANDLE_NONE); + } + + ble_gatts_reset(); + ble_svc_gap_init(); + ble_svc_gatt_init(); + + const struct ble_gatt_svc_def* svcs = gatt_svcs_none; + const uint8_t* new_rpt_map = nullptr; + size_t new_rpt_map_len = 0; + switch (profile) { + case BleHidProfile::KbConsumer: svcs = gatt_svcs_kb_consumer; new_rpt_map = hid_rpt_map_kb_consumer; new_rpt_map_len = sizeof(hid_rpt_map_kb_consumer); break; + case BleHidProfile::Mouse: svcs = gatt_svcs_mouse; new_rpt_map = hid_rpt_map_mouse; new_rpt_map_len = sizeof(hid_rpt_map_mouse); break; + case BleHidProfile::KbMouse: svcs = gatt_svcs_kb_mouse; new_rpt_map = hid_rpt_map_kb_mouse; new_rpt_map_len = sizeof(hid_rpt_map_kb_mouse); break; + case BleHidProfile::Gamepad: svcs = gatt_svcs_gamepad; new_rpt_map = hid_rpt_map_gamepad; new_rpt_map_len = sizeof(hid_rpt_map_gamepad); break; + default: svcs = gatt_svcs_none; break; + } + + int rc = ble_gatts_count_cfg(svcs); + if (rc == 0) { + rc = ble_gatts_add_svcs(svcs); + if (rc != 0) { + LOG_E(TAG, "switchGattProfile: gatts_add_svcs failed rc=%d", rc); + return; // don't update profile — GATT state is inconsistent + } + } else { + LOG_E(TAG, "switchGattProfile: gatts_count_cfg failed rc=%d", rc); + return; + } + + // ble_gatts_add_svcs() only adds definitions to a pending list. + // ble_gatts_start() converts them into live ATT entries. + // Without this call, all GATT reads return ATT errors and Windows + // cannot install the HID driver → Phase 2 reconnect never occurs. + rc = ble_gatts_start(); + if (rc != 0) { + LOG_E(TAG, "switchGattProfile: gatts_start failed rc=%d", rc); + return; + } + + active_hid_rpt_map = new_rpt_map; + active_hid_rpt_map_len = new_rpt_map_len; + + ble_svc_gap_device_name_set(CONFIG_TT_DEVICE_NAME); + ble_att_set_preferred_mtu(BLE_ATT_MTU_MAX); + ble_svc_gatt_changed(0, 0xFFFF); + + current_hid_profile = profile; +} + +void ble_hid_device_init_gatt() { + current_hid_profile = BleHidProfile::None; + active_hid_rpt_map = nullptr; + active_hid_rpt_map_len = 0; + int rc = ble_gatts_count_cfg(gatt_svcs_none); + if (rc != 0) { + LOG_E(TAG, "gatts_count_cfg failed (rc=%d)", rc); + } else { + rc = ble_gatts_add_svcs(gatt_svcs_none); + if (rc != 0) { + LOG_E(TAG, "gatts_add_svcs failed (rc=%d)", rc); + } + } +} + +void ble_hid_device_init_gatt_handles() { + // val_handle pointers in char arrays are updated by NimBLE at registration time. + // No explicit action needed here; called for symmetry with spp/midi init. + (void)hid_kb_input_handle; + (void)hid_consumer_input_handle; + (void)hid_mouse_input_handle; + (void)hid_gamepad_input_handle; +} + +// ---- HID Device sub-API implementations ---- + +static error_t hid_device_start(struct Device* /*device*/, enum BtHidDeviceMode mode) { + BleCtx* ctx = g_ctx; + if (ctx == nullptr) return ERROR_INVALID_STATE; + + BleHidProfile profile; + uint16_t appearance; + switch (mode) { + case BT_HID_DEVICE_MODE_MOUSE: + profile = BleHidProfile::Mouse; + appearance = 0x03C2; + break; + case BT_HID_DEVICE_MODE_KEYBOARD_MOUSE: + profile = BleHidProfile::KbMouse; + appearance = 0x03C0; + break; + case BT_HID_DEVICE_MODE_GAMEPAD: + profile = BleHidProfile::Gamepad; + appearance = 0x03C4; + break; + default: // BT_HID_DEVICE_MODE_KEYBOARD + profile = BleHidProfile::KbConsumer; + appearance = 0x03C1; + break; + } + + hid_appearance = appearance; + ble_hid_device_switch_profile(ctx, profile); + ctx->hid_active.store(true); + ble_start_advertising_hid(hid_appearance); + return ERROR_NONE; +} + +static error_t hid_device_stop(struct Device* /*device*/) { + BleCtx* ctx = g_ctx; + if (ctx == nullptr) return ERROR_NONE; + ctx->hid_active.store(false); + ble_gap_adv_stop(); + uint16_t conn = ctx->hid_conn_handle.load(); + if (conn != BLE_HS_CONN_HANDLE_NONE) { + // Connected: terminate and let the DISCONNECT handler switch profile to None. + // ble_gatts_mutable() returns false while a connection is live, so calling + // switch_profile here would assert inside ble_svc_gap_init(). + ble_gap_terminate(conn, BLE_ERR_REM_USER_CONN_TERM); + // Do NOT clear hid_conn_handle — DISCONNECT handler uses it for was_hid detection. + } else { + // Not connected: GATT is mutable, switch profile immediately. + if (current_hid_profile != BleHidProfile::None) { + ble_hid_device_switch_profile(ctx, BleHidProfile::None); + } + } + return ERROR_NONE; +} + +static error_t hid_device_send_key(struct Device* /*device*/, uint8_t keycode, bool pressed) { + BleCtx* ctx = g_ctx; + if (ctx == nullptr || ctx->hid_conn_handle.load() == BLE_HS_CONN_HANDLE_NONE) { + return ERROR_INVALID_STATE; + } + uint8_t report[8] = {}; + if (pressed) report[2] = keycode; + struct os_mbuf* om = ble_hs_mbuf_from_flat(report, sizeof(report)); + if (om == nullptr) return ERROR_INVALID_STATE; + int rc = ble_gatts_notify_custom(ctx->hid_conn_handle.load(), hid_kb_input_handle, om); + if (rc != 0) os_mbuf_free_chain(om); + return (rc == 0) ? ERROR_NONE : ERROR_INVALID_STATE; +} + +static error_t hid_notify(uint16_t conn_handle, uint16_t attr_handle, + const uint8_t* data, size_t len) { + if (conn_handle == BLE_HS_CONN_HANDLE_NONE) return ERROR_INVALID_STATE; + if (attr_handle == 0) return ERROR_INVALID_STATE; // handle not registered for current profile + struct os_mbuf* om = ble_hs_mbuf_from_flat(data, len); + if (om == nullptr) return ERROR_INVALID_STATE; + int rc = ble_gatts_notify_custom(conn_handle, attr_handle, om); + if (rc != 0) os_mbuf_free_chain(om); + return (rc == 0) ? ERROR_NONE : ERROR_INVALID_STATE; +} + +static error_t hid_device_send_keyboard(struct Device* /*device*/, const uint8_t* report, size_t len) { + BleCtx* ctx = g_ctx; + if (ctx == nullptr) return ERROR_INVALID_STATE; + uint8_t buf[8] = {}; + memcpy(buf, report, len < sizeof(buf) ? len : sizeof(buf)); + return hid_notify(ctx->hid_conn_handle.load(), hid_kb_input_handle, buf, sizeof(buf)); +} + +static error_t hid_device_send_consumer(struct Device* /*device*/, const uint8_t* report, size_t len) { + BleCtx* ctx = g_ctx; + if (ctx == nullptr) return ERROR_INVALID_STATE; + uint8_t buf[2] = {}; + memcpy(buf, report, len < sizeof(buf) ? len : sizeof(buf)); + return hid_notify(ctx->hid_conn_handle.load(), hid_consumer_input_handle, buf, sizeof(buf)); +} + +static error_t hid_device_send_mouse(struct Device* /*device*/, const uint8_t* report, size_t len) { + BleCtx* ctx = g_ctx; + if (ctx == nullptr) return ERROR_INVALID_STATE; + uint8_t buf[4] = {}; + memcpy(buf, report, len < sizeof(buf) ? len : sizeof(buf)); + return hid_notify(ctx->hid_conn_handle.load(), hid_mouse_input_handle, buf, sizeof(buf)); +} + +static error_t hid_device_send_gamepad(struct Device* /*device*/, const uint8_t* report, size_t len) { + BleCtx* ctx = g_ctx; + if (ctx == nullptr) return ERROR_INVALID_STATE; + uint8_t buf[8] = {}; + memcpy(buf, report, len < sizeof(buf) ? len : sizeof(buf)); + return hid_notify(ctx->hid_conn_handle.load(), hid_gamepad_input_handle, buf, sizeof(buf)); +} + +static bool hid_device_is_connected(struct Device* /*device*/) { + BleCtx* ctx = g_ctx; + return ctx != nullptr && ctx->hid_conn_handle.load() != BLE_HS_CONN_HANDLE_NONE; +} + +const BtHidDeviceApi nimble_hid_device_api = { + .start = hid_device_start, + .stop = hid_device_stop, + .send_key = hid_device_send_key, + .send_keyboard = hid_device_send_keyboard, + .send_consumer = hid_device_send_consumer, + .send_mouse = hid_device_send_mouse, + .send_gamepad = hid_device_send_gamepad, + .is_connected = hid_device_is_connected, +}; + +#pragma GCC diagnostic pop + +#endif // CONFIG_BT_NIMBLE_ENABLED diff --git a/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_midi.cpp b/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_midi.cpp new file mode 100644 index 000000000..a8ea67af9 --- /dev/null +++ b/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_midi.cpp @@ -0,0 +1,177 @@ +#ifdef ESP_PLATFORM +#include +#endif + +#if defined(CONFIG_BT_NIMBLE_ENABLED) + +#include + +#include +#include +#include +#include +#include + +#define TAG "esp32_ble_midi" +#include +#include + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wmissing-field-initializers" + +// ---- BLE MIDI UUIDs ---- + +// 03B80E5A-EDE8-4B33-A751-6CE34EC4C700 +const ble_uuid128_t MIDI_SVC_UUID = BLE_UUID128_INIT( + 0x00, 0xC7, 0xC4, 0x4E, 0xE3, 0x6C, 0x51, 0xA7, + 0x33, 0x4B, 0xE8, 0xED, 0x5A, 0x0E, 0xB8, 0x03 +); + +// 7772E5DB-3868-4112-A1A9-F2669D106BF3 +static const ble_uuid128_t MIDI_IO_UUID = BLE_UUID128_INIT( + 0xF3, 0x6B, 0x10, 0x9D, 0x66, 0xF2, 0xA9, 0xA1, + 0x12, 0x41, 0x68, 0x38, 0xDB, 0xE5, 0x72, 0x77 +); + +uint16_t midi_io_handle; + +static int midi_chr_access(uint16_t conn_handle, uint16_t attr_handle, + struct ble_gatt_access_ctxt* ctxt, void* arg) { + if (ctxt->op == BLE_GATT_ACCESS_OP_WRITE_CHR) { + uint16_t len = OS_MBUF_PKTLEN(ctxt->om); + LOG_I(TAG, "MIDI RX %u bytes", (unsigned)len); + BleCtx* ctx = g_ctx; + if (ctx != nullptr && len > 0) { + std::vector packet(len); + os_mbuf_copydata(ctxt->om, 0, len, packet.data()); + { + xSemaphoreTake(ctx->data_mutex, portMAX_DELAY); + ctx->midi_rx_queue.push_back(std::move(packet)); + while (ctx->midi_rx_queue.size() > 16) ctx->midi_rx_queue.pop_front(); + xSemaphoreGive(ctx->data_mutex); + } + struct BtEvent e = {}; + e.type = BT_EVENT_MIDI_DATA_RECEIVED; + ble_publish_event(ctx, e); + } + } + return 0; +} + +const struct ble_gatt_chr_def midi_chars[] = { + { + .uuid = &MIDI_IO_UUID.u, + .access_cb = midi_chr_access, + .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_WRITE_NO_RSP | BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_INDICATE, + .val_handle = &midi_io_handle, + }, + { 0 } +}; + +void ble_midi_init_gatt_handles(BleCtx* /*ctx*/) { + // midi_io_handle is written by NimBLE via the val_handle pointer above. + // Nothing else needed; the extern variable is accessed directly by esp32_ble.cpp. +} + +// ---- MIDI Active Sensing keepalive ---- + +static void midi_keepalive_cb(void* /*arg*/) { + BleCtx* ctx = g_ctx; + if (ctx == nullptr || ctx->midi_conn_handle.load() == BLE_HS_CONN_HANDLE_NONE) return; + static const uint8_t as_pkt[3] = { 0x80, 0x80, 0xFE }; + struct os_mbuf* om = ble_hs_mbuf_from_flat(as_pkt, 3); + if (om == nullptr) return; + int rc = ctx->midi_use_indicate.load() + ? ble_gatts_indicate_custom(ctx->midi_conn_handle.load(), midi_io_handle, om) + : ble_gatts_notify_custom(ctx->midi_conn_handle.load(), midi_io_handle, om); + if (rc != 0) os_mbuf_free_chain(om); +} + +// ---- MIDI sub-API implementations ---- + +static error_t midi_start(struct Device* device) { + BleCtx* ctx = g_ctx; + if (ctx == nullptr) return ERROR_INVALID_STATE; + ctx->midi_active.store(true); + // Create 2-second periodic Active Sensing timer to prevent Windows BLE MIDI + // driver from declaring the connection idle and disconnecting (~8-10 s timeout). + if (ctx->midi_keepalive_timer == nullptr) { + esp_timer_create_args_t args = {}; + args.callback = midi_keepalive_cb; + args.dispatch_method = ESP_TIMER_TASK; + args.name = "ble_midi_as"; + int rc = esp_timer_create(&args, &ctx->midi_keepalive_timer); + if (rc != ESP_OK) { + LOG_E(TAG, "midi_start: keepalive timer create failed (rc=%d)", rc); + return ERROR_INVALID_STATE; + } + } + int rc = esp_timer_start_periodic(ctx->midi_keepalive_timer, 2'000'000); + if (rc != ESP_OK) { + LOG_E(TAG, "midi_start: keepalive timer start failed (rc=%d)", rc); + } + ble_start_advertising(&MIDI_SVC_UUID); + return ERROR_NONE; +} + +error_t ble_midi_start_internal(BleCtx* ctx) { + return midi_start(nullptr); +} + +static error_t midi_stop(struct Device* device) { + BleCtx* ctx = g_ctx; + if (ctx == nullptr) return ERROR_NONE; + ctx->midi_active.store(false); + if (ctx->midi_keepalive_timer != nullptr) { + esp_timer_stop(ctx->midi_keepalive_timer); + } + if (ctx->midi_conn_handle.load() != BLE_HS_CONN_HANDLE_NONE) { + ble_gap_terminate(ctx->midi_conn_handle.load(), BLE_ERR_REM_USER_CONN_TERM); + ctx->midi_conn_handle.store(BLE_HS_CONN_HANDLE_NONE); + } + if (!ctx->spp_active.load() && !ctx->hid_active.load()) { + ble_gap_adv_stop(); + } + return ERROR_NONE; +} + +static error_t midi_send(struct Device* device, const uint8_t* msg, size_t len) { + BleCtx* ctx = g_ctx; + if (ctx == nullptr || ctx->midi_conn_handle.load() == BLE_HS_CONN_HANDLE_NONE) { + return ERROR_INVALID_STATE; + } + // BLE MIDI 2-byte header: [0x80|(ts_high&0x3F)][0x80|(ts_low&0x7F)] + uint8_t header[2] = { 0x80, 0x80 }; + struct os_mbuf* om = ble_hs_mbuf_from_flat(header, 2); + if (om == nullptr) return ERROR_INVALID_STATE; + if (os_mbuf_append(om, msg, len) != 0) { + os_mbuf_free_chain(om); + LOG_E(TAG, "midi_send: mbuf append failed"); + return ERROR_INVALID_STATE; + } + LOG_I(TAG, "midi_send %u bytes (indicate=%d)", (unsigned)len, (int)ctx->midi_use_indicate.load()); + int rc = ctx->midi_use_indicate.load() + ? ble_gatts_indicate_custom(ctx->midi_conn_handle.load(), midi_io_handle, om) + : ble_gatts_notify_custom(ctx->midi_conn_handle.load(), midi_io_handle, om); + if (rc != 0) { + os_mbuf_free_chain(om); + LOG_E(TAG, "midi_send failed rc=%d", rc); + } + return (rc == 0) ? ERROR_NONE : ERROR_INVALID_STATE; +} + +static bool midi_is_connected(struct Device* device) { + BleCtx* ctx = g_ctx; + return ctx != nullptr && ctx->midi_conn_handle.load() != BLE_HS_CONN_HANDLE_NONE; +} + +const BtMidiApi nimble_midi_api = { + .start = midi_start, + .stop = midi_stop, + .send = midi_send, + .is_connected = midi_is_connected, +}; + +#pragma GCC diagnostic pop + +#endif // CONFIG_BT_NIMBLE_ENABLED diff --git a/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_scan.cpp b/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_scan.cpp new file mode 100644 index 000000000..9ae15674b --- /dev/null +++ b/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_scan.cpp @@ -0,0 +1,219 @@ +#ifdef ESP_PLATFORM +#include +#endif + +#if defined(CONFIG_BT_NIMBLE_ENABLED) + +#include + +#include +#include + +#include +#include + +#define TAG "esp32_ble_scan" +#include + +// ---- GAP scan callback ---- + +int ble_gap_disc_event_handler(struct ble_gap_event* event, void* arg) { + BleCtx* ctx = g_ctx; + if (ctx == nullptr) return 0; + + switch (event->type) { + case BLE_GAP_EVENT_DISC: { + const auto& disc = event->disc; + + BtPeerRecord record = {}; + memcpy(record.addr, disc.addr.val, BT_ADDR_LEN); + record.addr_type = disc.addr.type; + record.rssi = disc.rssi; + record.paired = false; + record.connected = false; + + struct ble_hs_adv_fields fields; + if (ble_hs_adv_parse_fields(&fields, disc.data, disc.length_data) == 0) { + if (fields.name != nullptr && fields.name_len > 0) { + size_t copy_len = std::min(fields.name_len, BT_NAME_MAX); + memcpy(record.name, fields.name, copy_len); + record.name[copy_len] = '\0'; + } + } + + { + xSemaphoreTake(ctx->data_mutex, portMAX_DELAY); + bool found = false; + for (size_t i = 0; i < ctx->scan_count; ++i) { + if (memcmp(ctx->scan_results[i].addr, record.addr, BT_ADDR_LEN) == 0) { + // Deduplicate: merge name from SCAN_RSP without clobbering ADV_IND name + if (record.name[0] != '\0') { + memcpy(ctx->scan_results[i].name, record.name, BT_NAME_MAX + 1); + } + ctx->scan_results[i].rssi = record.rssi; + found = true; + break; + } + } + if (!found && ctx->scan_count < BLE_SCAN_MAX) { + ctx->scan_results[ctx->scan_count] = record; + ctx->scan_addrs[ctx->scan_count] = disc.addr; // full addr (type+val) + ctx->scan_count++; + } + xSemaphoreGive(ctx->data_mutex); + } + + struct BtEvent e = {}; + e.type = BT_EVENT_PEER_FOUND; + e.peer = record; + ble_publish_event(ctx, e); + break; + } + + case BLE_GAP_EVENT_DISC_COMPLETE: + LOG_I(TAG, "Scan complete (reason=%d)", event->disc_complete.reason); + // Keep scan_active=true; resolveNextUnnamedPeer clears it and fires ScanFinished + // once name resolution finishes, so the UI spinner stays active throughout. + ble_resolve_next_unnamed_peer(ctx, 0); + break; + + default: + break; + } + return 0; +} + +// ---- GATT Device Name resolution ---- +// +// After a scan completes, briefly connect to each device that didn't include its +// name in advertising data and read Generic Access Device Name (UUID 0x2A00). +// +// Resolution is sequential: connect → read → disconnect → next device. +// Skip resolution if a profile server or HID host connection is active — +// a simultaneous central connection would fail with BLE_HS_EALREADY. + +static int name_read_callback(uint16_t conn_handle, const struct ble_gatt_error* error, + struct ble_gatt_attr* attr, void* arg) { + BleCtx* ctx = g_ctx; + if (ctx == nullptr) return 0; + + if (error->status == 0 && attr != nullptr) { + size_t idx = (size_t)(uintptr_t)arg; + uint16_t len = OS_MBUF_PKTLEN(attr->om); + if (len > 0 && len <= (uint16_t)BT_NAME_MAX) { + char name_buf[BT_NAME_MAX + 1] = {}; + os_mbuf_copydata(attr->om, 0, len, name_buf); + { + xSemaphoreTake(ctx->data_mutex, portMAX_DELAY); + if (idx < ctx->scan_count && ctx->scan_results[idx].name[0] == '\0') { + memcpy(ctx->scan_results[idx].name, name_buf, len); + ctx->scan_results[idx].name[len] = '\0'; + LOG_I(TAG, "Name resolved (idx=%u): %s", (unsigned)idx, name_buf); + } + BtPeerRecord record = (idx < ctx->scan_count) ? ctx->scan_results[idx] : BtPeerRecord{}; + xSemaphoreGive(ctx->data_mutex); + + struct BtEvent e = {}; + e.type = BT_EVENT_PEER_FOUND; + e.peer = record; + ble_publish_event(ctx, e); + } + } + return 0; // wait for BLE_HS_EDONE + } + + // BLE_HS_EDONE, ATT error, or timeout — done with this device + ble_gap_terminate(conn_handle, BLE_ERR_REM_USER_CONN_TERM); + return 0; +} + +static int name_res_gap_callback(struct ble_gap_event* event, void* arg) { + size_t idx = (size_t)(uintptr_t)arg; + BleCtx* ctx = g_ctx; + if (ctx == nullptr) return 0; + + switch (event->type) { + case BLE_GAP_EVENT_CONNECT: + if (event->connect.status == 0) { + LOG_I(TAG, "Name resolution: connected (idx=%u handle=%u)", (unsigned)idx, event->connect.conn_handle); + static const ble_uuid16_t device_name_uuid = BLE_UUID16_INIT(0x2A00); + int rc = ble_gattc_read_by_uuid(event->connect.conn_handle, + 1, 0xFFFF, + &device_name_uuid.u, + name_read_callback, arg); + if (rc != 0) { + LOG_W(TAG, "Name resolution: read_by_uuid failed rc=%d", rc); + ble_gap_terminate(event->connect.conn_handle, BLE_ERR_REM_USER_CONN_TERM); + } + } else { + LOG_I(TAG, "Name resolution: connect failed (idx=%u status=%d)", (unsigned)idx, event->connect.status); + ble_resolve_next_unnamed_peer(ctx, idx + 1); + } + break; + + case BLE_GAP_EVENT_DISCONNECT: + LOG_I(TAG, "Name resolution: disconnected (idx=%u)", (unsigned)idx); + ble_resolve_next_unnamed_peer(ctx, idx + 1); + break; + + default: + break; + } + return 0; +} + +void ble_resolve_next_unnamed_peer(BleCtx* ctx, size_t start_idx) { + // Skip if a profile server or HID host connection attempt is active — + // initiating a central connection simultaneously would fail (BLE_HS_EALREADY). + if (ctx->midi_active.load() || ctx->spp_active.load() || + ctx->hid_active.load() || ctx->hid_host_active.load()) { + LOG_I(TAG, "Name resolution: skipping (server or HID host active)"); + ctx->scan_active.store(false); + struct BtEvent e = {}; + e.type = BT_EVENT_SCAN_FINISHED; + ble_publish_event(ctx, e); + return; + } + + size_t i = start_idx; + while (true) { + ble_addr_t addr = {}; + bool found = false; + { + xSemaphoreTake(ctx->data_mutex, portMAX_DELAY); + while (i < ctx->scan_count) { + if (ctx->scan_results[i].name[0] == '\0') { + addr = ctx->scan_addrs[i]; + found = true; + break; + } + ++i; + } + xSemaphoreGive(ctx->data_mutex); + } + + if (!found) { + LOG_I(TAG, "Name resolution: complete (%u devices)", (unsigned)i); + ctx->scan_active.store(false); + struct BtEvent e = {}; + e.type = BT_EVENT_SCAN_FINISHED; + ble_publish_event(ctx, e); + return; + } + + uint8_t own_addr_type; + ble_hs_id_infer_auto(0, &own_addr_type); + + void* idx_arg = (void*)(uintptr_t)i; + int rc = ble_gap_connect(own_addr_type, &addr, 1500, nullptr, + name_res_gap_callback, idx_arg); + if (rc == 0) { + return; // name_res_gap_callback continues the chain + } + + LOG_I(TAG, "Name resolution: ble_gap_connect failed idx=%u rc=%d, skipping", (unsigned)i, rc); + ++i; + } +} + +#endif // CONFIG_BT_NIMBLE_ENABLED diff --git a/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_spp.cpp b/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_spp.cpp new file mode 100644 index 000000000..83dbb7843 --- /dev/null +++ b/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_spp.cpp @@ -0,0 +1,173 @@ +#ifdef ESP_PLATFORM +#include +#endif + +#if defined(CONFIG_BT_NIMBLE_ENABLED) + +#include + +#include +#include +#include +#include +#include + +#define TAG "esp32_ble_spp" +#include + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wmissing-field-initializers" + +// ---- NUS (Nordic UART Service) UUIDs ---- + +// 6E400001-B5A3-F393-E0A9-E50E24DCCA9E +const ble_uuid128_t NUS_SVC_UUID = BLE_UUID128_INIT( + 0x9E, 0xCA, 0xDC, 0x24, 0x0E, 0xE5, 0xA9, 0xE0, + 0x93, 0xF3, 0xA3, 0xB5, 0x01, 0x00, 0x40, 0x6E +); + +// 6E400002 RX (write from client → device) +static const ble_uuid128_t NUS_RX_UUID = BLE_UUID128_INIT( + 0x9E, 0xCA, 0xDC, 0x24, 0x0E, 0xE5, 0xA9, 0xE0, + 0x93, 0xF3, 0xA3, 0xB5, 0x02, 0x00, 0x40, 0x6E +); + +// 6E400003 TX (notify device → client) +static const ble_uuid128_t NUS_TX_UUID = BLE_UUID128_INIT( + 0x9E, 0xCA, 0xDC, 0x24, 0x0E, 0xE5, 0xA9, 0xE0, + 0x93, 0xF3, 0xA3, 0xB5, 0x03, 0x00, 0x40, 0x6E +); + +uint16_t nus_tx_handle; + +static int nus_chr_access(uint16_t conn_handle, uint16_t attr_handle, + struct ble_gatt_access_ctxt* ctxt, void* arg) { + if (ctxt->op == BLE_GATT_ACCESS_OP_WRITE_CHR) { + uint16_t len = OS_MBUF_PKTLEN(ctxt->om); + LOG_I(TAG, "NUS RX %u bytes", (unsigned)len); + BleCtx* ctx = g_ctx; + if (ctx != nullptr && len > 0) { + std::vector packet(len); + os_mbuf_copydata(ctxt->om, 0, len, packet.data()); + { + xSemaphoreTake(ctx->data_mutex, portMAX_DELAY); + ctx->spp_rx_queue.push_back(std::move(packet)); + while (ctx->spp_rx_queue.size() > 16) ctx->spp_rx_queue.pop_front(); + xSemaphoreGive(ctx->data_mutex); + } + struct BtEvent e = {}; + e.type = BT_EVENT_SPP_DATA_RECEIVED; + ble_publish_event(ctx, e); + } + } + return 0; +} + +const struct ble_gatt_chr_def nus_chars_with_handle[] = { + { + .uuid = &NUS_RX_UUID.u, + .access_cb = nus_chr_access, + .flags = BLE_GATT_CHR_F_WRITE | BLE_GATT_CHR_F_WRITE_NO_RSP, + }, + { + .uuid = &NUS_TX_UUID.u, + .access_cb = nus_chr_access, + .flags = BLE_GATT_CHR_F_NOTIFY, + .val_handle = &nus_tx_handle, + }, + { 0 } +}; + +void ble_spp_init_gatt_handles(BleCtx* /*ctx*/) { + // nus_tx_handle is written by NimBLE via the val_handle pointer above. + // Nothing else needed; the extern variable is accessed directly by esp32_ble.cpp. +} + +// ---- SPP sub-API implementations ---- + +static error_t spp_start(struct Device* device) { + BleCtx* ctx = g_ctx; + if (ctx == nullptr) return ERROR_INVALID_STATE; + ctx->spp_active.store(true); + ble_start_advertising(&NUS_SVC_UUID); + return ERROR_NONE; +} + +error_t ble_spp_start_internal(BleCtx* ctx) { + return spp_start(nullptr); +} + +static error_t spp_stop(struct Device* device) { + BleCtx* ctx = g_ctx; + if (ctx == nullptr) return ERROR_NONE; + ctx->spp_active.store(false); + if (ctx->spp_conn_handle.load() != BLE_HS_CONN_HANDLE_NONE) { + ble_gap_terminate(ctx->spp_conn_handle.load(), BLE_ERR_REM_USER_CONN_TERM); + ctx->spp_conn_handle.store(BLE_HS_CONN_HANDLE_NONE); + } + // Do NOT restart advertising after user-initiated stop — restarting name-only + // advertising causes bonded Windows hosts to auto-reconnect in a tight loop. + if (!ctx->midi_active.load() && !ctx->hid_active.load()) { + ble_gap_adv_stop(); + } + return ERROR_NONE; +} + +static error_t spp_write(struct Device* device, const uint8_t* data, size_t len, size_t* written) { + BleCtx* ctx = g_ctx; + if (ctx == nullptr || ctx->spp_conn_handle.load() == BLE_HS_CONN_HANDLE_NONE) { + if (written) *written = 0; + return ERROR_INVALID_STATE; + } + struct os_mbuf* om = ble_hs_mbuf_from_flat(data, len); + if (om == nullptr) { + if (written) *written = 0; + return ERROR_INVALID_STATE; + } + int rc = ble_gatts_notify_custom(ctx->spp_conn_handle.load(), nus_tx_handle, om); + if (rc != 0) { + os_mbuf_free_chain(om); + if (written) *written = 0; + return ERROR_INVALID_STATE; + } + if (written) *written = len; + return ERROR_NONE; +} + +static error_t spp_read(struct Device* device, uint8_t* data, size_t max_len, size_t* read_out) { + BleCtx* ctx = g_ctx; + if (ctx == nullptr || data == nullptr || max_len == 0) { + if (read_out) *read_out = 0; + return ERROR_NONE; + } + xSemaphoreTake(ctx->data_mutex, portMAX_DELAY); + if (ctx->spp_rx_queue.empty()) { + xSemaphoreGive(ctx->data_mutex); + if (read_out) *read_out = 0; + return ERROR_NONE; + } + auto& front = ctx->spp_rx_queue.front(); + size_t copy_len = std::min(front.size(), max_len); + memcpy(data, front.data(), copy_len); + ctx->spp_rx_queue.pop_front(); + xSemaphoreGive(ctx->data_mutex); + if (read_out) *read_out = copy_len; + return ERROR_NONE; +} + +static bool spp_is_connected(struct Device* device) { + BleCtx* ctx = g_ctx; + return ctx != nullptr && ctx->spp_conn_handle.load() != BLE_HS_CONN_HANDLE_NONE; +} + +const BtSerialApi nimble_serial_api = { + .start = spp_start, + .stop = spp_stop, + .write = spp_write, + .read = spp_read, + .is_connected = spp_is_connected, +}; + +#pragma GCC diagnostic pop + +#endif // CONFIG_BT_NIMBLE_ENABLED diff --git a/Platforms/platform-esp32/source/module.cpp b/Platforms/platform-esp32/source/module.cpp index 84a73aa6c..16235e757 100644 --- a/Platforms/platform-esp32/source/module.cpp +++ b/Platforms/platform-esp32/source/module.cpp @@ -1,4 +1,9 @@ +#ifdef ESP_PLATFORM +#include +#endif + #include +#include #include #include @@ -14,6 +19,9 @@ extern Driver esp32_sdmmc_driver; #endif extern Driver esp32_spi_driver; extern Driver esp32_uart_driver; +#if defined(CONFIG_BT_NIMBLE_ENABLED) +extern Driver esp32_bluetooth_driver; +#endif static error_t start() { /* We crash when construct fails, because if a single driver fails to construct, @@ -26,12 +34,18 @@ static error_t start() { #endif check(driver_construct_add(&esp32_spi_driver) == ERROR_NONE); check(driver_construct_add(&esp32_uart_driver) == ERROR_NONE); +#if defined(CONFIG_BT_NIMBLE_ENABLED) + check(driver_construct_add(&esp32_bluetooth_driver) == ERROR_NONE); +#endif return ERROR_NONE; } static error_t stop() { /* We crash when destruct fails, because if a single driver fails to destruct, * there is no guarantee that the previously destroyed drivers can be recovered */ +#if defined(CONFIG_BT_NIMBLE_ENABLED) + check(driver_remove_destruct(&esp32_bluetooth_driver) == ERROR_NONE); +#endif check(driver_remove_destruct(&esp32_gpio_driver) == ERROR_NONE); check(driver_remove_destruct(&esp32_i2c_driver) == ERROR_NONE); check(driver_remove_destruct(&esp32_i2s_driver) == ERROR_NONE); diff --git a/Tactility/CMakeLists.txt b/Tactility/CMakeLists.txt index 8230b11dc..9d95c370b 100644 --- a/Tactility/CMakeLists.txt +++ b/Tactility/CMakeLists.txt @@ -58,6 +58,8 @@ tactility_add_module(Tactility ) if (DEFINED ENV{ESP_IDF_VERSION}) + idf_component_optional_requires(PRIVATE bt) + if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU") target_compile_options(${COMPONENT_LIB} PUBLIC -Wno-unused-variable) endif () diff --git a/Tactility/Include/Tactility/app/btmanage/BtManage.h b/Tactility/Include/Tactility/app/btmanage/BtManage.h new file mode 100644 index 000000000..9ce9928f6 --- /dev/null +++ b/Tactility/Include/Tactility/app/btmanage/BtManage.h @@ -0,0 +1,9 @@ +#pragma once + +#include + +namespace tt::app::btmanage { + +LaunchId start(); + +} // namespace tt::app::btmanage diff --git a/Tactility/Include/Tactility/bluetooth/Bluetooth.h b/Tactility/Include/Tactility/bluetooth/Bluetooth.h new file mode 100644 index 000000000..630804fe3 --- /dev/null +++ b/Tactility/Include/Tactility/bluetooth/Bluetooth.h @@ -0,0 +1,128 @@ +#pragma once + +#include +#include +#include +#include + +struct Device; + +namespace tt::bluetooth { + +enum class RadioState { + Off, + OnPending, + On, + OffPending, +}; + +struct PeerRecord { + std::array addr; + std::string name; + int8_t rssi; + bool paired; + bool connected; + /** Profile used to pair (BtProfileId value). Only meaningful for paired peers. */ + int profileId = 0; +}; + +/** Find the first ready BLE device in the kernel device registry. Returns nullptr if unavailable. */ +struct Device* findFirstDevice(); + +/** @return the current radio state */ +RadioState getRadioState(); + +/** For logging purposes */ +const char* radioStateToString(RadioState state); + +/** @return the peers found during the last scan */ +std::vector getScanResults(); + +/** @return the list of currently paired peers */ +std::vector getPairedPeers(); + +/** + * @brief Initiate pairing with a peer. + * Returns immediately; result is delivered via kernel event callback (BT_EVENT_PAIR_RESULT). + */ +void pair(const std::array& addr); + +/** + * @brief Remove a previously paired peer. + * @param[in] addr the peer address + */ +void unpair(const std::array& addr); + +/** + * @brief Connect to a peer using the specified profile. + * @param[in] addr the peer address + * @param[in] profileId the BtProfileId value (from bluetooth.h) + */ +void connect(const std::array& addr, int profileId); + +/** + * @brief Disconnect a peer from the specified profile. + * @param[in] addr the peer address + * @param[in] profileId the BtProfileId value (from bluetooth.h) + */ +void disconnect(const std::array& addr, int profileId); + +/** + * @brief Check whether a given profile is supported on this build/SOC. + * @param[in] profileId the BtProfileId value to query + * @return true when the profile is available + */ +bool isProfileSupported(int profileId); + +// ---- BLE HID Host (central role — connect to external BLE keyboard/mouse) ---- + +/** + * @brief Connect to a remote BLE HID device (keyboard, mouse, etc.) as a host. + * Discovery, CCCD subscription, and LVGL indev registration happen automatically. + * @param[in] addr 6-byte BLE address of the HID peripheral + */ +void hidHostConnect(const std::array& addr); + +/** @brief Disconnect from the currently connected BLE HID device. */ +void hidHostDisconnect(); + +/** @return true when a BLE HID peripheral is fully subscribed and acting as LVGL input device */ +bool hidHostIsConnected(); + +// ---- BLE HID Device ---- + +/** + * @brief Start advertising as a BLE HID device. + * @param[in] appearance BLE GAP Appearance value: + * 0x03C0=Generic HID, 0x03C1=Keyboard (default), 0x03C2=Mouse, 0x03C4=Gamepad + * @return true on success + */ +bool hidDeviceStart(uint16_t appearance = 0x03C1); + +/** @brief Stop the HID device server and close any active connection. */ +void hidDeviceStop(); + +// ---- BLE SPP (Nordic UART Service) ---- + +/** @brief Start advertising as a NUS (Nordic UART Service) server. @return true on success */ +bool sppStart(); + +/** @brief Stop the SPP server and close any active connection. */ +void sppStop(); + +// ---- BLE MIDI ---- + +/** @brief Start advertising as a BLE MIDI device. @return true on success */ +bool midiStart(); + +/** @brief Stop the MIDI server and close any active connection. */ +void midiStop(); + +/** + * @brief Initialize the Bluetooth bridge layer and optionally enable the radio. + * Called once from Tactility startup (after kernel drivers are ready). + * Reads settings and enables the radio if configured to auto-start. + */ +void systemStart(); + +} // namespace tt::bluetooth diff --git a/Tactility/Include/Tactility/bluetooth/BluetoothPairedDevice.h b/Tactility/Include/Tactility/bluetooth/BluetoothPairedDevice.h new file mode 100644 index 000000000..e4bbf5239 --- /dev/null +++ b/Tactility/Include/Tactility/bluetooth/BluetoothPairedDevice.h @@ -0,0 +1,30 @@ +#pragma once + +#include +#include +#include +#include + +namespace tt::bluetooth::settings { + +struct PairedDevice { + std::string name; + std::array addr; + bool autoConnect = false; + /** Profile used to pair (BtProfileId value). Defaults to BT_PROFILE_SPP=2. */ + int profileId = 2; +}; + +std::string addrToHex(const std::array& addr); + +bool contains(const std::string& addr_hex); + +bool load(const std::string& addr_hex, PairedDevice& device); + +bool save(const PairedDevice& device); + +bool remove(const std::string& addr_hex); + +std::vector loadAll(); + +} // namespace tt::bluetooth::settings diff --git a/Tactility/Include/Tactility/bluetooth/BluetoothSettings.h b/Tactility/Include/Tactility/bluetooth/BluetoothSettings.h new file mode 100644 index 000000000..e0656f4be --- /dev/null +++ b/Tactility/Include/Tactility/bluetooth/BluetoothSettings.h @@ -0,0 +1,14 @@ +#pragma once + +namespace tt::bluetooth::settings { + +void setEnableOnBoot(bool enable); +bool shouldEnableOnBoot(); + +void setSppAutoStart(bool enable); +bool shouldSppAutoStart(); + +void setMidiAutoStart(bool enable); +bool shouldMidiAutoStart(); + +} // namespace tt::bluetooth::settings diff --git a/Tactility/Private/Tactility/app/btmanage/Bindings.h b/Tactility/Private/Tactility/app/btmanage/Bindings.h new file mode 100644 index 000000000..4ca3668c9 --- /dev/null +++ b/Tactility/Private/Tactility/app/btmanage/Bindings.h @@ -0,0 +1,24 @@ +#pragma once + +#include +#include + +namespace tt::app::btmanage { + +typedef void (*OnBtToggled)(bool enable); +typedef void (*OnScanToggled)(bool enable); +typedef void (*OnConnectPeer)(const std::array& addr, int profileId); +typedef void (*OnDisconnectPeer)(const std::array& addr, int profileId); +typedef void (*OnPairPeer)(const std::array& addr); +typedef void (*OnForgetPeer)(const std::array& addr); + +struct Bindings { + OnBtToggled onBtToggled; + OnScanToggled onScanToggled; + OnConnectPeer onConnectPeer; + OnDisconnectPeer onDisconnectPeer; + OnPairPeer onPairPeer; + OnForgetPeer onForgetPeer; +}; + +} // namespace tt::app::btmanage diff --git a/Tactility/Private/Tactility/app/btmanage/BtManagePrivate.h b/Tactility/Private/Tactility/app/btmanage/BtManagePrivate.h new file mode 100644 index 000000000..2e03793ce --- /dev/null +++ b/Tactility/Private/Tactility/app/btmanage/BtManagePrivate.h @@ -0,0 +1,40 @@ +#pragma once + +#include "./View.h" +#include "./State.h" + +#include +#include +#include +#include + +namespace tt::app::btmanage { + +class BtManage final : public App { + + Mutex mutex; + Bindings bindings = { }; + State state; + View view = View(&bindings, &state); + bool isViewEnabled = false; + struct Device* btDevice = nullptr; + +public: + + void onBtEvent(const struct BtEvent& event); + + BtManage(); + + void lock(); + void unlock(); + + void onShow(AppContext& app, lv_obj_t* parent) override; + void onHide(AppContext& app) override; + + Bindings& getBindings() { return bindings; } + State& getState() { return state; } + + void requestViewUpdate(); +}; + +} // namespace tt::app::btmanage diff --git a/Tactility/Private/Tactility/app/btmanage/State.h b/Tactility/Private/Tactility/app/btmanage/State.h new file mode 100644 index 000000000..a9456583d --- /dev/null +++ b/Tactility/Private/Tactility/app/btmanage/State.h @@ -0,0 +1,41 @@ +#pragma once + +#include +#include + +namespace tt::app::btmanage { + +class State final { + + mutable RecursiveMutex mutex; + bool scanning = false; + bluetooth::RadioState radioState = bluetooth::RadioState::Off; + std::vector scanResults; + std::vector pairedPeers; + +public: + State() = default; + + void setScanning(bool isScanning); + bool isScanning() const; + + void setRadioState(bluetooth::RadioState state); + bluetooth::RadioState getRadioState() const; + + void updateScanResults(); + void updatePairedPeers(); + + std::vector getScanResults() const { + auto lock = mutex.asScopedLock(); + lock.lock(); + return scanResults; + } + + std::vector getPairedPeers() const { + auto lock = mutex.asScopedLock(); + lock.lock(); + return pairedPeers; + } +}; + +} // namespace tt::app::btmanage diff --git a/Tactility/Private/Tactility/app/btmanage/View.h b/Tactility/Private/Tactility/app/btmanage/View.h new file mode 100644 index 000000000..7e2f9e56f --- /dev/null +++ b/Tactility/Private/Tactility/app/btmanage/View.h @@ -0,0 +1,41 @@ +#pragma once + +#include "./Bindings.h" +#include "./State.h" + +#include +#include + +#include + +namespace tt::app::btmanage { + +class View final { + + Bindings* bindings; + State* state; + std::unique_ptr paths; + lv_obj_t* root = nullptr; + lv_obj_t* enable_switch = nullptr; + lv_obj_t* enable_on_boot_switch = nullptr; + lv_obj_t* scanning_spinner = nullptr; + lv_obj_t* peers_list = nullptr; + + void updateBtToggle(); + void updateEnableOnBootToggle(); + void updateScanning(); + void updatePeerList(); + + void createPeerListItem(const bluetooth::PeerRecord& record, bool isPaired, size_t index); + + static void onConnect(lv_event_t* event); + +public: + + View(Bindings* bindings, State* state) : bindings(bindings), state(state) {} + + void init(const AppContext& app, lv_obj_t* parent); + void update(); +}; + +} // namespace tt::app::btmanage diff --git a/Tactility/Private/Tactility/app/btpeersettings/BtPeerSettings.h b/Tactility/Private/Tactility/app/btpeersettings/BtPeerSettings.h new file mode 100644 index 000000000..69a4a425c --- /dev/null +++ b/Tactility/Private/Tactility/app/btpeersettings/BtPeerSettings.h @@ -0,0 +1,9 @@ +#pragma once + +#include + +namespace tt::app::btpeersettings { + +void start(const std::string& addrHex); + +} // namespace tt::app::btpeersettings diff --git a/Tactility/Private/Tactility/bluetooth/BluetoothPrivate.h b/Tactility/Private/Tactility/bluetooth/BluetoothPrivate.h new file mode 100644 index 000000000..ec082066a --- /dev/null +++ b/Tactility/Private/Tactility/bluetooth/BluetoothPrivate.h @@ -0,0 +1,29 @@ +#pragma once + +#include + +#include + +struct Device; + +namespace tt::bluetooth { + +/** Cache a BLE address and its type from a scan result (used by HID host for addr_type lookup). */ +void cacheScanAddr(const uint8_t addr[6], uint8_t addr_type); + +/** + * Look up the ble_addr_t (including address type) for a peer address in the last scan. + * Returns false and fills type=0 (PUBLIC) if not found. + */ +bool getCachedScanAddrType(const uint8_t addr[6], uint8_t* addr_type_out); + +/** Called from BluetoothHidHost.cpp to trigger auto-connect check after scan finishes. */ +void autoConnectHidHost(); + +/** + * Returns the BLE address of the currently fully-connected HID host peer (i.e. + * all CCCDs subscribed and ready). Returns false if no HID host peer is connected. + */ +bool hidHostGetConnectedPeer(std::array& addr_out); + +} // namespace tt::bluetooth diff --git a/Tactility/Source/Tactility.cpp b/Tactility/Source/Tactility.cpp index 52ff1ef4f..64b6ebab1 100644 --- a/Tactility/Source/Tactility.cpp +++ b/Tactility/Source/Tactility.cpp @@ -38,6 +38,8 @@ #include #endif +#include + namespace tt { static auto LOGGER = Logger("Tactility"); @@ -105,6 +107,8 @@ namespace app { namespace timedatesettings { extern const AppManifest manifest; } namespace timezone { extern const AppManifest manifest; } namespace usbsettings { extern const AppManifest manifest; } + namespace btmanage { extern const AppManifest manifest; } + namespace btpeersettings { extern const AppManifest manifest; } namespace wifiapsettings { extern const AppManifest manifest; } namespace wificonnect { extern const AppManifest manifest; } namespace wifimanage { extern const AppManifest manifest; } @@ -154,6 +158,8 @@ static void registerInternalApps() { addAppManifest(app::systeminfo::manifest); addAppManifest(app::timedatesettings::manifest); addAppManifest(app::timezone::manifest); + addAppManifest(app::btmanage::manifest); + addAppManifest(app::btpeersettings::manifest); addAppManifest(app::wifiapsettings::manifest); addAppManifest(app::wificonnect::manifest); addAppManifest(app::wifimanage::manifest); @@ -333,6 +339,7 @@ void run(const Configuration& config, Module* dtsModules[], DtsDevice dtsDevices settings::initTimeZone(); hal::init(*config.hardware); network::ntp::init(); + bluetooth::systemStart(); registerAndStartPrimaryServices(); diff --git a/Tactility/Source/app/btmanage/BtManage.cpp b/Tactility/Source/app/btmanage/BtManage.cpp new file mode 100644 index 000000000..2c774cfad --- /dev/null +++ b/Tactility/Source/app/btmanage/BtManage.cpp @@ -0,0 +1,181 @@ +#include +#include + +#include +#include +#include +#include +#include + +#include + +namespace tt::app::btmanage { + +static const auto LOGGER = Logger("BtManage"); + +extern const AppManifest manifest; + +static void onBtToggled(bool enabled) { + struct Device* dev = bluetooth::findFirstDevice(); + if (!dev) return; + bluetooth_set_radio_enabled(dev, enabled); +} + +static void onScanToggled(bool enabled) { + struct Device* dev = bluetooth::findFirstDevice(); + if (!dev) return; + if (enabled) { + bluetooth_scan_start(dev); + } else { + bluetooth_scan_stop(dev); + } +} + +static void onConnectPeer(const std::array& addr, int profileId) { + bluetooth::connect(addr, profileId); +} + +static void onDisconnectPeer(const std::array& addr, int profileId) { + bluetooth::disconnect(addr, profileId); +} + +static void onPairPeer(const std::array& addr) { + // Clicking an unrecognised scan result initiates a HID host connection. + // Bond exchange happens automatically during the first connection. + bluetooth::hidHostConnect(addr); +} + +static void onForgetPeer(const std::array& addr) { + bluetooth::unpair(addr); +} + +BtManage::BtManage() { + bindings = (Bindings) { + .onBtToggled = onBtToggled, + .onScanToggled = onScanToggled, + .onConnectPeer = onConnectPeer, + .onDisconnectPeer = onDisconnectPeer, + .onPairPeer = onPairPeer, + .onForgetPeer = onForgetPeer, + }; +} + +void BtManage::lock() { + mutex.lock(); +} + +void BtManage::unlock() { + mutex.unlock(); +} + +void BtManage::requestViewUpdate() { + lock(); + if (isViewEnabled) { + if (lvgl::lock(1000)) { + view.update(); + lvgl::unlock(); + } else { + LOGGER.error(LOG_MESSAGE_MUTEX_LOCK_FAILED_FMT, "LVGL"); + } + } + unlock(); +} + +void BtManage::onBtEvent(const struct BtEvent& event) { + auto radio_state = bluetooth::getRadioState(); + LOGGER.info("Update with state {}", bluetooth::radioStateToString(radio_state)); + getState().setRadioState(radio_state); + switch (event.type) { + case BT_EVENT_SCAN_STARTED: + getState().setScanning(true); + break; + case BT_EVENT_SCAN_FINISHED: + getState().setScanning(false); + getState().updateScanResults(); + getState().updatePairedPeers(); + break; + case BT_EVENT_PEER_FOUND: + getState().updateScanResults(); + break; + case BT_EVENT_PAIR_RESULT: + getState().updatePairedPeers(); + break; + case BT_EVENT_PROFILE_STATE_CHANGED: + getState().updateScanResults(); + getState().updatePairedPeers(); + break; + case BT_EVENT_RADIO_STATE_CHANGED: + if (event.radio_state == BT_RADIO_STATE_ON) { + getState().updatePairedPeers(); + struct Device* dev = bluetooth::findFirstDevice(); + if (dev && !bluetooth_is_scanning(dev)) { + bluetooth_scan_start(dev); + } + } + break; + default: + break; + } + + requestViewUpdate(); +} + +static void onKernelBtEvent(struct Device* /*device*/, void* context, struct BtEvent event) { + auto* self = static_cast(context); + self->onBtEvent(event); +} + +void BtManage::onShow(AppContext& app, lv_obj_t* parent) { + // Initialise state and view before subscribing to avoid incoming events + // racing with state initialisation. + state.setRadioState(bluetooth::getRadioState()); + struct Device* dev = bluetooth::findFirstDevice(); + state.setScanning(dev ? bluetooth_is_scanning(dev) : false); + state.updateScanResults(); + state.updatePairedPeers(); + + lock(); + isViewEnabled = true; + view.init(app, parent); + view.update(); + unlock(); + + btDevice = dev; + if (btDevice) { + bluetooth_add_event_callback(btDevice, this, onKernelBtEvent); + } + + auto radio_state = bluetooth::getRadioState(); + bool can_scan = radio_state == bluetooth::RadioState::On; + LOGGER.info("Radio: {}, Scanning: {}, Can scan: {}", + bluetooth::radioStateToString(radio_state), + dev ? bluetooth_is_scanning(dev) : false, + can_scan); + if (can_scan && dev && !bluetooth_is_scanning(dev)) { + bluetooth_scan_start(dev); + } +} + +void BtManage::onHide(AppContext& app) { + lock(); + if (btDevice) { + bluetooth_remove_event_callback(btDevice, onKernelBtEvent); + btDevice = nullptr; + } + isViewEnabled = false; + unlock(); +} + +extern const AppManifest manifest = { + .appId = "BtManage", + .appName = "Bluetooth", + .appIcon = LVGL_ICON_SHARED_BLUETOOTH, + .appCategory = Category::Settings, + .createApp = create +}; + +LaunchId start() { + return app::start(manifest.appId); +} + +} // namespace tt::app::btmanage diff --git a/Tactility/Source/app/btmanage/State.cpp b/Tactility/Source/app/btmanage/State.cpp new file mode 100644 index 000000000..5f2ff2db6 --- /dev/null +++ b/Tactility/Source/app/btmanage/State.cpp @@ -0,0 +1,44 @@ +#include + +namespace tt::app::btmanage { + +void State::setScanning(bool isScanning) { + auto lock = mutex.asScopedLock(); + lock.lock(); + scanning = isScanning; +} + +bool State::isScanning() const { + auto lock = mutex.asScopedLock(); + lock.lock(); + return scanning; +} + +void State::setRadioState(bluetooth::RadioState s) { + auto lock = mutex.asScopedLock(); + lock.lock(); + radioState = s; +} + +bluetooth::RadioState State::getRadioState() const { + auto lock = mutex.asScopedLock(); + lock.lock(); + return radioState; +} + +void State::updateScanResults() { + // Fetch outside the lock to avoid holding it during a service call. + auto results = bluetooth::getScanResults(); + auto lock = mutex.asScopedLock(); + lock.lock(); + scanResults = std::move(results); +} + +void State::updatePairedPeers() { + auto peers = bluetooth::getPairedPeers(); + auto lock = mutex.asScopedLock(); + lock.lock(); + pairedPeers = std::move(peers); +} + +} // namespace tt::app::btmanage diff --git a/Tactility/Source/app/btmanage/View.cpp b/Tactility/Source/app/btmanage/View.cpp new file mode 100644 index 000000000..99b18310d --- /dev/null +++ b/Tactility/Source/app/btmanage/View.cpp @@ -0,0 +1,246 @@ +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace tt::app::btmanage { + +static const auto LOGGER = Logger("BtManageView"); + +static void onEnableSwitchChanged(lv_event_t* event) { + auto* enable_switch = static_cast(lv_event_get_target(event)); + bool is_on = lv_obj_has_state(enable_switch, LV_STATE_CHECKED); + auto bt = std::static_pointer_cast(getCurrentApp()); + bt->getBindings().onBtToggled(is_on); +} + +static void onEnableOnBootSwitchChanged(lv_event_t* event) { + auto* enable_switch = static_cast(lv_event_get_target(event)); + bool is_on = lv_obj_has_state(enable_switch, LV_STATE_CHECKED); + getMainDispatcher().dispatch([is_on] { + bluetooth::settings::setEnableOnBoot(is_on); + }); +} + +static void onEnableOnBootParentClicked(lv_event_t* event) { + auto* enable_switch = static_cast(lv_event_get_user_data(event)); + bool new_state = !lv_obj_has_state(enable_switch, LV_STATE_CHECKED); + if (new_state) { + lv_obj_add_state(enable_switch, LV_STATE_CHECKED); + } else { + lv_obj_remove_state(enable_switch, LV_STATE_CHECKED); + } + // add/remove_state does not fire LV_EVENT_VALUE_CHANGED, so persist here directly. + getMainDispatcher().dispatch([new_state] { + bluetooth::settings::setEnableOnBoot(new_state); + }); +} + +static void onScanButtonClicked(lv_event_t* event) { + auto bt = std::static_pointer_cast(getCurrentApp()); + struct Device* dev = bluetooth::findFirstDevice(); + bool scanning = dev ? bluetooth_is_scanning(dev) : false; + bt->getBindings().onScanToggled(!scanning); +} + +// region Peer list callbacks + +struct PeerListItemData { + View* view; + size_t index; + bool isPaired; +}; + +void View::onConnect(lv_event_t* event) { + auto* data = static_cast(lv_event_get_user_data(event)); + auto bt = std::static_pointer_cast(getCurrentApp()); + auto& state = bt->getState(); + + if (data->isPaired) { + // Open the per-device settings screen for paired devices + auto peers = state.getPairedPeers(); + if (data->index < peers.size()) { + btpeersettings::start(bluetooth::settings::addrToHex(peers[data->index].addr)); + } + } else { + // Unrecognised scan result — initiate pairing + auto peers = state.getScanResults(); + if (data->index < peers.size()) { + bt->getBindings().onPairPeer(peers[data->index].addr); + } + } +} + +// endregion Peer list callbacks + +static uint8_t mapRssiToPercentage(int8_t rssi) { + auto abs_rssi = std::abs(rssi); + if (abs_rssi < 30) abs_rssi = 30; + if (abs_rssi > 90) abs_rssi = 90; + return static_cast((float)(90 - abs_rssi) / 60.f * 100.f); +} + +void View::createPeerListItem(const bluetooth::PeerRecord& record, bool isPaired, size_t index) { + const auto percentage = mapRssiToPercentage(record.rssi); + const auto label = record.name.empty() + ? std::format("Unknown ({:02x}{:02x}{:02x}{:02x}{:02x}{:02x}) {}%", + record.addr[0], record.addr[1], record.addr[2], + record.addr[3], record.addr[4], record.addr[5], + percentage) + : std::format("{} {}%", record.name, percentage); + + auto* button = lv_list_add_button(peers_list, nullptr, label.c_str()); + + auto* item_data = new PeerListItemData { this, index, isPaired }; + lv_obj_set_user_data(button, item_data); + lv_obj_add_event_cb(button, onConnect, LV_EVENT_SHORT_CLICKED, item_data); + lv_obj_add_event_cb(button, [](lv_event_t* e) { + delete static_cast(lv_obj_get_user_data(lv_event_get_current_target_obj(e))); + }, LV_EVENT_DELETE, nullptr); +} + +// region Secondary updates + +void View::updateBtToggle() { + lv_obj_clear_state(enable_switch, LV_STATE_ANY); + switch (state->getRadioState()) { + using enum bluetooth::RadioState; + case On: + lv_obj_add_state(enable_switch, LV_STATE_CHECKED); + break; + case OnPending: + lv_obj_add_state(enable_switch, LV_STATE_CHECKED); + lv_obj_add_state(enable_switch, LV_STATE_DISABLED); + break; + case Off: + lv_obj_remove_state(enable_switch, LV_STATE_CHECKED); + lv_obj_remove_state(enable_switch, LV_STATE_DISABLED); + break; + case OffPending: + lv_obj_remove_state(enable_switch, LV_STATE_CHECKED); + lv_obj_add_state(enable_switch, LV_STATE_DISABLED); + break; + } +} + +void View::updateEnableOnBootToggle() { + if (enable_on_boot_switch != nullptr) { + lv_obj_clear_state(enable_on_boot_switch, LV_STATE_ANY); + if (bluetooth::settings::shouldEnableOnBoot()) { + lv_obj_add_state(enable_on_boot_switch, LV_STATE_CHECKED); + } else { + lv_obj_remove_state(enable_on_boot_switch, LV_STATE_CHECKED); + } + } +} + +void View::updateScanning() { + if (state->getRadioState() == bluetooth::RadioState::On && state->isScanning()) { + lv_obj_remove_flag(scanning_spinner, LV_OBJ_FLAG_HIDDEN); + } else { + lv_obj_add_flag(scanning_spinner, LV_OBJ_FLAG_HIDDEN); + } +} + +void View::updatePeerList() { + lv_obj_clean(peers_list); + + // Enable on boot row + auto* enable_on_boot_wrapper = lv_obj_create(peers_list); + lv_obj_set_size(enable_on_boot_wrapper, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_style_pad_all(enable_on_boot_wrapper, 0, LV_STATE_DEFAULT); + lv_obj_set_style_border_width(enable_on_boot_wrapper, 0, LV_STATE_DEFAULT); + + auto* enable_label = lv_label_create(enable_on_boot_wrapper); + lv_label_set_text(enable_label, "Enable on boot"); + lv_obj_align(enable_label, LV_ALIGN_LEFT_MID, 0, 0); + + enable_on_boot_switch = lv_switch_create(enable_on_boot_wrapper); + lv_obj_align(enable_on_boot_switch, LV_ALIGN_RIGHT_MID, 0, 0); + lv_obj_add_event_cb(enable_on_boot_switch, onEnableOnBootSwitchChanged, LV_EVENT_VALUE_CHANGED, nullptr); + lv_obj_add_event_cb(enable_on_boot_wrapper, onEnableOnBootParentClicked, LV_EVENT_SHORT_CLICKED, enable_on_boot_switch); + + if (lvgl_get_ui_density() == LVGL_UI_DENSITY_COMPACT) { + lv_obj_set_style_pad_ver(enable_on_boot_wrapper, 2, LV_STATE_DEFAULT); + } else { + lv_obj_set_style_pad_ver(enable_on_boot_wrapper, 8, LV_STATE_DEFAULT); + } + + updateEnableOnBootToggle(); + + using enum bluetooth::RadioState; + if (state->getRadioState() == On) { + // Paired peers section + auto paired = state->getPairedPeers(); + if (!paired.empty()) { + lv_list_add_text(peers_list, "Paired"); + for (size_t i = 0; i < paired.size(); ++i) { + createPeerListItem(paired[i], true, i); + } + } + + // Scan results section + auto scan_results = state->getScanResults(); + lv_list_add_text(peers_list, "Available"); + if (!scan_results.empty()) { + for (size_t i = 0; i < scan_results.size(); ++i) { + createPeerListItem(scan_results[i], false, i); + } + } else if (!state->isScanning()) { + auto* no_devices_label = lv_label_create(peers_list); + lv_label_set_text(no_devices_label, "No devices found."); + } + // Never hide peers_list: it always contains the "Enable on boot" row. + // While scanning with no results the spinner in the toolbar provides feedback. + + // Scan button + auto* scan_button = lv_button_create(peers_list); + lv_obj_set_width(scan_button, LV_PCT(100)); + lv_obj_set_style_margin_ver(scan_button, 4, LV_STATE_DEFAULT); + auto* scan_label = lv_label_create(scan_button); + lv_label_set_text(scan_label, state->isScanning() ? "Stop scan" : "Scan"); + lv_obj_add_event_cb(scan_button, onScanButtonClicked, LV_EVENT_SHORT_CLICKED, nullptr); + } +} + +// endregion Secondary updates + +void View::init(const AppContext& app, lv_obj_t* parent) { + lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT); + + root = parent; + paths = app.getPaths(); + + // Toolbar + auto* toolbar = lvgl::toolbar_create(parent, app); + + scanning_spinner = lvgl::toolbar_add_spinner_action(toolbar); + + enable_switch = lvgl::toolbar_add_switch_action(toolbar); + lv_obj_add_event_cb(enable_switch, onEnableSwitchChanged, LV_EVENT_VALUE_CHANGED, nullptr); + + // Peer list + peers_list = lv_list_create(parent); + lv_obj_set_flex_grow(peers_list, 1); + lv_obj_set_width(peers_list, LV_PCT(100)); +} + +void View::update() { + updateBtToggle(); + updateScanning(); + updatePeerList(); +} + +} // namespace tt::app::btmanage diff --git a/Tactility/Source/app/btpeersettings/BtPeerSettings.cpp b/Tactility/Source/app/btpeersettings/BtPeerSettings.cpp new file mode 100644 index 000000000..3643abe9a --- /dev/null +++ b/Tactility/Source/app/btpeersettings/BtPeerSettings.cpp @@ -0,0 +1,227 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace tt::app::btpeersettings { + +static const auto LOGGER = Logger("BtPeerSettings"); + +extern const AppManifest manifest; + +void start(const std::string& addrHex) { + auto bundle = std::make_shared(); + bundle->putString("addr", addrHex); + app::start(manifest.appId, bundle); +} + +class BtPeerSettings : public App { + + bool viewEnabled = false; + lv_obj_t* connectButton = nullptr; + lv_obj_t* disconnectButton = nullptr; + std::string addrHex; + std::array addr = {}; + int profileId = BT_PROFILE_HID_HOST; + bool isCurrentlyConnected() const { + for (const auto& p : bluetooth::getPairedPeers()) { + if (p.addr == addr) return p.connected; + } + return false; + } + + static void onPressConnect(lv_event_t* event) { + auto* self = static_cast(lv_event_get_user_data(event)); + if (self->profileId == BT_PROFILE_HID_HOST) { + bluetooth::hidHostConnect(self->addr); + } else { + bluetooth::connect(self->addr, self->profileId); + } + lv_obj_add_state(lv_event_get_target_obj(event), LV_STATE_DISABLED); + } + + static void onPressDisconnect(lv_event_t* event) { + auto* self = static_cast(lv_event_get_user_data(event)); + if (self->profileId == BT_PROFILE_HID_HOST) { + bluetooth::hidHostDisconnect(); + } else { + bluetooth::disconnect(self->addr, self->profileId); + } + lv_obj_add_state(lv_event_get_target_obj(event), LV_STATE_DISABLED); + } + + static void onPressForget(lv_event_t* event) { + std::vector choices = { "Yes", "No" }; + alertdialog::start("Confirmation", "Forget this device?", choices); + } + + static void onToggleAutoConnect(lv_event_t* event) { + auto* self = static_cast(lv_event_get_user_data(event)); + bool is_on = lv_obj_has_state(lv_event_get_target_obj(event), LV_STATE_CHECKED); + bluetooth::settings::PairedDevice device; + if (bluetooth::settings::load(self->addrHex, device)) { + device.autoConnect = is_on; + if (!bluetooth::settings::save(device)) { + LOGGER.error("Failed to save auto-connect setting"); + } + } + } + + void requestViewUpdate() const { + if (viewEnabled) { + if (lvgl::lock(1000)) { + updateViews(); + lvgl::unlock(); + } else { + LOGGER.error(LOG_MESSAGE_MUTEX_LOCK_FAILED_FMT, "LVGL"); + } + } + } + + void updateViews() const { + if (isCurrentlyConnected()) { + lv_obj_remove_flag(disconnectButton, LV_OBJ_FLAG_HIDDEN); + lv_obj_add_flag(connectButton, LV_OBJ_FLAG_HIDDEN); + lv_obj_remove_state(disconnectButton, LV_STATE_DISABLED); + } else { + lv_obj_add_flag(disconnectButton, LV_OBJ_FLAG_HIDDEN); + lv_obj_remove_flag(connectButton, LV_OBJ_FLAG_HIDDEN); + lv_obj_remove_state(connectButton, LV_STATE_DISABLED); + } + } + +public: + + void onCreate(AppContext& app) override { + const auto parameters = app.getParameters(); + check(parameters != nullptr, "Parameters missing"); + addrHex = parameters->getString("addr"); + + // Load addr and profileId from stored settings — avoids manual hex parsing + // (std::stoul throws on invalid input and exceptions are disabled). + bluetooth::settings::PairedDevice device; + if (bluetooth::settings::load(addrHex, device)) { + addr = device.addr; + profileId = device.profileId; + } + } + + static void onKernelBtEvent(struct Device* /*device*/, void* context, struct BtEvent /*event*/) { + static_cast(context)->requestViewUpdate(); + } + + void onShow(AppContext& app, lv_obj_t* parent) override { + if (struct Device* dev = bluetooth::findFirstDevice()) { + bluetooth_add_event_callback(dev, this, onKernelBtEvent); + } + + // Load stored settings (name, autoConnect) + bluetooth::settings::PairedDevice device; + bool deviceLoaded = bluetooth::settings::load(addrHex, device); + std::string title = (deviceLoaded && !device.name.empty()) ? device.name : addrHex; + + lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT); + + lvgl::toolbar_create(parent, title); + + auto* wrapper = lv_obj_create(parent); + lv_obj_set_width(wrapper, LV_PCT(100)); + lv_obj_set_flex_grow(wrapper, 1); + lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_border_width(wrapper, 0, LV_STATE_DEFAULT); + lvgl::obj_set_style_bg_invisible(wrapper); + + connectButton = lv_button_create(wrapper); + lv_obj_set_width(connectButton, LV_PCT(100)); + lv_obj_add_event_cb(connectButton, onPressConnect, LV_EVENT_SHORT_CLICKED, this); + auto* connect_label = lv_label_create(connectButton); + lv_obj_align(connect_label, LV_ALIGN_CENTER, 0, 0); + lv_label_set_text(connect_label, "Connect"); + + disconnectButton = lv_button_create(wrapper); + lv_obj_set_width(disconnectButton, LV_PCT(100)); + lv_obj_add_event_cb(disconnectButton, onPressDisconnect, LV_EVENT_SHORT_CLICKED, this); + auto* disconnect_label = lv_label_create(disconnectButton); + lv_obj_align(disconnect_label, LV_ALIGN_CENTER, 0, 0); + lv_label_set_text(disconnect_label, "Disconnect"); + + auto* forget_button = lv_button_create(wrapper); + lv_obj_set_width(forget_button, LV_PCT(100)); + lv_obj_add_event_cb(forget_button, onPressForget, LV_EVENT_SHORT_CLICKED, this); + auto* forget_label = lv_label_create(forget_button); + lv_obj_align(forget_label, LV_ALIGN_CENTER, 0, 0); + lv_label_set_text(forget_label, "Forget"); + + // Auto-connect toggle row + auto* auto_connect_wrapper = lv_obj_create(wrapper); + lv_obj_set_size(auto_connect_wrapper, LV_PCT(100), LV_SIZE_CONTENT); + lvgl::obj_set_style_bg_invisible(auto_connect_wrapper); + lv_obj_set_style_pad_all(auto_connect_wrapper, 0, LV_STATE_DEFAULT); + lv_obj_set_style_border_width(auto_connect_wrapper, 0, LV_STATE_DEFAULT); + + auto* auto_connect_label = lv_label_create(auto_connect_wrapper); + lv_label_set_text(auto_connect_label, "Auto-connect"); + lv_obj_align(auto_connect_label, LV_ALIGN_LEFT_MID, 0, 0); + + auto* auto_connect_switch = lv_switch_create(auto_connect_wrapper); + lv_obj_add_event_cb(auto_connect_switch, onToggleAutoConnect, LV_EVENT_VALUE_CHANGED, this); + lv_obj_align(auto_connect_switch, LV_ALIGN_RIGHT_MID, 0, 0); + + if (deviceLoaded && device.autoConnect) { + lv_obj_add_state(auto_connect_switch, LV_STATE_CHECKED); + } else { + lv_obj_remove_state(auto_connect_switch, LV_STATE_CHECKED); + } + + viewEnabled = true; + updateViews(); + } + + void onHide(AppContext& app) override { + if (struct Device* dev = bluetooth::findFirstDevice()) { + bluetooth_remove_event_callback(dev, onKernelBtEvent); + } + viewEnabled = false; + } + + void onResult(AppContext& appContext, LaunchId /*launchId*/, Result result, std::unique_ptr bundle) override { + if (result != Result::Ok || bundle == nullptr) return; + if (alertdialog::getResultIndex(*bundle) != 0) return; // 0 = Yes + + // Disconnect first if connected + if (isCurrentlyConnected()) { + if (profileId == BT_PROFILE_HID_HOST) { + bluetooth::hidHostDisconnect(); + } else { + bluetooth::disconnect(addr, profileId); + } + } + + bluetooth::unpair(addr); + stop(); + } +}; + +extern const AppManifest manifest = { + .appId = "BtPeerSettings", + .appName = "BT Device Settings", + .appCategory = Category::System, + .appFlags = AppManifest::Flags::Hidden, + .createApp = create +}; + +} // namespace tt::app::btpeersettings diff --git a/Tactility/Source/bluetooth/Bluetooth.cpp b/Tactility/Source/bluetooth/Bluetooth.cpp new file mode 100644 index 000000000..4fe2aad8f --- /dev/null +++ b/Tactility/Source/bluetooth/Bluetooth.cpp @@ -0,0 +1,375 @@ +#ifdef ESP_PLATFORM +#include +#endif + +#if defined(CONFIG_BT_NIMBLE_ENABLED) + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace tt::bluetooth { + +static const auto LOGGER = Logger("Bluetooth"); + +// ---- Scan result cache (C++ PeerRecord list, updated from BT_EVENT_PEER_FOUND) ---- + +static Mutex scan_cache_mutex; +static std::vector scan_results_cache; + +struct CachedAddr { + uint8_t addr[6]; + uint8_t addr_type; +}; +static std::vector scan_addr_cache; // parallel to scan_results_cache + +// ---- Device accessor ---- + +struct Device* findFirstDevice() { + struct Device* found = nullptr; + device_for_each_of_type(&BLUETOOTH_TYPE, &found, [](struct Device* dev, void* ctx) -> bool { + if (device_is_ready(dev)) { + *static_cast(ctx) = dev; + return false; + } + return true; + }); + return found; +} + +// ---- Scan cache helpers ---- + +void cacheScanAddr(const uint8_t addr[6], uint8_t addr_type) { + auto lock = scan_cache_mutex.asScopedLock(); + lock.lock(); + for (auto& entry : scan_addr_cache) { + if (memcmp(entry.addr, addr, 6) == 0) { + entry.addr_type = addr_type; + return; + } + } + CachedAddr e = {}; + memcpy(e.addr, addr, 6); + e.addr_type = addr_type; + scan_addr_cache.push_back(e); +} + +bool getCachedScanAddrType(const uint8_t addr[6], uint8_t* addr_type_out) { + auto lock = scan_cache_mutex.asScopedLock(); + lock.lock(); + for (const auto& entry : scan_addr_cache) { + if (memcmp(entry.addr, addr, 6) == 0) { + if (addr_type_out) *addr_type_out = entry.addr_type; + return true; + } + } + if (addr_type_out) *addr_type_out = 0; + return false; +} + +static void cachePeerRecord(const BtPeerRecord& krecord) { + PeerRecord rec; + memcpy(rec.addr.data(), krecord.addr, 6); + rec.name = krecord.name[0] != '\0' ? krecord.name : ""; + rec.rssi = krecord.rssi; + rec.paired = krecord.paired; + rec.connected = krecord.connected; + rec.profileId = 0; + + cacheScanAddr(krecord.addr, krecord.addr_type); + + auto lock = scan_cache_mutex.asScopedLock(); + lock.lock(); + for (auto& existing : scan_results_cache) { + if (existing.addr == rec.addr) { + if (!rec.name.empty()) existing.name = rec.name; + existing.rssi = rec.rssi; + return; + } + } + scan_results_cache.push_back(std::move(rec)); +} + +// ---- Bridge callback (registered with kernel driver) ---- +// This callback listens to platform driver events to perform auto-start logic +// and settings management. Consumers should register their own callbacks via +// bluetooth_add_event_callback() to receive events directly. + +static void bt_event_bridge(struct Device* /*device*/, void* /*context*/, struct BtEvent event) { + switch (event.type) { + case BT_EVENT_RADIO_STATE_CHANGED: + switch (event.radio_state) { + case BT_RADIO_STATE_ON: + getMainDispatcher().dispatch([] { + auto peers = settings::loadAll(); + bool has_hid_auto = false; + for (const auto& p : peers) { + if (p.autoConnect && p.profileId == BT_PROFILE_HID_HOST) { + has_hid_auto = true; + break; + } + } + if (has_hid_auto) { + LOGGER.info("HID host auto-connect peer found — starting scan"); + if (struct Device* dev = findFirstDevice()) { + bluetooth_scan_start(dev); + } + } else if (settings::shouldSppAutoStart()) { + LOGGER.info("Auto-starting SPP server"); + sppStart(); + } else if (settings::shouldMidiAutoStart()) { + LOGGER.info("Auto-starting MIDI server"); + midiStart(); + } + }); + break; + default: + break; + } + break; + + case BT_EVENT_SCAN_STARTED: + { + auto lock = scan_cache_mutex.asScopedLock(); + lock.lock(); + scan_results_cache.clear(); + scan_addr_cache.clear(); + } + break; + + case BT_EVENT_SCAN_FINISHED: + getMainDispatcher().dispatch([] { autoConnectHidHost(); }); + break; + + case BT_EVENT_PEER_FOUND: + cachePeerRecord(event.peer); + break; + + case BT_EVENT_PAIR_RESULT: + if (event.pair_result.result == BT_PAIR_RESULT_SUCCESS) { + uint8_t addr_buf[6]; + int profile_copy = event.pair_result.profile; + memcpy(addr_buf, event.pair_result.addr, 6); + getMainDispatcher().dispatch([addr_buf, profile_copy]() mutable { + std::array peer_addr; + memcpy(peer_addr.data(), addr_buf, 6); + const auto hex = settings::addrToHex(peer_addr); + if (!settings::contains(hex)) { + settings::PairedDevice dev; + dev.addr = peer_addr; + dev.name = ""; + dev.autoConnect = true; + dev.profileId = profile_copy; + if (settings::save(dev)) { + LOGGER.info("Saved paired peer {} (profile={})", hex, profile_copy); + } + } + }); + } else if (event.pair_result.result == BT_PAIR_RESULT_BOND_LOST) { + uint8_t addr_buf[6]; + memcpy(addr_buf, event.pair_result.addr, 6); + getMainDispatcher().dispatch([addr_buf]() mutable { + std::array peer_addr; + memcpy(peer_addr.data(), addr_buf, 6); + settings::remove(settings::addrToHex(peer_addr)); + }); + } + break; + + case BT_EVENT_PROFILE_STATE_CHANGED: + if (event.profile_state.state == BT_PROFILE_STATE_CONNECTED) { + uint8_t addr_buf[6]; + int profile_copy = (int)event.profile_state.profile; + memcpy(addr_buf, event.profile_state.addr, 6); + getMainDispatcher().dispatch([addr_buf, profile_copy]() mutable { + std::array peer_addr; + memcpy(peer_addr.data(), addr_buf, 6); + const auto hex = settings::addrToHex(peer_addr); + settings::PairedDevice stored; + if (settings::load(hex, stored) && stored.profileId != profile_copy) { + stored.profileId = profile_copy; + settings::save(stored); + } + }); + } else if (event.profile_state.state == BT_PROFILE_STATE_IDLE && + event.profile_state.profile == BT_PROFILE_HID_HOST) { + // HID host disconnected — check if any peer has autoConnect and re-scan + // so that autoConnectHidHost() fires when the scan finishes. + getMainDispatcher().dispatch([] { + auto peers = settings::loadAll(); + bool has_auto = false; + for (const auto& p : peers) { + if (p.autoConnect && p.profileId == BT_PROFILE_HID_HOST) { + has_auto = true; + break; + } + } + if (has_auto) { + if (struct Device* dev = findFirstDevice()) { + if (!bluetooth_is_scanning(dev)) { + bluetooth_scan_start(dev); + } + } + } + }); + } + break; + + default: + break; + } +} + +// ---- systemStart ---- + +void systemStart() { + struct Device* dev = findFirstDevice(); + if (dev == nullptr) { + LOGGER.warn("systemStart: no BLE device found"); + return; + } + bluetooth_add_event_callback(dev, nullptr, bt_event_bridge); + + if (settings::shouldEnableOnBoot()) { + LOGGER.info("Auto-enabling Bluetooth on boot"); + bluetooth_set_radio_enabled(dev, true); + } +} + +// ---- Public API ---- + +const char* radioStateToString(RadioState state) { + switch (state) { + using enum RadioState; + case Off: return "Off"; + case OnPending: return "OnPending"; + case On: return "On"; + case OffPending: return "OffPending"; + } + check(false, "not implemented"); +} + +RadioState getRadioState() { + struct Device* dev = findFirstDevice(); + if (dev == nullptr) return RadioState::Off; + BtRadioState state = BT_RADIO_STATE_OFF; + bluetooth_get_radio_state(dev, &state); + switch (state) { + case BT_RADIO_STATE_OFF: return RadioState::Off; + case BT_RADIO_STATE_ON_PENDING: return RadioState::OnPending; + case BT_RADIO_STATE_ON: return RadioState::On; + case BT_RADIO_STATE_OFF_PENDING: return RadioState::OffPending; + } + return RadioState::Off; +} + +std::vector getScanResults() { + auto lock = scan_cache_mutex.asScopedLock(); + lock.lock(); + return scan_results_cache; +} + +std::vector getPairedPeers() { + auto stored = settings::loadAll(); + std::vector result; + result.reserve(stored.size()); + std::array connected_addr = {}; + bool hid_host_connected = hidHostGetConnectedPeer(connected_addr); + for (const auto& device : stored) { + PeerRecord record; + record.addr = device.addr; + record.name = device.name; + record.rssi = 0; + record.paired = true; + record.profileId = device.profileId; + record.connected = hid_host_connected && device.addr == connected_addr; + result.push_back(std::move(record)); + } + // Synthesize fallback: LittleFS readdir can lag behind fwrite by one tick, so the + // connected peer may not appear in loadAll() yet. Always ensure it is in the list. + if (hid_host_connected) { + bool found = false; + for (const auto& r : result) { + if (r.addr == connected_addr) { found = true; break; } + } + if (!found) { + PeerRecord record; + record.addr = connected_addr; + record.rssi = 0; + record.paired = true; + record.connected = true; + record.profileId = BT_PROFILE_HID_HOST; + // Try to get the name from the scan cache. + { + auto lock = scan_cache_mutex.asScopedLock(); + lock.lock(); + for (const auto& sr : scan_results_cache) { + if (sr.addr == connected_addr) { record.name = sr.name; break; } + } + } + result.push_back(std::move(record)); + } + } + return result; +} + +void pair(const std::array& /*addr*/) { + // Pairing is handled automatically during connection by NimBLE SM. +} + +void unpair(const std::array& addr) { + struct Device* dev = findFirstDevice(); + if (dev != nullptr) { + bluetooth_unpair(dev, addr.data()); + } + settings::remove(settings::addrToHex(addr)); +} + +void connect(const std::array& addr, int profileId) { + LOGGER.info("connect(profile={})", profileId); + if (profileId == BT_PROFILE_HID_HOST) { + hidHostConnect(addr); + } else if (profileId == BT_PROFILE_HID_DEVICE) { + hidDeviceStart(); + } else if (profileId == BT_PROFILE_SPP) { + sppStart(); + } else if (profileId == BT_PROFILE_MIDI) { + midiStart(); + } +} + +void disconnect(const std::array& addr, int profileId) { + LOGGER.info("disconnect(profile={})", profileId); + if (profileId == BT_PROFILE_HID_HOST) { + hidHostDisconnect(); + } else if (profileId == BT_PROFILE_HID_DEVICE) { + hidDeviceStop(); + } else { + struct Device* dev = findFirstDevice(); + if (dev == nullptr) return; + bluetooth_disconnect(dev, addr.data(), (BtProfileId)profileId); + } +} + +bool isProfileSupported(int profileId) { + return profileId == BT_PROFILE_HID_HOST || + profileId == BT_PROFILE_HID_DEVICE || + profileId == BT_PROFILE_SPP || + profileId == BT_PROFILE_MIDI; +} + +} // namespace tt::bluetooth + +#endif // CONFIG_BT_NIMBLE_ENABLED diff --git a/Tactility/Source/bluetooth/BluetoothHidDevice.cpp b/Tactility/Source/bluetooth/BluetoothHidDevice.cpp new file mode 100644 index 000000000..3a373d5bc --- /dev/null +++ b/Tactility/Source/bluetooth/BluetoothHidDevice.cpp @@ -0,0 +1,34 @@ +#ifdef ESP_PLATFORM +#include +#endif + +#if defined(CONFIG_BT_NIMBLE_ENABLED) + +#include + +#include + +namespace tt::bluetooth { + +bool hidDeviceStart(uint16_t appearance) { + struct Device* dev = bluetooth_hid_device_get_device(); + if (dev == nullptr) return false; + BtHidDeviceMode mode; + switch (appearance) { + case 0x03C2: mode = BT_HID_DEVICE_MODE_MOUSE; break; + case 0x03C4: mode = BT_HID_DEVICE_MODE_GAMEPAD; break; + case 0x03C0: mode = BT_HID_DEVICE_MODE_KEYBOARD_MOUSE; break; + default: mode = BT_HID_DEVICE_MODE_KEYBOARD; break; + } + return bluetooth_hid_device_start(dev, mode) == ERROR_NONE; +} + +void hidDeviceStop() { + struct Device* dev = bluetooth_hid_device_get_device(); + if (dev == nullptr) return; + bluetooth_hid_device_stop(dev); +} + +} // namespace tt::bluetooth + +#endif // CONFIG_BT_NIMBLE_ENABLED diff --git a/Tactility/Source/bluetooth/BluetoothHidHost.cpp b/Tactility/Source/bluetooth/BluetoothHidHost.cpp new file mode 100644 index 000000000..708b1580e --- /dev/null +++ b/Tactility/Source/bluetooth/BluetoothHidHost.cpp @@ -0,0 +1,885 @@ +#ifdef ESP_PLATFORM +#include +#endif + +#if defined(CONFIG_BT_NIMBLE_ENABLED) + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#define TAG "BtHidHost" +#include + +namespace tt::bluetooth { + +static const auto LOGGER = Logger("BtHidHost"); + +// ---- Report type ---- + +enum class HidReportType : uint8_t { Unknown = 0, Keyboard, Mouse, Consumer }; + +struct HidHostInputRpt { + uint16_t valHandle; + uint16_t cccdHandle; + uint16_t rptRefHandle; + uint8_t reportId; + HidReportType type; +}; + +struct HidHostCtx { + uint16_t connHandle = BLE_HS_CONN_HANDLE_NONE; + uint16_t hidSvcStart = 0; + uint16_t hidSvcEnd = 0; + std::vector inputRpts; + std::vector allChrDefHandles; + int subscribeIdx = 0; + int dscDiscIdx = 0; + int rptRefReadIdx = 0; + uint16_t rptMapHandle = 0; + std::vector rptMap; + bool securityInitiated = false; + bool typeResolutionDone = false; + bool readyBlockFired = false; + lv_indev_t* kbIndev = nullptr; + lv_indev_t* mouseIndev = nullptr; + lv_obj_t* mouseCursor = nullptr; + std::array peerAddr = {}; +}; + +// ---- Globals ---- + +static std::unique_ptr hid_host_ctx; +static QueueHandle_t hid_host_key_queue = nullptr; +static uint8_t hid_host_prev_keys[6] = {}; +static esp_timer_handle_t hid_enc_retry_timer = nullptr; + +static std::atomic hid_host_mouse_x{0}; +static std::atomic hid_host_mouse_y{0}; +static std::atomic hid_host_mouse_btn{false}; +static std::atomic hid_host_mouse_active{false}; + +#define HID_HOST_KEY_QUEUE_SIZE 64 +struct HidHostKeyEvt { uint32_t key; bool pressed; }; + +// ---- Forward declarations ---- + +static void hidHostSubscribeNext(HidHostCtx& ctx); +static void hidHostStartRptRefRead(HidHostCtx& ctx); +static void hidHostReadReportMap(HidHostCtx& ctx); +static uint16_t getDescEndHandle(const HidHostCtx& ctx, uint16_t valHandle); + +// ---- Keycode mapping ---- + +static uint32_t hidHostMapKeycode(uint8_t mod, uint8_t kc) { + bool shift = (mod & 0x22) != 0; + switch (kc) { + case 0x28: return LV_KEY_ENTER; + case 0x29: return LV_KEY_ESC; + case 0x2A: return LV_KEY_BACKSPACE; + case 0x4C: return LV_KEY_DEL; + case 0x2B: return shift ? (uint32_t)LV_KEY_PREV : (uint32_t)LV_KEY_NEXT; + case 0x52: return LV_KEY_UP; + case 0x51: return LV_KEY_DOWN; + case 0x50: return LV_KEY_LEFT; + case 0x4F: return LV_KEY_RIGHT; + case 0x4A: return LV_KEY_HOME; + case 0x4D: return LV_KEY_END; + default: break; + } + if (kc >= 0x04 && kc <= 0x1D) { + uint32_t c = static_cast('a' + (kc - 0x04)); + return shift ? (c - 0x20) : c; + } + if (kc >= 0x1E && kc <= 0x27) { + static const char nums[] = "1234567890"; + static const char snums[] = "!@#$%^&*()"; + int i = kc - 0x1E; + return shift ? static_cast(snums[i]) : static_cast(nums[i]); + } + if (kc == 0x2C) return ' '; + return 0; +} + +static void hidHostKeyboardReadCb(lv_indev_t* /*indev*/, lv_indev_data_t* data) { + if (!hid_host_key_queue) { data->state = LV_INDEV_STATE_RELEASED; return; } + HidHostKeyEvt evt = {}; + if (xQueueReceive(hid_host_key_queue, &evt, 0) == pdTRUE) { + data->key = evt.key; + data->state = evt.pressed ? LV_INDEV_STATE_PRESSED : LV_INDEV_STATE_RELEASED; + data->continue_reading = (uxQueueMessagesWaiting(hid_host_key_queue) > 0); + } else { + data->state = LV_INDEV_STATE_RELEASED; + } +} + +static void hidHostHandleKeyboardReport(const uint8_t* data, uint16_t len) { + if (len < 3 || !hid_host_key_queue) return; + uint8_t mod = data[0]; + const uint8_t* curr = &data[2]; + int nkeys = std::min((int)(len - 2), 6); + + for (int i = 0; i < 6; i++) { + uint8_t kc = hid_host_prev_keys[i]; + if (kc == 0) continue; + bool still = false; + for (int j = 0; j < nkeys; j++) { if (curr[j] == kc) { still = true; break; } } + if (!still) { + uint32_t lv = hidHostMapKeycode(0, kc); + if (lv) { HidHostKeyEvt e{lv, false}; xQueueSend(hid_host_key_queue, &e, 0); } + } + } + for (int i = 0; i < nkeys; i++) { + uint8_t kc = curr[i]; + if (kc == 0) continue; + bool had = false; + for (int j = 0; j < 6; j++) { if (hid_host_prev_keys[j] == kc) { had = true; break; } } + if (!had) { + uint32_t lv = hidHostMapKeycode(mod, kc); + if (lv) { HidHostKeyEvt e{lv, true}; xQueueSend(hid_host_key_queue, &e, 0); } + } + } + std::memcpy(hid_host_prev_keys, curr, nkeys); + if (nkeys < 6) std::memset(hid_host_prev_keys + nkeys, 0, 6 - nkeys); +} + +static void hidHostMouseReadCb(lv_indev_t* /*indev*/, lv_indev_data_t* data) { + int32_t cx = hid_host_mouse_x.load(); + int32_t cy = hid_host_mouse_y.load(); + + lv_display_t* disp = lv_display_get_default(); + if (disp) { + int32_t ow = lv_display_get_original_horizontal_resolution(disp); + int32_t oh = lv_display_get_original_vertical_resolution(disp); + switch (lv_display_get_rotation(disp)) { + case LV_DISPLAY_ROTATION_0: + data->point.x = (lv_coord_t)cx; + data->point.y = (lv_coord_t)cy; + break; + case LV_DISPLAY_ROTATION_90: + data->point.x = (lv_coord_t)cy; + data->point.y = (lv_coord_t)(oh - cx - 1); + break; + case LV_DISPLAY_ROTATION_180: + data->point.x = (lv_coord_t)(ow - cx - 1); + data->point.y = (lv_coord_t)(oh - cy - 1); + break; + case LV_DISPLAY_ROTATION_270: + data->point.x = (lv_coord_t)(ow - cy - 1); + data->point.y = (lv_coord_t)cx; + break; + } + } else { + data->point.x = (lv_coord_t)cx; + data->point.y = (lv_coord_t)cy; + } + data->state = hid_host_mouse_btn.load() ? LV_INDEV_STATE_PRESSED : LV_INDEV_STATE_RELEASED; + + if (!hid_host_mouse_active.load()) { + hid_host_mouse_active.store(true); + if (hid_host_ctx && hid_host_ctx->mouseCursor) { + lv_obj_remove_flag(hid_host_ctx->mouseCursor, LV_OBJ_FLAG_HIDDEN); + } + } +} + +static void hidHostHandleMouseReport(const uint8_t* data, uint16_t len) { + if (len < 3) return; + bool btn = (data[0] & 0x01) != 0; + int8_t dx = (int8_t)data[1]; + int8_t dy = (int8_t)data[2]; + + lv_display_t* disp = lv_display_get_default(); + int32_t w = disp ? lv_display_get_horizontal_resolution(disp) : 320; + int32_t h = disp ? lv_display_get_vertical_resolution(disp) : 240; + + int32_t nx = hid_host_mouse_x.load() + dx; + int32_t ny = hid_host_mouse_y.load() + dy; + if (nx < 0) nx = 0; + if (nx >= w) nx = w - 1; + if (ny < 0) ny = 0; + if (ny >= h) ny = h - 1; + + hid_host_mouse_x.store(nx); + hid_host_mouse_y.store(ny); + hid_host_mouse_btn.store(btn); + + if (hid_host_ctx && hid_host_ctx->mouseIndev == nullptr) { + getMainDispatcher().dispatch([] { + if (!hid_host_ctx || hid_host_ctx->mouseIndev != nullptr) return; + if (!tt::lvgl::lock(1000)) { LOGGER.warn("LVGL lock failed for mouse indev"); return; } + auto* ms = lv_indev_create(); + lv_indev_set_type(ms, LV_INDEV_TYPE_POINTER); + lv_indev_set_read_cb(ms, hidHostMouseReadCb); + auto* cur = lv_image_create(lv_layer_sys()); + lv_obj_remove_flag(cur, LV_OBJ_FLAG_CLICKABLE); + lv_obj_add_flag(cur, LV_OBJ_FLAG_HIDDEN); + lv_image_set_src(cur, TT_ASSETS_UI_CURSOR); + lv_indev_set_cursor(ms, cur); + hid_host_ctx->mouseIndev = ms; + hid_host_ctx->mouseCursor = cur; + tt::lvgl::unlock(); + LOGGER.info("Mouse indev registered"); + }); + } +} + +// ---- Timer callback for post-encryption CCCD retry ---- + +static void hidEncRetryTimerCb(void* /*arg*/) { + if (hid_host_ctx) { + if (!hid_host_ctx->typeResolutionDone) { + LOGGER.warn("Post-encryption delay — type resolution timed out, proceeding"); + hid_host_ctx->typeResolutionDone = true; + hid_host_ctx->subscribeIdx = 0; + } else { + LOGGER.info("Post-encryption delay complete — starting CCCD subscriptions"); + } + hidHostSubscribeNext(*hid_host_ctx); + } +} + +// ---- Report Map parsing ---- + +static void applyReportMapTypes(HidHostCtx& ctx) { + const uint8_t* data = ctx.rptMap.data(); + size_t len = ctx.rptMap.size(); + + uint16_t usagePage = 0, usage = 0; + uint8_t reportId = 0; + int depth = 0; + HidReportType collType = HidReportType::Unknown; + + struct Entry { uint8_t id; HidReportType type; }; + std::vector typeMap; + std::vector collOrder; + bool collHadInput = false; + + size_t i = 0; + while (i < len) { + uint8_t prefix = data[i++]; + if (prefix == 0xFE) { + if (i + 1 >= len) break; + uint8_t lsz = data[i++]; i++; i += lsz; continue; + } + uint8_t bSize = prefix & 0x03; + uint8_t bType = (prefix >> 2) & 0x03; + uint8_t bTag = (prefix >> 4) & 0x0F; + uint8_t dataLen = (bSize == 3) ? 4 : bSize; + if (i + dataLen > len) break; + uint32_t value = 0; + for (uint8_t j = 0; j < dataLen; j++) value |= (uint32_t)data[i++] << (8 * j); + + if (bType == 0) { + if (bTag == 0xA) { + if (depth == 0 && value == 0x01) { + if (usagePage == 0x01 && usage == 0x06) collType = HidReportType::Keyboard; + else if (usagePage == 0x01 && usage == 0x02) collType = HidReportType::Mouse; + else if (usagePage == 0x0C) collType = HidReportType::Consumer; + else collType = HidReportType::Unknown; + collHadInput = false; + } + depth++; usage = 0; + } else if (bTag == 0xC) { + if (depth > 0) depth--; + if (depth == 0) { collType = HidReportType::Unknown; collHadInput = false; } + usage = 0; + } else if (bTag == 0x8) { + if (depth > 0 && collType != HidReportType::Unknown) { + if (!collHadInput) { collOrder.push_back(collType); collHadInput = true; } + if (reportId != 0) { + bool found = false; + for (const auto& e : typeMap) { if (e.id == reportId) { found = true; break; } } + if (!found) typeMap.push_back({reportId, collType}); + } + } + usage = 0; + } else { usage = 0; } + } else if (bType == 1) { + if (bTag == 0x0) usagePage = (uint16_t)value; + else if (bTag == 0x8) reportId = (uint8_t)value; + } else if (bType == 2) { + if (bTag == 0x0) usage = (uint16_t)value; + } + } + + bool anyNonZeroId = false; + for (const auto& rpt : ctx.inputRpts) { if (rpt.reportId != 0) { anyNonZeroId = true; break; } } + + size_t zeroRptIdx = 0; + for (auto& rpt : ctx.inputRpts) { + if (anyNonZeroId) { + for (const auto& e : typeMap) { if (e.id == rpt.reportId) { rpt.type = e.type; break; } } + } else { + if (zeroRptIdx < collOrder.size()) rpt.type = collOrder[zeroRptIdx]; + zeroRptIdx++; + } + LOGGER.info("Report val_handle={} reportId={} type={}", rpt.valHandle, rpt.reportId, (int)rpt.type); + } + ctx.rptMap.clear(); +} + +// ---- Report Reference read chain ---- + +static void hidHostStartRptRefRead(HidHostCtx& ctx) { + while (ctx.rptRefReadIdx < (int)ctx.inputRpts.size() && + ctx.inputRpts[ctx.rptRefReadIdx].rptRefHandle == 0) { + ctx.rptRefReadIdx++; + } + if (ctx.rptRefReadIdx >= (int)ctx.inputRpts.size()) { + hidHostReadReportMap(ctx); + return; + } + uint16_t handle = ctx.inputRpts[ctx.rptRefReadIdx].rptRefHandle; + int rc = ble_gattc_read(ctx.connHandle, handle, [](uint16_t conn_handle, + const struct ble_gatt_error* error, + struct ble_gatt_attr* attr, void* /*arg*/) -> int { + if (!hid_host_ctx) return 0; + auto& ctx = *hid_host_ctx; + if (conn_handle != ctx.connHandle) return 0; + if (error->status == BLE_HS_EDONE) return 0; + if (error->status == 0 && attr != nullptr) { + if (OS_MBUF_PKTLEN(attr->om) >= 2 && ctx.rptRefReadIdx < (int)ctx.inputRpts.size()) { + uint8_t rpt_ref[2] = {}; + os_mbuf_copydata(attr->om, 0, 2, rpt_ref); + ctx.inputRpts[ctx.rptRefReadIdx].reportId = rpt_ref[0]; + LOGGER.info("Report[{}] val_handle={} reportId={}", ctx.rptRefReadIdx, + ctx.inputRpts[ctx.rptRefReadIdx].valHandle, rpt_ref[0]); + } + } + ctx.rptRefReadIdx++; + hidHostStartRptRefRead(ctx); + return 0; + }, nullptr); + if (rc != 0) { + LOGGER.warn("rptRef read[{}] failed rc={} — skipping", ctx.rptRefReadIdx, rc); + ctx.rptRefReadIdx++; + hidHostStartRptRefRead(ctx); + } +} + +// ---- Report Map read ---- + +static void hidHostReadReportMap(HidHostCtx& ctx) { + if (ctx.rptMapHandle == 0) { + LOGGER.info("No Report Map char — skipping type resolution"); + ctx.typeResolutionDone = true; + ctx.subscribeIdx = 0; + hidHostSubscribeNext(ctx); + return; + } + int rc = ble_gattc_read_long(ctx.connHandle, ctx.rptMapHandle, 0, + [](uint16_t conn_handle, const struct ble_gatt_error* error, + struct ble_gatt_attr* attr, void* /*arg*/) -> int { + if (!hid_host_ctx) return 0; + auto& ctx = *hid_host_ctx; + if (conn_handle != ctx.connHandle) return 0; + if (error->status == 0 && attr != nullptr) { + uint16_t chunk = OS_MBUF_PKTLEN(attr->om); + size_t old_sz = ctx.rptMap.size(); + ctx.rptMap.resize(old_sz + chunk); + os_mbuf_copydata(attr->om, 0, chunk, ctx.rptMap.data() + old_sz); + return 0; + } + if (!ctx.rptMap.empty()) { + LOGGER.info("Report map read ({} bytes)", ctx.rptMap.size()); + applyReportMapTypes(ctx); + } else { + LOGGER.warn("Report map read failed — types remain Unknown"); + } + ctx.typeResolutionDone = true; + ctx.subscribeIdx = 0; + hidHostSubscribeNext(ctx); + return 0; + }, nullptr); + if (rc != 0) { + LOGGER.warn("Report map read_long failed rc={} — skipping", rc); + ctx.typeResolutionDone = true; + ctx.subscribeIdx = 0; + hidHostSubscribeNext(ctx); + } +} + +// ---- CCCD subscription chain ---- + +static int hidHostCccdWriteCb(uint16_t conn_handle, const struct ble_gatt_error* error, + struct ble_gatt_attr* /*attr*/, void* /*arg*/) { + if (!hid_host_ctx) return 0; + auto& ctx = *hid_host_ctx; + if (conn_handle != ctx.connHandle) return 0; + + if (error->status != 0 && error->status != BLE_HS_EDONE) { + if ((error->status == BLE_HS_ATT_ERR(BLE_ATT_ERR_INSUFFICIENT_AUTHEN) || + error->status == BLE_HS_ATT_ERR(BLE_ATT_ERR_INSUFFICIENT_ENC)) + && !ctx.securityInitiated) { + LOGGER.info("CCCD auth required — initiating security"); + ctx.securityInitiated = true; + ble_gap_security_initiate(conn_handle); + return 0; + } + if (error->status == BLE_HS_ETIMEOUT) { + LOGGER.warn("CCCD write timed out for report[{}] — skipping", ctx.subscribeIdx); + ctx.subscribeIdx++; + hidHostSubscribeNext(ctx); + return 0; + } + if (error->status == BLE_HS_ENOTCONN) { + LOGGER.warn("CCCD write failed — not connected"); + return 0; + } + LOGGER.warn("CCCD write failed status={}", error->status); + } + ctx.subscribeIdx++; + hidHostSubscribeNext(ctx); + return 0; +} + +static void hidHostSubscribeNext(HidHostCtx& ctx) { + if (ctx.subscribeIdx >= (int)ctx.inputRpts.size()) { + if (ctx.readyBlockFired) { + LOGGER.info("Subscribe ready block already ran — ignoring duplicate"); + return; + } + ctx.readyBlockFired = true; + LOGGER.info("All {} reports subscribed — ready", ctx.inputRpts.size()); + if (hid_enc_retry_timer) esp_timer_stop(hid_enc_retry_timer); + + if (!hid_host_key_queue) { + hid_host_key_queue = xQueueCreate(HID_HOST_KEY_QUEUE_SIZE, sizeof(HidHostKeyEvt)); + } + getMainDispatcher().dispatch([] { + if (!hid_host_ctx || hid_host_ctx->kbIndev != nullptr) return; + if (!tt::lvgl::lock(1000)) { LOGGER.warn("LVGL lock failed for kb indev"); return; } + auto* kb = lv_indev_create(); + lv_indev_set_type(kb, LV_INDEV_TYPE_KEYPAD); + lv_indev_set_read_cb(kb, hidHostKeyboardReadCb); + hid_host_ctx->kbIndev = kb; + tt::lvgl::hardware_keyboard_set_indev(kb); + tt::lvgl::unlock(); + LOGGER.info("Keyboard indev registered"); + }); + + auto peer_addr = ctx.peerAddr; + getMainDispatcher().dispatch([peer_addr] { + // Find name from cached scan results + std::string name; + { + auto results = getScanResults(); + for (const auto& r : results) { + if (r.addr == peer_addr) { name = r.name; break; } + } + } + settings::PairedDevice device; + device.addr = peer_addr; + device.profileId = BT_PROFILE_HID_HOST; + device.autoConnect = true; + const auto addr_hex = settings::addrToHex(peer_addr); + settings::PairedDevice existing; + if (settings::load(addr_hex, existing)) { + device.autoConnect = existing.autoConnect; + } + device.name = name; + settings::save(device); + if (struct Device* dev = findFirstDevice()) { + struct BtEvent e = {}; + e.type = BT_EVENT_PROFILE_STATE_CHANGED; + e.profile_state.state = BT_PROFILE_STATE_CONNECTED; + e.profile_state.profile = BT_PROFILE_HID_HOST; + bluetooth_fire_event(dev, e); + } + }); + return; + } + auto& rpt = ctx.inputRpts[ctx.subscribeIdx]; + if (rpt.cccdHandle == 0) { + ctx.subscribeIdx++; + hidHostSubscribeNext(ctx); + return; + } + static const uint16_t notify_val = 0x0001; + int rc = ble_gattc_write_flat(ctx.connHandle, rpt.cccdHandle, + ¬ify_val, sizeof(notify_val), + hidHostCccdWriteCb, nullptr); + if (rc != 0) { + LOGGER.warn("gattc_write_flat CCCD failed rc={}", rc); + ctx.subscribeIdx++; + hidHostSubscribeNext(ctx); + } +} + +// ---- Descriptor discovery ---- + +static int hidHostDscDiscCb(uint16_t conn_handle, const struct ble_gatt_error* error, + uint16_t chr_val_handle, const struct ble_gatt_dsc* dsc, void* /*arg*/) { + if (!hid_host_ctx) return 0; + auto& ctx = *hid_host_ctx; + if (conn_handle != ctx.connHandle) return 0; + + if (error->status == 0 && dsc != nullptr) { + uint16_t dsc_uuid = ble_uuid_u16(&dsc->uuid.u); + for (auto& rpt : ctx.inputRpts) { + if (rpt.valHandle != chr_val_handle) continue; + if (dsc_uuid == 0x2902) { rpt.cccdHandle = dsc->handle; } + else if (dsc_uuid == 0x2908) { rpt.rptRefHandle = dsc->handle; } + break; + } + } else if (error->status == BLE_HS_EDONE) { + int next_idx = ctx.dscDiscIdx + 1; + if (next_idx < (int)ctx.inputRpts.size()) { + ctx.dscDiscIdx = next_idx; + auto& next_rpt = ctx.inputRpts[next_idx]; + uint16_t end = getDescEndHandle(ctx, next_rpt.valHandle); + int rc = ble_gattc_disc_all_dscs(ctx.connHandle, next_rpt.valHandle, end, + hidHostDscDiscCb, nullptr); + if (rc != 0) { + LOGGER.warn("disc_all_dscs[{}] failed rc={}", next_idx, rc); + ctx.rptRefReadIdx = 0; + hidHostStartRptRefRead(ctx); + } + } else { + ctx.rptRefReadIdx = 0; + hidHostStartRptRefRead(ctx); + } + } + return 0; +} + +static uint16_t getDescEndHandle(const HidHostCtx& ctx, uint16_t valHandle) { + for (uint16_t dh : ctx.allChrDefHandles) { + if (dh > valHandle) return dh - 1; + } + return ctx.hidSvcEnd; +} + +// ---- Characteristic discovery ---- + +static int hidHostChrDiscCb(uint16_t conn_handle, const struct ble_gatt_error* error, + const struct ble_gatt_chr* chr, void* /*arg*/) { + if (!hid_host_ctx) return 0; + auto& ctx = *hid_host_ctx; + if (conn_handle != ctx.connHandle) return 0; + + if (error->status == 0 && chr != nullptr) { + ctx.allChrDefHandles.push_back(chr->def_handle); + uint16_t uuid16 = ble_uuid_u16(&chr->uuid.u); + if (uuid16 == 0x2A4D && (chr->properties & BLE_GATT_CHR_PROP_NOTIFY)) { + HidHostInputRpt rpt = {}; + rpt.valHandle = chr->val_handle; + ctx.inputRpts.push_back(rpt); + LOGGER.info("Input Report chr val_handle={}", chr->val_handle); + } else if (uuid16 == 0x2A4B) { + ctx.rptMapHandle = chr->val_handle; + } + } else if (error->status == BLE_HS_EDONE) { + std::sort(ctx.allChrDefHandles.begin(), ctx.allChrDefHandles.end()); + if (ctx.inputRpts.empty()) { + LOGGER.warn("No Input Report chars — disconnecting"); + ble_gap_terminate(ctx.connHandle, BLE_ERR_REM_USER_CONN_TERM); + return 0; + } + ctx.dscDiscIdx = 0; + auto& first = ctx.inputRpts[0]; + uint16_t end = getDescEndHandle(ctx, first.valHandle); + int rc = ble_gattc_disc_all_dscs(ctx.connHandle, first.valHandle, end, + hidHostDscDiscCb, nullptr); + if (rc != 0) { + LOGGER.warn("disc_all_dscs[0] failed rc={}", rc); + ctx.rptRefReadIdx = 0; + hidHostStartRptRefRead(ctx); + } + } + return 0; +} + +// ---- Service discovery ---- + +static int hidHostSvcDiscCb(uint16_t conn_handle, const struct ble_gatt_error* error, + const struct ble_gatt_svc* svc, void* /*arg*/) { + if (!hid_host_ctx) return 0; + auto& ctx = *hid_host_ctx; + if (conn_handle != ctx.connHandle) return 0; + + if (error->status == 0 && svc != nullptr) { + if (ble_uuid_u16(&svc->uuid.u) == 0x1812) { + ctx.hidSvcStart = svc->start_handle; + ctx.hidSvcEnd = svc->end_handle; + LOGGER.info("HID service start={} end={}", ctx.hidSvcStart, ctx.hidSvcEnd); + } + } else if (error->status == BLE_HS_EDONE) { + if (ctx.hidSvcStart == 0) { + LOGGER.warn("No HID service found — disconnecting"); + ble_gap_terminate(ctx.connHandle, BLE_ERR_REM_USER_CONN_TERM); + return 0; + } + int rc = ble_gattc_disc_all_chrs(ctx.connHandle, ctx.hidSvcStart, ctx.hidSvcEnd, + hidHostChrDiscCb, nullptr); + if (rc != 0) { + LOGGER.warn("disc_all_chrs failed rc={}", rc); + ble_gap_terminate(ctx.connHandle, BLE_ERR_REM_USER_CONN_TERM); + } + } + return 0; +} + +// ---- GAP callback for HID host central connection ---- + +static int hidHostGapCb(struct ble_gap_event* event, void* /*arg*/) { + if (!hid_host_ctx) return 0; + auto& ctx = *hid_host_ctx; + + switch (event->type) { + case BLE_GAP_EVENT_CONNECT: + if (event->connect.status == 0) { + ctx.connHandle = event->connect.conn_handle; + LOGGER.info("Connected (handle={})", ctx.connHandle); + int rc = ble_gattc_disc_all_svcs(ctx.connHandle, hidHostSvcDiscCb, nullptr); + if (rc != 0) { + LOGGER.warn("disc_all_svcs failed rc={}", rc); + ble_gap_terminate(ctx.connHandle, BLE_ERR_REM_USER_CONN_TERM); + } + } else { + LOGGER.warn("Connect failed status={}", event->connect.status); + hid_host_ctx.reset(); + if (struct Device* dev = findFirstDevice()) { + bluetooth_set_hid_host_active(dev, false); + struct BtEvent e = {}; + e.type = BT_EVENT_PROFILE_STATE_CHANGED; + e.profile_state.state = BT_PROFILE_STATE_IDLE; + e.profile_state.profile = BT_PROFILE_HID_HOST; + bluetooth_fire_event(dev, e); + } + } + break; + + case BLE_GAP_EVENT_DISCONNECT: { + LOGGER.info("Disconnected reason={}", event->disconnect.reason); + lv_indev_t* saved_kb = hid_host_ctx ? hid_host_ctx->kbIndev : nullptr; + lv_indev_t* saved_mouse = hid_host_ctx ? hid_host_ctx->mouseIndev : nullptr; + lv_obj_t* saved_cursor = hid_host_ctx ? hid_host_ctx->mouseCursor : nullptr; + QueueHandle_t saved_queue = hid_host_key_queue; + hid_host_ctx.reset(); + hid_host_key_queue = nullptr; + std::memset(hid_host_prev_keys, 0, sizeof(hid_host_prev_keys)); + hid_host_mouse_x.store(0); + hid_host_mouse_y.store(0); + hid_host_mouse_btn.store(false); + hid_host_mouse_active.store(false); + + if (struct Device* dev = findFirstDevice()) { + bluetooth_set_hid_host_active(dev, false); + struct BtEvent e = {}; + e.type = BT_EVENT_PROFILE_STATE_CHANGED; + e.profile_state.state = BT_PROFILE_STATE_IDLE; + e.profile_state.profile = BT_PROFILE_HID_HOST; + bluetooth_fire_event(dev, e); + } + + getMainDispatcher().dispatch([saved_kb, saved_mouse, saved_cursor, saved_queue] { + if (!tt::lvgl::lock(1000)) { + LOGGER.warn("Failed to acquire LVGL lock for indev cleanup"); + if (saved_queue) vQueueDelete(saved_queue); + return; + } + if (saved_kb) { + tt::lvgl::hardware_keyboard_set_indev(nullptr); + lv_indev_delete(saved_kb); + } + if (saved_mouse) lv_indev_delete(saved_mouse); + if (saved_cursor) lv_obj_delete(saved_cursor); + tt::lvgl::unlock(); + if (saved_queue) vQueueDelete(saved_queue); + }); + break; + } + + case BLE_GAP_EVENT_ENC_CHANGE: + if (event->enc_change.conn_handle == ctx.connHandle) { + if (event->enc_change.status == 0) { + LOGGER.info("Encryption established — retrying CCCD in 500ms"); + ctx.subscribeIdx = 0; + if (hid_enc_retry_timer) { + esp_timer_stop(hid_enc_retry_timer); + esp_timer_start_once(hid_enc_retry_timer, 500 * 1000); + } else { + hidHostSubscribeNext(ctx); + } + } else { + LOGGER.warn("Encryption failed status={}", event->enc_change.status); + } + } + break; + + case BLE_GAP_EVENT_NOTIFY_RX: + if (event->notify_rx.conn_handle == ctx.connHandle) { + uint16_t len = OS_MBUF_PKTLEN(event->notify_rx.om); + if (len > 0 && len <= 64) { + uint8_t buf[64] = {}; + os_mbuf_copydata(event->notify_rx.om, 0, len, buf); + for (const auto& rpt : ctx.inputRpts) { + if (rpt.valHandle != event->notify_rx.attr_handle) continue; + switch (rpt.type) { + case HidReportType::Keyboard: hidHostHandleKeyboardReport(buf, len); break; + case HidReportType::Mouse: hidHostHandleMouseReport(buf, len); break; + case HidReportType::Consumer: + LOGGER.info("Consumer report len={}", len); + break; + case HidReportType::Unknown: + if (len >= 6) hidHostHandleKeyboardReport(buf, len); + else if (len >= 3) hidHostHandleMouseReport(buf, len); + break; + } + break; + } + } + } + break; + + default: + break; + } + return 0; +} + +// ---- Public functions ---- + +void hidHostConnect(const std::array& addr) { + if (getRadioState() != RadioState::On) { + LOGGER.warn("hidHostConnect: radio not on"); + return; + } + if (hid_host_ctx) { + LOGGER.warn("hidHostConnect: already connecting/connected"); + return; + } + + hid_host_mouse_x.store(0); + hid_host_mouse_y.store(0); + hid_host_mouse_btn.store(false); + hid_host_mouse_active.store(false); + + hid_host_ctx = std::make_unique(); + hid_host_ctx->peerAddr = addr; + + // Create enc retry timer lazily + if (hid_enc_retry_timer == nullptr) { + esp_timer_create_args_t args = {}; + args.callback = hidEncRetryTimerCb; + args.dispatch_method = ESP_TIMER_TASK; + args.name = "hid_enc_retry"; + if (esp_timer_create(&args, &hid_enc_retry_timer) != ESP_OK) { + LOGGER.error("Failed to create hid_enc_retry timer"); + hid_enc_retry_timer = nullptr; + } + } + + // Notify driver that a HID host central connection is starting. + if (struct Device* dev = findFirstDevice()) bluetooth_set_hid_host_active(dev, true); + + // Look up the addr_type from the cached scan results. + ble_addr_t ble_addr = {}; + ble_addr.type = BLE_ADDR_PUBLIC; + std::memcpy(ble_addr.val, addr.data(), 6); + uint8_t addr_type = 0; + if (getCachedScanAddrType(addr.data(), &addr_type)) { + ble_addr.type = addr_type; + } + + uint8_t own_addr_type; + if (ble_hs_id_infer_auto(0, &own_addr_type) != 0) { + own_addr_type = BLE_OWN_ADDR_PUBLIC; + } + + int rc = ble_gap_connect(own_addr_type, &ble_addr, 5000, nullptr, hidHostGapCb, nullptr); + if (rc != 0) { + LOGGER.warn("ble_gap_connect failed rc={}", rc); + hid_host_ctx.reset(); + if (struct Device* dev = findFirstDevice()) { + bluetooth_set_hid_host_active(dev, false); + // Fire IDLE so bt_event_bridge can start a new scan and retry. + struct BtEvent e = {}; + e.type = BT_EVENT_PROFILE_STATE_CHANGED; + e.profile_state.state = BT_PROFILE_STATE_IDLE; + e.profile_state.profile = BT_PROFILE_HID_HOST; + bluetooth_fire_event(dev, e); + } + } else { + LOGGER.info("Connecting..."); + } +} + +void hidHostDisconnect() { + if (!hid_host_ctx || hid_host_ctx->connHandle == BLE_HS_CONN_HANDLE_NONE) return; + ble_gap_terminate(hid_host_ctx->connHandle, BLE_ERR_REM_USER_CONN_TERM); +} + +bool hidHostIsConnected() { + return hid_host_ctx != nullptr && + hid_host_ctx->connHandle != BLE_HS_CONN_HANDLE_NONE && + !hid_host_ctx->inputRpts.empty() && + hid_host_ctx->subscribeIdx >= (int)hid_host_ctx->inputRpts.size(); +} + +bool hidHostGetConnectedPeer(std::array& addr_out) { + if (!hidHostIsConnected()) return false; + addr_out = hid_host_ctx->peerAddr; + return true; +} + +void autoConnectHidHost() { + if (hidHostIsConnected()) return; + + // Connect to the first saved HID host peer that appeared in the last scan. + // cacheScanAddr() is populated during scanning so addr_type is available for ble_gap_connect. + auto scan = getScanResults(); + for (const auto& r : scan) { + settings::PairedDevice stored; + if (settings::load(settings::addrToHex(r.addr), stored) && + stored.autoConnect && + stored.profileId == BT_PROFILE_HID_HOST) { + LOGGER.info("Auto-connecting HID host to {}", settings::addrToHex(r.addr)); + hidHostConnect(r.addr); + return; + } + } + + // Device not in the last scan. If we have an autoConnect HID host peer, restart + // scanning so we keep checking until the device powers back on. + auto peers = settings::loadAll(); + for (const auto& peer : peers) { + if (peer.autoConnect && peer.profileId == BT_PROFILE_HID_HOST) { + if (struct Device* dev = findFirstDevice()) { + if (!bluetooth_is_scanning(dev)) { + LOGGER.info("Auto-connect HID host: device not in scan, retrying scan"); + bluetooth_scan_start(dev); + } + } + break; + } + } +} + +} // namespace tt::bluetooth + +#endif // CONFIG_BT_NIMBLE_ENABLED diff --git a/Tactility/Source/bluetooth/BluetoothMidi.cpp b/Tactility/Source/bluetooth/BluetoothMidi.cpp new file mode 100644 index 000000000..1738653f0 --- /dev/null +++ b/Tactility/Source/bluetooth/BluetoothMidi.cpp @@ -0,0 +1,31 @@ +#ifdef ESP_PLATFORM +#include +#endif + +#if defined(CONFIG_BT_NIMBLE_ENABLED) + +#include +#include + +#include + +namespace tt::bluetooth { + +bool midiStart() { + struct Device* dev = bluetooth_midi_get_device(); + if (dev == nullptr) return false; + if (bluetooth_midi_start(dev) != ERROR_NONE) return false; + settings::setMidiAutoStart(true); + return true; +} + +void midiStop() { + struct Device* dev = bluetooth_midi_get_device(); + if (dev == nullptr) return; + settings::setMidiAutoStart(false); + bluetooth_midi_stop(dev); +} + +} // namespace tt::bluetooth + +#endif // CONFIG_BT_NIMBLE_ENABLED diff --git a/Tactility/Source/bluetooth/BluetoothMock.cpp b/Tactility/Source/bluetooth/BluetoothMock.cpp new file mode 100644 index 000000000..7fa906004 --- /dev/null +++ b/Tactility/Source/bluetooth/BluetoothMock.cpp @@ -0,0 +1,53 @@ +#ifdef ESP_PLATFORM +#include +#endif + +#if !defined(CONFIG_BT_NIMBLE_ENABLED) + +#include + +namespace tt::bluetooth { + +struct Device* findFirstDevice() { return nullptr; } + +const char* radioStateToString(RadioState state) { + switch (state) { + using enum RadioState; + case Off: return "Off"; + case OnPending: return "OnPending"; + case On: return "On"; + case OffPending: return "OffPending"; + } + return "Unknown"; +} + +RadioState getRadioState() { return RadioState::Off; } + +std::vector getScanResults() { return {}; } +std::vector getPairedPeers() { return {}; } + +void pair(const std::array& /*addr*/) {} +void unpair(const std::array& /*addr*/) {} +void connect(const std::array& /*addr*/, int /*profileId*/) {} +void disconnect(const std::array& /*addr*/, int /*profileId*/) {} + +bool isProfileSupported(int /*profileId*/) { return false; } + +void hidHostConnect(const std::array& /*addr*/) {} +void hidHostDisconnect() {} +bool hidHostIsConnected() { return false; } + +bool hidDeviceStart(uint16_t /*appearance*/) { return false; } +void hidDeviceStop() {} + +bool sppStart() { return false; } +void sppStop() {} + +bool midiStart() { return false; } +void midiStop() {} + +void systemStart() {} + +} // namespace tt::bluetooth + +#endif // !CONFIG_BT_NIMBLE_ENABLED diff --git a/Tactility/Source/bluetooth/BluetoothPairedDevice.cpp b/Tactility/Source/bluetooth/BluetoothPairedDevice.cpp new file mode 100644 index 000000000..81802cd06 --- /dev/null +++ b/Tactility/Source/bluetooth/BluetoothPairedDevice.cpp @@ -0,0 +1,117 @@ +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace tt::bluetooth::settings { + +static const auto LOGGER = Logger("BluetoothPairedDevice"); + +// Use the same directory as the old service for backward compatibility. +constexpr auto* DATA_DIR = "/data/service/bluetooth"; +constexpr auto* DEVICE_SETTINGS_FORMAT = "{}/{}.device.properties"; +constexpr auto* KEY_NAME = "name"; +constexpr auto* KEY_ADDR = "addr"; +constexpr auto* KEY_AUTO_CONNECT = "autoConnect"; +constexpr auto* KEY_PROFILE_ID = "profileId"; + +std::string addrToHex(const std::array& addr) { + std::stringstream stream; + stream << std::hex; + for (int i = 0; i < 6; ++i) { + stream << std::setw(2) << std::setfill('0') << static_cast(addr[i]); + } + return stream.str(); +} + +static bool hexToAddr(const std::string& hex, std::array& addr) { + if (hex.size() != 12) { + LOGGER.error("hexToAddr() length mismatch: expected 12, got {}", hex.size()); + return false; + } + char buf[3] = { 0 }; + for (int i = 0; i < 6; ++i) { + buf[0] = hex[i * 2]; + buf[1] = hex[i * 2 + 1]; + char* endptr = nullptr; + addr[i] = static_cast(strtoul(buf, &endptr, 16)); + if (endptr != buf + 2) { + LOGGER.error("hexToAddr() invalid hex at byte {}: '{}{}'", i, buf[0], buf[1]); + return false; + } + } + return true; +} + +static std::string getFilePath(const std::string& addr_hex) { + return std::format(DEVICE_SETTINGS_FORMAT, DATA_DIR, addr_hex); +} + +bool contains(const std::string& addr_hex) { + return file::isFile(getFilePath(addr_hex)); +} + +bool load(const std::string& addr_hex, PairedDevice& device) { + std::map map; + if (!file::loadPropertiesFile(getFilePath(addr_hex), map)) return false; + if (!map.contains(KEY_ADDR)) return false; + if (!hexToAddr(map[KEY_ADDR], device.addr)) return false; + + device.name = map.contains(KEY_NAME) ? map[KEY_NAME] : ""; + + device.autoConnect = !map.contains(KEY_AUTO_CONNECT) || (map[KEY_AUTO_CONNECT] == "true"); + + if (map.contains(KEY_PROFILE_ID)) { + device.profileId = std::stoi(map[KEY_PROFILE_ID]); + } + return true; +} + +bool save(const PairedDevice& device) { + const auto addr_hex = addrToHex(device.addr); + std::map map; + map[KEY_NAME] = device.name; + map[KEY_ADDR] = addr_hex; + map[KEY_AUTO_CONNECT] = device.autoConnect ? "true" : "false"; + map[KEY_PROFILE_ID] = std::to_string(device.profileId); + return file::savePropertiesFile(getFilePath(addr_hex), map); +} + +bool remove(const std::string& addr_hex) { + const auto file_path = getFilePath(addr_hex); + if (!file::isFile(file_path)) return false; + return ::remove(file_path.c_str()) == 0; +} + +std::vector loadAll() { + std::vector entries; + file::scandir(DATA_DIR, entries, [](const dirent* entry) -> int { + if (entry->d_type != file::TT_DT_REG && entry->d_type != file::TT_DT_UNKNOWN) return -1; + std::string name = entry->d_name; + return name.ends_with(".device.properties") ? 0 : -1; + }, nullptr); + + std::vector result; + result.reserve(entries.size()); + for (const auto& entry : entries) { + std::string filename = entry.d_name; + constexpr std::string_view suffix = ".device.properties"; + if (filename.size() <= suffix.size()) continue; + const std::string addr_hex = filename.substr(0, filename.size() - suffix.size()); + PairedDevice device; + if (load(addr_hex, device)) { + result.push_back(std::move(device)); + } + } + return result; +} + +} // namespace tt::bluetooth::settings diff --git a/Tactility/Source/bluetooth/BluetoothSettings.cpp b/Tactility/Source/bluetooth/BluetoothSettings.cpp new file mode 100644 index 000000000..f9f23dc64 --- /dev/null +++ b/Tactility/Source/bluetooth/BluetoothSettings.cpp @@ -0,0 +1,103 @@ +#include + +#include +#include +#include +#include + +namespace tt::bluetooth::settings { + +static const auto LOGGER = Logger("BluetoothSettings"); + +// Use the same path as the old service so existing settings survive migration. +constexpr auto* SETTINGS_PATH = "/data/service/bluetooth/settings.properties"; +constexpr auto* KEY_ENABLE_ON_BOOT = "enableOnBoot"; +constexpr auto* KEY_SPP_AUTO_START = "sppAutoStart"; +constexpr auto* KEY_MIDI_AUTO_START = "midiAutoStart"; + +struct BluetoothSettings { + bool enableOnBoot = false; + bool sppAutoStart = false; + bool midiAutoStart = false; +}; + +static Mutex settings_mutex; +static BluetoothSettings cached; +static bool cached_valid = false; + +static bool load(BluetoothSettings& out) { + std::map map; + if (!file::loadPropertiesFile(SETTINGS_PATH, map)) { + return false; + } + auto it = map.find(KEY_ENABLE_ON_BOOT); + if (it == map.end()) return false; + out.enableOnBoot = (it->second == "true"); + + auto spp_it = map.find(KEY_SPP_AUTO_START); + out.sppAutoStart = (spp_it != map.end() && spp_it->second == "true"); + + auto midi_it = map.find(KEY_MIDI_AUTO_START); + out.midiAutoStart = (midi_it != map.end() && midi_it->second == "true"); + return true; +} + +static bool save(const BluetoothSettings& s) { + std::map map; + file::loadPropertiesFile(SETTINGS_PATH, map); // ignore failure — may not exist yet + map[KEY_ENABLE_ON_BOOT] = s.enableOnBoot ? "true" : "false"; + map[KEY_SPP_AUTO_START] = s.sppAutoStart ? "true" : "false"; + map[KEY_MIDI_AUTO_START] = s.midiAutoStart ? "true" : "false"; + return file::savePropertiesFile(SETTINGS_PATH, map); +} + +static BluetoothSettings getCachedOrLoad() { + settings_mutex.lock(); + if (!cached_valid) { + if (!load(cached)) { + cached = BluetoothSettings{}; + } + cached_valid = true; + } + auto result = cached; + settings_mutex.unlock(); + return result; +} + +void setEnableOnBoot(bool enable) { + settings_mutex.lock(); + cached.enableOnBoot = enable; + cached_valid = true; + settings_mutex.unlock(); + if (!save(cached)) LOGGER.error("Failed to save"); +} + +bool shouldEnableOnBoot() { + return getCachedOrLoad().enableOnBoot; +} + +void setSppAutoStart(bool enable) { + settings_mutex.lock(); + cached.sppAutoStart = enable; + cached_valid = true; + settings_mutex.unlock(); + if (!save(cached)) LOGGER.error("Failed to save (setSppAutoStart)"); +} + +bool shouldSppAutoStart() { + return getCachedOrLoad().sppAutoStart; +} + +void setMidiAutoStart(bool enable) { + settings_mutex.lock(); + cached.midiAutoStart = enable; + cached_valid = true; + settings_mutex.unlock(); + if (!save(cached)) LOGGER.error("Failed to save (setMidiAutoStart)"); +} + +bool shouldMidiAutoStart() { + return getCachedOrLoad().midiAutoStart; +} + +} // namespace tt::bluetooth::settings diff --git a/Tactility/Source/bluetooth/BluetoothSpp.cpp b/Tactility/Source/bluetooth/BluetoothSpp.cpp new file mode 100644 index 000000000..10ac7a045 --- /dev/null +++ b/Tactility/Source/bluetooth/BluetoothSpp.cpp @@ -0,0 +1,31 @@ +#ifdef ESP_PLATFORM +#include +#endif + +#if defined(CONFIG_BT_NIMBLE_ENABLED) + +#include +#include + +#include + +namespace tt::bluetooth { + +bool sppStart() { + struct Device* dev = bluetooth_serial_get_device(); + if (dev == nullptr) return false; + if (bluetooth_serial_start(dev) != ERROR_NONE) return false; + settings::setSppAutoStart(true); + return true; +} + +void sppStop() { + struct Device* dev = bluetooth_serial_get_device(); + if (dev == nullptr) return; + settings::setSppAutoStart(false); + bluetooth_serial_stop(dev); +} + +} // namespace tt::bluetooth + +#endif // CONFIG_BT_NIMBLE_ENABLED diff --git a/Tactility/Source/bluetooth/README.md b/Tactility/Source/bluetooth/README.md new file mode 100644 index 000000000..aac7cd7d6 --- /dev/null +++ b/Tactility/Source/bluetooth/README.md @@ -0,0 +1,52 @@ +# Bluetooth Bridge (Tactility Layer) + +Bridges the kernel `BluetoothApi` driver to the Tactility C++ application layer. + +## Architecture + +``` +Apps / Services + │ tt::bluetooth:: public API (Bluetooth.h) + ▼ +Bluetooth.cpp — registers BtEventCallback, translates events, dispatches I/O +BluetoothSpp.cpp — SPP start/stop (uses bluetooth_serial child device) +BluetoothMidi.cpp — MIDI start/stop (uses bluetooth_midi child device) +BluetoothHidDevice.cpp — HID device start/stop (uses bluetooth_hid_device child device) +BluetoothHidHost.cpp — BLE HID central: GATT discovery, LVGL indev management +BluetoothSettings.cpp / BluetoothPairedDevice.cpp — settings persistence + │ bluetooth_*() public C functions + │ (TactilityKernel/drivers/bluetooth.h + bluetooth_serial/midi/hid_device.h) + ▼ +esp32_ble*.cpp — NimBLE kernel driver (Platforms/platform-esp32) +``` + +## Source Files + +| File | Purpose | +|------|---------| +| `Bluetooth.cpp` | Bridge: event callback, scan cache, public C++ API (`tt::bluetooth::`) | +| `BluetoothSpp.cpp` | SPP start/stop, auto-start setting persistence | +| `BluetoothMidi.cpp` | MIDI start/stop, auto-start setting persistence | +| `BluetoothHidDevice.cpp` | HID device start/stop, appearance → mode mapping | +| `BluetoothHidHost.cpp` | HID host: GATT discovery, report parsing, LVGL indev registration | +| `BluetoothMock.cpp` | No-op stubs for non-BLE builds | +| `BluetoothPairedDevice.cpp` | Paired peer persistence (`.device.properties` files) | +| `BluetoothSettings.cpp` | Global BT settings (`enableOnBoot`, `sppAutoStart`, `midiAutoStart`) | + +## Key Design Points + +- **No direct struct access**: external code calls `bluetooth_scan_start(device)` etc. + (public C API in ``), never the `BluetoothApi` struct. + SPP, MIDI, and HID Device profiles use their own child device headers + (`bluetooth_serial.h`, `bluetooth_midi.h`, `bluetooth_hid_device.h`). +- **File I/O off NimBLE task**: the bridge callback (`bt_event_bridge`) runs on the NimBLE + host task (4 KB stack). All `settings::load/save` calls are dispatched to `getMainDispatcher()`. +- **Scan result cache**: `Bluetooth.cpp` maintains a `std::vector` populated from + `BT_EVENT_PEER_FOUND` events, since the kernel driver does not expose a `get_scan_results()` call. +- **Addr-type cache**: parallel cache of `{addr, addr_type}` entries used by `BluetoothHidHost.cpp` + for `ble_gap_connect()`. +- **Settings paths**: stored under `/data/service/bluetooth/` (legacy path kept for + backward compatibility with existing device data). + +See `Platforms/platform-esp32/source/drivers/bluetooth/README.md` for the driver internals +and `bluetooth.puml` for the thread model diagram. diff --git a/Tactility/Source/lvgl/Keyboard.cpp b/Tactility/Source/lvgl/Keyboard.cpp index ecf7a0cb0..9092731d8 100644 --- a/Tactility/Source/lvgl/Keyboard.cpp +++ b/Tactility/Source/lvgl/Keyboard.cpp @@ -6,6 +6,7 @@ namespace tt::lvgl { static lv_indev_t* keyboard_device = nullptr; +static lv_group_t* pending_keyboard_group = nullptr; void software_keyboard_show(lv_obj_t* textarea) { auto gui_service = service::gui::findService(); @@ -31,12 +32,14 @@ bool software_keyboard_is_enabled() { } void software_keyboard_activate(lv_group_t* group) { + pending_keyboard_group = group; if (keyboard_device != nullptr) { lv_indev_set_group(keyboard_device, group); } } void software_keyboard_deactivate() { + pending_keyboard_group = nullptr; if (keyboard_device != nullptr) { lv_indev_set_group(keyboard_device, nullptr); } @@ -48,6 +51,11 @@ bool hardware_keyboard_is_available() { void hardware_keyboard_set_indev(lv_indev_t* device) { keyboard_device = device; + // If an app already activated a keyboard group while no hardware keyboard was + // connected, apply the pending group now that the device is available. + if (device != nullptr && pending_keyboard_group != nullptr) { + lv_indev_set_group(device, pending_keyboard_group); + } } } diff --git a/Tactility/Source/service/statusbar/Statusbar.cpp b/Tactility/Source/service/statusbar/Statusbar.cpp index 0af4c2007..fa9a156ff 100644 --- a/Tactility/Source/service/statusbar/Statusbar.cpp +++ b/Tactility/Source/service/statusbar/Statusbar.cpp @@ -11,6 +11,10 @@ #include #include #include +#include +#include +#include +#include #include #include #include @@ -57,6 +61,21 @@ static const char* getWifiStatusIcon(wifi::RadioState state) { } } +static const char* getBluetoothStatusIcon(tt::bluetooth::RadioState state, bool scanning, bool connected) { + switch (state) { + using enum tt::bluetooth::RadioState; + case Off: + case OffPending: + return nullptr; // hidden when off + case OnPending: + case On: + if (connected) return LVGL_ICON_STATUSBAR_BLUETOOTH_CONNECTED; + if (scanning) return LVGL_ICON_STATUSBAR_BLUETOOTH_SEARCHING; + return LVGL_ICON_STATUSBAR_BLUETOOTH; + } + return nullptr; +} + static const char* getSdCardStatusIcon(bool mounted) { if (mounted) return LVGL_ICON_STATUSBAR_SD_CARD; return LVGL_ICON_STATUSBAR_SD_CARD_ALERT; @@ -107,6 +126,8 @@ class StatusbarService final : public Service { std::unique_ptr updateTimer; int8_t gps_icon_id; bool gps_last_state = false; + int8_t bt_icon_id; + const char* bt_last_icon = nullptr; int8_t wifi_icon_id; const char* wifi_last_icon = nullptr; int8_t sdcard_icon_id; @@ -136,6 +157,26 @@ class StatusbarService final : public Service { } } + void updateBluetoothIcon() { + auto radio_state = tt::bluetooth::getRadioState(); + struct Device* btdev = tt::bluetooth::findFirstDevice(); + bool scanning = btdev ? bluetooth_is_scanning(btdev) : false; + struct Device* serial_dev = bluetooth_serial_get_device(); + struct Device* midi_dev = bluetooth_midi_get_device(); + bool connected = (serial_dev && bluetooth_serial_is_connected(serial_dev)) || + (midi_dev && bluetooth_midi_is_connected(midi_dev)); + const char* desired_icon = getBluetoothStatusIcon(radio_state, scanning, connected); + if (bt_last_icon != desired_icon) { + if (desired_icon != nullptr) { + lvgl::statusbar_icon_set_image(bt_icon_id, desired_icon); + lvgl::statusbar_icon_set_visibility(bt_icon_id, true); + } else { + lvgl::statusbar_icon_set_visibility(bt_icon_id, false); + } + bt_last_icon = desired_icon; + } + } + void updateWifiIcon() { wifi::RadioState radio_state = wifi::getRadioState(); const char* desired_icon = getWifiStatusIcon(radio_state); @@ -186,6 +227,7 @@ class StatusbarService final : public Service { if (lvgl::isStarted()) { if (lvgl::lock(100)) { updateGpsIcon(); + updateBluetoothIcon(); updateWifiIcon(); updateSdCardIcon(); updatePowerStatusIcon(); @@ -198,6 +240,7 @@ class StatusbarService final : public Service { StatusbarService() { gps_icon_id = lvgl::statusbar_icon_add(); + bt_icon_id = lvgl::statusbar_icon_add(); sdcard_icon_id = lvgl::statusbar_icon_add(); wifi_icon_id = lvgl::statusbar_icon_add(); power_icon_id = lvgl::statusbar_icon_add(); @@ -206,6 +249,7 @@ class StatusbarService final : public Service { ~StatusbarService() override { lvgl::statusbar_icon_remove(wifi_icon_id); lvgl::statusbar_icon_remove(sdcard_icon_id); + lvgl::statusbar_icon_remove(bt_icon_id); lvgl::statusbar_icon_remove(power_icon_id); lvgl::statusbar_icon_remove(gps_icon_id); } diff --git a/TactilityC/Source/tt_init.cpp b/TactilityC/Source/tt_init.cpp index 5a67c9ad0..5f4db1d1a 100644 --- a/TactilityC/Source/tt_init.cpp +++ b/TactilityC/Source/tt_init.cpp @@ -447,6 +447,7 @@ const esp_elfsym main_symbols[] { ESP_ELFSYM_EXPORT(esp_timer_stop), ESP_ELFSYM_EXPORT(esp_timer_delete), ESP_ELFSYM_EXPORT(esp_timer_start_periodic), + ESP_ELFSYM_EXPORT(esp_timer_start_once), // delimiter ESP_ELFSYM_END }; diff --git a/TactilityKernel/include/tactility/drivers/bluetooth.h b/TactilityKernel/include/tactility/drivers/bluetooth.h new file mode 100644 index 000000000..fa376b64a --- /dev/null +++ b/TactilityKernel/include/tactility/drivers/bluetooth.h @@ -0,0 +1,276 @@ +#pragma once + +#include +#include +#include + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +struct Device; +struct DeviceType; + +// ---- Address ---- + +#define BT_ADDR_LEN 6 + +typedef uint8_t BtAddr[BT_ADDR_LEN]; + +// ---- Radio ---- + +enum BtRadioState { + BT_RADIO_STATE_OFF, + BT_RADIO_STATE_ON_PENDING, + BT_RADIO_STATE_ON, + BT_RADIO_STATE_OFF_PENDING, +}; + +// ---- Peer record ---- + +#define BT_NAME_MAX 248 + +struct BtPeerRecord { + BtAddr addr; + /** BLE address type (BLE_ADDR_PUBLIC=0, BLE_ADDR_RANDOM=1, etc.) */ + uint8_t addr_type; + char name[BT_NAME_MAX + 1]; + int8_t rssi; + bool paired; + bool connected; +}; + +// ---- Profile identifiers ---- + +enum BtProfileId { + /** Connect to a BLE HID device (keyboard, mouse, gamepad) */ + BT_PROFILE_HID_HOST, + /** Present this device as a BLE HID peripheral (keyboard, gamepad) */ + BT_PROFILE_HID_DEVICE, + /** BLE SPP serial port (Nordic UART Service / custom GATT) */ + BT_PROFILE_SPP, + /** BLE MIDI (GATT-based) */ + BT_PROFILE_MIDI, +}; + +enum BtProfileState { + BT_PROFILE_STATE_IDLE, + BT_PROFILE_STATE_CONNECTING, + BT_PROFILE_STATE_CONNECTED, + BT_PROFILE_STATE_DISCONNECTING, +}; + +// ---- Events ---- + +enum BtEventType { + /** Radio state changed */ + BT_EVENT_RADIO_STATE_CHANGED, + /** Started scanning for peers */ + BT_EVENT_SCAN_STARTED, + /** Finished scanning for peers */ + BT_EVENT_SCAN_FINISHED, + /** A new peer was found during scan */ + BT_EVENT_PEER_FOUND, + /** Pairing requires user confirmation (passkey displayed or entry required) */ + BT_EVENT_PAIR_REQUEST, + /** Pairing attempt completed */ + BT_EVENT_PAIR_RESULT, + /** A peer's connection state changed */ + BT_EVENT_CONNECT_STATE_CHANGED, + /** A profile's state changed */ + BT_EVENT_PROFILE_STATE_CHANGED, + /** Data was received on the BLE SPP (NUS) RX characteristic */ + BT_EVENT_SPP_DATA_RECEIVED, + /** Data was received on the BLE MIDI I/O characteristic */ + BT_EVENT_MIDI_DATA_RECEIVED, +}; + +enum BtPairResult { + BT_PAIR_RESULT_SUCCESS, + BT_PAIR_RESULT_FAILED, + BT_PAIR_RESULT_REJECTED, + /** Stale bond detected and removed; fresh pairing will follow */ + BT_PAIR_RESULT_BOND_LOST, +}; + +struct BtPairRequestData { + BtAddr addr; + uint32_t passkey; /**< Passkey to display (0 if not applicable) */ + bool needs_confirmation; /**< true: just confirm, false: user must enter passkey */ +}; + +struct BtPairResultData { + BtAddr addr; + enum BtPairResult result; + /** Profile active when pairing completed (BtProfileId value) */ + int profile; +}; + +struct BtProfileStateData { + BtAddr addr; + enum BtProfileId profile; + enum BtProfileState state; +}; + +struct BtEvent { + enum BtEventType type; + union { + enum BtRadioState radio_state; + struct BtPeerRecord peer; + struct BtPairRequestData pair_request; + struct BtPairResultData pair_result; + struct BtProfileStateData profile_state; + }; +}; + +typedef void (*BtEventCallback)(struct Device* device, void* context, struct BtEvent event); + +// ---- Top-level Bluetooth API ---- + +struct BluetoothApi { + /** + * Get the radio state. + * @param[in] device the bluetooth device + * @param[out] state the current radio state + * @return ERROR_NONE on success + */ + error_t (*get_radio_state)(struct Device* device, enum BtRadioState* state); + + /** + * Enable or disable the Bluetooth radio. + * @param[in] device the bluetooth device + * @param[in] enabled true to enable, false to disable + * @return ERROR_NONE on success + */ + error_t (*set_radio_enabled)(struct Device* device, bool enabled); + + /** + * Start scanning for nearby BLE devices. + * @param[in] device the bluetooth device + * @return ERROR_NONE on success + */ + error_t (*scan_start)(struct Device* device); + + /** + * Stop an active scan. + * @param[in] device the bluetooth device + * @return ERROR_NONE on success + */ + error_t (*scan_stop)(struct Device* device); + + /** + * @param[in] device the bluetooth device + * @return true when a scan is in progress + */ + bool (*is_scanning)(struct Device* device); + + /** + * Initiate pairing with a peer. + * @param[in] device the bluetooth device + * @param[in] addr the peer address + * @return ERROR_NONE on success + */ + error_t (*pair)(struct Device* device, const BtAddr addr); + + /** + * Remove a previously paired peer. + * @param[in] device the bluetooth device + * @param[in] addr the peer address + * @return ERROR_NONE on success + */ + error_t (*unpair)(struct Device* device, const BtAddr addr); + + /** + * Get the list of currently paired peers. + * @param[in] device the bluetooth device + * @param[out] out the buffer to write records into (may be NULL to query count only) + * @param[in, out] count in: capacity of out, out: actual number of paired peers + * @return ERROR_NONE on success + */ + error_t (*get_paired_peers)(struct Device* device, struct BtPeerRecord* out, size_t* count); + + /** + * Connect to a peer using the specified profile. + * @param[in] device the bluetooth device + * @param[in] addr the peer address + * @param[in] profile the profile to connect with + * @return ERROR_NONE on success + */ + error_t (*connect)(struct Device* device, const BtAddr addr, enum BtProfileId profile); + + /** + * Disconnect a peer from the specified profile. + * @param[in] device the bluetooth device + * @param[in] addr the peer address + * @param[in] profile the profile to disconnect from + * @return ERROR_NONE on success + */ + error_t (*disconnect)(struct Device* device, const BtAddr addr, enum BtProfileId profile); + + /** + * Add an event callback. + * @param[in] device the bluetooth device + * @param[in] context context pointer passed to the callback + * @param[in] callback the callback function + * @return ERROR_NONE on success + */ + error_t (*add_event_callback)(struct Device* device, void* context, BtEventCallback callback); + + /** + * Remove a previously added event callback. + * @param[in] device the bluetooth device + * @param[in] callback the callback to remove + * @return ERROR_NONE on success + */ + error_t (*remove_event_callback)(struct Device* device, BtEventCallback callback); + + /** + * Notify the driver that a HID host connection is in progress or complete. + * Called by the Tactility HID host module to prevent name resolution from + * initiating a simultaneous central connection (BLE_HS_EALREADY). + * @param[in] device the bluetooth device + * @param[in] active true when HID host is connecting/connected, false when idle + */ + void (*set_hid_host_active)(struct Device* device, bool active); + + /** + * Fire an event through all registered event callbacks. + * Used by the Tactility HID host module to inject profile-state events that + * originate outside the platform driver (e.g. HID host connect/disconnect). + */ + void (*fire_event)(struct Device* device, struct BtEvent event); +}; + +extern const struct DeviceType BLUETOOTH_TYPE; + +// ---- Public C API ---- +// These are the only functions external code should call. +// The BluetoothApi struct above is the internal driver interface only. + +/** + * Find the first ready Bluetooth device. + * Use this instead of referencing BLUETOOTH_TYPE directly from external apps, + * since data symbols may not be exported by the ELF loader. + * @return the first ready Device of BLUETOOTH_TYPE, or NULL if none found. + */ +struct Device* bluetooth_get_device(void); + +error_t bluetooth_get_radio_state(struct Device* device, enum BtRadioState* state); +error_t bluetooth_set_radio_enabled(struct Device* device, bool enabled); +error_t bluetooth_scan_start(struct Device* device); +error_t bluetooth_scan_stop(struct Device* device); +bool bluetooth_is_scanning(struct Device* device); +error_t bluetooth_pair(struct Device* device, const BtAddr addr); +error_t bluetooth_unpair(struct Device* device, const BtAddr addr); +error_t bluetooth_connect(struct Device* device, const BtAddr addr, enum BtProfileId profile); +error_t bluetooth_disconnect(struct Device* device, const BtAddr addr, enum BtProfileId profile); +error_t bluetooth_add_event_callback(struct Device* device, void* context, BtEventCallback callback); +error_t bluetooth_remove_event_callback(struct Device* device, BtEventCallback callback); +void bluetooth_set_hid_host_active(struct Device* device, bool active); +void bluetooth_fire_event(struct Device* device, struct BtEvent event); + +#ifdef __cplusplus +} +#endif diff --git a/TactilityKernel/include/tactility/drivers/bluetooth_hid_device.h b/TactilityKernel/include/tactility/drivers/bluetooth_hid_device.h new file mode 100644 index 000000000..0068e19a3 --- /dev/null +++ b/TactilityKernel/include/tactility/drivers/bluetooth_hid_device.h @@ -0,0 +1,124 @@ +#pragma once + +#include +#include +#include + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +struct Device; +struct DeviceType; + +// ---- HID device mode ---- + +/** + * Selects the HID report descriptor and appearance used when this device + * operates as a BLE HID peripheral. + */ +enum BtHidDeviceMode { + /** Keyboard (report ID 1, 8 bytes) + Consumer (report ID 2, 2 bytes). */ + BT_HID_DEVICE_MODE_KEYBOARD, + /** Mouse only (report ID 1, 4 bytes). */ + BT_HID_DEVICE_MODE_MOUSE, + /** Keyboard + Consumer + Mouse (report IDs 1, 2, 3). */ + BT_HID_DEVICE_MODE_KEYBOARD_MOUSE, + /** Gamepad (report ID 1, 8 bytes: 2-byte buttons + 6-byte axes). */ + BT_HID_DEVICE_MODE_GAMEPAD, +}; + +// ---- BLE HID Device child device ---- + +/** + * BLE HID device profile API (present this device as a BLE HID peripheral). + * This API is exposed by a child device of the Bluetooth device. + */ +struct BtHidDeviceApi { + /** + * Start advertising as a BLE HID device with the given mode. + * Sets up the appropriate GATT report descriptor and starts advertising. + * @param[in] device the HID device child device + * @param[in] mode the HID device mode (keyboard, mouse, gamepad, etc.) + * @return ERROR_NONE on success + */ + error_t (*start)(struct Device* device, enum BtHidDeviceMode mode); + + /** + * Stop advertising as a BLE HID device and close any active connection. + * @param[in] device the HID device child device + * @return ERROR_NONE on success + */ + error_t (*stop)(struct Device* device); + + /** + * Send a single key event when operating as a HID keyboard device. + * @param[in] device the HID device child device + * @param[in] keycode the HID keycode + * @param[in] pressed true for key down, false for key up + * @return ERROR_NONE on success + */ + error_t (*send_key)(struct Device* device, uint8_t keycode, bool pressed); + + /** + * Send a full keyboard HID report (8 bytes: modifier, reserved, keycodes[6]). + * @param[in] device the HID device child device + * @param[in] report pointer to the 8-byte keyboard report + * @param[in] len number of bytes (up to 8) + * @return ERROR_NONE on success + */ + error_t (*send_keyboard)(struct Device* device, const uint8_t* report, size_t len); + + /** + * Send a consumer control HID report (2 bytes: 16-bit usage code, little-endian). + * @param[in] device the HID device child device + * @param[in] report pointer to the 2-byte consumer report + * @param[in] len number of bytes (up to 2) + * @return ERROR_NONE on success + */ + error_t (*send_consumer)(struct Device* device, const uint8_t* report, size_t len); + + /** + * Send a mouse HID report (4 bytes: buttons, X, Y, wheel). + * @param[in] device the HID device child device + * @param[in] report pointer to the 4-byte mouse report + * @param[in] len number of bytes (up to 4) + * @return ERROR_NONE on success + */ + error_t (*send_mouse)(struct Device* device, const uint8_t* report, size_t len); + + /** + * Send a gamepad HID report (8 bytes: buttons[2] + axes[6]). + * @param[in] device the HID device child device + * @param[in] report pointer to the 8-byte gamepad report + * @param[in] len number of bytes (up to 8) + * @return ERROR_NONE on success + */ + error_t (*send_gamepad)(struct Device* device, const uint8_t* report, size_t len); + + /** + * @param[in] device the HID device child device + * @return true when a remote host is connected to this HID device + */ + bool (*is_connected)(struct Device* device); +}; + +extern const struct DeviceType BLUETOOTH_HID_DEVICE_TYPE; + +/** Find the first ready BLE HID device child device. Returns NULL if unavailable. */ +struct Device* bluetooth_hid_device_get_device(void); + +error_t bluetooth_hid_device_start(struct Device* device, enum BtHidDeviceMode mode); +error_t bluetooth_hid_device_stop(struct Device* device); +error_t bluetooth_hid_device_send_key(struct Device* device, uint8_t keycode, bool pressed); +error_t bluetooth_hid_device_send_keyboard(struct Device* device, const uint8_t* report, size_t len); +error_t bluetooth_hid_device_send_consumer(struct Device* device, const uint8_t* report, size_t len); +error_t bluetooth_hid_device_send_mouse(struct Device* device, const uint8_t* report, size_t len); +error_t bluetooth_hid_device_send_gamepad(struct Device* device, const uint8_t* report, size_t len); +bool bluetooth_hid_device_is_connected(struct Device* device); + +#ifdef __cplusplus +} +#endif diff --git a/TactilityKernel/include/tactility/drivers/bluetooth_midi.h b/TactilityKernel/include/tactility/drivers/bluetooth_midi.h new file mode 100644 index 000000000..b5cf44bd5 --- /dev/null +++ b/TactilityKernel/include/tactility/drivers/bluetooth_midi.h @@ -0,0 +1,65 @@ +#pragma once + +#include +#include +#include + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +struct Device; +struct DeviceType; + +// ---- BLE MIDI child device ---- + +/** + * BLE MIDI profile API (MIDI over Bluetooth Low Energy specification). + * This API is exposed by a child device of the Bluetooth device. + */ +struct BtMidiApi { + /** + * Start advertising the BLE MIDI service and accept incoming connections. + * @param[in] device the MIDI child device + * @return ERROR_NONE on success + */ + error_t (*start)(struct Device* device); + + /** + * Stop the BLE MIDI service and close any active connections. + * @param[in] device the MIDI child device + * @return ERROR_NONE on success + */ + error_t (*stop)(struct Device* device); + + /** + * Send MIDI message bytes over the BLE MIDI connection. + * @param[in] device the MIDI child device + * @param[in] msg the raw MIDI bytes + * @param[in] len the number of bytes + * @return ERROR_NONE on success + */ + error_t (*send)(struct Device* device, const uint8_t* msg, size_t len); + + /** + * @param[in] device the MIDI child device + * @return true when a remote device is connected + */ + bool (*is_connected)(struct Device* device); +}; + +extern const struct DeviceType BLUETOOTH_MIDI_TYPE; + +/** Find the first ready BLE MIDI child device. Returns NULL if unavailable. */ +struct Device* bluetooth_midi_get_device(void); + +error_t bluetooth_midi_start(struct Device* device); +error_t bluetooth_midi_stop(struct Device* device); +error_t bluetooth_midi_send(struct Device* device, const uint8_t* msg, size_t len); +bool bluetooth_midi_is_connected(struct Device* device); + +#ifdef __cplusplus +} +#endif diff --git a/TactilityKernel/include/tactility/drivers/bluetooth_serial.h b/TactilityKernel/include/tactility/drivers/bluetooth_serial.h new file mode 100644 index 000000000..d956013a3 --- /dev/null +++ b/TactilityKernel/include/tactility/drivers/bluetooth_serial.h @@ -0,0 +1,77 @@ +#pragma once + +#include +#include +#include + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +struct Device; +struct DeviceType; + +// ---- BLE Serial (Nordic UART Service) child device ---- + +/** + * BLE serial port profile API (Nordic UART Service or equivalent GATT-based SPP). + * This API is exposed by a child device of the Bluetooth device. + */ +struct BtSerialApi { + /** + * Start advertising the BLE serial service and accept incoming connections. + * @param[in] device the serial child device + * @return ERROR_NONE on success + */ + error_t (*start)(struct Device* device); + + /** + * Stop the BLE serial service and close any active connections. + * @param[in] device the serial child device + * @return ERROR_NONE on success + */ + error_t (*stop)(struct Device* device); + + /** + * Write data over the BLE serial connection. + * @param[in] device the serial child device + * @param[in] data the data to send + * @param[in] len the number of bytes to send + * @param[out] written the number of bytes actually written + * @return ERROR_NONE on success + */ + error_t (*write)(struct Device* device, const uint8_t* data, size_t len, size_t* written); + + /** + * Read data from the BLE serial receive buffer. + * @param[in] device the serial child device + * @param[out] data the buffer to read into + * @param[in] max_len the maximum number of bytes to read + * @param[out] read_out the number of bytes actually read + * @return ERROR_NONE on success + */ + error_t (*read)(struct Device* device, uint8_t* data, size_t max_len, size_t* read_out); + + /** + * @param[in] device the serial child device + * @return true when a remote device is connected + */ + bool (*is_connected)(struct Device* device); +}; + +extern const struct DeviceType BLUETOOTH_SERIAL_TYPE; + +/** Find the first ready BLE serial child device. Returns NULL if unavailable. */ +struct Device* bluetooth_serial_get_device(void); + +error_t bluetooth_serial_start(struct Device* device); +error_t bluetooth_serial_stop(struct Device* device); +error_t bluetooth_serial_write(struct Device* device, const uint8_t* data, size_t len, size_t* written); +error_t bluetooth_serial_read(struct Device* device, uint8_t* data, size_t max_len, size_t* read_out); +bool bluetooth_serial_is_connected(struct Device* device); + +#ifdef __cplusplus +} +#endif diff --git a/TactilityKernel/source/drivers/bluetooth.cpp b/TactilityKernel/source/drivers/bluetooth.cpp new file mode 100644 index 000000000..ff418c319 --- /dev/null +++ b/TactilityKernel/source/drivers/bluetooth.cpp @@ -0,0 +1,91 @@ +#include +#include +#include + +#define BT_API(device) ((const struct BluetoothApi*)device_get_driver(device)->api) + +extern "C" { + +// ---- Device lookup ---- + +struct Device* bluetooth_get_device() { + struct Device* found = nullptr; + device_for_each_of_type(&BLUETOOTH_TYPE, &found, [](struct Device* dev, void* ctx) -> bool { + if (device_is_ready(dev)) { + *static_cast(ctx) = dev; + return false; + } + return true; + }); + return found; +} + +// ---- Core radio / scan ---- + +error_t bluetooth_get_radio_state(struct Device* device, enum BtRadioState* state) { + return BT_API(device)->get_radio_state(device, state); +} + +error_t bluetooth_set_radio_enabled(struct Device* device, bool enabled) { + return BT_API(device)->set_radio_enabled(device, enabled); +} + +error_t bluetooth_scan_start(struct Device* device) { + return BT_API(device)->scan_start(device); +} + +error_t bluetooth_scan_stop(struct Device* device) { + return BT_API(device)->scan_stop(device); +} + +bool bluetooth_is_scanning(struct Device* device) { + return BT_API(device)->is_scanning(device); +} + +// ---- Pairing ---- + +error_t bluetooth_pair(struct Device* device, const BtAddr addr) { + return BT_API(device)->pair(device, addr); +} + +error_t bluetooth_unpair(struct Device* device, const BtAddr addr) { + return BT_API(device)->unpair(device, addr); +} + +// ---- Connect / disconnect ---- + +error_t bluetooth_connect(struct Device* device, const BtAddr addr, enum BtProfileId profile) { + return BT_API(device)->connect(device, addr, profile); +} + +error_t bluetooth_disconnect(struct Device* device, const BtAddr addr, enum BtProfileId profile) { + return BT_API(device)->disconnect(device, addr, profile); +} + +// ---- Event callbacks ---- + +error_t bluetooth_add_event_callback(struct Device* device, void* context, BtEventCallback callback) { + return BT_API(device)->add_event_callback(device, context, callback); +} + +error_t bluetooth_remove_event_callback(struct Device* device, BtEventCallback callback) { + return BT_API(device)->remove_event_callback(device, callback); +} + +// ---- HID host active flag ---- + +void bluetooth_set_hid_host_active(struct Device* device, bool active) { + BT_API(device)->set_hid_host_active(device, active); +} + +void bluetooth_fire_event(struct Device* device, struct BtEvent event) { + BT_API(device)->fire_event(device, event); +} + +// ---- Device type ---- + +const struct DeviceType BLUETOOTH_TYPE = { + .name = "bluetooth", +}; + +} // extern "C" diff --git a/TactilityKernel/source/drivers/bluetooth_hid_device.cpp b/TactilityKernel/source/drivers/bluetooth_hid_device.cpp new file mode 100644 index 000000000..6a78e2ed9 --- /dev/null +++ b/TactilityKernel/source/drivers/bluetooth_hid_device.cpp @@ -0,0 +1,57 @@ +#include +#include +#include + +#define BT_HID_DEVICE_API(device) ((const struct BtHidDeviceApi*)device_get_driver(device)->api) + +extern "C" { + +const struct DeviceType BLUETOOTH_HID_DEVICE_TYPE = { + .name = "bluetooth-hid-device", +}; + +struct Device* bluetooth_hid_device_get_device() { + struct Device* found = nullptr; + device_for_each_of_type(&BLUETOOTH_HID_DEVICE_TYPE, &found, [](struct Device* dev, void* ctx) -> bool { + if (device_is_ready(dev)) { + *static_cast(ctx) = dev; + return false; + } + return true; + }); + return found; +} + +error_t bluetooth_hid_device_start(struct Device* device, enum BtHidDeviceMode mode) { + return BT_HID_DEVICE_API(device)->start(device, mode); +} + +error_t bluetooth_hid_device_stop(struct Device* device) { + return BT_HID_DEVICE_API(device)->stop(device); +} + +error_t bluetooth_hid_device_send_key(struct Device* device, uint8_t keycode, bool pressed) { + return BT_HID_DEVICE_API(device)->send_key(device, keycode, pressed); +} + +error_t bluetooth_hid_device_send_keyboard(struct Device* device, const uint8_t* report, size_t len) { + return BT_HID_DEVICE_API(device)->send_keyboard(device, report, len); +} + +error_t bluetooth_hid_device_send_consumer(struct Device* device, const uint8_t* report, size_t len) { + return BT_HID_DEVICE_API(device)->send_consumer(device, report, len); +} + +error_t bluetooth_hid_device_send_mouse(struct Device* device, const uint8_t* report, size_t len) { + return BT_HID_DEVICE_API(device)->send_mouse(device, report, len); +} + +error_t bluetooth_hid_device_send_gamepad(struct Device* device, const uint8_t* report, size_t len) { + return BT_HID_DEVICE_API(device)->send_gamepad(device, report, len); +} + +bool bluetooth_hid_device_is_connected(struct Device* device) { + return BT_HID_DEVICE_API(device)->is_connected(device); +} + +} // extern "C" diff --git a/TactilityKernel/source/drivers/bluetooth_midi.cpp b/TactilityKernel/source/drivers/bluetooth_midi.cpp new file mode 100644 index 000000000..ef753af9d --- /dev/null +++ b/TactilityKernel/source/drivers/bluetooth_midi.cpp @@ -0,0 +1,41 @@ +#include +#include +#include + +#define BT_MIDI_API(device) ((const struct BtMidiApi*)device_get_driver(device)->api) + +extern "C" { + +const struct DeviceType BLUETOOTH_MIDI_TYPE = { + .name = "bluetooth-midi", +}; + +struct Device* bluetooth_midi_get_device() { + struct Device* found = nullptr; + device_for_each_of_type(&BLUETOOTH_MIDI_TYPE, &found, [](struct Device* dev, void* ctx) -> bool { + if (device_is_ready(dev)) { + *static_cast(ctx) = dev; + return false; + } + return true; + }); + return found; +} + +error_t bluetooth_midi_start(struct Device* device) { + return BT_MIDI_API(device)->start(device); +} + +error_t bluetooth_midi_stop(struct Device* device) { + return BT_MIDI_API(device)->stop(device); +} + +error_t bluetooth_midi_send(struct Device* device, const uint8_t* msg, size_t len) { + return BT_MIDI_API(device)->send(device, msg, len); +} + +bool bluetooth_midi_is_connected(struct Device* device) { + return BT_MIDI_API(device)->is_connected(device); +} + +} // extern "C" diff --git a/TactilityKernel/source/drivers/bluetooth_serial.cpp b/TactilityKernel/source/drivers/bluetooth_serial.cpp new file mode 100644 index 000000000..924fa13fc --- /dev/null +++ b/TactilityKernel/source/drivers/bluetooth_serial.cpp @@ -0,0 +1,45 @@ +#include +#include +#include + +#define BT_SERIAL_API(device) ((const struct BtSerialApi*)device_get_driver(device)->api) + +extern "C" { + +const struct DeviceType BLUETOOTH_SERIAL_TYPE = { + .name = "bluetooth-serial", +}; + +struct Device* bluetooth_serial_get_device() { + struct Device* found = nullptr; + device_for_each_of_type(&BLUETOOTH_SERIAL_TYPE, &found, [](struct Device* dev, void* ctx) -> bool { + if (device_is_ready(dev)) { + *static_cast(ctx) = dev; + return false; + } + return true; + }); + return found; +} + +error_t bluetooth_serial_start(struct Device* device) { + return BT_SERIAL_API(device)->start(device); +} + +error_t bluetooth_serial_stop(struct Device* device) { + return BT_SERIAL_API(device)->stop(device); +} + +error_t bluetooth_serial_write(struct Device* device, const uint8_t* data, size_t len, size_t* written) { + return BT_SERIAL_API(device)->write(device, data, len, written); +} + +error_t bluetooth_serial_read(struct Device* device, uint8_t* data, size_t max_len, size_t* read_out) { + return BT_SERIAL_API(device)->read(device, data, max_len, read_out); +} + +bool bluetooth_serial_is_connected(struct Device* device) { + return BT_SERIAL_API(device)->is_connected(device); +} + +} // extern "C" diff --git a/TactilityKernel/source/kernel_symbols.c b/TactilityKernel/source/kernel_symbols.c index 7ed2a8eb4..4bfacdb87 100644 --- a/TactilityKernel/source/kernel_symbols.c +++ b/TactilityKernel/source/kernel_symbols.c @@ -4,6 +4,10 @@ #include #include #include +#include +#include +#include +#include #include #include #include @@ -117,6 +121,48 @@ const struct ModuleSymbol KERNEL_SYMBOLS[] = { DEFINE_MODULE_SYMBOL(uart_controller_get_available), DEFINE_MODULE_SYMBOL(uart_controller_flush_input), DEFINE_MODULE_SYMBOL(UART_CONTROLLER_TYPE), + // drivers/bluetooth + DEFINE_MODULE_SYMBOL(bluetooth_get_device), + DEFINE_MODULE_SYMBOL(bluetooth_get_radio_state), + DEFINE_MODULE_SYMBOL(bluetooth_set_radio_enabled), + DEFINE_MODULE_SYMBOL(bluetooth_scan_start), + DEFINE_MODULE_SYMBOL(bluetooth_scan_stop), + DEFINE_MODULE_SYMBOL(bluetooth_is_scanning), + DEFINE_MODULE_SYMBOL(bluetooth_pair), + DEFINE_MODULE_SYMBOL(bluetooth_unpair), + DEFINE_MODULE_SYMBOL(bluetooth_connect), + DEFINE_MODULE_SYMBOL(bluetooth_disconnect), + DEFINE_MODULE_SYMBOL(bluetooth_add_event_callback), + DEFINE_MODULE_SYMBOL(bluetooth_remove_event_callback), + DEFINE_MODULE_SYMBOL(bluetooth_set_hid_host_active), + DEFINE_MODULE_SYMBOL(bluetooth_fire_event), + DEFINE_MODULE_SYMBOL(BLUETOOTH_TYPE), + // drivers/bluetooth_serial + DEFINE_MODULE_SYMBOL(bluetooth_serial_get_device), + DEFINE_MODULE_SYMBOL(bluetooth_serial_start), + DEFINE_MODULE_SYMBOL(bluetooth_serial_stop), + DEFINE_MODULE_SYMBOL(bluetooth_serial_write), + DEFINE_MODULE_SYMBOL(bluetooth_serial_read), + DEFINE_MODULE_SYMBOL(bluetooth_serial_is_connected), + DEFINE_MODULE_SYMBOL(BLUETOOTH_SERIAL_TYPE), + // drivers/bluetooth_midi + DEFINE_MODULE_SYMBOL(bluetooth_midi_get_device), + DEFINE_MODULE_SYMBOL(bluetooth_midi_start), + DEFINE_MODULE_SYMBOL(bluetooth_midi_stop), + DEFINE_MODULE_SYMBOL(bluetooth_midi_send), + DEFINE_MODULE_SYMBOL(bluetooth_midi_is_connected), + DEFINE_MODULE_SYMBOL(BLUETOOTH_MIDI_TYPE), + // drivers/bluetooth_hid_device + DEFINE_MODULE_SYMBOL(bluetooth_hid_device_get_device), + DEFINE_MODULE_SYMBOL(bluetooth_hid_device_start), + DEFINE_MODULE_SYMBOL(bluetooth_hid_device_stop), + DEFINE_MODULE_SYMBOL(bluetooth_hid_device_send_key), + DEFINE_MODULE_SYMBOL(bluetooth_hid_device_send_keyboard), + DEFINE_MODULE_SYMBOL(bluetooth_hid_device_send_consumer), + DEFINE_MODULE_SYMBOL(bluetooth_hid_device_send_mouse), + DEFINE_MODULE_SYMBOL(bluetooth_hid_device_send_gamepad), + DEFINE_MODULE_SYMBOL(bluetooth_hid_device_is_connected), + DEFINE_MODULE_SYMBOL(BLUETOOTH_HID_DEVICE_TYPE), // concurrent/dispatcher DEFINE_MODULE_SYMBOL(dispatcher_alloc), DEFINE_MODULE_SYMBOL(dispatcher_free), diff --git a/device.py b/device.py index 599007194..7f0f3f698 100644 --- a/device.py +++ b/device.py @@ -307,6 +307,20 @@ def write_usb_variables(output_file, device_properties: ConfigParser): output_file.write("CONFIG_TINYUSB_MSC_ENABLED=y\n") output_file.write("CONFIG_TINYUSB_MSC_MOUNT_PATH=\"/sdcard\"\n") +def write_bluetooth_variables(output_file, device_properties: ConfigParser): + has_bluetooth = get_boolean_property_or_false(device_properties, "hardware", "bluetooth") + if has_bluetooth: + output_file.write("# Bluetooth (NimBLE)\n") + output_file.write("CONFIG_BT_ENABLED=y\n") + output_file.write("CONFIG_BT_NIMBLE_ENABLED=y\n") + output_file.write("CONFIG_BT_NIMBLE_ROLE_CENTRAL=y\n") + output_file.write("CONFIG_BT_NIMBLE_ROLE_PERIPHERAL=y\n") + output_file.write("CONFIG_BT_BLE_42_FEATURES_SUPPORTED=y\n") + # Increase NimBLE host task stack from the 4096-byte default. + # GAP/GATT event processing + C++ frames push the default over the limit, + # causing stack-protection faults on events like BLE_GAP_EVENT_SUBSCRIBE. + output_file.write("CONFIG_BT_NIMBLE_HOST_TASK_STACK_SIZE=8192\n") + def write_custom_sdkconfig(output_file, device_properties: ConfigParser): if "sdkconfig" in device_properties.sections(): output_file.write("# Custom\n") @@ -325,6 +339,7 @@ def write_properties(output_file, device_properties: ConfigParser, device_id: st write_spiram_variables(output_file, device_properties) write_performance_improvements(output_file, device_properties) write_usb_variables(output_file, device_properties) + write_bluetooth_variables(output_file, device_properties) write_custom_sdkconfig(output_file, device_properties) write_lvgl_variables(output_file, device_properties) From c4afdc1eb1330f3e47a66796753cf4f83b90f415 Mon Sep 17 00:00:00 2001 From: Shadowtrance Date: Thu, 2 Apr 2026 03:17:57 +1000 Subject: [PATCH 02/12] fixes --- Devices/m5stack-tab5/device.properties | 2 -- Firmware/CMakeLists.txt | 8 ++++++++ device.py | 7 ++++--- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/Devices/m5stack-tab5/device.properties b/Devices/m5stack-tab5/device.properties index ba5416e4c..4c1ecf80f 100644 --- a/Devices/m5stack-tab5/device.properties +++ b/Devices/m5stack-tab5/device.properties @@ -42,5 +42,3 @@ CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_D1_4BIT_BUS_SLOT_1=10 CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_D2_4BIT_BUS_SLOT_1=9 CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_D3_4BIT_BUS_SLOT_1=8 CONFIG_ESP_HOSTED_SDIO_GPIO_RESET_SLAVE=15 -CONFIG_BT_NIMBLE_TRANSPORT_UART=n -CONFIG_ESP_HOSTED_ENABLE_BT_NIMBLE=y diff --git a/Firmware/CMakeLists.txt b/Firmware/CMakeLists.txt index 27d739be4..a344f7027 100644 --- a/Firmware/CMakeLists.txt +++ b/Firmware/CMakeLists.txt @@ -18,6 +18,14 @@ endif () set(DEVICETREE_LOCATION "${PROJECT_ROOT}/Devices/${TACTILITY_DEVICE_ID}") +# Check if device has Bluetooth enabled +if (DEFINED ENV{ESP_IDF_VERSION}) + file(READ "${DEVICETREE_LOCATION}/device.properties" device_properties_content) + if (device_properties_content MATCHES "bluetooth=true") + list(APPEND REQUIRES_LIST bt) + endif() +endif() + # # DTS compiler python dependencies # diff --git a/device.py b/device.py index 7f0f3f698..a8855b202 100644 --- a/device.py +++ b/device.py @@ -308,14 +308,15 @@ def write_usb_variables(output_file, device_properties: ConfigParser): output_file.write("CONFIG_TINYUSB_MSC_MOUNT_PATH=\"/sdcard\"\n") def write_bluetooth_variables(output_file, device_properties: ConfigParser): + idf_target = get_property_or_exit(device_properties, "hardware", "target").lower() has_bluetooth = get_boolean_property_or_false(device_properties, "hardware", "bluetooth") if has_bluetooth: output_file.write("# Bluetooth (NimBLE)\n") output_file.write("CONFIG_BT_ENABLED=y\n") output_file.write("CONFIG_BT_NIMBLE_ENABLED=y\n") - output_file.write("CONFIG_BT_NIMBLE_ROLE_CENTRAL=y\n") - output_file.write("CONFIG_BT_NIMBLE_ROLE_PERIPHERAL=y\n") - output_file.write("CONFIG_BT_BLE_42_FEATURES_SUPPORTED=y\n") + if idf_target == "esp32p4": + output_file.write(f"CONFIG_BT_NIMBLE_TRANSPORT_UART=n\n") + output_file.write(f"CONFIG_ESP_HOSTED_ENABLE_BT_NIMBLE=y\n") # Increase NimBLE host task stack from the 4096-byte default. # GAP/GATT event processing + C++ frames push the default over the limit, # causing stack-protection faults on events like BLE_GAP_EVENT_SUBSCRIBE. From 4c9bff9519e9efdf94b1378c2ac3c92e5e8e0f90 Mon Sep 17 00:00:00 2001 From: Shadowtrance Date: Thu, 2 Apr 2026 04:33:04 +1000 Subject: [PATCH 03/12] use the psram! helps a little on S3 (t-deck) --- device.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/device.py b/device.py index a8855b202..bf319116a 100644 --- a/device.py +++ b/device.py @@ -317,6 +317,16 @@ def write_bluetooth_variables(output_file, device_properties: ConfigParser): if idf_target == "esp32p4": output_file.write(f"CONFIG_BT_NIMBLE_TRANSPORT_UART=n\n") output_file.write(f"CONFIG_ESP_HOSTED_ENABLE_BT_NIMBLE=y\n") + # Move NimBLE host buffers to SPIRAM when available, regardless of target. + # The default (INTERNAL) mode causes heap fragmentation after a disable+deinit + # cycle, preventing a subsequent nimble_port_init() from allocating its buffers + # ("hci inits failed" / rc=-1). EXTERNAL mode uses SPIRAM, which is much larger + # and does not suffer from the same fragmentation — enabling reliable re-init. + # Also frees significant internal RAM on memory-constrained targets (e.g. S3). + # Dependency: CONFIG_SPIRAM_USE_CAPS_ALLOC || CONFIG_SPIRAM_USE_MALLOC (set by write_spiram_variables). + has_spiram = get_boolean_property_or_false(device_properties, "hardware", "spiRam") + if has_spiram: + output_file.write("CONFIG_BT_NIMBLE_MEM_ALLOC_MODE_EXTERNAL=y\n") # Increase NimBLE host task stack from the 4096-byte default. # GAP/GATT event processing + C++ frames push the default over the limit, # causing stack-protection faults on events like BLE_GAP_EVENT_SUBSCRIBE. From ec26757114f7d530a610c66099d4f539c706e3a2 Mon Sep 17 00:00:00 2001 From: Shadowtrance Date: Thu, 2 Apr 2026 17:17:02 +1000 Subject: [PATCH 04/12] custom device name --- .../private/bluetooth/esp32_ble_internal.h | 3 ++ .../source/drivers/bluetooth/esp32_ble.cpp | 43 ++++++++++++++++++- .../include/tactility/drivers/bluetooth.h | 30 +++++++++++++ TactilityKernel/source/drivers/bluetooth.cpp | 8 ++++ TactilityKernel/source/kernel_symbols.c | 2 + device.py | 3 ++ 6 files changed, 88 insertions(+), 1 deletion(-) diff --git a/Platforms/platform-esp32/private/bluetooth/esp32_ble_internal.h b/Platforms/platform-esp32/private/bluetooth/esp32_ble_internal.h index 700126474..e7fdb9ac5 100644 --- a/Platforms/platform-esp32/private/bluetooth/esp32_ble_internal.h +++ b/Platforms/platform-esp32/private/bluetooth/esp32_ble_internal.h @@ -86,6 +86,9 @@ struct BleCtx { // nimble_port_stop() must not be called from the NimBLE host task itself. esp_timer_handle_t disable_timer; + // BLE device name (set before or after radio enable; applied in dispatch_enable) + char device_name[BLE_DEVICE_NAME_MAX + 1]; + // Device reference (passed to BtEventCallback) struct Device* device; diff --git a/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble.cpp b/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble.cpp index 66134e514..f1b60542a 100644 --- a/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble.cpp +++ b/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble.cpp @@ -417,6 +417,10 @@ static void ble_host_task(void* param) { void ble_start_advertising(const ble_uuid128_t* svc_uuid) { ble_gap_adv_stop(); + // Always sync the GAP name from ctx right before building the advertising packet, + // so bluetooth_set_device_name() is honoured regardless of call timing. + if (g_ctx) ble_svc_gap_device_name_set(g_ctx->device_name); + int rc; if (svc_uuid != nullptr) { const char* name = ble_svc_gap_device_name(); @@ -492,6 +496,9 @@ void ble_start_advertising(const ble_uuid128_t* svc_uuid) { void ble_start_advertising_hid(uint16_t appearance) { ble_gap_adv_stop(); + // Always sync the GAP name from ctx right before building the advertising packet. + if (g_ctx) ble_svc_gap_device_name_set(g_ctx->device_name); + const char* name = ble_svc_gap_device_name(); uint8_t name_len = (uint8_t)strlen(name); uint8_t short_len = (name_len > 8) ? 8 : name_len; @@ -598,7 +605,7 @@ static void dispatch_enable(BleCtx* ctx) { // Register base GATT services (NUS + MIDI; HID added by switch_profile when started) ble_hid_device_init_gatt(); - ble_svc_gap_device_name_set(CONFIG_TT_DEVICE_NAME); + ble_svc_gap_device_name_set(ctx->device_name); ble_att_set_preferred_mtu(BLE_ATT_MTU_MAX); // Start NimBLE host task (on_sync will fire when ready) @@ -821,6 +828,33 @@ static error_t api_remove_event_callback(struct Device* device, BtEventCallback return ERROR_NOT_FOUND; } +static error_t api_set_device_name(struct Device* device, const char* name) { + BleCtx* ctx = (BleCtx*)device_get_driver_data(device); + if (!ctx || !name) return ERROR_INVALID_ARGUMENT; + if (strlen(name) > BLE_DEVICE_NAME_MAX) return ERROR_INVALID_ARGUMENT; + xSemaphoreTake(ctx->radio_mutex, portMAX_DELAY); + strncpy(ctx->device_name, name, BLE_DEVICE_NAME_MAX); + ctx->device_name[BLE_DEVICE_NAME_MAX] = '\0'; + if (ctx->radio_state.load() == BT_RADIO_STATE_ON) { + ble_svc_gap_device_name_set(ctx->device_name); + // Restart advertising so the new name is broadcast immediately. + // ble_schedule_adv_restart checks active profiles; no-op if nothing is advertising. + ble_schedule_adv_restart(ctx, 0); + } + xSemaphoreGive(ctx->radio_mutex); + return ERROR_NONE; +} + +static error_t api_get_device_name(struct Device* device, char* buf, size_t buf_len) { + BleCtx* ctx = (BleCtx*)device_get_driver_data(device); + if (!ctx || !buf || buf_len == 0) return ERROR_INVALID_ARGUMENT; + xSemaphoreTake(ctx->radio_mutex, portMAX_DELAY); + strncpy(buf, ctx->device_name, buf_len - 1); + buf[buf_len - 1] = '\0'; + xSemaphoreGive(ctx->radio_mutex); + return ERROR_NONE; +} + static void api_set_hid_host_active(struct Device* device, bool active) { BleCtx* ctx = (BleCtx*)device_get_driver_data(device); if (ctx) ctx->hid_host_active.store(active); @@ -846,6 +880,8 @@ const BluetoothApi nimble_bluetooth_api = { .disconnect = api_disconnect, .add_event_callback = api_add_event_callback, .remove_event_callback = api_remove_event_callback, + .set_device_name = api_set_device_name, + .get_device_name = api_get_device_name, .set_hid_host_active = api_set_hid_host_active, .fire_event = api_fire_event, }; @@ -940,6 +976,11 @@ static error_t esp32_ble_start_device(struct Device* device) { ctx->midi_keepalive_timer = nullptr; ctx->adv_restart_timer = nullptr; ctx->disable_timer = nullptr; + // Device name: prefer the Kconfig-supplied TT_DEVICE_NAME, fall back to "Tactility" + const char* cfg_name = CONFIG_TT_DEVICE_NAME; + if (cfg_name == nullptr || cfg_name[0] == '\0') cfg_name = "Tactility"; + strncpy(ctx->device_name, cfg_name, BLE_DEVICE_NAME_MAX); + ctx->device_name[BLE_DEVICE_NAME_MAX] = '\0'; ctx->device = device; ctx->serial_child = nullptr; ctx->midi_child = nullptr; diff --git a/TactilityKernel/include/tactility/drivers/bluetooth.h b/TactilityKernel/include/tactility/drivers/bluetooth.h index fa376b64a..6ab48b38a 100644 --- a/TactilityKernel/include/tactility/drivers/bluetooth.h +++ b/TactilityKernel/include/tactility/drivers/bluetooth.h @@ -13,6 +13,15 @@ extern "C" { struct Device; struct DeviceType; +// ---- Device name ---- + +/** + * Maximum BLE device name length in bytes, excluding the NUL terminator. + * Must match CONFIG_BT_NIMBLE_GAP_DEVICE_NAME_MAX_LEN (set in device.py for BT devices). + * ble_svc_gap_device_name_set() returns BLE_HS_EINVAL for names longer than this. + */ +#define BLE_DEVICE_NAME_MAX 64 + // ---- Address ---- #define BT_ADDR_LEN 6 @@ -226,6 +235,25 @@ struct BluetoothApi { */ error_t (*remove_event_callback)(struct Device* device, BtEventCallback callback); + /** + * Set the BLE device name used in advertising and the GAP service. + * Can be called before or after the radio is enabled. + * If called while advertising is active, advertising restarts with the new name. + * @param[in] device the bluetooth device + * @param[in] name NUL-terminated name (max BLE_DEVICE_NAME_MAX bytes) + * @return ERROR_NONE on success, ERROR_INVALID_ARGUMENT if name is too long or NULL + */ + error_t (*set_device_name)(struct Device* device, const char* name); + + /** + * Get the current BLE device name. + * @param[in] device the bluetooth device + * @param[out] buf buffer to write the name into + * @param[in] buf_len size of buf (must be >= BLE_DEVICE_NAME_MAX + 1) + * @return ERROR_NONE on success + */ + error_t (*get_device_name)(struct Device* device, char* buf, size_t buf_len); + /** * Notify the driver that a HID host connection is in progress or complete. * Called by the Tactility HID host module to prevent name resolution from @@ -268,6 +296,8 @@ error_t bluetooth_connect(struct Device* device, const BtAddr addr, enum BtProfi error_t bluetooth_disconnect(struct Device* device, const BtAddr addr, enum BtProfileId profile); error_t bluetooth_add_event_callback(struct Device* device, void* context, BtEventCallback callback); error_t bluetooth_remove_event_callback(struct Device* device, BtEventCallback callback); +error_t bluetooth_set_device_name(struct Device* device, const char* name); +error_t bluetooth_get_device_name(struct Device* device, char* buf, size_t buf_len); void bluetooth_set_hid_host_active(struct Device* device, bool active); void bluetooth_fire_event(struct Device* device, struct BtEvent event); diff --git a/TactilityKernel/source/drivers/bluetooth.cpp b/TactilityKernel/source/drivers/bluetooth.cpp index ff418c319..76040c9ac 100644 --- a/TactilityKernel/source/drivers/bluetooth.cpp +++ b/TactilityKernel/source/drivers/bluetooth.cpp @@ -72,6 +72,14 @@ error_t bluetooth_remove_event_callback(struct Device* device, BtEventCallback c return BT_API(device)->remove_event_callback(device, callback); } +error_t bluetooth_set_device_name(struct Device* device, const char* name) { + return BT_API(device)->set_device_name(device, name); +} + +error_t bluetooth_get_device_name(struct Device* device, char* buf, size_t buf_len) { + return BT_API(device)->get_device_name(device, buf, buf_len); +} + // ---- HID host active flag ---- void bluetooth_set_hid_host_active(struct Device* device, bool active) { diff --git a/TactilityKernel/source/kernel_symbols.c b/TactilityKernel/source/kernel_symbols.c index 4bfacdb87..f48caaa70 100644 --- a/TactilityKernel/source/kernel_symbols.c +++ b/TactilityKernel/source/kernel_symbols.c @@ -134,6 +134,8 @@ const struct ModuleSymbol KERNEL_SYMBOLS[] = { DEFINE_MODULE_SYMBOL(bluetooth_disconnect), DEFINE_MODULE_SYMBOL(bluetooth_add_event_callback), DEFINE_MODULE_SYMBOL(bluetooth_remove_event_callback), + DEFINE_MODULE_SYMBOL(bluetooth_set_device_name), + DEFINE_MODULE_SYMBOL(bluetooth_get_device_name), DEFINE_MODULE_SYMBOL(bluetooth_set_hid_host_active), DEFINE_MODULE_SYMBOL(bluetooth_fire_event), DEFINE_MODULE_SYMBOL(BLUETOOTH_TYPE), diff --git a/device.py b/device.py index bf319116a..09a7bf576 100644 --- a/device.py +++ b/device.py @@ -327,6 +327,9 @@ def write_bluetooth_variables(output_file, device_properties: ConfigParser): has_spiram = get_boolean_property_or_false(device_properties, "hardware", "spiRam") if has_spiram: output_file.write("CONFIG_BT_NIMBLE_MEM_ALLOC_MODE_EXTERNAL=y\n") + # Expand NimBLE's GAP device name buffer to match BLE_DEVICE_NAME_MAX. + # The default (31) is too short for some device names and leaves no headroom. + output_file.write("CONFIG_BT_NIMBLE_GAP_DEVICE_NAME_MAX_LEN=64\n") # Increase NimBLE host task stack from the 4096-byte default. # GAP/GATT event processing + C++ frames push the default over the limit, # causing stack-protection faults on events like BLE_GAP_EVENT_SUBSCRIBE. From bfde0702823a269679b404530d14d1b16914c98b Mon Sep 17 00:00:00 2001 From: Shadowtrance Date: Thu, 2 Apr 2026 18:23:24 +1000 Subject: [PATCH 05/12] Update symbols.c --- Modules/lvgl-module/source/symbols.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Modules/lvgl-module/source/symbols.c b/Modules/lvgl-module/source/symbols.c index 319021f05..f3aa14fab 100644 --- a/Modules/lvgl-module/source/symbols.c +++ b/Modules/lvgl-module/source/symbols.c @@ -400,6 +400,8 @@ const struct ModuleSymbol lvgl_module_symbols[] = { DEFINE_MODULE_SYMBOL(lv_tabview_create), DEFINE_MODULE_SYMBOL(lv_tabview_set_tab_bar_position), DEFINE_MODULE_SYMBOL(lv_tabview_set_tab_bar_size), + DEFINE_MODULE_SYMBOL(lv_tabview_get_tab_active), + DEFINE_MODULE_SYMBOL(lv_tabview_get_content), // lv_screen DEFINE_MODULE_SYMBOL(lv_scr_act), DEFINE_MODULE_SYMBOL(lv_screen_active), From 87868df954e8c165a0bd575bdfe42a580e7cff32 Mon Sep 17 00:00:00 2001 From: Shadowtrance Date: Fri, 3 Apr 2026 11:24:06 +1000 Subject: [PATCH 06/12] Feedback + fixes Fixes external app start/stop server (child devices) Fixes BtManage causing a full system hang upon disabling bt when a device is connected to the host. --- .../private/bluetooth/esp32_ble_internal.h | 33 +++-- .../source/drivers/bluetooth/README.md | 2 +- .../source/drivers/bluetooth/esp32_ble.cpp | 130 +++++++++++------- .../bluetooth/esp32_ble_hid_device.cpp | 41 +++--- .../drivers/bluetooth/esp32_ble_midi.cpp | 33 +++-- .../drivers/bluetooth/esp32_ble_scan.cpp | 34 +++-- .../drivers/bluetooth/esp32_ble_spp.cpp | 31 +++-- Tactility/Source/app/btmanage/BtManage.cpp | 10 +- 8 files changed, 188 insertions(+), 126 deletions(-) diff --git a/Platforms/platform-esp32/private/bluetooth/esp32_ble_internal.h b/Platforms/platform-esp32/private/bluetooth/esp32_ble_internal.h index e7fdb9ac5..296c16d5a 100644 --- a/Platforms/platform-esp32/private/bluetooth/esp32_ble_internal.h +++ b/Platforms/platform-esp32/private/bluetooth/esp32_ble_internal.h @@ -98,38 +98,41 @@ struct BleCtx { struct Device* hid_device_child; }; -// ---- Global context pointer ---- -// Set in start_device; used by NimBLE callbacks that cannot receive a Device* argument. -extern BleCtx* g_ctx; +// ---- Context accessor (defined in esp32_ble.cpp) ---- +// Retrieves the BleCtx stored in the device driver-data slot. +// All cross-module functions accept Device* and call this internally. +BleCtx* ble_get_ctx(struct Device* device); // ---- Event publishing ---- -void ble_publish_event(BleCtx* ctx, struct BtEvent event); +void ble_publish_event(struct Device* device, struct BtEvent event); // ---- Advertising helpers (defined in esp32_ble.cpp) ---- -void ble_start_advertising(const ble_uuid128_t* svc_uuid); // svc_uuid=nullptr → name-only -void ble_start_advertising_hid(uint16_t appearance); -void ble_schedule_adv_restart(BleCtx* ctx, uint64_t delay_us); +void ble_start_advertising(struct Device* device, const ble_uuid128_t* svc_uuid); // svc_uuid=nullptr → name-only +void ble_start_advertising_hid(struct Device* device, uint16_t appearance); +void ble_schedule_adv_restart(struct Device* device, uint64_t delay_us); // ---- GAP scan callback (defined in esp32_ble_scan.cpp) ---- int ble_gap_disc_event_handler(struct ble_gap_event* event, void* arg); -void ble_resolve_next_unnamed_peer(BleCtx* ctx, size_t start_idx); +void ble_resolve_next_unnamed_peer(struct Device* device, size_t start_idx); // ---- SPP GATT (defined in esp32_ble_spp.cpp) ---- -void ble_spp_init_gatt_handles(BleCtx* ctx); -error_t ble_spp_start_internal(BleCtx* ctx); +void ble_spp_init_gatt_handles(struct Device* device); +error_t ble_spp_start_internal(struct Device* device); // ---- MIDI GATT (defined in esp32_ble_midi.cpp) ---- -void ble_midi_init_gatt_handles(BleCtx* ctx); -error_t ble_midi_start_internal(BleCtx* ctx); +void ble_midi_init_gatt_handles(struct Device* device); +error_t ble_midi_start_internal(struct Device* device); // ---- HID device GATT (defined in esp32_ble_hid_device.cpp) ---- void ble_hid_device_init_gatt(); void ble_hid_device_init_gatt_handles(); -void ble_hid_device_switch_profile(BleCtx* ctx, BleHidProfile profile); +void ble_hid_device_switch_profile(struct Device* device, BleHidProfile profile); // ---- Cross-module GATT char / service arrays ---- -extern const struct ble_gatt_chr_def nus_chars_with_handle[]; // esp32_ble_spp.cpp -extern const struct ble_gatt_chr_def midi_chars[]; // esp32_ble_midi.cpp +// Non-const: the .arg field is set to the parent Device* at init time so that +// NimBLE access callbacks can retrieve the context without a global pointer. +extern struct ble_gatt_chr_def nus_chars_with_handle[]; // esp32_ble_spp.cpp +extern struct ble_gatt_chr_def midi_chars[]; // esp32_ble_midi.cpp // ---- Cross-module service UUIDs ---- extern const ble_uuid128_t NUS_SVC_UUID; // esp32_ble_spp.cpp diff --git a/Platforms/platform-esp32/source/drivers/bluetooth/README.md b/Platforms/platform-esp32/source/drivers/bluetooth/README.md index d821bfdfc..3974ebe55 100644 --- a/Platforms/platform-esp32/source/drivers/bluetooth/README.md +++ b/Platforms/platform-esp32/source/drivers/bluetooth/README.md @@ -25,7 +25,7 @@ SPP, MIDI, and HID Device profiles are exposed as **child devices** created by `esp32_ble_start_device()`. Each gets its own `DeviceType` and is found at runtime via `bluetooth_serial_get_device()`, `bluetooth_midi_get_device()`, or `bluetooth_hid_device_get_device()`. Their drivers have `start_device=nullptr` since -initialization is handled by the parent driver; they share the global `BleCtx` (`g_ctx`). +initialization is handled by the parent driver; they obtain the shared `BleCtx` via `ble_get_ctx(device)`. ## Profiles diff --git a/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble.cpp b/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble.cpp index f1b60542a..6108168dd 100644 --- a/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble.cpp +++ b/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble.cpp @@ -32,9 +32,23 @@ // ble_store_config_init() is not declared in the public header in some IDF versions. extern "C" void ble_store_config_init(void); -// ---- Global context pointer ---- -// Set in start_device(); used by NimBLE callbacks that cannot receive a Device*. -BleCtx* g_ctx = nullptr; +// ---- Context accessor ---- + +BleCtx* ble_get_ctx(struct Device* device) { + void* data = device_get_driver_data(device); + if (data == nullptr) { + // Child devices (serial, midi, hid-device) have no driver data of their own — + // their context lives in the parent BLE device. + struct Device* parent = device_get_parent(device); + if (parent != nullptr) data = device_get_driver_data(parent); + } + return (BleCtx*)data; +} + +// File-static device pointer used only by NimBLE host callbacks whose signature +// is fixed by the NimBLE API (on_sync, on_reset) and cannot carry a Device*. +// All other callbacks receive Device* via their void* arg parameter. +static struct Device* s_device = nullptr; // ---- Forward declarations ---- static void ble_host_task(void* param); @@ -46,7 +60,8 @@ static int gap_event_handler(struct ble_gap_event* event, void* arg); // ---- Event publishing ---- -void ble_publish_event(BleCtx* ctx, struct BtEvent event) { +void ble_publish_event(struct Device* device, struct BtEvent event) { + BleCtx* ctx = ble_get_ctx(device); // Copy under mutex so callbacks can safely call add/remove_event_callback BleCallbackEntry local[BLE_MAX_CALLBACKS]; size_t count; @@ -61,26 +76,29 @@ void ble_publish_event(BleCtx* ctx, struct BtEvent event) { // ---- Advertising restart helper ---- -static void adv_restart_callback(void* /*arg*/) { - BleCtx* ctx = g_ctx; +static void adv_restart_callback(void* arg) { + struct Device* device = (struct Device*)arg; + BleCtx* ctx = ble_get_ctx(device); if (ctx == nullptr || ctx->radio_state.load() != BT_RADIO_STATE_ON) return; if (ctx->midi_active.load() && ctx->midi_conn_handle.load() == BLE_HS_CONN_HANDLE_NONE) { - ble_start_advertising(&MIDI_SVC_UUID); + ble_start_advertising(device, &MIDI_SVC_UUID); } else if (ctx->spp_active.load() && ctx->spp_conn_handle.load() == BLE_HS_CONN_HANDLE_NONE) { - ble_start_advertising(&NUS_SVC_UUID); + ble_start_advertising(device, &NUS_SVC_UUID); } else if (ctx->hid_active.load() && ctx->hid_conn_handle.load() == BLE_HS_CONN_HANDLE_NONE) { - ble_start_advertising_hid(hid_appearance); + ble_start_advertising_hid(device, hid_appearance); } } -void ble_schedule_adv_restart(BleCtx* ctx, uint64_t delay_us) { +void ble_schedule_adv_restart(struct Device* device, uint64_t delay_us) { + BleCtx* ctx = ble_get_ctx(device); if (delay_us == 0) { - adv_restart_callback(nullptr); + adv_restart_callback(device); return; } if (ctx->adv_restart_timer == nullptr) { esp_timer_create_args_t args = {}; args.callback = adv_restart_callback; + args.arg = device; args.dispatch_method = ESP_TIMER_TASK; args.name = "ble_adv_restart"; int rc = esp_timer_create(&args, &ctx->adv_restart_timer); @@ -99,7 +117,8 @@ void ble_schedule_adv_restart(BleCtx* ctx, uint64_t delay_us) { // ---- GAP connection event handler ---- static int gap_event_handler(struct ble_gap_event* event, void* arg) { - BleCtx* ctx = g_ctx; + struct Device* device = (struct Device*)arg; + BleCtx* ctx = ble_get_ctx(device); if (ctx == nullptr) return 0; switch (event->type) { @@ -117,7 +136,7 @@ static int gap_event_handler(struct ble_gap_event* event, void* arg) { } else { LOG_W(TAG, "Connection failed (status=%d)", event->connect.status); // Delay restart so NimBLE can clean up SMP/connection state before peer retries. - ble_schedule_adv_restart(ctx, 500'000); + ble_schedule_adv_restart(device, 500'000); } break; @@ -134,17 +153,24 @@ static int gap_event_handler(struct ble_gap_event* event, void* arg) { // If HID was stopped while connected, switch profile to None now that the // connection is gone. ble_gatts_mutable() is true here (no active connection, // advertising stopped by hid_device_stop), so switch_profile is safe. - if (was_hid && !ctx->hid_active.load() && current_hid_profile != BleHidProfile::None) { - ble_hid_device_switch_profile(ctx, BleHidProfile::None); + // Skip during shutdown — ble_gatts_reset() is unsafe while nimble_port_stop() runs. + if (was_hid && !ctx->hid_active.load() && current_hid_profile != BleHidProfile::None && + ctx->radio_state.load() != BT_RADIO_STATE_OFF_PENDING) { + ble_hid_device_switch_profile(device, BleHidProfile::None); } // Restart advertising whenever a service is active without a live connection. // Covers both normal disconnect and Windows discovery-only connections. - if (ctx->midi_active.load() && ctx->midi_conn_handle.load() == BLE_HS_CONN_HANDLE_NONE) { - ble_start_advertising(&MIDI_SVC_UUID); - } else if (ctx->spp_active.load() && ctx->spp_conn_handle.load() == BLE_HS_CONN_HANDLE_NONE) { - ble_start_advertising(&NUS_SVC_UUID); - } else if (ctx->hid_active.load() && ctx->hid_conn_handle.load() == BLE_HS_CONN_HANDLE_NONE) { - ble_start_advertising_hid(hid_appearance); + // Skip if the radio is going OFF_PENDING — nimble_port_stop() is in progress + // and calling ble_gap_adv_start() from within the NimBLE host task while the + // controller is shutting down would block the host task and hang nimble_port_stop(). + if (ctx->radio_state.load() != BT_RADIO_STATE_OFF_PENDING) { + if (ctx->midi_active.load() && ctx->midi_conn_handle.load() == BLE_HS_CONN_HANDLE_NONE) { + ble_start_advertising(device, &MIDI_SVC_UUID); + } else if (ctx->spp_active.load() && ctx->spp_conn_handle.load() == BLE_HS_CONN_HANDLE_NONE) { + ble_start_advertising(device, &NUS_SVC_UUID); + } else if (ctx->hid_active.load() && ctx->hid_conn_handle.load() == BLE_HS_CONN_HANDLE_NONE) { + ble_start_advertising_hid(device, hid_appearance); + } } break; } @@ -178,7 +204,7 @@ static int gap_event_handler(struct ble_gap_event* event, void* arg) { memcpy(e.profile_state.addr, sub_desc.peer_id_addr.val, 6); e.profile_state.profile = BT_PROFILE_SPP; e.profile_state.state = BT_PROFILE_STATE_CONNECTED; - ble_publish_event(ctx, e); + ble_publish_event(device, e); } } } else if (event->subscribe.attr_handle == midi_io_handle) { @@ -200,7 +226,7 @@ static int gap_event_handler(struct ble_gap_event* event, void* arg) { memcpy(e.profile_state.addr, sub_desc.peer_id_addr.val, 6); e.profile_state.profile = BT_PROFILE_MIDI; e.profile_state.state = BT_PROFILE_STATE_CONNECTED; - ble_publish_event(ctx, e); + ble_publish_event(device, e); } } // Send MIDI Active Sensing immediately after subscription. @@ -287,7 +313,7 @@ static int gap_event_handler(struct ble_gap_event* event, void* arg) { : ctx->spp_active.load() ? BT_PROFILE_SPP : ctx->hid_active.load() ? BT_PROFILE_HID_DEVICE : BT_PROFILE_HID_HOST; - ble_publish_event(ctx, e); + ble_publish_event(device, e); } } // Re-send Active Sensing now that the link is encrypted. @@ -328,7 +354,7 @@ static int gap_event_handler(struct ble_gap_event* event, void* arg) { e.type = BT_EVENT_PAIR_RESULT; memcpy(e.pair_result.addr, desc.peer_id_addr.val, 6); e.pair_result.result = BT_PAIR_RESULT_BOND_LOST; - ble_publish_event(ctx, e); + ble_publish_event(device, e); } } // If already encrypted, IGNORE so the current session continues. @@ -354,7 +380,7 @@ static void on_sync() { nus_tx_handle, midi_io_handle, hid_kb_input_handle, hid_consumer_input_handle, hid_mouse_input_handle, hid_gamepad_input_handle); - BleCtx* ctx = g_ctx; + BleCtx* ctx = ble_get_ctx(s_device); if (ctx == nullptr) return; ctx->pending_reset_count.store(0); @@ -365,21 +391,21 @@ static void on_sync() { rc = ble_hs_id_infer_auto(0, &own_addr_type); if (rc != 0) LOG_E(TAG, "infer addr type failed (rc=%d)", rc); - // Sync GATT handle values - ble_spp_init_gatt_handles(ctx); - ble_midi_init_gatt_handles(ctx); + // Sync GATT handle values (also sets .arg = device on chr def entries) + ble_spp_init_gatt_handles(s_device); + ble_midi_init_gatt_handles(s_device); ble_hid_device_init_gatt_handles(); ctx->radio_state.store(BT_RADIO_STATE_ON); struct BtEvent e = {}; e.type = BT_EVENT_RADIO_STATE_CHANGED; e.radio_state = BT_RADIO_STATE_ON; - ble_publish_event(ctx, e); + ble_publish_event(s_device, e); // The Tactility bridge handles auto-start (SPP/MIDI) in response to // BT_EVENT_RADIO_STATE_CHANGED(ON). We just start name-only advertising // so the device is visible immediately. - ble_start_advertising(nullptr); + ble_start_advertising(s_device, nullptr); } static void dispatch_disable_timer_cb(void* arg) { @@ -389,7 +415,7 @@ static void dispatch_disable_timer_cb(void* arg) { static void on_reset(int reason) { LOG_W(TAG, "NimBLE host reset (reason=%d)", reason); - BleCtx* ctx = g_ctx; + BleCtx* ctx = ble_get_ctx(s_device); if (ctx == nullptr) return; if (ctx->radio_state.load() == BT_RADIO_STATE_ON_PENDING) { @@ -414,12 +440,13 @@ static void ble_host_task(void* param) { // ---- Advertising helpers ---- -void ble_start_advertising(const ble_uuid128_t* svc_uuid) { +void ble_start_advertising(struct Device* device, const ble_uuid128_t* svc_uuid) { ble_gap_adv_stop(); // Always sync the GAP name from ctx right before building the advertising packet, // so bluetooth_set_device_name() is honoured regardless of call timing. - if (g_ctx) ble_svc_gap_device_name_set(g_ctx->device_name); + BleCtx* _name_ctx = ble_get_ctx(device); + if (_name_ctx) ble_svc_gap_device_name_set(_name_ctx->device_name); int rc; if (svc_uuid != nullptr) { @@ -485,7 +512,7 @@ void ble_start_advertising(const ble_uuid128_t* svc_uuid) { adv_params.itvl_max = 240; // 150 ms rc = ble_gap_adv_start(BLE_OWN_ADDR_PUBLIC, nullptr, BLE_HS_FOREVER, - &adv_params, gap_event_handler, nullptr); + &adv_params, gap_event_handler, device); if (rc != 0 && rc != BLE_HS_EALREADY) { LOG_E(TAG, "startAdvertising: adv_start failed rc=%d", rc); } else { @@ -493,11 +520,12 @@ void ble_start_advertising(const ble_uuid128_t* svc_uuid) { } } -void ble_start_advertising_hid(uint16_t appearance) { +void ble_start_advertising_hid(struct Device* device, uint16_t appearance) { ble_gap_adv_stop(); // Always sync the GAP name from ctx right before building the advertising packet. - if (g_ctx) ble_svc_gap_device_name_set(g_ctx->device_name); + BleCtx* _name_ctx = ble_get_ctx(device); + if (_name_ctx) ble_svc_gap_device_name_set(_name_ctx->device_name); const char* name = ble_svc_gap_device_name(); uint8_t name_len = (uint8_t)strlen(name); @@ -546,7 +574,7 @@ void ble_start_advertising_hid(uint16_t appearance) { adv_params.itvl_max = 240; rc = ble_gap_adv_start(BLE_OWN_ADDR_PUBLIC, nullptr, BLE_HS_FOREVER, - &adv_params, gap_event_handler, nullptr); + &adv_params, gap_event_handler, device); if (rc != 0 && rc != BLE_HS_EALREADY) { LOG_E(TAG, "startAdvertisingHid: adv_start failed rc=%d", rc); } else { @@ -569,7 +597,7 @@ static void dispatch_enable(BleCtx* ctx) { struct BtEvent e = {}; e.type = BT_EVENT_RADIO_STATE_CHANGED; e.radio_state = BT_RADIO_STATE_ON_PENDING; - ble_publish_event(ctx, e); + ble_publish_event(ctx->device, e); } int rc = nimble_port_init(); @@ -579,7 +607,7 @@ static void dispatch_enable(BleCtx* ctx) { struct BtEvent e = {}; e.type = BT_EVENT_RADIO_STATE_CHANGED; e.radio_state = BT_RADIO_STATE_OFF; - ble_publish_event(ctx, e); + ble_publish_event(ctx->device, e); return; } @@ -625,7 +653,7 @@ static void dispatch_disable(BleCtx* ctx) { struct BtEvent e = {}; e.type = BT_EVENT_RADIO_STATE_CHANGED; e.radio_state = BT_RADIO_STATE_OFF_PENDING; - ble_publish_event(ctx, e); + ble_publish_event(ctx->device, e); } // Blocking: waits for nimble_port_run() to exit. @@ -662,7 +690,7 @@ static void dispatch_disable(BleCtx* ctx) { struct BtEvent e = {}; e.type = BT_EVENT_RADIO_STATE_CHANGED; e.radio_state = BT_RADIO_STATE_OFF; - ble_publish_event(ctx, e); + ble_publish_event(ctx->device, e); } } @@ -715,7 +743,7 @@ static error_t api_scan_start(struct Device* device) { uint8_t own_addr_type; ble_hs_id_infer_auto(0, &own_addr_type); - int rc = ble_gap_disc(own_addr_type, 5000, &disc_params, ble_gap_disc_event_handler, nullptr); + int rc = ble_gap_disc(own_addr_type, 5000, &disc_params, ble_gap_disc_event_handler, device); if (rc != 0 && rc != BLE_HS_EALREADY) { LOG_E(TAG, "ble_gap_disc failed (rc=%d)", rc); return ERROR_UNDEFINED; @@ -725,7 +753,7 @@ static error_t api_scan_start(struct Device* device) { { struct BtEvent e = {}; e.type = BT_EVENT_SCAN_STARTED; - ble_publish_event(ctx, e); + ble_publish_event(device, e); } return ERROR_NONE; } @@ -737,7 +765,7 @@ static error_t api_scan_stop(struct Device* device) { ctx->scan_active.store(false); struct BtEvent e = {}; e.type = BT_EVENT_SCAN_FINISHED; - ble_publish_event(ctx, e); + ble_publish_event(device, e); return ERROR_NONE; } @@ -773,9 +801,9 @@ static error_t api_connect(struct Device* device, const BtAddr addr, enum BtProf if (profile == BT_PROFILE_HID_DEVICE) { return nimble_hid_device_api.start(device, BT_HID_DEVICE_MODE_KEYBOARD); } else if (profile == BT_PROFILE_SPP) { - return ble_spp_start_internal(ctx); + return ble_spp_start_internal(device); } else if (profile == BT_PROFILE_MIDI) { - return ble_midi_start_internal(ctx); + return ble_midi_start_internal(device); } // BT_PROFILE_HID_HOST is handled entirely in the Tactility layer. return ERROR_NOT_SUPPORTED; @@ -839,7 +867,7 @@ static error_t api_set_device_name(struct Device* device, const char* name) { ble_svc_gap_device_name_set(ctx->device_name); // Restart advertising so the new name is broadcast immediately. // ble_schedule_adv_restart checks active profiles; no-op if nothing is advertising. - ble_schedule_adv_restart(ctx, 0); + ble_schedule_adv_restart(device, 0); } xSemaphoreGive(ctx->radio_mutex); return ERROR_NONE; @@ -862,7 +890,7 @@ static void api_set_hid_host_active(struct Device* device, bool active) { static void api_fire_event(struct Device* device, struct BtEvent event) { BleCtx* ctx = (BleCtx*)device_get_driver_data(device); - if (ctx) ble_publish_event(ctx, event); + if (ctx) ble_publish_event(device, event); } // ---- BluetoothApi struct ---- @@ -998,7 +1026,7 @@ static error_t esp32_ble_start_device(struct Device* device) { } device_set_driver_data(device, ctx); - g_ctx = ctx; + s_device = device; // Create child devices for the serial, MIDI and HID device profiles. create_child_device(device, "ble-serial", &esp32_ble_serial_driver, ctx->serial_child); @@ -1027,7 +1055,7 @@ static error_t esp32_ble_stop_device(struct Device* device) { ctx->disable_timer = nullptr; } - g_ctx = nullptr; + s_device = nullptr; device_set_driver_data(device, nullptr); delete ctx; return ERROR_NONE; diff --git a/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_hid_device.cpp b/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_hid_device.cpp index 9607c391d..ccbd2f8c5 100644 --- a/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_hid_device.cpp +++ b/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_hid_device.cpp @@ -300,12 +300,13 @@ static const struct ble_gatt_svc_def gatt_svcs_gamepad[] = { // ---- GATT profile switch ---- -void ble_hid_device_switch_profile(BleCtx* ctx, BleHidProfile profile) { +void ble_hid_device_switch_profile(struct Device* device, BleHidProfile profile) { if (profile == current_hid_profile) return; LOG_I(TAG, "switchGattProfile: %d -> %d", (int)current_hid_profile, (int)profile); ble_gap_adv_stop(); + BleCtx* ctx = ble_get_ctx(device); if (ctx && ctx->hid_conn_handle.load() != BLE_HS_CONN_HANDLE_NONE) { ble_gap_terminate(ctx->hid_conn_handle.load(), BLE_ERR_REM_USER_CONN_TERM); ctx->hid_conn_handle.store(BLE_HS_CONN_HANDLE_NONE); @@ -384,8 +385,8 @@ void ble_hid_device_init_gatt_handles() { // ---- HID Device sub-API implementations ---- -static error_t hid_device_start(struct Device* /*device*/, enum BtHidDeviceMode mode) { - BleCtx* ctx = g_ctx; +static error_t hid_device_start(struct Device* device, enum BtHidDeviceMode mode) { + BleCtx* ctx = ble_get_ctx(device); if (ctx == nullptr) return ERROR_INVALID_STATE; BleHidProfile profile; @@ -410,14 +411,14 @@ static error_t hid_device_start(struct Device* /*device*/, enum BtHidDeviceMode } hid_appearance = appearance; - ble_hid_device_switch_profile(ctx, profile); + ble_hid_device_switch_profile(device, profile); ctx->hid_active.store(true); - ble_start_advertising_hid(hid_appearance); + ble_start_advertising_hid(device, hid_appearance); return ERROR_NONE; } -static error_t hid_device_stop(struct Device* /*device*/) { - BleCtx* ctx = g_ctx; +static error_t hid_device_stop(struct Device* device) { + BleCtx* ctx = ble_get_ctx(device); if (ctx == nullptr) return ERROR_NONE; ctx->hid_active.store(false); ble_gap_adv_stop(); @@ -431,14 +432,14 @@ static error_t hid_device_stop(struct Device* /*device*/) { } else { // Not connected: GATT is mutable, switch profile immediately. if (current_hid_profile != BleHidProfile::None) { - ble_hid_device_switch_profile(ctx, BleHidProfile::None); + ble_hid_device_switch_profile(device, BleHidProfile::None); } } return ERROR_NONE; } -static error_t hid_device_send_key(struct Device* /*device*/, uint8_t keycode, bool pressed) { - BleCtx* ctx = g_ctx; +static error_t hid_device_send_key(struct Device* device, uint8_t keycode, bool pressed) { + BleCtx* ctx = ble_get_ctx(device); if (ctx == nullptr || ctx->hid_conn_handle.load() == BLE_HS_CONN_HANDLE_NONE) { return ERROR_INVALID_STATE; } @@ -462,40 +463,40 @@ static error_t hid_notify(uint16_t conn_handle, uint16_t attr_handle, return (rc == 0) ? ERROR_NONE : ERROR_INVALID_STATE; } -static error_t hid_device_send_keyboard(struct Device* /*device*/, const uint8_t* report, size_t len) { - BleCtx* ctx = g_ctx; +static error_t hid_device_send_keyboard(struct Device* device, const uint8_t* report, size_t len) { + BleCtx* ctx = ble_get_ctx(device); if (ctx == nullptr) return ERROR_INVALID_STATE; uint8_t buf[8] = {}; memcpy(buf, report, len < sizeof(buf) ? len : sizeof(buf)); return hid_notify(ctx->hid_conn_handle.load(), hid_kb_input_handle, buf, sizeof(buf)); } -static error_t hid_device_send_consumer(struct Device* /*device*/, const uint8_t* report, size_t len) { - BleCtx* ctx = g_ctx; +static error_t hid_device_send_consumer(struct Device* device, const uint8_t* report, size_t len) { + BleCtx* ctx = ble_get_ctx(device); if (ctx == nullptr) return ERROR_INVALID_STATE; uint8_t buf[2] = {}; memcpy(buf, report, len < sizeof(buf) ? len : sizeof(buf)); return hid_notify(ctx->hid_conn_handle.load(), hid_consumer_input_handle, buf, sizeof(buf)); } -static error_t hid_device_send_mouse(struct Device* /*device*/, const uint8_t* report, size_t len) { - BleCtx* ctx = g_ctx; +static error_t hid_device_send_mouse(struct Device* device, const uint8_t* report, size_t len) { + BleCtx* ctx = ble_get_ctx(device); if (ctx == nullptr) return ERROR_INVALID_STATE; uint8_t buf[4] = {}; memcpy(buf, report, len < sizeof(buf) ? len : sizeof(buf)); return hid_notify(ctx->hid_conn_handle.load(), hid_mouse_input_handle, buf, sizeof(buf)); } -static error_t hid_device_send_gamepad(struct Device* /*device*/, const uint8_t* report, size_t len) { - BleCtx* ctx = g_ctx; +static error_t hid_device_send_gamepad(struct Device* device, const uint8_t* report, size_t len) { + BleCtx* ctx = ble_get_ctx(device); if (ctx == nullptr) return ERROR_INVALID_STATE; uint8_t buf[8] = {}; memcpy(buf, report, len < sizeof(buf) ? len : sizeof(buf)); return hid_notify(ctx->hid_conn_handle.load(), hid_gamepad_input_handle, buf, sizeof(buf)); } -static bool hid_device_is_connected(struct Device* /*device*/) { - BleCtx* ctx = g_ctx; +static bool hid_device_is_connected(struct Device* device) { + BleCtx* ctx = ble_get_ctx(device); return ctx != nullptr && ctx->hid_conn_handle.load() != BLE_HS_CONN_HANDLE_NONE; } diff --git a/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_midi.cpp b/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_midi.cpp index a8ea67af9..d08bddc17 100644 --- a/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_midi.cpp +++ b/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_midi.cpp @@ -40,7 +40,8 @@ static int midi_chr_access(uint16_t conn_handle, uint16_t attr_handle, if (ctxt->op == BLE_GATT_ACCESS_OP_WRITE_CHR) { uint16_t len = OS_MBUF_PKTLEN(ctxt->om); LOG_I(TAG, "MIDI RX %u bytes", (unsigned)len); - BleCtx* ctx = g_ctx; + struct Device* device = (struct Device*)arg; + BleCtx* ctx = ble_get_ctx(device); if (ctx != nullptr && len > 0) { std::vector packet(len); os_mbuf_copydata(ctxt->om, 0, len, packet.data()); @@ -52,31 +53,34 @@ static int midi_chr_access(uint16_t conn_handle, uint16_t attr_handle, } struct BtEvent e = {}; e.type = BT_EVENT_MIDI_DATA_RECEIVED; - ble_publish_event(ctx, e); + ble_publish_event(device, e); } } return 0; } -const struct ble_gatt_chr_def midi_chars[] = { +struct ble_gatt_chr_def midi_chars[] = { { .uuid = &MIDI_IO_UUID.u, .access_cb = midi_chr_access, + .arg = nullptr, // set to Device* in ble_midi_init_gatt_handles() .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_WRITE_NO_RSP | BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_INDICATE, .val_handle = &midi_io_handle, }, { 0 } }; -void ble_midi_init_gatt_handles(BleCtx* /*ctx*/) { +void ble_midi_init_gatt_handles(struct Device* device) { + // Set the Device* arg so that midi_chr_access can retrieve context without a global. // midi_io_handle is written by NimBLE via the val_handle pointer above. - // Nothing else needed; the extern variable is accessed directly by esp32_ble.cpp. + midi_chars[0].arg = device; } // ---- MIDI Active Sensing keepalive ---- -static void midi_keepalive_cb(void* /*arg*/) { - BleCtx* ctx = g_ctx; +static void midi_keepalive_cb(void* arg) { + struct Device* device = (struct Device*)arg; + BleCtx* ctx = ble_get_ctx(device); if (ctx == nullptr || ctx->midi_conn_handle.load() == BLE_HS_CONN_HANDLE_NONE) return; static const uint8_t as_pkt[3] = { 0x80, 0x80, 0xFE }; struct os_mbuf* om = ble_hs_mbuf_from_flat(as_pkt, 3); @@ -90,7 +94,7 @@ static void midi_keepalive_cb(void* /*arg*/) { // ---- MIDI sub-API implementations ---- static error_t midi_start(struct Device* device) { - BleCtx* ctx = g_ctx; + BleCtx* ctx = ble_get_ctx(device); if (ctx == nullptr) return ERROR_INVALID_STATE; ctx->midi_active.store(true); // Create 2-second periodic Active Sensing timer to prevent Windows BLE MIDI @@ -98,6 +102,7 @@ static error_t midi_start(struct Device* device) { if (ctx->midi_keepalive_timer == nullptr) { esp_timer_create_args_t args = {}; args.callback = midi_keepalive_cb; + args.arg = device; args.dispatch_method = ESP_TIMER_TASK; args.name = "ble_midi_as"; int rc = esp_timer_create(&args, &ctx->midi_keepalive_timer); @@ -110,16 +115,16 @@ static error_t midi_start(struct Device* device) { if (rc != ESP_OK) { LOG_E(TAG, "midi_start: keepalive timer start failed (rc=%d)", rc); } - ble_start_advertising(&MIDI_SVC_UUID); + ble_start_advertising(device, &MIDI_SVC_UUID); return ERROR_NONE; } -error_t ble_midi_start_internal(BleCtx* ctx) { - return midi_start(nullptr); +error_t ble_midi_start_internal(struct Device* device) { + return midi_start(device); } static error_t midi_stop(struct Device* device) { - BleCtx* ctx = g_ctx; + BleCtx* ctx = ble_get_ctx(device); if (ctx == nullptr) return ERROR_NONE; ctx->midi_active.store(false); if (ctx->midi_keepalive_timer != nullptr) { @@ -136,7 +141,7 @@ static error_t midi_stop(struct Device* device) { } static error_t midi_send(struct Device* device, const uint8_t* msg, size_t len) { - BleCtx* ctx = g_ctx; + BleCtx* ctx = ble_get_ctx(device); if (ctx == nullptr || ctx->midi_conn_handle.load() == BLE_HS_CONN_HANDLE_NONE) { return ERROR_INVALID_STATE; } @@ -161,7 +166,7 @@ static error_t midi_send(struct Device* device, const uint8_t* msg, size_t len) } static bool midi_is_connected(struct Device* device) { - BleCtx* ctx = g_ctx; + BleCtx* ctx = ble_get_ctx(device); return ctx != nullptr && ctx->midi_conn_handle.load() != BLE_HS_CONN_HANDLE_NONE; } diff --git a/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_scan.cpp b/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_scan.cpp index 9ae15674b..73f3eb629 100644 --- a/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_scan.cpp +++ b/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_scan.cpp @@ -15,10 +15,17 @@ #define TAG "esp32_ble_scan" #include +// Module-static device pointer used only by the name-resolution GAP/GATT callbacks +// whose void* arg is already occupied by the peer index (uintptr_t). +// Set at the start of ble_resolve_next_unnamed_peer; valid for the duration of +// the sequential resolution chain (single-device, single-scan at a time). +static struct Device* s_scan_device = nullptr; + // ---- GAP scan callback ---- int ble_gap_disc_event_handler(struct ble_gap_event* event, void* arg) { - BleCtx* ctx = g_ctx; + struct Device* device = (struct Device*)arg; + BleCtx* ctx = ble_get_ctx(device); if (ctx == nullptr) return 0; switch (event->type) { @@ -66,7 +73,7 @@ int ble_gap_disc_event_handler(struct ble_gap_event* event, void* arg) { struct BtEvent e = {}; e.type = BT_EVENT_PEER_FOUND; e.peer = record; - ble_publish_event(ctx, e); + ble_publish_event(device, e); break; } @@ -74,7 +81,7 @@ int ble_gap_disc_event_handler(struct ble_gap_event* event, void* arg) { LOG_I(TAG, "Scan complete (reason=%d)", event->disc_complete.reason); // Keep scan_active=true; resolveNextUnnamedPeer clears it and fires ScanFinished // once name resolution finishes, so the UI spinner stays active throughout. - ble_resolve_next_unnamed_peer(ctx, 0); + ble_resolve_next_unnamed_peer(device, 0); break; default: @@ -94,7 +101,8 @@ int ble_gap_disc_event_handler(struct ble_gap_event* event, void* arg) { static int name_read_callback(uint16_t conn_handle, const struct ble_gatt_error* error, struct ble_gatt_attr* attr, void* arg) { - BleCtx* ctx = g_ctx; + struct Device* device = s_scan_device; + BleCtx* ctx = ble_get_ctx(device); if (ctx == nullptr) return 0; if (error->status == 0 && attr != nullptr) { @@ -116,7 +124,7 @@ static int name_read_callback(uint16_t conn_handle, const struct ble_gatt_error* struct BtEvent e = {}; e.type = BT_EVENT_PEER_FOUND; e.peer = record; - ble_publish_event(ctx, e); + ble_publish_event(device, e); } } return 0; // wait for BLE_HS_EDONE @@ -129,7 +137,8 @@ static int name_read_callback(uint16_t conn_handle, const struct ble_gatt_error* static int name_res_gap_callback(struct ble_gap_event* event, void* arg) { size_t idx = (size_t)(uintptr_t)arg; - BleCtx* ctx = g_ctx; + struct Device* device = s_scan_device; + BleCtx* ctx = ble_get_ctx(device); if (ctx == nullptr) return 0; switch (event->type) { @@ -147,13 +156,13 @@ static int name_res_gap_callback(struct ble_gap_event* event, void* arg) { } } else { LOG_I(TAG, "Name resolution: connect failed (idx=%u status=%d)", (unsigned)idx, event->connect.status); - ble_resolve_next_unnamed_peer(ctx, idx + 1); + ble_resolve_next_unnamed_peer(device, idx + 1); } break; case BLE_GAP_EVENT_DISCONNECT: LOG_I(TAG, "Name resolution: disconnected (idx=%u)", (unsigned)idx); - ble_resolve_next_unnamed_peer(ctx, idx + 1); + ble_resolve_next_unnamed_peer(device, idx + 1); break; default: @@ -162,7 +171,10 @@ static int name_res_gap_callback(struct ble_gap_event* event, void* arg) { return 0; } -void ble_resolve_next_unnamed_peer(BleCtx* ctx, size_t start_idx) { +void ble_resolve_next_unnamed_peer(struct Device* device, size_t start_idx) { + BleCtx* ctx = ble_get_ctx(device); + s_scan_device = device; + // Skip if a profile server or HID host connection attempt is active — // initiating a central connection simultaneously would fail (BLE_HS_EALREADY). if (ctx->midi_active.load() || ctx->spp_active.load() || @@ -171,7 +183,7 @@ void ble_resolve_next_unnamed_peer(BleCtx* ctx, size_t start_idx) { ctx->scan_active.store(false); struct BtEvent e = {}; e.type = BT_EVENT_SCAN_FINISHED; - ble_publish_event(ctx, e); + ble_publish_event(device, e); return; } @@ -197,7 +209,7 @@ void ble_resolve_next_unnamed_peer(BleCtx* ctx, size_t start_idx) { ctx->scan_active.store(false); struct BtEvent e = {}; e.type = BT_EVENT_SCAN_FINISHED; - ble_publish_event(ctx, e); + ble_publish_event(device, e); return; } diff --git a/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_spp.cpp b/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_spp.cpp index 83dbb7843..0ac953aa4 100644 --- a/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_spp.cpp +++ b/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_spp.cpp @@ -45,7 +45,8 @@ static int nus_chr_access(uint16_t conn_handle, uint16_t attr_handle, if (ctxt->op == BLE_GATT_ACCESS_OP_WRITE_CHR) { uint16_t len = OS_MBUF_PKTLEN(ctxt->om); LOG_I(TAG, "NUS RX %u bytes", (unsigned)len); - BleCtx* ctx = g_ctx; + struct Device* device = (struct Device*)arg; + BleCtx* ctx = ble_get_ctx(device); if (ctx != nullptr && len > 0) { std::vector packet(len); os_mbuf_copydata(ctxt->om, 0, len, packet.data()); @@ -57,48 +58,52 @@ static int nus_chr_access(uint16_t conn_handle, uint16_t attr_handle, } struct BtEvent e = {}; e.type = BT_EVENT_SPP_DATA_RECEIVED; - ble_publish_event(ctx, e); + ble_publish_event(device, e); } } return 0; } -const struct ble_gatt_chr_def nus_chars_with_handle[] = { +struct ble_gatt_chr_def nus_chars_with_handle[] = { { .uuid = &NUS_RX_UUID.u, .access_cb = nus_chr_access, + .arg = nullptr, // set to Device* in ble_spp_init_gatt_handles() .flags = BLE_GATT_CHR_F_WRITE | BLE_GATT_CHR_F_WRITE_NO_RSP, }, { .uuid = &NUS_TX_UUID.u, .access_cb = nus_chr_access, + .arg = nullptr, // set to Device* in ble_spp_init_gatt_handles() .flags = BLE_GATT_CHR_F_NOTIFY, .val_handle = &nus_tx_handle, }, { 0 } }; -void ble_spp_init_gatt_handles(BleCtx* /*ctx*/) { +void ble_spp_init_gatt_handles(struct Device* device) { + // Set the Device* arg so that nus_chr_access can retrieve context without a global. // nus_tx_handle is written by NimBLE via the val_handle pointer above. - // Nothing else needed; the extern variable is accessed directly by esp32_ble.cpp. + nus_chars_with_handle[0].arg = device; + nus_chars_with_handle[1].arg = device; } // ---- SPP sub-API implementations ---- static error_t spp_start(struct Device* device) { - BleCtx* ctx = g_ctx; + BleCtx* ctx = ble_get_ctx(device); if (ctx == nullptr) return ERROR_INVALID_STATE; ctx->spp_active.store(true); - ble_start_advertising(&NUS_SVC_UUID); + ble_start_advertising(device, &NUS_SVC_UUID); return ERROR_NONE; } -error_t ble_spp_start_internal(BleCtx* ctx) { - return spp_start(nullptr); +error_t ble_spp_start_internal(struct Device* device) { + return spp_start(device); } static error_t spp_stop(struct Device* device) { - BleCtx* ctx = g_ctx; + BleCtx* ctx = ble_get_ctx(device); if (ctx == nullptr) return ERROR_NONE; ctx->spp_active.store(false); if (ctx->spp_conn_handle.load() != BLE_HS_CONN_HANDLE_NONE) { @@ -114,7 +119,7 @@ static error_t spp_stop(struct Device* device) { } static error_t spp_write(struct Device* device, const uint8_t* data, size_t len, size_t* written) { - BleCtx* ctx = g_ctx; + BleCtx* ctx = ble_get_ctx(device); if (ctx == nullptr || ctx->spp_conn_handle.load() == BLE_HS_CONN_HANDLE_NONE) { if (written) *written = 0; return ERROR_INVALID_STATE; @@ -135,7 +140,7 @@ static error_t spp_write(struct Device* device, const uint8_t* data, size_t len, } static error_t spp_read(struct Device* device, uint8_t* data, size_t max_len, size_t* read_out) { - BleCtx* ctx = g_ctx; + BleCtx* ctx = ble_get_ctx(device); if (ctx == nullptr || data == nullptr || max_len == 0) { if (read_out) *read_out = 0; return ERROR_NONE; @@ -156,7 +161,7 @@ static error_t spp_read(struct Device* device, uint8_t* data, size_t max_len, si } static bool spp_is_connected(struct Device* device) { - BleCtx* ctx = g_ctx; + BleCtx* ctx = ble_get_ctx(device); return ctx != nullptr && ctx->spp_conn_handle.load() != BLE_HS_CONN_HANDLE_NONE; } diff --git a/Tactility/Source/app/btmanage/BtManage.cpp b/Tactility/Source/app/btmanage/BtManage.cpp index 2c774cfad..09ea2257c 100644 --- a/Tactility/Source/app/btmanage/BtManage.cpp +++ b/Tactility/Source/app/btmanage/BtManage.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include @@ -121,8 +122,15 @@ void BtManage::onBtEvent(const struct BtEvent& event) { } static void onKernelBtEvent(struct Device* /*device*/, void* context, struct BtEvent event) { + // BT event callbacks can fire from the NimBLE host task (e.g. DISCONNECT during + // nimble_port_stop shutdown). Calling onBtEvent() synchronously from the NimBLE + // task would block it on the LVGL mutex (held by the LVGL task waiting in + // nimble_port_stop), creating a permanent deadlock. Dispatch to the main task so + // the NimBLE host task is never blocked by BtManage's state updates or LVGL lock. auto* self = static_cast(context); - self->onBtEvent(event); + getMainDispatcher().dispatch([self, event] { + self->onBtEvent(event); + }); } void BtManage::onShow(AppContext& app, lv_obj_t* parent) { From ee17aa927fba37d6c92fe2d54f9428cb7f16bd20 Mon Sep 17 00:00:00 2001 From: Shadowtrance Date: Fri, 3 Apr 2026 18:48:45 +1000 Subject: [PATCH 07/12] updoot --- .../private/bluetooth/esp32_ble_internal.h | 123 +++---- .../source/drivers/bluetooth/esp32_ble.cpp | 319 +++++++++++++++--- .../bluetooth/esp32_ble_hid_device.cpp | 67 ++-- .../drivers/bluetooth/esp32_ble_midi.cpp | 105 +++--- .../drivers/bluetooth/esp32_ble_scan.cpp | 91 +++-- .../drivers/bluetooth/esp32_ble_spp.cpp | 80 ++--- 6 files changed, 503 insertions(+), 282 deletions(-) diff --git a/Platforms/platform-esp32/private/bluetooth/esp32_ble_internal.h b/Platforms/platform-esp32/private/bluetooth/esp32_ble_internal.h index 296c16d5a..737f23b5d 100644 --- a/Platforms/platform-esp32/private/bluetooth/esp32_ble_internal.h +++ b/Platforms/platform-esp32/private/bluetooth/esp32_ble_internal.h @@ -33,75 +33,57 @@ enum class BleHidProfile { None, KbConsumer, Mouse, KbMouse, Gamepad }; -// ---- BleCtx ---- +// ---- Child device driver data structs ---- +// Each struct is stored as driver data on the corresponding child Device*. -#define BLE_MAX_CALLBACKS 8 -#define BLE_SCAN_MAX 64 +struct BleHidDeviceCtx { + std::atomic hid_conn_handle; +}; -struct BleCallbackEntry { - BtEventCallback fn; - void* ctx; +struct BleMidiCtx { + SemaphoreHandle_t rx_mutex; + std::deque> rx_queue; }; -struct BleCtx { - // Mutexes - SemaphoreHandle_t radio_mutex; // guards radio state transitions - SemaphoreHandle_t data_mutex; // guards scan results + RX queues - SemaphoreHandle_t cb_mutex; // guards callbacks array - - // Radio / scan state (atomic — read from multiple tasks) - std::atomic radio_state; - std::atomic scan_active; - // Set by Tactility HID host to prevent simultaneous central connection during name resolution - std::atomic hid_host_active; - - // Event callbacks (guarded by cb_mutex) - BleCallbackEntry callbacks[BLE_MAX_CALLBACKS]; - size_t callback_count; - - // Scan results (guarded by data_mutex) - BtPeerRecord scan_results[BLE_SCAN_MAX]; - ble_addr_t scan_addrs[BLE_SCAN_MAX]; // parallel: full ble_addr_t (type+val) for connections - size_t scan_count; - - // RX queues (guarded by data_mutex, capped at 16 packets each) - std::deque> spp_rx_queue; - std::deque> midi_rx_queue; - - // Connection handles + active flags (atomic — accessed from multiple tasks) - std::atomic spp_conn_handle; - std::atomic spp_active; - std::atomic midi_conn_handle; - std::atomic midi_active; - std::atomic midi_use_indicate; // true when client subscribed for INDICATE (e.g. Windows) - std::atomic hid_conn_handle; - std::atomic hid_active; - std::atomic link_encrypted; - std::atomic pending_reset_count; - - // Timers - esp_timer_handle_t midi_keepalive_timer; // 2-second periodic Active Sensing - esp_timer_handle_t adv_restart_timer; // one-shot after connect failure (500 ms) - // One-shot timer used to dispatch dispatchDisable off the NimBLE host task. - // nimble_port_stop() must not be called from the NimBLE host task itself. - esp_timer_handle_t disable_timer; - - // BLE device name (set before or after radio enable; applied in dispatch_enable) - char device_name[BLE_DEVICE_NAME_MAX + 1]; - - // Device reference (passed to BtEventCallback) - struct Device* device; - - // Child devices (created by esp32_ble_start_device, destroyed by stop_device) - struct Device* serial_child; - struct Device* midi_child; - struct Device* hid_device_child; +struct BleSppCtx { + SemaphoreHandle_t rx_mutex; + std::deque> rx_queue; }; -// ---- Context accessor (defined in esp32_ble.cpp) ---- -// Retrieves the BleCtx stored in the device driver-data slot. -// All cross-module functions accept Device* and call this internally. -BleCtx* ble_get_ctx(struct Device* device); +// ---- BleCtx field accessors (defined in esp32_ble.cpp) ---- +// Work correctly when called with either the parent BLE device or any child device. + +BtRadioState ble_ctx_get_radio_state(struct Device* device); + +bool ble_ctx_get_hid_active(struct Device* device); +void ble_ctx_set_hid_active(struct Device* device, bool v); + +bool ble_ctx_get_spp_active(struct Device* device); +void ble_ctx_set_spp_active(struct Device* device, bool v); +uint16_t ble_ctx_get_spp_conn_handle(struct Device* device); +void ble_ctx_set_spp_conn_handle(struct Device* device, uint16_t h); + +bool ble_ctx_get_midi_active(struct Device* device); +void ble_ctx_set_midi_active(struct Device* device, bool v); +uint16_t ble_ctx_get_midi_conn_handle(struct Device* device); +void ble_ctx_set_midi_conn_handle(struct Device* device, uint16_t h); +bool ble_ctx_get_midi_use_indicate(struct Device* device); +void ble_ctx_set_midi_use_indicate(struct Device* device, bool v); + +bool ble_ctx_get_hid_host_active(struct Device* device); +bool ble_ctx_get_scan_active(struct Device* device); +void ble_ctx_set_scan_active(struct Device* device, bool v); + +// MIDI keepalive timer helpers — timer handle lives in BleCtx. +// ble_ctx_ensure_midi_keepalive creates the timer if needed and starts it periodically. +// ble_ctx_stop_midi_keepalive stops (but does not delete) the timer. +error_t ble_ctx_ensure_midi_keepalive(struct Device* device, esp_timer_cb_t cb, uint64_t period_us); +void ble_ctx_stop_midi_keepalive(struct Device* device); + +// ---- Scan data management (defined in esp32_ble_scan.cpp) ---- +void ble_scan_init(); +void ble_scan_deinit(); +void ble_scan_clear_results(); // ---- Event publishing ---- void ble_publish_event(struct Device* device, struct BtEvent event); @@ -116,20 +98,23 @@ int ble_gap_disc_event_handler(struct ble_gap_event* event, void* arg); void ble_resolve_next_unnamed_peer(struct Device* device, size_t start_idx); // ---- SPP GATT (defined in esp32_ble_spp.cpp) ---- -void ble_spp_init_gatt_handles(struct Device* device); -error_t ble_spp_start_internal(struct Device* device); +// device must be the serial child Device*. +void ble_spp_init_gatt_handles(struct Device* serial_child); +error_t ble_spp_start_internal(struct Device* serial_child); // ---- MIDI GATT (defined in esp32_ble_midi.cpp) ---- -void ble_midi_init_gatt_handles(struct Device* device); -error_t ble_midi_start_internal(struct Device* device); +// device must be the midi child Device*. +void ble_midi_init_gatt_handles(struct Device* midi_child); +error_t ble_midi_start_internal(struct Device* midi_child); // ---- HID device GATT (defined in esp32_ble_hid_device.cpp) ---- void ble_hid_device_init_gatt(); void ble_hid_device_init_gatt_handles(); -void ble_hid_device_switch_profile(struct Device* device, BleHidProfile profile); +// device must be the hid_device child Device*. +void ble_hid_device_switch_profile(struct Device* hid_child, BleHidProfile profile); // ---- Cross-module GATT char / service arrays ---- -// Non-const: the .arg field is set to the parent Device* at init time so that +// Non-const: the .arg field is set to the child Device* at init time so that // NimBLE access callbacks can retrieve the context without a global pointer. extern struct ble_gatt_chr_def nus_chars_with_handle[]; // esp32_ble_spp.cpp extern struct ble_gatt_chr_def midi_chars[]; // esp32_ble_midi.cpp diff --git a/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble.cpp b/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble.cpp index 6108168dd..66d2ad609 100644 --- a/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble.cpp +++ b/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble.cpp @@ -32,24 +32,77 @@ // ble_store_config_init() is not declared in the public header in some IDF versions. extern "C" void ble_store_config_init(void); -// ---- Context accessor ---- - -BleCtx* ble_get_ctx(struct Device* device) { - void* data = device_get_driver_data(device); - if (data == nullptr) { - // Child devices (serial, midi, hid-device) have no driver data of their own — - // their context lives in the parent BLE device. - struct Device* parent = device_get_parent(device); - if (parent != nullptr) data = device_get_driver_data(parent); - } - return (BleCtx*)data; -} +// ---- BleCtx ---- +// Private to this translation unit. Sub-modules access BleCtx fields exclusively +// through the accessor functions declared in esp32_ble_internal.h. + +#define BLE_MAX_CALLBACKS 8 +#define BLE_SCAN_MAX 64 + +struct BleCallbackEntry { + BtEventCallback fn; + void* ctx; +}; + +struct BleCtx { + // Mutexes + SemaphoreHandle_t radio_mutex; // guards radio state transitions + SemaphoreHandle_t cb_mutex; // guards callbacks array + + // Radio / scan state (atomic — read from multiple tasks) + std::atomic radio_state; + std::atomic scan_active; + // Set by Tactility HID host to prevent simultaneous central connection during name resolution + std::atomic hid_host_active; + + // Event callbacks (guarded by cb_mutex) + BleCallbackEntry callbacks[BLE_MAX_CALLBACKS]; + size_t callback_count; + + // Connection handles + active flags (atomic — accessed from multiple tasks) + std::atomic spp_conn_handle; + std::atomic spp_active; + std::atomic midi_conn_handle; + std::atomic midi_active; + std::atomic midi_use_indicate; // true when client subscribed for INDICATE (e.g. Windows) + std::atomic hid_active; + std::atomic link_encrypted; + std::atomic pending_reset_count; + + // Timers + esp_timer_handle_t midi_keepalive_timer; // 2-second periodic Active Sensing + esp_timer_handle_t adv_restart_timer; // one-shot after connect failure (500 ms) + // One-shot timer used to dispatch dispatchDisable off the NimBLE host task. + // nimble_port_stop() must not be called from the NimBLE host task itself. + esp_timer_handle_t disable_timer; + + // BLE device name (set before or after radio enable; applied in dispatch_enable) + char device_name[BLE_DEVICE_NAME_MAX + 1]; + + // Device reference (passed to BtEventCallback) + struct Device* device; + + // Child devices (created by esp32_ble_start_device, destroyed by stop_device) + struct Device* serial_child; + struct Device* midi_child; + struct Device* hid_device_child; +}; // File-static device pointer used only by NimBLE host callbacks whose signature // is fixed by the NimBLE API (on_sync, on_reset) and cannot carry a Device*. // All other callbacks receive Device* via their void* arg parameter. static struct Device* s_device = nullptr; +// ---- Context accessor (file-private) ---- +// Always returns the root BLE device's BleCtx regardless of which device is passed +// (root, child, or grandchild). Using s_device directly avoids device-tree traversal +// ambiguity: the root BLE device may itself have a parent in the device tree, and +// walking up from it would land on the wrong node. + +static BleCtx* ble_get_ctx(struct Device* /*device*/) { + return s_device ? (BleCtx*)device_get_driver_data(s_device) : nullptr; +} + // ---- Forward declarations ---- static void ble_host_task(void* param); static void on_sync(); @@ -58,10 +111,112 @@ static void dispatch_enable(BleCtx* ctx); static void dispatch_disable(BleCtx* ctx); static int gap_event_handler(struct ble_gap_event* event, void* arg); +// ---- BleCtx field accessor implementations ---- + +BtRadioState ble_ctx_get_radio_state(struct Device* device) { + BleCtx* ctx = ble_get_ctx(device); + return ctx ? ctx->radio_state.load() : BT_RADIO_STATE_OFF; +} + +bool ble_ctx_get_hid_active(struct Device* device) { + BleCtx* ctx = ble_get_ctx(device); + return ctx && ctx->hid_active.load(); +} +void ble_ctx_set_hid_active(struct Device* device, bool v) { + BleCtx* ctx = ble_get_ctx(device); + if (ctx) ctx->hid_active.store(v); +} + +bool ble_ctx_get_spp_active(struct Device* device) { + BleCtx* ctx = ble_get_ctx(device); + return ctx && ctx->spp_active.load(); +} +void ble_ctx_set_spp_active(struct Device* device, bool v) { + BleCtx* ctx = ble_get_ctx(device); + if (ctx) ctx->spp_active.store(v); +} +uint16_t ble_ctx_get_spp_conn_handle(struct Device* device) { + BleCtx* ctx = ble_get_ctx(device); + return ctx ? ctx->spp_conn_handle.load() : (uint16_t)BLE_HS_CONN_HANDLE_NONE; +} +void ble_ctx_set_spp_conn_handle(struct Device* device, uint16_t h) { + BleCtx* ctx = ble_get_ctx(device); + if (ctx) ctx->spp_conn_handle.store(h); +} + +bool ble_ctx_get_midi_active(struct Device* device) { + BleCtx* ctx = ble_get_ctx(device); + return ctx && ctx->midi_active.load(); +} +void ble_ctx_set_midi_active(struct Device* device, bool v) { + BleCtx* ctx = ble_get_ctx(device); + if (ctx) ctx->midi_active.store(v); +} +uint16_t ble_ctx_get_midi_conn_handle(struct Device* device) { + BleCtx* ctx = ble_get_ctx(device); + return ctx ? ctx->midi_conn_handle.load() : (uint16_t)BLE_HS_CONN_HANDLE_NONE; +} +void ble_ctx_set_midi_conn_handle(struct Device* device, uint16_t h) { + BleCtx* ctx = ble_get_ctx(device); + if (ctx) ctx->midi_conn_handle.store(h); +} +bool ble_ctx_get_midi_use_indicate(struct Device* device) { + BleCtx* ctx = ble_get_ctx(device); + return ctx && ctx->midi_use_indicate.load(); +} +void ble_ctx_set_midi_use_indicate(struct Device* device, bool v) { + BleCtx* ctx = ble_get_ctx(device); + if (ctx) ctx->midi_use_indicate.store(v); +} + +bool ble_ctx_get_hid_host_active(struct Device* device) { + BleCtx* ctx = ble_get_ctx(device); + return ctx && ctx->hid_host_active.load(); +} +bool ble_ctx_get_scan_active(struct Device* device) { + BleCtx* ctx = ble_get_ctx(device); + return ctx && ctx->scan_active.load(); +} +void ble_ctx_set_scan_active(struct Device* device, bool v) { + BleCtx* ctx = ble_get_ctx(device); + if (ctx) ctx->scan_active.store(v); +} + +error_t ble_ctx_ensure_midi_keepalive(struct Device* device, esp_timer_cb_t cb, uint64_t period_us) { + BleCtx* ctx = ble_get_ctx(device); + if (!ctx) return ERROR_INVALID_STATE; + if (ctx->midi_keepalive_timer == nullptr) { + esp_timer_create_args_t args = {}; + args.callback = cb; + args.arg = device; + args.dispatch_method = ESP_TIMER_TASK; + args.name = "ble_midi_as"; + int rc = esp_timer_create(&args, &ctx->midi_keepalive_timer); + if (rc != ESP_OK) { + LOG_E(TAG, "midi keepalive timer create failed (rc=%d)", rc); + return ERROR_INVALID_STATE; + } + } + int rc = esp_timer_start_periodic(ctx->midi_keepalive_timer, period_us); + if (rc != ESP_OK) { + LOG_E(TAG, "midi keepalive timer start failed (rc=%d)", rc); + return ERROR_INVALID_STATE; + } + return ERROR_NONE; +} + +void ble_ctx_stop_midi_keepalive(struct Device* device) { + BleCtx* ctx = ble_get_ctx(device); + if (ctx && ctx->midi_keepalive_timer != nullptr) { + esp_timer_stop(ctx->midi_keepalive_timer); + } +} + // ---- Event publishing ---- void ble_publish_event(struct Device* device, struct BtEvent event) { BleCtx* ctx = ble_get_ctx(device); + if (!ctx) return; // Copy under mutex so callbacks can safely call add/remove_event_callback BleCallbackEntry local[BLE_MAX_CALLBACKS]; size_t count; @@ -80,11 +235,16 @@ static void adv_restart_callback(void* arg) { struct Device* device = (struct Device*)arg; BleCtx* ctx = ble_get_ctx(device); if (ctx == nullptr || ctx->radio_state.load() != BT_RADIO_STATE_ON) return; + + BleHidDeviceCtx* hid_ctx = ctx->hid_device_child + ? (BleHidDeviceCtx*)device_get_driver_data(ctx->hid_device_child) : nullptr; + uint16_t hid_conn = hid_ctx ? hid_ctx->hid_conn_handle.load() : (uint16_t)BLE_HS_CONN_HANDLE_NONE; + if (ctx->midi_active.load() && ctx->midi_conn_handle.load() == BLE_HS_CONN_HANDLE_NONE) { ble_start_advertising(device, &MIDI_SVC_UUID); } else if (ctx->spp_active.load() && ctx->spp_conn_handle.load() == BLE_HS_CONN_HANDLE_NONE) { ble_start_advertising(device, &NUS_SVC_UUID); - } else if (ctx->hid_active.load() && ctx->hid_conn_handle.load() == BLE_HS_CONN_HANDLE_NONE) { + } else if (ctx->hid_active.load() && hid_conn == BLE_HS_CONN_HANDLE_NONE) { ble_start_advertising_hid(device, hid_appearance); } } @@ -121,13 +281,17 @@ static int gap_event_handler(struct ble_gap_event* event, void* arg) { BleCtx* ctx = ble_get_ctx(device); if (ctx == nullptr) return 0; + // Resolve HID device child context once for the whole handler. + BleHidDeviceCtx* hid_ctx = ctx->hid_device_child + ? (BleHidDeviceCtx*)device_get_driver_data(ctx->hid_device_child) : nullptr; + switch (event->type) { case BLE_GAP_EVENT_CONNECT: if (event->connect.status == 0) { LOG_I(TAG, "Connected (handle=%u hid_active=%d hid_conn=%u)", event->connect.conn_handle, (int)ctx->hid_active.load(), - (unsigned)ctx->hid_conn_handle.load()); + (unsigned)(hid_ctx ? hid_ctx->hid_conn_handle.load() : BLE_HS_CONN_HANDLE_NONE)); // Do NOT call ble_gap_security_initiate() here. // Windows BLE MIDI initiates encryption itself; calling here creates a race // with REPEAT_PAIRING+RETRY → two concurrent SM procedures → disconnect. @@ -145,10 +309,10 @@ static int gap_event_handler(struct ble_gap_event* event, void* arg) { uint16_t hdl = event->disconnect.conn.conn_handle; bool was_spp = (ctx->spp_conn_handle.load() == hdl); bool was_midi = (ctx->midi_conn_handle.load() == hdl); - bool was_hid = (ctx->hid_conn_handle.load() == hdl); + bool was_hid = hid_ctx && (hid_ctx->hid_conn_handle.load() == hdl); if (was_spp) ctx->spp_conn_handle.store(BLE_HS_CONN_HANDLE_NONE); if (was_midi) { ctx->midi_conn_handle.store(BLE_HS_CONN_HANDLE_NONE); ctx->midi_use_indicate.store(false); } - if (was_hid) ctx->hid_conn_handle.store(BLE_HS_CONN_HANDLE_NONE); + if (was_hid && hid_ctx) hid_ctx->hid_conn_handle.store(BLE_HS_CONN_HANDLE_NONE); ctx->link_encrypted.store(false); // If HID was stopped while connected, switch profile to None now that the // connection is gone. ble_gatts_mutable() is true here (no active connection, @@ -156,7 +320,7 @@ static int gap_event_handler(struct ble_gap_event* event, void* arg) { // Skip during shutdown — ble_gatts_reset() is unsafe while nimble_port_stop() runs. if (was_hid && !ctx->hid_active.load() && current_hid_profile != BleHidProfile::None && ctx->radio_state.load() != BT_RADIO_STATE_OFF_PENDING) { - ble_hid_device_switch_profile(device, BleHidProfile::None); + ble_hid_device_switch_profile(ctx->hid_device_child, BleHidProfile::None); } // Restart advertising whenever a service is active without a live connection. // Covers both normal disconnect and Windows discovery-only connections. @@ -164,11 +328,12 @@ static int gap_event_handler(struct ble_gap_event* event, void* arg) { // and calling ble_gap_adv_start() from within the NimBLE host task while the // controller is shutting down would block the host task and hang nimble_port_stop(). if (ctx->radio_state.load() != BT_RADIO_STATE_OFF_PENDING) { + uint16_t hid_conn_now = hid_ctx ? hid_ctx->hid_conn_handle.load() : (uint16_t)BLE_HS_CONN_HANDLE_NONE; if (ctx->midi_active.load() && ctx->midi_conn_handle.load() == BLE_HS_CONN_HANDLE_NONE) { ble_start_advertising(device, &MIDI_SVC_UUID); } else if (ctx->spp_active.load() && ctx->spp_conn_handle.load() == BLE_HS_CONN_HANDLE_NONE) { ble_start_advertising(device, &NUS_SVC_UUID); - } else if (ctx->hid_active.load() && ctx->hid_conn_handle.load() == BLE_HS_CONN_HANDLE_NONE) { + } else if (ctx->hid_active.load() && hid_conn_now == BLE_HS_CONN_HANDLE_NONE) { ble_start_advertising_hid(device, hid_appearance); } } @@ -260,8 +425,8 @@ static int gap_event_handler(struct ble_gap_event* event, void* arg) { } LOG_I(TAG, "HID CCCD subscribed: %s (attr=%u conn=%u)", rpt_name, event->subscribe.attr_handle, event->subscribe.conn_handle); - if (ctx->hid_conn_handle.load() == BLE_HS_CONN_HANDLE_NONE) { - ctx->hid_conn_handle.store(event->subscribe.conn_handle); + if (hid_ctx && hid_ctx->hid_conn_handle.load() == BLE_HS_CONN_HANDLE_NONE) { + hid_ctx->hid_conn_handle.store(event->subscribe.conn_handle); } } break; @@ -295,9 +460,9 @@ static int gap_event_handler(struct ble_gap_event* event, void* arg) { // Windows only writes HID CCCDs in Phase 2; NimBLE may restore them from NVS // silently (no SUBSCRIBE event). Without this, hid_conn_handle stays NONE // and hid_device_is_connected() returns false for the entire Phase 2 session. - if (ctx->hid_active.load() && - ctx->hid_conn_handle.load() == BLE_HS_CONN_HANDLE_NONE) { - ctx->hid_conn_handle.store(event->enc_change.conn_handle); + if (ctx->hid_active.load() && hid_ctx && + hid_ctx->hid_conn_handle.load() == BLE_HS_CONN_HANDLE_NONE) { + hid_ctx->hid_conn_handle.store(event->enc_change.conn_handle); LOG_I(TAG, "HID conn handle set on ENC_CHANGE (conn=%u)", event->enc_change.conn_handle); } @@ -391,9 +556,10 @@ static void on_sync() { rc = ble_hs_id_infer_auto(0, &own_addr_type); if (rc != 0) LOG_E(TAG, "infer addr type failed (rc=%d)", rc); - // Sync GATT handle values (also sets .arg = device on chr def entries) - ble_spp_init_gatt_handles(s_device); - ble_midi_init_gatt_handles(s_device); + // Sync GATT handle values — pass the child device so the GATT callbacks + // can retrieve child driver data (BleSppCtx / BleMidiCtx) without globals. + ble_spp_init_gatt_handles(ctx->serial_child); + ble_midi_init_gatt_handles(ctx->midi_child); ble_hid_device_init_gatt_handles(); ctx->radio_state.store(BT_RADIO_STATE_ON); @@ -547,6 +713,7 @@ void ble_start_advertising_hid(struct Device* device, uint16_t appearance) { int rc = ble_gap_adv_set_fields(&fields); if (rc != 0) { + LOG_W(TAG, "startAdvertisingHid: set_fields with name failed rc=%d, retrying", rc); fields.name = nullptr; fields.name_len = 0; rc = ble_gap_adv_set_fields(&fields); @@ -666,7 +833,6 @@ static void dispatch_disable(BleCtx* ctx) { ctx->spp_active.store(false); ctx->midi_conn_handle.store(BLE_HS_CONN_HANDLE_NONE); ctx->midi_active.store(false); - ctx->hid_conn_handle.store(BLE_HS_CONN_HANDLE_NONE); ctx->hid_active.store(false); ctx->link_encrypted.store(false); ctx->pending_reset_count.store(0); @@ -674,6 +840,12 @@ static void dispatch_disable(BleCtx* ctx) { active_hid_rpt_map = nullptr; active_hid_rpt_map_len = 0; + // Clear hid_conn_handle in the child device driver data if still alive. + if (ctx->hid_device_child != nullptr) { + BleHidDeviceCtx* hid_ctx = (BleHidDeviceCtx*)device_get_driver_data(ctx->hid_device_child); + if (hid_ctx) hid_ctx->hid_conn_handle.store(BLE_HS_CONN_HANDLE_NONE); + } + if (ctx->midi_keepalive_timer != nullptr) { esp_timer_stop(ctx->midi_keepalive_timer); esp_timer_delete(ctx->midi_keepalive_timer); @@ -729,12 +901,7 @@ static error_t api_scan_start(struct Device* device) { return ERROR_INVALID_STATE; } - { - xSemaphoreTake(ctx->data_mutex, portMAX_DELAY); - ctx->scan_count = 0; - memset(ctx->scan_results, 0, sizeof(ctx->scan_results)); - xSemaphoreGive(ctx->data_mutex); - } + ble_scan_clear_results(); struct ble_gap_disc_params disc_params = {}; disc_params.passive = 0; @@ -799,11 +966,11 @@ static error_t api_connect(struct Device* device, const BtAddr addr, enum BtProf BleCtx* ctx = (BleCtx*)device_get_driver_data(device); if (!ctx) return ERROR_INVALID_STATE; if (profile == BT_PROFILE_HID_DEVICE) { - return nimble_hid_device_api.start(device, BT_HID_DEVICE_MODE_KEYBOARD); + return nimble_hid_device_api.start(ctx->hid_device_child, BT_HID_DEVICE_MODE_KEYBOARD); } else if (profile == BT_PROFILE_SPP) { - return ble_spp_start_internal(device); + return ble_spp_start_internal(ctx->serial_child); } else if (profile == BT_PROFILE_MIDI) { - return ble_midi_start_internal(device); + return ble_midi_start_internal(ctx->midi_child); } // BT_PROFILE_HID_HOST is handled entirely in the Tactility layer. return ERROR_NOT_SUPPORTED; @@ -813,11 +980,11 @@ static error_t api_disconnect(struct Device* device, const BtAddr addr, enum BtP BleCtx* ctx = (BleCtx*)device_get_driver_data(device); if (!ctx) return ERROR_INVALID_STATE; if (profile == BT_PROFILE_HID_DEVICE) { - return nimble_hid_device_api.stop(device); + return nimble_hid_device_api.stop(ctx->hid_device_child); } else if (profile == BT_PROFILE_SPP) { - return nimble_serial_api.stop(device); + return nimble_serial_api.stop(ctx->serial_child); } else if (profile == BT_PROFILE_MIDI) { - return nimble_midi_api.stop(device); + return nimble_midi_api.stop(ctx->midi_child); } return ERROR_NOT_SUPPORTED; } @@ -914,16 +1081,61 @@ const BluetoothApi nimble_bluetooth_api = { .fire_event = api_fire_event, }; -// ---- Child device drivers ---- -// Serial, MIDI and HID device are child devices of the Bluetooth parent device. -// Their drivers have no start/stop of their own (lifecycle is tied to the parent BleCtx). -// driver_construct is called once at first start; the static DriverInternal persists. +// ---- Child device lifecycle functions ---- +// Serial (SPP) and MIDI children create/destroy their driver data (BleSppCtx / BleMidiCtx) +// in their device start/stop lifecycle functions. The HID device child manages its own +// driver data (BleHidDeviceCtx) in the API hid_device_start/stop functions. + +static error_t esp32_ble_serial_start_device(struct Device* device) { + BleSppCtx* sctx = new BleSppCtx(); + sctx->rx_mutex = xSemaphoreCreateMutex(); + device_set_driver_data(device, sctx); + return ERROR_NONE; +} + +static error_t esp32_ble_serial_stop_device(struct Device* device) { + BleSppCtx* sctx = (BleSppCtx*)device_get_driver_data(device); + if (sctx != nullptr) { + vSemaphoreDelete(sctx->rx_mutex); + delete sctx; + device_set_driver_data(device, nullptr); + } + return ERROR_NONE; +} + +static error_t esp32_ble_midi_start_device(struct Device* device) { + BleMidiCtx* mctx = new BleMidiCtx(); + mctx->rx_mutex = xSemaphoreCreateMutex(); + device_set_driver_data(device, mctx); + return ERROR_NONE; +} + +static error_t esp32_ble_midi_stop_device(struct Device* device) { + BleMidiCtx* mctx = (BleMidiCtx*)device_get_driver_data(device); + if (mctx != nullptr) { + vSemaphoreDelete(mctx->rx_mutex); + delete mctx; + device_set_driver_data(device, nullptr); + } + return ERROR_NONE; +} + +static error_t esp32_ble_hid_device_stop_device(struct Device* device) { + // Safety cleanup: free any BleHidDeviceCtx that was not deleted by hid_device_stop() + // (e.g. if the BLE device is stopped while HID is still connected). + BleHidDeviceCtx* hid_ctx = (BleHidDeviceCtx*)device_get_driver_data(device); + delete hid_ctx; // safe even if nullptr + device_set_driver_data(device, nullptr); + return ERROR_NONE; +} + +// ---- Driver definitions ---- static Driver esp32_ble_serial_driver = { .name = "esp32-ble-serial", .compatible = nullptr, - .start_device = nullptr, - .stop_device = nullptr, + .start_device = esp32_ble_serial_start_device, + .stop_device = esp32_ble_serial_stop_device, .api = &nimble_serial_api, .device_type = &BLUETOOTH_SERIAL_TYPE, .owner = nullptr, @@ -933,8 +1145,8 @@ static Driver esp32_ble_serial_driver = { static Driver esp32_ble_midi_driver = { .name = "esp32-ble-midi", .compatible = nullptr, - .start_device = nullptr, - .stop_device = nullptr, + .start_device = esp32_ble_midi_start_device, + .stop_device = esp32_ble_midi_stop_device, .api = &nimble_midi_api, .device_type = &BLUETOOTH_MIDI_TYPE, .owner = nullptr, @@ -945,7 +1157,7 @@ static Driver esp32_ble_hid_device_driver = { .name = "esp32-ble-hid-device", .compatible = nullptr, .start_device = nullptr, - .stop_device = nullptr, + .stop_device = esp32_ble_hid_device_stop_device, .api = &nimble_hid_device_api, .device_type = &BLUETOOTH_HID_DEVICE_TYPE, .owner = nullptr, @@ -985,19 +1197,16 @@ static error_t esp32_ble_start_device(struct Device* device) { BleCtx* ctx = new BleCtx(); ctx->radio_mutex = xSemaphoreCreateRecursiveMutex(); - ctx->data_mutex = xSemaphoreCreateMutex(); ctx->cb_mutex = xSemaphoreCreateMutex(); ctx->radio_state.store(BT_RADIO_STATE_OFF); ctx->scan_active.store(false); ctx->hid_host_active.store(false); ctx->callback_count = 0; - ctx->scan_count = 0; ctx->spp_conn_handle.store(BLE_HS_CONN_HANDLE_NONE); ctx->spp_active.store(false); ctx->midi_conn_handle.store(BLE_HS_CONN_HANDLE_NONE); ctx->midi_active.store(false); ctx->midi_use_indicate.store(false); - ctx->hid_conn_handle.store(BLE_HS_CONN_HANDLE_NONE); ctx->hid_active.store(false); ctx->link_encrypted.store(false); ctx->pending_reset_count.store(0); @@ -1028,7 +1237,11 @@ static error_t esp32_ble_start_device(struct Device* device) { device_set_driver_data(device, ctx); s_device = device; + ble_scan_init(); + // Create child devices for the serial, MIDI and HID device profiles. + // device_start() on each child will invoke start_device (for serial/midi) + // which initialises their driver data (BleSppCtx / BleMidiCtx). create_child_device(device, "ble-serial", &esp32_ble_serial_driver, ctx->serial_child); create_child_device(device, "ble-midi", &esp32_ble_midi_driver, ctx->midi_child); create_child_device(device, "ble-hid-device", &esp32_ble_hid_device_driver, ctx->hid_device_child); @@ -1041,6 +1254,8 @@ static error_t esp32_ble_stop_device(struct Device* device) { if (!ctx) return ERROR_NONE; // Destroy child devices before stopping the radio and freeing the context. + // device_stop() on each child will invoke stop_device (for serial/midi/hid) + // which frees their driver data. destroy_child_device(ctx->hid_device_child); destroy_child_device(ctx->midi_child); destroy_child_device(ctx->serial_child); @@ -1049,6 +1264,8 @@ static error_t esp32_ble_stop_device(struct Device* device) { dispatch_disable(ctx); } + ble_scan_deinit(); + if (ctx->disable_timer != nullptr) { esp_timer_stop(ctx->disable_timer); esp_timer_delete(ctx->disable_timer); diff --git a/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_hid_device.cpp b/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_hid_device.cpp index ccbd2f8c5..93481c31e 100644 --- a/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_hid_device.cpp +++ b/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_hid_device.cpp @@ -15,6 +15,7 @@ #include #define TAG "esp32_ble_hid" +#include #include #pragma GCC diagnostic push @@ -299,6 +300,7 @@ static const struct ble_gatt_svc_def gatt_svcs_gamepad[] = { }; // ---- GATT profile switch ---- +// device must be the HID device child Device*. void ble_hid_device_switch_profile(struct Device* device, BleHidProfile profile) { if (profile == current_hid_profile) return; @@ -306,10 +308,10 @@ void ble_hid_device_switch_profile(struct Device* device, BleHidProfile profile) ble_gap_adv_stop(); - BleCtx* ctx = ble_get_ctx(device); - if (ctx && ctx->hid_conn_handle.load() != BLE_HS_CONN_HANDLE_NONE) { - ble_gap_terminate(ctx->hid_conn_handle.load(), BLE_ERR_REM_USER_CONN_TERM); - ctx->hid_conn_handle.store(BLE_HS_CONN_HANDLE_NONE); + BleHidDeviceCtx* hid_ctx = (BleHidDeviceCtx*)device_get_driver_data(device); + if (hid_ctx && hid_ctx->hid_conn_handle.load() != BLE_HS_CONN_HANDLE_NONE) { + ble_gap_terminate(hid_ctx->hid_conn_handle.load(), BLE_ERR_REM_USER_CONN_TERM); + hid_ctx->hid_conn_handle.store(BLE_HS_CONN_HANDLE_NONE); } ble_gatts_reset(); @@ -384,10 +386,14 @@ void ble_hid_device_init_gatt_handles() { } // ---- HID Device sub-API implementations ---- +// All functions receive the HID device child Device* and operate on BleHidDeviceCtx +// stored as its driver data. static error_t hid_device_start(struct Device* device, enum BtHidDeviceMode mode) { - BleCtx* ctx = ble_get_ctx(device); - if (ctx == nullptr) return ERROR_INVALID_STATE; + // Create driver data for this HID session. + BleHidDeviceCtx* hid_ctx = new BleHidDeviceCtx(); + hid_ctx->hid_conn_handle.store(BLE_HS_CONN_HANDLE_NONE); + device_set_driver_data(device, hid_ctx); BleHidProfile profile; uint16_t appearance; @@ -412,42 +418,45 @@ static error_t hid_device_start(struct Device* device, enum BtHidDeviceMode mode hid_appearance = appearance; ble_hid_device_switch_profile(device, profile); - ctx->hid_active.store(true); + ble_ctx_set_hid_active(device, true); ble_start_advertising_hid(device, hid_appearance); return ERROR_NONE; } static error_t hid_device_stop(struct Device* device) { - BleCtx* ctx = ble_get_ctx(device); - if (ctx == nullptr) return ERROR_NONE; - ctx->hid_active.store(false); + ble_ctx_set_hid_active(device, false); ble_gap_adv_stop(); - uint16_t conn = ctx->hid_conn_handle.load(); + BleHidDeviceCtx* hid_ctx = (BleHidDeviceCtx*)device_get_driver_data(device); + uint16_t conn = hid_ctx ? hid_ctx->hid_conn_handle.load() : (uint16_t)BLE_HS_CONN_HANDLE_NONE; if (conn != BLE_HS_CONN_HANDLE_NONE) { // Connected: terminate and let the DISCONNECT handler switch profile to None. // ble_gatts_mutable() returns false while a connection is live, so calling // switch_profile here would assert inside ble_svc_gap_init(). ble_gap_terminate(conn, BLE_ERR_REM_USER_CONN_TERM); - // Do NOT clear hid_conn_handle — DISCONNECT handler uses it for was_hid detection. + // Do NOT clear hid_conn_handle or delete hid_ctx: + // the DISCONNECT handler in esp32_ble.cpp uses hid_conn_handle for was_hid detection. + // esp32_ble_hid_device_stop_device() (device lifecycle) will free hid_ctx later. } else { // Not connected: GATT is mutable, switch profile immediately. if (current_hid_profile != BleHidProfile::None) { ble_hid_device_switch_profile(device, BleHidProfile::None); } + delete hid_ctx; + device_set_driver_data(device, nullptr); } return ERROR_NONE; } static error_t hid_device_send_key(struct Device* device, uint8_t keycode, bool pressed) { - BleCtx* ctx = ble_get_ctx(device); - if (ctx == nullptr || ctx->hid_conn_handle.load() == BLE_HS_CONN_HANDLE_NONE) { + BleHidDeviceCtx* hid_ctx = (BleHidDeviceCtx*)device_get_driver_data(device); + if (hid_ctx == nullptr || hid_ctx->hid_conn_handle.load() == BLE_HS_CONN_HANDLE_NONE) { return ERROR_INVALID_STATE; } uint8_t report[8] = {}; if (pressed) report[2] = keycode; struct os_mbuf* om = ble_hs_mbuf_from_flat(report, sizeof(report)); if (om == nullptr) return ERROR_INVALID_STATE; - int rc = ble_gatts_notify_custom(ctx->hid_conn_handle.load(), hid_kb_input_handle, om); + int rc = ble_gatts_notify_custom(hid_ctx->hid_conn_handle.load(), hid_kb_input_handle, om); if (rc != 0) os_mbuf_free_chain(om); return (rc == 0) ? ERROR_NONE : ERROR_INVALID_STATE; } @@ -464,40 +473,40 @@ static error_t hid_notify(uint16_t conn_handle, uint16_t attr_handle, } static error_t hid_device_send_keyboard(struct Device* device, const uint8_t* report, size_t len) { - BleCtx* ctx = ble_get_ctx(device); - if (ctx == nullptr) return ERROR_INVALID_STATE; + BleHidDeviceCtx* hid_ctx = (BleHidDeviceCtx*)device_get_driver_data(device); + if (hid_ctx == nullptr) return ERROR_INVALID_STATE; uint8_t buf[8] = {}; memcpy(buf, report, len < sizeof(buf) ? len : sizeof(buf)); - return hid_notify(ctx->hid_conn_handle.load(), hid_kb_input_handle, buf, sizeof(buf)); + return hid_notify(hid_ctx->hid_conn_handle.load(), hid_kb_input_handle, buf, sizeof(buf)); } static error_t hid_device_send_consumer(struct Device* device, const uint8_t* report, size_t len) { - BleCtx* ctx = ble_get_ctx(device); - if (ctx == nullptr) return ERROR_INVALID_STATE; + BleHidDeviceCtx* hid_ctx = (BleHidDeviceCtx*)device_get_driver_data(device); + if (hid_ctx == nullptr) return ERROR_INVALID_STATE; uint8_t buf[2] = {}; memcpy(buf, report, len < sizeof(buf) ? len : sizeof(buf)); - return hid_notify(ctx->hid_conn_handle.load(), hid_consumer_input_handle, buf, sizeof(buf)); + return hid_notify(hid_ctx->hid_conn_handle.load(), hid_consumer_input_handle, buf, sizeof(buf)); } static error_t hid_device_send_mouse(struct Device* device, const uint8_t* report, size_t len) { - BleCtx* ctx = ble_get_ctx(device); - if (ctx == nullptr) return ERROR_INVALID_STATE; + BleHidDeviceCtx* hid_ctx = (BleHidDeviceCtx*)device_get_driver_data(device); + if (hid_ctx == nullptr) return ERROR_INVALID_STATE; uint8_t buf[4] = {}; memcpy(buf, report, len < sizeof(buf) ? len : sizeof(buf)); - return hid_notify(ctx->hid_conn_handle.load(), hid_mouse_input_handle, buf, sizeof(buf)); + return hid_notify(hid_ctx->hid_conn_handle.load(), hid_mouse_input_handle, buf, sizeof(buf)); } static error_t hid_device_send_gamepad(struct Device* device, const uint8_t* report, size_t len) { - BleCtx* ctx = ble_get_ctx(device); - if (ctx == nullptr) return ERROR_INVALID_STATE; + BleHidDeviceCtx* hid_ctx = (BleHidDeviceCtx*)device_get_driver_data(device); + if (hid_ctx == nullptr) return ERROR_INVALID_STATE; uint8_t buf[8] = {}; memcpy(buf, report, len < sizeof(buf) ? len : sizeof(buf)); - return hid_notify(ctx->hid_conn_handle.load(), hid_gamepad_input_handle, buf, sizeof(buf)); + return hid_notify(hid_ctx->hid_conn_handle.load(), hid_gamepad_input_handle, buf, sizeof(buf)); } static bool hid_device_is_connected(struct Device* device) { - BleCtx* ctx = ble_get_ctx(device); - return ctx != nullptr && ctx->hid_conn_handle.load() != BLE_HS_CONN_HANDLE_NONE; + BleHidDeviceCtx* hid_ctx = (BleHidDeviceCtx*)device_get_driver_data(device); + return hid_ctx != nullptr && hid_ctx->hid_conn_handle.load() != BLE_HS_CONN_HANDLE_NONE; } const BtHidDeviceApi nimble_hid_device_api = { diff --git a/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_midi.cpp b/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_midi.cpp index d08bddc17..77980202f 100644 --- a/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_midi.cpp +++ b/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_midi.cpp @@ -6,15 +6,16 @@ #include +#include +#include #include #include #include -#include -#include #define TAG "esp32_ble_midi" -#include #include +#include +#include #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wmissing-field-initializers" @@ -35,21 +36,22 @@ static const ble_uuid128_t MIDI_IO_UUID = BLE_UUID128_INIT( uint16_t midi_io_handle; +// midi_chr_access is called with the midi child Device* (set via ble_midi_init_gatt_handles). static int midi_chr_access(uint16_t conn_handle, uint16_t attr_handle, struct ble_gatt_access_ctxt* ctxt, void* arg) { if (ctxt->op == BLE_GATT_ACCESS_OP_WRITE_CHR) { uint16_t len = OS_MBUF_PKTLEN(ctxt->om); LOG_I(TAG, "MIDI RX %u bytes", (unsigned)len); - struct Device* device = (struct Device*)arg; - BleCtx* ctx = ble_get_ctx(device); - if (ctx != nullptr && len > 0) { + struct Device* device = (struct Device*)arg; // midi child device + BleMidiCtx* mctx = (BleMidiCtx*)device_get_driver_data(device); + if (mctx != nullptr && len > 0) { std::vector packet(len); os_mbuf_copydata(ctxt->om, 0, len, packet.data()); { - xSemaphoreTake(ctx->data_mutex, portMAX_DELAY); - ctx->midi_rx_queue.push_back(std::move(packet)); - while (ctx->midi_rx_queue.size() > 16) ctx->midi_rx_queue.pop_front(); - xSemaphoreGive(ctx->data_mutex); + xSemaphoreTake(mctx->rx_mutex, portMAX_DELAY); + mctx->rx_queue.push_back(std::move(packet)); + while (mctx->rx_queue.size() > 16) mctx->rx_queue.pop_front(); + xSemaphoreGive(mctx->rx_mutex); } struct BtEvent e = {}; e.type = BT_EVENT_MIDI_DATA_RECEIVED; @@ -63,88 +65,70 @@ struct ble_gatt_chr_def midi_chars[] = { { .uuid = &MIDI_IO_UUID.u, .access_cb = midi_chr_access, - .arg = nullptr, // set to Device* in ble_midi_init_gatt_handles() + .arg = nullptr, // set to midi child Device* in ble_midi_init_gatt_handles() .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_WRITE_NO_RSP | BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_INDICATE, .val_handle = &midi_io_handle, }, { 0 } }; -void ble_midi_init_gatt_handles(struct Device* device) { - // Set the Device* arg so that midi_chr_access can retrieve context without a global. +void ble_midi_init_gatt_handles(struct Device* midi_child) { + // Store the midi child Device* as the GATT callback arg so that midi_chr_access + // can retrieve BleMidiCtx via device_get_driver_data without a global pointer. // midi_io_handle is written by NimBLE via the val_handle pointer above. - midi_chars[0].arg = device; + midi_chars[0].arg = midi_child; } // ---- MIDI Active Sensing keepalive ---- +// Callback arg is the midi child Device*. static void midi_keepalive_cb(void* arg) { - struct Device* device = (struct Device*)arg; - BleCtx* ctx = ble_get_ctx(device); - if (ctx == nullptr || ctx->midi_conn_handle.load() == BLE_HS_CONN_HANDLE_NONE) return; + struct Device* device = (struct Device*)arg; // midi child device + uint16_t conn = ble_ctx_get_midi_conn_handle(device); + if (conn == BLE_HS_CONN_HANDLE_NONE) return; static const uint8_t as_pkt[3] = { 0x80, 0x80, 0xFE }; struct os_mbuf* om = ble_hs_mbuf_from_flat(as_pkt, 3); if (om == nullptr) return; - int rc = ctx->midi_use_indicate.load() - ? ble_gatts_indicate_custom(ctx->midi_conn_handle.load(), midi_io_handle, om) - : ble_gatts_notify_custom(ctx->midi_conn_handle.load(), midi_io_handle, om); + int rc = ble_ctx_get_midi_use_indicate(device) + ? ble_gatts_indicate_custom(conn, midi_io_handle, om) + : ble_gatts_notify_custom(conn, midi_io_handle, om); if (rc != 0) os_mbuf_free_chain(om); } // ---- MIDI sub-API implementations ---- +// All functions receive the midi child Device*. static error_t midi_start(struct Device* device) { - BleCtx* ctx = ble_get_ctx(device); - if (ctx == nullptr) return ERROR_INVALID_STATE; - ctx->midi_active.store(true); + ble_ctx_set_midi_active(device, true); // Create 2-second periodic Active Sensing timer to prevent Windows BLE MIDI // driver from declaring the connection idle and disconnecting (~8-10 s timeout). - if (ctx->midi_keepalive_timer == nullptr) { - esp_timer_create_args_t args = {}; - args.callback = midi_keepalive_cb; - args.arg = device; - args.dispatch_method = ESP_TIMER_TASK; - args.name = "ble_midi_as"; - int rc = esp_timer_create(&args, &ctx->midi_keepalive_timer); - if (rc != ESP_OK) { - LOG_E(TAG, "midi_start: keepalive timer create failed (rc=%d)", rc); - return ERROR_INVALID_STATE; - } - } - int rc = esp_timer_start_periodic(ctx->midi_keepalive_timer, 2'000'000); - if (rc != ESP_OK) { - LOG_E(TAG, "midi_start: keepalive timer start failed (rc=%d)", rc); - } + error_t rc = ble_ctx_ensure_midi_keepalive(device, midi_keepalive_cb, 2'000'000); + if (rc != ERROR_NONE) return rc; ble_start_advertising(device, &MIDI_SVC_UUID); return ERROR_NONE; } -error_t ble_midi_start_internal(struct Device* device) { - return midi_start(device); +error_t ble_midi_start_internal(struct Device* midi_child) { + return midi_start(midi_child); } static error_t midi_stop(struct Device* device) { - BleCtx* ctx = ble_get_ctx(device); - if (ctx == nullptr) return ERROR_NONE; - ctx->midi_active.store(false); - if (ctx->midi_keepalive_timer != nullptr) { - esp_timer_stop(ctx->midi_keepalive_timer); + ble_ctx_set_midi_active(device, false); + ble_ctx_stop_midi_keepalive(device); + uint16_t conn = ble_ctx_get_midi_conn_handle(device); + if (conn != BLE_HS_CONN_HANDLE_NONE) { + ble_gap_terminate(conn, BLE_ERR_REM_USER_CONN_TERM); + ble_ctx_set_midi_conn_handle(device, BLE_HS_CONN_HANDLE_NONE); } - if (ctx->midi_conn_handle.load() != BLE_HS_CONN_HANDLE_NONE) { - ble_gap_terminate(ctx->midi_conn_handle.load(), BLE_ERR_REM_USER_CONN_TERM); - ctx->midi_conn_handle.store(BLE_HS_CONN_HANDLE_NONE); - } - if (!ctx->spp_active.load() && !ctx->hid_active.load()) { + if (!ble_ctx_get_spp_active(device) && !ble_ctx_get_hid_active(device)) { ble_gap_adv_stop(); } return ERROR_NONE; } static error_t midi_send(struct Device* device, const uint8_t* msg, size_t len) { - BleCtx* ctx = ble_get_ctx(device); - if (ctx == nullptr || ctx->midi_conn_handle.load() == BLE_HS_CONN_HANDLE_NONE) { - return ERROR_INVALID_STATE; - } + uint16_t conn = ble_ctx_get_midi_conn_handle(device); + if (conn == BLE_HS_CONN_HANDLE_NONE) return ERROR_INVALID_STATE; // BLE MIDI 2-byte header: [0x80|(ts_high&0x3F)][0x80|(ts_low&0x7F)] uint8_t header[2] = { 0x80, 0x80 }; struct os_mbuf* om = ble_hs_mbuf_from_flat(header, 2); @@ -154,10 +138,10 @@ static error_t midi_send(struct Device* device, const uint8_t* msg, size_t len) LOG_E(TAG, "midi_send: mbuf append failed"); return ERROR_INVALID_STATE; } - LOG_I(TAG, "midi_send %u bytes (indicate=%d)", (unsigned)len, (int)ctx->midi_use_indicate.load()); - int rc = ctx->midi_use_indicate.load() - ? ble_gatts_indicate_custom(ctx->midi_conn_handle.load(), midi_io_handle, om) - : ble_gatts_notify_custom(ctx->midi_conn_handle.load(), midi_io_handle, om); + LOG_I(TAG, "midi_send %u bytes (indicate=%d)", (unsigned)len, (int)ble_ctx_get_midi_use_indicate(device)); + int rc = ble_ctx_get_midi_use_indicate(device) + ? ble_gatts_indicate_custom(conn, midi_io_handle, om) + : ble_gatts_notify_custom(conn, midi_io_handle, om); if (rc != 0) { os_mbuf_free_chain(om); LOG_E(TAG, "midi_send failed rc=%d", rc); @@ -166,8 +150,7 @@ static error_t midi_send(struct Device* device, const uint8_t* msg, size_t len) } static bool midi_is_connected(struct Device* device) { - BleCtx* ctx = ble_get_ctx(device); - return ctx != nullptr && ctx->midi_conn_handle.load() != BLE_HS_CONN_HANDLE_NONE; + return ble_ctx_get_midi_conn_handle(device) != BLE_HS_CONN_HANDLE_NONE; } const BtMidiApi nimble_midi_api = { diff --git a/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_scan.cpp b/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_scan.cpp index 73f3eb629..2852fd2b5 100644 --- a/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_scan.cpp +++ b/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_scan.cpp @@ -15,18 +15,50 @@ #define TAG "esp32_ble_scan" #include +// ---- Module-static scan data ---- +// Scan results and their associated state are owned entirely by this module. +// esp32_ble.cpp clears them via ble_scan_clear_results() before each new scan. + +#define BLE_SCAN_MAX 64 + +static SemaphoreHandle_t s_scan_mutex = nullptr; +static BtPeerRecord s_scan_results[BLE_SCAN_MAX]; +static ble_addr_t s_scan_addrs[BLE_SCAN_MAX]; // full ble_addr_t (type+val) for connections +static size_t s_scan_count = 0; + // Module-static device pointer used only by the name-resolution GAP/GATT callbacks // whose void* arg is already occupied by the peer index (uintptr_t). // Set at the start of ble_resolve_next_unnamed_peer; valid for the duration of // the sequential resolution chain (single-device, single-scan at a time). static struct Device* s_scan_device = nullptr; +// ---- Scan data lifecycle ---- + +void ble_scan_init() { + s_scan_mutex = xSemaphoreCreateMutex(); + s_scan_count = 0; + memset(s_scan_results, 0, sizeof(s_scan_results)); +} + +void ble_scan_deinit() { + if (s_scan_mutex != nullptr) { + vSemaphoreDelete(s_scan_mutex); + s_scan_mutex = nullptr; + } + s_scan_count = 0; +} + +void ble_scan_clear_results() { + xSemaphoreTake(s_scan_mutex, portMAX_DELAY); + s_scan_count = 0; + memset(s_scan_results, 0, sizeof(s_scan_results)); + xSemaphoreGive(s_scan_mutex); +} + // ---- GAP scan callback ---- int ble_gap_disc_event_handler(struct ble_gap_event* event, void* arg) { struct Device* device = (struct Device*)arg; - BleCtx* ctx = ble_get_ctx(device); - if (ctx == nullptr) return 0; switch (event->type) { case BLE_GAP_EVENT_DISC: { @@ -49,25 +81,25 @@ int ble_gap_disc_event_handler(struct ble_gap_event* event, void* arg) { } { - xSemaphoreTake(ctx->data_mutex, portMAX_DELAY); + xSemaphoreTake(s_scan_mutex, portMAX_DELAY); bool found = false; - for (size_t i = 0; i < ctx->scan_count; ++i) { - if (memcmp(ctx->scan_results[i].addr, record.addr, BT_ADDR_LEN) == 0) { + for (size_t i = 0; i < s_scan_count; ++i) { + if (memcmp(s_scan_results[i].addr, record.addr, BT_ADDR_LEN) == 0) { // Deduplicate: merge name from SCAN_RSP without clobbering ADV_IND name if (record.name[0] != '\0') { - memcpy(ctx->scan_results[i].name, record.name, BT_NAME_MAX + 1); + memcpy(s_scan_results[i].name, record.name, BT_NAME_MAX + 1); } - ctx->scan_results[i].rssi = record.rssi; + s_scan_results[i].rssi = record.rssi; found = true; break; } } - if (!found && ctx->scan_count < BLE_SCAN_MAX) { - ctx->scan_results[ctx->scan_count] = record; - ctx->scan_addrs[ctx->scan_count] = disc.addr; // full addr (type+val) - ctx->scan_count++; + if (!found && s_scan_count < BLE_SCAN_MAX) { + s_scan_results[s_scan_count] = record; + s_scan_addrs[s_scan_count] = disc.addr; // full addr (type+val) + s_scan_count++; } - xSemaphoreGive(ctx->data_mutex); + xSemaphoreGive(s_scan_mutex); } struct BtEvent e = {}; @@ -102,8 +134,6 @@ int ble_gap_disc_event_handler(struct ble_gap_event* event, void* arg) { static int name_read_callback(uint16_t conn_handle, const struct ble_gatt_error* error, struct ble_gatt_attr* attr, void* arg) { struct Device* device = s_scan_device; - BleCtx* ctx = ble_get_ctx(device); - if (ctx == nullptr) return 0; if (error->status == 0 && attr != nullptr) { size_t idx = (size_t)(uintptr_t)arg; @@ -112,14 +142,14 @@ static int name_read_callback(uint16_t conn_handle, const struct ble_gatt_error* char name_buf[BT_NAME_MAX + 1] = {}; os_mbuf_copydata(attr->om, 0, len, name_buf); { - xSemaphoreTake(ctx->data_mutex, portMAX_DELAY); - if (idx < ctx->scan_count && ctx->scan_results[idx].name[0] == '\0') { - memcpy(ctx->scan_results[idx].name, name_buf, len); - ctx->scan_results[idx].name[len] = '\0'; + xSemaphoreTake(s_scan_mutex, portMAX_DELAY); + if (idx < s_scan_count && s_scan_results[idx].name[0] == '\0') { + memcpy(s_scan_results[idx].name, name_buf, len); + s_scan_results[idx].name[len] = '\0'; LOG_I(TAG, "Name resolved (idx=%u): %s", (unsigned)idx, name_buf); } - BtPeerRecord record = (idx < ctx->scan_count) ? ctx->scan_results[idx] : BtPeerRecord{}; - xSemaphoreGive(ctx->data_mutex); + BtPeerRecord record = (idx < s_scan_count) ? s_scan_results[idx] : BtPeerRecord{}; + xSemaphoreGive(s_scan_mutex); struct BtEvent e = {}; e.type = BT_EVENT_PEER_FOUND; @@ -138,8 +168,6 @@ static int name_read_callback(uint16_t conn_handle, const struct ble_gatt_error* static int name_res_gap_callback(struct ble_gap_event* event, void* arg) { size_t idx = (size_t)(uintptr_t)arg; struct Device* device = s_scan_device; - BleCtx* ctx = ble_get_ctx(device); - if (ctx == nullptr) return 0; switch (event->type) { case BLE_GAP_EVENT_CONNECT: @@ -172,15 +200,14 @@ static int name_res_gap_callback(struct ble_gap_event* event, void* arg) { } void ble_resolve_next_unnamed_peer(struct Device* device, size_t start_idx) { - BleCtx* ctx = ble_get_ctx(device); s_scan_device = device; // Skip if a profile server or HID host connection attempt is active — // initiating a central connection simultaneously would fail (BLE_HS_EALREADY). - if (ctx->midi_active.load() || ctx->spp_active.load() || - ctx->hid_active.load() || ctx->hid_host_active.load()) { + if (ble_ctx_get_midi_active(device) || ble_ctx_get_spp_active(device) || + ble_ctx_get_hid_active(device) || ble_ctx_get_hid_host_active(device)) { LOG_I(TAG, "Name resolution: skipping (server or HID host active)"); - ctx->scan_active.store(false); + ble_ctx_set_scan_active(device, false); struct BtEvent e = {}; e.type = BT_EVENT_SCAN_FINISHED; ble_publish_event(device, e); @@ -192,21 +219,21 @@ void ble_resolve_next_unnamed_peer(struct Device* device, size_t start_idx) { ble_addr_t addr = {}; bool found = false; { - xSemaphoreTake(ctx->data_mutex, portMAX_DELAY); - while (i < ctx->scan_count) { - if (ctx->scan_results[i].name[0] == '\0') { - addr = ctx->scan_addrs[i]; + xSemaphoreTake(s_scan_mutex, portMAX_DELAY); + while (i < s_scan_count) { + if (s_scan_results[i].name[0] == '\0') { + addr = s_scan_addrs[i]; found = true; break; } ++i; } - xSemaphoreGive(ctx->data_mutex); + xSemaphoreGive(s_scan_mutex); } if (!found) { LOG_I(TAG, "Name resolution: complete (%u devices)", (unsigned)i); - ctx->scan_active.store(false); + ble_ctx_set_scan_active(device, false); struct BtEvent e = {}; e.type = BT_EVENT_SCAN_FINISHED; ble_publish_event(device, e); diff --git a/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_spp.cpp b/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_spp.cpp index 0ac953aa4..3db016f93 100644 --- a/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_spp.cpp +++ b/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_spp.cpp @@ -6,13 +6,14 @@ #include +#include +#include #include #include #include -#include -#include #define TAG "esp32_ble_spp" +#include #include #pragma GCC diagnostic push @@ -40,21 +41,22 @@ static const ble_uuid128_t NUS_TX_UUID = BLE_UUID128_INIT( uint16_t nus_tx_handle; +// nus_chr_access is called with the serial child Device* (set via ble_spp_init_gatt_handles). static int nus_chr_access(uint16_t conn_handle, uint16_t attr_handle, struct ble_gatt_access_ctxt* ctxt, void* arg) { if (ctxt->op == BLE_GATT_ACCESS_OP_WRITE_CHR) { uint16_t len = OS_MBUF_PKTLEN(ctxt->om); LOG_I(TAG, "NUS RX %u bytes", (unsigned)len); - struct Device* device = (struct Device*)arg; - BleCtx* ctx = ble_get_ctx(device); - if (ctx != nullptr && len > 0) { + struct Device* device = (struct Device*)arg; // serial child device + BleSppCtx* sctx = (BleSppCtx*)device_get_driver_data(device); + if (sctx != nullptr && len > 0) { std::vector packet(len); os_mbuf_copydata(ctxt->om, 0, len, packet.data()); { - xSemaphoreTake(ctx->data_mutex, portMAX_DELAY); - ctx->spp_rx_queue.push_back(std::move(packet)); - while (ctx->spp_rx_queue.size() > 16) ctx->spp_rx_queue.pop_front(); - xSemaphoreGive(ctx->data_mutex); + xSemaphoreTake(sctx->rx_mutex, portMAX_DELAY); + sctx->rx_queue.push_back(std::move(packet)); + while (sctx->rx_queue.size() > 16) sctx->rx_queue.pop_front(); + xSemaphoreGive(sctx->rx_mutex); } struct BtEvent e = {}; e.type = BT_EVENT_SPP_DATA_RECEIVED; @@ -68,59 +70,58 @@ struct ble_gatt_chr_def nus_chars_with_handle[] = { { .uuid = &NUS_RX_UUID.u, .access_cb = nus_chr_access, - .arg = nullptr, // set to Device* in ble_spp_init_gatt_handles() + .arg = nullptr, // set to serial child Device* in ble_spp_init_gatt_handles() .flags = BLE_GATT_CHR_F_WRITE | BLE_GATT_CHR_F_WRITE_NO_RSP, }, { .uuid = &NUS_TX_UUID.u, .access_cb = nus_chr_access, - .arg = nullptr, // set to Device* in ble_spp_init_gatt_handles() + .arg = nullptr, // set to serial child Device* in ble_spp_init_gatt_handles() .flags = BLE_GATT_CHR_F_NOTIFY, .val_handle = &nus_tx_handle, }, { 0 } }; -void ble_spp_init_gatt_handles(struct Device* device) { - // Set the Device* arg so that nus_chr_access can retrieve context without a global. +void ble_spp_init_gatt_handles(struct Device* serial_child) { + // Store the serial child Device* as the GATT callback arg so that nus_chr_access + // can retrieve BleSppCtx via device_get_driver_data without a global pointer. // nus_tx_handle is written by NimBLE via the val_handle pointer above. - nus_chars_with_handle[0].arg = device; - nus_chars_with_handle[1].arg = device; + nus_chars_with_handle[0].arg = serial_child; + nus_chars_with_handle[1].arg = serial_child; } // ---- SPP sub-API implementations ---- +// All functions receive the serial child Device*. static error_t spp_start(struct Device* device) { - BleCtx* ctx = ble_get_ctx(device); - if (ctx == nullptr) return ERROR_INVALID_STATE; - ctx->spp_active.store(true); + ble_ctx_set_spp_active(device, true); ble_start_advertising(device, &NUS_SVC_UUID); return ERROR_NONE; } -error_t ble_spp_start_internal(struct Device* device) { - return spp_start(device); +error_t ble_spp_start_internal(struct Device* serial_child) { + return spp_start(serial_child); } static error_t spp_stop(struct Device* device) { - BleCtx* ctx = ble_get_ctx(device); - if (ctx == nullptr) return ERROR_NONE; - ctx->spp_active.store(false); - if (ctx->spp_conn_handle.load() != BLE_HS_CONN_HANDLE_NONE) { - ble_gap_terminate(ctx->spp_conn_handle.load(), BLE_ERR_REM_USER_CONN_TERM); - ctx->spp_conn_handle.store(BLE_HS_CONN_HANDLE_NONE); + ble_ctx_set_spp_active(device, false); + uint16_t conn = ble_ctx_get_spp_conn_handle(device); + if (conn != BLE_HS_CONN_HANDLE_NONE) { + ble_gap_terminate(conn, BLE_ERR_REM_USER_CONN_TERM); + ble_ctx_set_spp_conn_handle(device, BLE_HS_CONN_HANDLE_NONE); } // Do NOT restart advertising after user-initiated stop — restarting name-only // advertising causes bonded Windows hosts to auto-reconnect in a tight loop. - if (!ctx->midi_active.load() && !ctx->hid_active.load()) { + if (!ble_ctx_get_midi_active(device) && !ble_ctx_get_hid_active(device)) { ble_gap_adv_stop(); } return ERROR_NONE; } static error_t spp_write(struct Device* device, const uint8_t* data, size_t len, size_t* written) { - BleCtx* ctx = ble_get_ctx(device); - if (ctx == nullptr || ctx->spp_conn_handle.load() == BLE_HS_CONN_HANDLE_NONE) { + uint16_t conn = ble_ctx_get_spp_conn_handle(device); + if (conn == BLE_HS_CONN_HANDLE_NONE) { if (written) *written = 0; return ERROR_INVALID_STATE; } @@ -129,7 +130,7 @@ static error_t spp_write(struct Device* device, const uint8_t* data, size_t len, if (written) *written = 0; return ERROR_INVALID_STATE; } - int rc = ble_gatts_notify_custom(ctx->spp_conn_handle.load(), nus_tx_handle, om); + int rc = ble_gatts_notify_custom(conn, nus_tx_handle, om); if (rc != 0) { os_mbuf_free_chain(om); if (written) *written = 0; @@ -140,29 +141,28 @@ static error_t spp_write(struct Device* device, const uint8_t* data, size_t len, } static error_t spp_read(struct Device* device, uint8_t* data, size_t max_len, size_t* read_out) { - BleCtx* ctx = ble_get_ctx(device); - if (ctx == nullptr || data == nullptr || max_len == 0) { + BleSppCtx* sctx = (BleSppCtx*)device_get_driver_data(device); + if (sctx == nullptr || data == nullptr || max_len == 0) { if (read_out) *read_out = 0; return ERROR_NONE; } - xSemaphoreTake(ctx->data_mutex, portMAX_DELAY); - if (ctx->spp_rx_queue.empty()) { - xSemaphoreGive(ctx->data_mutex); + xSemaphoreTake(sctx->rx_mutex, portMAX_DELAY); + if (sctx->rx_queue.empty()) { + xSemaphoreGive(sctx->rx_mutex); if (read_out) *read_out = 0; return ERROR_NONE; } - auto& front = ctx->spp_rx_queue.front(); + auto& front = sctx->rx_queue.front(); size_t copy_len = std::min(front.size(), max_len); memcpy(data, front.data(), copy_len); - ctx->spp_rx_queue.pop_front(); - xSemaphoreGive(ctx->data_mutex); + sctx->rx_queue.pop_front(); + xSemaphoreGive(sctx->rx_mutex); if (read_out) *read_out = copy_len; return ERROR_NONE; } static bool spp_is_connected(struct Device* device) { - BleCtx* ctx = ble_get_ctx(device); - return ctx != nullptr && ctx->spp_conn_handle.load() != BLE_HS_CONN_HANDLE_NONE; + return ble_ctx_get_spp_conn_handle(device) != BLE_HS_CONN_HANDLE_NONE; } const BtSerialApi nimble_serial_api = { From 83a4ce94ff39c5d2e55024dcd7f476668fea57ee Mon Sep 17 00:00:00 2001 From: Shadowtrance Date: Fri, 3 Apr 2026 22:40:57 +1000 Subject: [PATCH 08/12] more updoot --- Devices/lilygo-tdeck/device.properties | 1 + Devices/lilygo-tdeck/lilygo,tdeck.dts | 5 + .../private/bluetooth/esp32_ble_hid.h | 32 ++++ .../private/bluetooth/esp32_ble_internal.h | 128 +++++++------- .../private/bluetooth/esp32_ble_midi.h | 42 +++++ .../private/bluetooth/esp32_ble_spp.h | 33 ++++ .../source/drivers/bluetooth/README.md | 2 +- .../source/drivers/bluetooth/esp32_ble.cpp | 157 ++---------------- ...2_ble_hid_device.cpp => esp32_ble_hid.cpp} | 25 ++- .../drivers/bluetooth/esp32_ble_midi.cpp | 83 +++++++-- .../drivers/bluetooth/esp32_ble_scan.cpp | 8 +- .../drivers/bluetooth/esp32_ble_spp.cpp | 33 +++- 12 files changed, 310 insertions(+), 239 deletions(-) create mode 100644 Platforms/platform-esp32/private/bluetooth/esp32_ble_hid.h create mode 100644 Platforms/platform-esp32/private/bluetooth/esp32_ble_midi.h create mode 100644 Platforms/platform-esp32/private/bluetooth/esp32_ble_spp.h rename Platforms/platform-esp32/source/drivers/bluetooth/{esp32_ble_hid_device.cpp => esp32_ble_hid.cpp} (97%) diff --git a/Devices/lilygo-tdeck/device.properties b/Devices/lilygo-tdeck/device.properties index d14d8369c..6e3fd1a1b 100644 --- a/Devices/lilygo-tdeck/device.properties +++ b/Devices/lilygo-tdeck/device.properties @@ -13,6 +13,7 @@ spiRamMode=OCT spiRamSpeed=120M tinyUsb=true esptoolFlashFreq=120M +bluetooth=true [display] size=2.8" diff --git a/Devices/lilygo-tdeck/lilygo,tdeck.dts b/Devices/lilygo-tdeck/lilygo,tdeck.dts index 40dadd8d0..361824719 100644 --- a/Devices/lilygo-tdeck/lilygo,tdeck.dts +++ b/Devices/lilygo-tdeck/lilygo,tdeck.dts @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -12,6 +13,10 @@ compatible = "root"; model = "LilyGO T-Deck"; + ble0 { + compatible = "esp32,ble-nimble"; + }; + gpio0 { compatible = "espressif,esp32-gpio"; gpio-count = <49>; diff --git a/Platforms/platform-esp32/private/bluetooth/esp32_ble_hid.h b/Platforms/platform-esp32/private/bluetooth/esp32_ble_hid.h new file mode 100644 index 000000000..c5c3102e4 --- /dev/null +++ b/Platforms/platform-esp32/private/bluetooth/esp32_ble_hid.h @@ -0,0 +1,32 @@ +#pragma once + +#ifdef ESP_PLATFORM +#include +#endif + +#if defined(CONFIG_BT_NIMBLE_ENABLED) + +#include + +#include +#include + +enum class BleHidProfile { None, KbConsumer, Mouse, KbMouse, Gamepad }; + +struct BleHidDeviceCtx { + std::atomic hid_conn_handle; +}; + +struct Device; + +bool ble_hid_get_active(struct Device* device); +void ble_hid_set_active(struct Device* device, bool v); + +// device must be the hid_device child Device*. +void ble_hid_init_gatt(); +void ble_hid_init_gatt_handles(); +void ble_hid_switch_profile(struct Device* hid_child, BleHidProfile profile); + +extern const BtHidDeviceApi nimble_hid_device_api; + +#endif // CONFIG_BT_NIMBLE_ENABLED diff --git a/Platforms/platform-esp32/private/bluetooth/esp32_ble_internal.h b/Platforms/platform-esp32/private/bluetooth/esp32_ble_internal.h index 737f23b5d..c74473d21 100644 --- a/Platforms/platform-esp32/private/bluetooth/esp32_ble_internal.h +++ b/Platforms/platform-esp32/private/bluetooth/esp32_ble_internal.h @@ -7,9 +7,6 @@ #if defined(CONFIG_BT_NIMBLE_ENABLED) #include -#include -#include -#include #include // Must be included before any NimBLE header: log_common.h (pulled in by ble_hs.h) // defines LOG_LEVEL_* as macros with the same names as tactility/log.h's LogLevel enum. @@ -29,56 +26,78 @@ #include #include -// ---- HID profile selection ---- +// ---- Per-module headers (structs, accessors, sub-API externs) ---- -enum class BleHidProfile { None, KbConsumer, Mouse, KbMouse, Gamepad }; +#include +#include +#include -// ---- Child device driver data structs ---- -// Each struct is stored as driver data on the corresponding child Device*. +// ---- BleCtx ---- +// Private shared state for the ESP32 BLE driver. +// Sub-modules access it exclusively through ble_get_ctx(). -struct BleHidDeviceCtx { - std::atomic hid_conn_handle; -}; +#define BLE_MAX_CALLBACKS 8 -struct BleMidiCtx { - SemaphoreHandle_t rx_mutex; - std::deque> rx_queue; +struct BleCallbackEntry { + BtEventCallback fn; + void* ctx; }; -struct BleSppCtx { - SemaphoreHandle_t rx_mutex; - std::deque> rx_queue; +struct BleCtx { + // Mutexes + SemaphoreHandle_t radio_mutex; // guards radio state transitions + SemaphoreHandle_t cb_mutex; // guards callbacks array + + // Radio / scan state (atomic — read from multiple tasks) + std::atomic radio_state; + std::atomic scan_active; + // Set by Tactility HID host to prevent simultaneous central connection during name resolution + std::atomic hid_host_active; + + // Event callbacks (guarded by cb_mutex) + BleCallbackEntry callbacks[BLE_MAX_CALLBACKS]; + size_t callback_count; + + // Connection handles + active flags (atomic — accessed from multiple tasks) + std::atomic spp_conn_handle; + std::atomic spp_active; + std::atomic midi_conn_handle; + std::atomic midi_active; + std::atomic midi_use_indicate; // true when client subscribed for INDICATE (e.g. Windows) + std::atomic hid_active; + std::atomic link_encrypted; + std::atomic pending_reset_count; + + // Timers + esp_timer_handle_t midi_keepalive_timer; // 2-second periodic Active Sensing + esp_timer_handle_t adv_restart_timer; // one-shot after connect failure (500 ms) + // One-shot timer used to dispatch dispatchDisable off the NimBLE host task. + // nimble_port_stop() must not be called from the NimBLE host task itself. + esp_timer_handle_t disable_timer; + + // BLE device name (set before or after radio enable; applied in dispatch_enable) + char device_name[BLE_DEVICE_NAME_MAX + 1]; + + // Device reference (passed to BtEventCallback) + struct Device* device; + + // Child devices (created by esp32_ble_start_device, destroyed by stop_device) + struct Device* serial_child; + struct Device* midi_child; + struct Device* hid_device_child; }; -// ---- BleCtx field accessors (defined in esp32_ble.cpp) ---- -// Work correctly when called with either the parent BLE device or any child device. - -BtRadioState ble_ctx_get_radio_state(struct Device* device); +// Always returns the root BLE device's BleCtx regardless of which device is passed. +BleCtx* ble_get_ctx(struct Device* device); -bool ble_ctx_get_hid_active(struct Device* device); -void ble_ctx_set_hid_active(struct Device* device, bool v); +// ---- General field accessors (defined in esp32_ble.cpp) ---- -bool ble_ctx_get_spp_active(struct Device* device); -void ble_ctx_set_spp_active(struct Device* device, bool v); -uint16_t ble_ctx_get_spp_conn_handle(struct Device* device); -void ble_ctx_set_spp_conn_handle(struct Device* device, uint16_t h); +BtRadioState ble_get_radio_state(struct Device* device); -bool ble_ctx_get_midi_active(struct Device* device); -void ble_ctx_set_midi_active(struct Device* device, bool v); -uint16_t ble_ctx_get_midi_conn_handle(struct Device* device); -void ble_ctx_set_midi_conn_handle(struct Device* device, uint16_t h); -bool ble_ctx_get_midi_use_indicate(struct Device* device); -void ble_ctx_set_midi_use_indicate(struct Device* device, bool v); +bool ble_hid_get_host_active(struct Device* device); -bool ble_ctx_get_hid_host_active(struct Device* device); -bool ble_ctx_get_scan_active(struct Device* device); -void ble_ctx_set_scan_active(struct Device* device, bool v); - -// MIDI keepalive timer helpers — timer handle lives in BleCtx. -// ble_ctx_ensure_midi_keepalive creates the timer if needed and starts it periodically. -// ble_ctx_stop_midi_keepalive stops (but does not delete) the timer. -error_t ble_ctx_ensure_midi_keepalive(struct Device* device, esp_timer_cb_t cb, uint64_t period_us); -void ble_ctx_stop_midi_keepalive(struct Device* device); +bool ble_get_scan_active(struct Device* device); +void ble_set_scan_active(struct Device* device, bool v); // ---- Scan data management (defined in esp32_ble_scan.cpp) ---- void ble_scan_init(); @@ -107,12 +126,6 @@ error_t ble_spp_start_internal(struct Device* serial_child); void ble_midi_init_gatt_handles(struct Device* midi_child); error_t ble_midi_start_internal(struct Device* midi_child); -// ---- HID device GATT (defined in esp32_ble_hid_device.cpp) ---- -void ble_hid_device_init_gatt(); -void ble_hid_device_init_gatt_handles(); -// device must be the hid_device child Device*. -void ble_hid_device_switch_profile(struct Device* hid_child, BleHidProfile profile); - // ---- Cross-module GATT char / service arrays ---- // Non-const: the .arg field is set to the child Device* at init time so that // NimBLE access callbacks can retrieve the context without a global pointer. @@ -126,20 +139,15 @@ extern const ble_uuid128_t MIDI_SVC_UUID; // esp32_ble_midi.cpp // ---- Cross-module GATT handle variables ---- extern uint16_t nus_tx_handle; // esp32_ble_spp.cpp extern uint16_t midi_io_handle; // esp32_ble_midi.cpp -extern uint16_t hid_kb_input_handle; // esp32_ble_hid_device.cpp -extern uint16_t hid_consumer_input_handle; // esp32_ble_hid_device.cpp -extern uint16_t hid_mouse_input_handle; // esp32_ble_hid_device.cpp -extern uint16_t hid_gamepad_input_handle; // esp32_ble_hid_device.cpp +extern uint16_t hid_kb_input_handle; // esp32_ble_hid.cpp +extern uint16_t hid_consumer_input_handle; // esp32_ble_hid.cpp +extern uint16_t hid_mouse_input_handle; // esp32_ble_hid.cpp +extern uint16_t hid_gamepad_input_handle; // esp32_ble_hid.cpp // ---- HID active report map / appearance ---- -extern const uint8_t* active_hid_rpt_map; // esp32_ble_hid_device.cpp -extern size_t active_hid_rpt_map_len; // esp32_ble_hid_device.cpp -extern uint16_t hid_appearance; // esp32_ble_hid_device.cpp -extern BleHidProfile current_hid_profile; // esp32_ble_hid_device.cpp - -// ---- Cross-module sub-API structs ---- -extern const BtHidDeviceApi nimble_hid_device_api; // esp32_ble_hid_device.cpp -extern const BtSerialApi nimble_serial_api; // esp32_ble_spp.cpp -extern const BtMidiApi nimble_midi_api; // esp32_ble_midi.cpp +extern const uint8_t* active_hid_rpt_map; // esp32_ble_hid.cpp +extern size_t active_hid_rpt_map_len; // esp32_ble_hid.cpp +extern uint16_t hid_appearance; // esp32_ble_hid.cpp +extern BleHidProfile current_hid_profile; // esp32_ble_hid.cpp #endif // CONFIG_BT_NIMBLE_ENABLED diff --git a/Platforms/platform-esp32/private/bluetooth/esp32_ble_midi.h b/Platforms/platform-esp32/private/bluetooth/esp32_ble_midi.h new file mode 100644 index 000000000..84377d39f --- /dev/null +++ b/Platforms/platform-esp32/private/bluetooth/esp32_ble_midi.h @@ -0,0 +1,42 @@ +#pragma once + +#ifdef ESP_PLATFORM +#include +#endif + +#if defined(CONFIG_BT_NIMBLE_ENABLED) + +#include +#include + +#include +#include +#include + +#include +#include +#include + +struct BleMidiCtx { + SemaphoreHandle_t rx_mutex; + std::deque> rx_queue; +}; + +struct Device; + +bool ble_midi_get_active(struct Device* device); +void ble_midi_set_active(struct Device* device, bool v); +uint16_t ble_midi_get_conn_handle(struct Device* device); +void ble_midi_set_conn_handle(struct Device* device, uint16_t h); +bool ble_midi_get_use_indicate(struct Device* device); +void ble_midi_set_use_indicate(struct Device* device, bool v); + +// MIDI keepalive timer helpers — timer handle lives in BleCtx. +// ble_midi_ensure_keepalive creates the timer if needed and starts it periodically. +// ble_midi_stop_keepalive stops (but does not delete) the timer. +error_t ble_midi_ensure_keepalive(struct Device* device, esp_timer_cb_t cb, uint64_t period_us); +void ble_midi_stop_keepalive(struct Device* device); + +extern const BtMidiApi nimble_midi_api; + +#endif // CONFIG_BT_NIMBLE_ENABLED diff --git a/Platforms/platform-esp32/private/bluetooth/esp32_ble_spp.h b/Platforms/platform-esp32/private/bluetooth/esp32_ble_spp.h new file mode 100644 index 000000000..a14dd8a0d --- /dev/null +++ b/Platforms/platform-esp32/private/bluetooth/esp32_ble_spp.h @@ -0,0 +1,33 @@ +#pragma once + +#ifdef ESP_PLATFORM +#include +#endif + +#if defined(CONFIG_BT_NIMBLE_ENABLED) + +#include +#include + +#include +#include + +#include +#include +#include + +struct BleSppCtx { + SemaphoreHandle_t rx_mutex; + std::deque> rx_queue; +}; + +struct Device; + +bool ble_spp_get_active(struct Device* device); +void ble_spp_set_active(struct Device* device, bool v); +uint16_t ble_spp_get_conn_handle(struct Device* device); +void ble_spp_set_conn_handle(struct Device* device, uint16_t h); + +extern const BtSerialApi nimble_serial_api; + +#endif // CONFIG_BT_NIMBLE_ENABLED diff --git a/Platforms/platform-esp32/source/drivers/bluetooth/README.md b/Platforms/platform-esp32/source/drivers/bluetooth/README.md index 3974ebe55..b350dd79d 100644 --- a/Platforms/platform-esp32/source/drivers/bluetooth/README.md +++ b/Platforms/platform-esp32/source/drivers/bluetooth/README.md @@ -11,7 +11,7 @@ Only compiled when `CONFIG_BT_NIMBLE_ENABLED=y`. | `esp32_ble_scan.cpp` | GAP discovery, name resolution, auto-connect dispatch | | `esp32_ble_spp.cpp` | NUS (Nordic UART Service) GATT server — `BtSerialApi` | | `esp32_ble_midi.cpp` | BLE MIDI GATT server — `BtMidiApi` | -| `esp32_ble_hid_device.cpp` | HID peripheral (keyboard / mouse / gamepad) — `BtHidDeviceApi` | +| `esp32_ble_hid.cpp` | HID peripheral (keyboard / mouse / gamepad) — `BtHidDeviceApi` | Internal shared state is defined in `Platforms/platform-esp32/private/bluetooth/esp32_ble_internal.h` (`BleCtx`). diff --git a/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble.cpp b/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble.cpp index 66d2ad609..f0e21607b 100644 --- a/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble.cpp +++ b/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble.cpp @@ -32,74 +32,18 @@ // ble_store_config_init() is not declared in the public header in some IDF versions. extern "C" void ble_store_config_init(void); -// ---- BleCtx ---- -// Private to this translation unit. Sub-modules access BleCtx fields exclusively -// through the accessor functions declared in esp32_ble_internal.h. - -#define BLE_MAX_CALLBACKS 8 -#define BLE_SCAN_MAX 64 - -struct BleCallbackEntry { - BtEventCallback fn; - void* ctx; -}; - -struct BleCtx { - // Mutexes - SemaphoreHandle_t radio_mutex; // guards radio state transitions - SemaphoreHandle_t cb_mutex; // guards callbacks array - - // Radio / scan state (atomic — read from multiple tasks) - std::atomic radio_state; - std::atomic scan_active; - // Set by Tactility HID host to prevent simultaneous central connection during name resolution - std::atomic hid_host_active; - - // Event callbacks (guarded by cb_mutex) - BleCallbackEntry callbacks[BLE_MAX_CALLBACKS]; - size_t callback_count; - - // Connection handles + active flags (atomic — accessed from multiple tasks) - std::atomic spp_conn_handle; - std::atomic spp_active; - std::atomic midi_conn_handle; - std::atomic midi_active; - std::atomic midi_use_indicate; // true when client subscribed for INDICATE (e.g. Windows) - std::atomic hid_active; - std::atomic link_encrypted; - std::atomic pending_reset_count; - - // Timers - esp_timer_handle_t midi_keepalive_timer; // 2-second periodic Active Sensing - esp_timer_handle_t adv_restart_timer; // one-shot after connect failure (500 ms) - // One-shot timer used to dispatch dispatchDisable off the NimBLE host task. - // nimble_port_stop() must not be called from the NimBLE host task itself. - esp_timer_handle_t disable_timer; - - // BLE device name (set before or after radio enable; applied in dispatch_enable) - char device_name[BLE_DEVICE_NAME_MAX + 1]; - - // Device reference (passed to BtEventCallback) - struct Device* device; - - // Child devices (created by esp32_ble_start_device, destroyed by stop_device) - struct Device* serial_child; - struct Device* midi_child; - struct Device* hid_device_child; -}; - // File-static device pointer used only by NimBLE host callbacks whose signature // is fixed by the NimBLE API (on_sync, on_reset) and cannot carry a Device*. // All other callbacks receive Device* via their void* arg parameter. static struct Device* s_device = nullptr; -// ---- Context accessor (file-private) ---- +// ---- Context accessor ---- // Always returns the root BLE device's BleCtx regardless of which device is passed // (root, child, or grandchild). Using s_device directly avoids device-tree traversal // ambiguity: the root BLE device may itself have a parent in the device tree, and // walking up from it would land on the wrong node. -static BleCtx* ble_get_ctx(struct Device* /*device*/) { +BleCtx* ble_get_ctx(struct Device* /*device*/) { return s_device ? (BleCtx*)device_get_driver_data(s_device) : nullptr; } @@ -111,107 +55,26 @@ static void dispatch_enable(BleCtx* ctx); static void dispatch_disable(BleCtx* ctx); static int gap_event_handler(struct ble_gap_event* event, void* arg); -// ---- BleCtx field accessor implementations ---- +// ---- General field accessor implementations ---- -BtRadioState ble_ctx_get_radio_state(struct Device* device) { +BtRadioState ble_get_radio_state(struct Device* device) { BleCtx* ctx = ble_get_ctx(device); return ctx ? ctx->radio_state.load() : BT_RADIO_STATE_OFF; } -bool ble_ctx_get_hid_active(struct Device* device) { - BleCtx* ctx = ble_get_ctx(device); - return ctx && ctx->hid_active.load(); -} -void ble_ctx_set_hid_active(struct Device* device, bool v) { - BleCtx* ctx = ble_get_ctx(device); - if (ctx) ctx->hid_active.store(v); -} - -bool ble_ctx_get_spp_active(struct Device* device) { - BleCtx* ctx = ble_get_ctx(device); - return ctx && ctx->spp_active.load(); -} -void ble_ctx_set_spp_active(struct Device* device, bool v) { - BleCtx* ctx = ble_get_ctx(device); - if (ctx) ctx->spp_active.store(v); -} -uint16_t ble_ctx_get_spp_conn_handle(struct Device* device) { - BleCtx* ctx = ble_get_ctx(device); - return ctx ? ctx->spp_conn_handle.load() : (uint16_t)BLE_HS_CONN_HANDLE_NONE; -} -void ble_ctx_set_spp_conn_handle(struct Device* device, uint16_t h) { - BleCtx* ctx = ble_get_ctx(device); - if (ctx) ctx->spp_conn_handle.store(h); -} - -bool ble_ctx_get_midi_active(struct Device* device) { - BleCtx* ctx = ble_get_ctx(device); - return ctx && ctx->midi_active.load(); -} -void ble_ctx_set_midi_active(struct Device* device, bool v) { - BleCtx* ctx = ble_get_ctx(device); - if (ctx) ctx->midi_active.store(v); -} -uint16_t ble_ctx_get_midi_conn_handle(struct Device* device) { - BleCtx* ctx = ble_get_ctx(device); - return ctx ? ctx->midi_conn_handle.load() : (uint16_t)BLE_HS_CONN_HANDLE_NONE; -} -void ble_ctx_set_midi_conn_handle(struct Device* device, uint16_t h) { - BleCtx* ctx = ble_get_ctx(device); - if (ctx) ctx->midi_conn_handle.store(h); -} -bool ble_ctx_get_midi_use_indicate(struct Device* device) { - BleCtx* ctx = ble_get_ctx(device); - return ctx && ctx->midi_use_indicate.load(); -} -void ble_ctx_set_midi_use_indicate(struct Device* device, bool v) { - BleCtx* ctx = ble_get_ctx(device); - if (ctx) ctx->midi_use_indicate.store(v); -} - -bool ble_ctx_get_hid_host_active(struct Device* device) { +bool ble_hid_get_host_active(struct Device* device) { BleCtx* ctx = ble_get_ctx(device); return ctx && ctx->hid_host_active.load(); } -bool ble_ctx_get_scan_active(struct Device* device) { +bool ble_get_scan_active(struct Device* device) { BleCtx* ctx = ble_get_ctx(device); return ctx && ctx->scan_active.load(); } -void ble_ctx_set_scan_active(struct Device* device, bool v) { +void ble_set_scan_active(struct Device* device, bool v) { BleCtx* ctx = ble_get_ctx(device); if (ctx) ctx->scan_active.store(v); } -error_t ble_ctx_ensure_midi_keepalive(struct Device* device, esp_timer_cb_t cb, uint64_t period_us) { - BleCtx* ctx = ble_get_ctx(device); - if (!ctx) return ERROR_INVALID_STATE; - if (ctx->midi_keepalive_timer == nullptr) { - esp_timer_create_args_t args = {}; - args.callback = cb; - args.arg = device; - args.dispatch_method = ESP_TIMER_TASK; - args.name = "ble_midi_as"; - int rc = esp_timer_create(&args, &ctx->midi_keepalive_timer); - if (rc != ESP_OK) { - LOG_E(TAG, "midi keepalive timer create failed (rc=%d)", rc); - return ERROR_INVALID_STATE; - } - } - int rc = esp_timer_start_periodic(ctx->midi_keepalive_timer, period_us); - if (rc != ESP_OK) { - LOG_E(TAG, "midi keepalive timer start failed (rc=%d)", rc); - return ERROR_INVALID_STATE; - } - return ERROR_NONE; -} - -void ble_ctx_stop_midi_keepalive(struct Device* device) { - BleCtx* ctx = ble_get_ctx(device); - if (ctx && ctx->midi_keepalive_timer != nullptr) { - esp_timer_stop(ctx->midi_keepalive_timer); - } -} - // ---- Event publishing ---- void ble_publish_event(struct Device* device, struct BtEvent event) { @@ -320,7 +183,7 @@ static int gap_event_handler(struct ble_gap_event* event, void* arg) { // Skip during shutdown — ble_gatts_reset() is unsafe while nimble_port_stop() runs. if (was_hid && !ctx->hid_active.load() && current_hid_profile != BleHidProfile::None && ctx->radio_state.load() != BT_RADIO_STATE_OFF_PENDING) { - ble_hid_device_switch_profile(ctx->hid_device_child, BleHidProfile::None); + ble_hid_switch_profile(ctx->hid_device_child, BleHidProfile::None); } // Restart advertising whenever a service is active without a live connection. // Covers both normal disconnect and Windows discovery-only connections. @@ -560,7 +423,7 @@ static void on_sync() { // can retrieve child driver data (BleSppCtx / BleMidiCtx) without globals. ble_spp_init_gatt_handles(ctx->serial_child); ble_midi_init_gatt_handles(ctx->midi_child); - ble_hid_device_init_gatt_handles(); + ble_hid_init_gatt_handles(); ctx->radio_state.store(BT_RADIO_STATE_ON); struct BtEvent e = {}; @@ -798,7 +661,7 @@ static void dispatch_enable(BleCtx* ctx) { ble_svc_gatt_init(); // Register base GATT services (NUS + MIDI; HID added by switch_profile when started) - ble_hid_device_init_gatt(); + ble_hid_init_gatt(); ble_svc_gap_device_name_set(ctx->device_name); ble_att_set_preferred_mtu(BLE_ATT_MTU_MAX); diff --git a/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_hid_device.cpp b/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_hid.cpp similarity index 97% rename from Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_hid_device.cpp rename to Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_hid.cpp index 93481c31e..f7ab42b0e 100644 --- a/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_hid_device.cpp +++ b/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_hid.cpp @@ -299,10 +299,21 @@ static const struct ble_gatt_svc_def gatt_svcs_gamepad[] = { { 0 } }; +// ---- HID field accessor implementations ---- + +bool ble_hid_get_active(struct Device* device) { + BleCtx* ctx = ble_get_ctx(device); + return ctx && ctx->hid_active.load(); +} +void ble_hid_set_active(struct Device* device, bool v) { + BleCtx* ctx = ble_get_ctx(device); + if (ctx) ctx->hid_active.store(v); +} + // ---- GATT profile switch ---- // device must be the HID device child Device*. -void ble_hid_device_switch_profile(struct Device* device, BleHidProfile profile) { +void ble_hid_switch_profile(struct Device* device, BleHidProfile profile) { if (profile == current_hid_profile) return; LOG_I(TAG, "switchGattProfile: %d -> %d", (int)current_hid_profile, (int)profile); @@ -361,7 +372,7 @@ void ble_hid_device_switch_profile(struct Device* device, BleHidProfile profile) current_hid_profile = profile; } -void ble_hid_device_init_gatt() { +void ble_hid_init_gatt() { current_hid_profile = BleHidProfile::None; active_hid_rpt_map = nullptr; active_hid_rpt_map_len = 0; @@ -376,7 +387,7 @@ void ble_hid_device_init_gatt() { } } -void ble_hid_device_init_gatt_handles() { +void ble_hid_init_gatt_handles() { // val_handle pointers in char arrays are updated by NimBLE at registration time. // No explicit action needed here; called for symmetry with spp/midi init. (void)hid_kb_input_handle; @@ -417,14 +428,14 @@ static error_t hid_device_start(struct Device* device, enum BtHidDeviceMode mode } hid_appearance = appearance; - ble_hid_device_switch_profile(device, profile); - ble_ctx_set_hid_active(device, true); + ble_hid_switch_profile(device, profile); + ble_hid_set_active(device, true); ble_start_advertising_hid(device, hid_appearance); return ERROR_NONE; } static error_t hid_device_stop(struct Device* device) { - ble_ctx_set_hid_active(device, false); + ble_hid_set_active(device, false); ble_gap_adv_stop(); BleHidDeviceCtx* hid_ctx = (BleHidDeviceCtx*)device_get_driver_data(device); uint16_t conn = hid_ctx ? hid_ctx->hid_conn_handle.load() : (uint16_t)BLE_HS_CONN_HANDLE_NONE; @@ -439,7 +450,7 @@ static error_t hid_device_stop(struct Device* device) { } else { // Not connected: GATT is mutable, switch profile immediately. if (current_hid_profile != BleHidProfile::None) { - ble_hid_device_switch_profile(device, BleHidProfile::None); + ble_hid_switch_profile(device, BleHidProfile::None); } delete hid_ctx; device_set_driver_data(device, nullptr); diff --git a/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_midi.cpp b/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_midi.cpp index 77980202f..91f07b970 100644 --- a/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_midi.cpp +++ b/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_midi.cpp @@ -36,6 +36,63 @@ static const ble_uuid128_t MIDI_IO_UUID = BLE_UUID128_INIT( uint16_t midi_io_handle; +// ---- MIDI field accessor implementations ---- + +bool ble_midi_get_active(struct Device* device) { + BleCtx* ctx = ble_get_ctx(device); + return ctx && ctx->midi_active.load(); +} +void ble_midi_set_active(struct Device* device, bool v) { + BleCtx* ctx = ble_get_ctx(device); + if (ctx) ctx->midi_active.store(v); +} +uint16_t ble_midi_get_conn_handle(struct Device* device) { + BleCtx* ctx = ble_get_ctx(device); + return ctx ? ctx->midi_conn_handle.load() : (uint16_t)BLE_HS_CONN_HANDLE_NONE; +} +void ble_midi_set_conn_handle(struct Device* device, uint16_t h) { + BleCtx* ctx = ble_get_ctx(device); + if (ctx) ctx->midi_conn_handle.store(h); +} +bool ble_midi_get_use_indicate(struct Device* device) { + BleCtx* ctx = ble_get_ctx(device); + return ctx && ctx->midi_use_indicate.load(); +} +void ble_midi_set_use_indicate(struct Device* device, bool v) { + BleCtx* ctx = ble_get_ctx(device); + if (ctx) ctx->midi_use_indicate.store(v); +} + +error_t ble_midi_ensure_keepalive(struct Device* device, esp_timer_cb_t cb, uint64_t period_us) { + BleCtx* ctx = ble_get_ctx(device); + if (!ctx) return ERROR_INVALID_STATE; + if (ctx->midi_keepalive_timer == nullptr) { + esp_timer_create_args_t args = {}; + args.callback = cb; + args.arg = device; + args.dispatch_method = ESP_TIMER_TASK; + args.name = "ble_midi_as"; + int rc = esp_timer_create(&args, &ctx->midi_keepalive_timer); + if (rc != ESP_OK) { + LOG_E(TAG, "midi keepalive timer create failed (rc=%d)", rc); + return ERROR_INVALID_STATE; + } + } + int rc = esp_timer_start_periodic(ctx->midi_keepalive_timer, period_us); + if (rc != ESP_OK) { + LOG_E(TAG, "midi keepalive timer start failed (rc=%d)", rc); + return ERROR_INVALID_STATE; + } + return ERROR_NONE; +} + +void ble_midi_stop_keepalive(struct Device* device) { + BleCtx* ctx = ble_get_ctx(device); + if (ctx && ctx->midi_keepalive_timer != nullptr) { + esp_timer_stop(ctx->midi_keepalive_timer); + } +} + // midi_chr_access is called with the midi child Device* (set via ble_midi_init_gatt_handles). static int midi_chr_access(uint16_t conn_handle, uint16_t attr_handle, struct ble_gatt_access_ctxt* ctxt, void* arg) { @@ -84,12 +141,12 @@ void ble_midi_init_gatt_handles(struct Device* midi_child) { static void midi_keepalive_cb(void* arg) { struct Device* device = (struct Device*)arg; // midi child device - uint16_t conn = ble_ctx_get_midi_conn_handle(device); + uint16_t conn = ble_midi_get_conn_handle(device); if (conn == BLE_HS_CONN_HANDLE_NONE) return; static const uint8_t as_pkt[3] = { 0x80, 0x80, 0xFE }; struct os_mbuf* om = ble_hs_mbuf_from_flat(as_pkt, 3); if (om == nullptr) return; - int rc = ble_ctx_get_midi_use_indicate(device) + int rc = ble_midi_get_use_indicate(device) ? ble_gatts_indicate_custom(conn, midi_io_handle, om) : ble_gatts_notify_custom(conn, midi_io_handle, om); if (rc != 0) os_mbuf_free_chain(om); @@ -99,10 +156,10 @@ static void midi_keepalive_cb(void* arg) { // All functions receive the midi child Device*. static error_t midi_start(struct Device* device) { - ble_ctx_set_midi_active(device, true); + ble_midi_set_active(device, true); // Create 2-second periodic Active Sensing timer to prevent Windows BLE MIDI // driver from declaring the connection idle and disconnecting (~8-10 s timeout). - error_t rc = ble_ctx_ensure_midi_keepalive(device, midi_keepalive_cb, 2'000'000); + error_t rc = ble_midi_ensure_keepalive(device, midi_keepalive_cb, 2'000'000); if (rc != ERROR_NONE) return rc; ble_start_advertising(device, &MIDI_SVC_UUID); return ERROR_NONE; @@ -113,21 +170,21 @@ error_t ble_midi_start_internal(struct Device* midi_child) { } static error_t midi_stop(struct Device* device) { - ble_ctx_set_midi_active(device, false); - ble_ctx_stop_midi_keepalive(device); - uint16_t conn = ble_ctx_get_midi_conn_handle(device); + ble_midi_set_active(device, false); + ble_midi_stop_keepalive(device); + uint16_t conn = ble_midi_get_conn_handle(device); if (conn != BLE_HS_CONN_HANDLE_NONE) { ble_gap_terminate(conn, BLE_ERR_REM_USER_CONN_TERM); - ble_ctx_set_midi_conn_handle(device, BLE_HS_CONN_HANDLE_NONE); + ble_midi_set_conn_handle(device, BLE_HS_CONN_HANDLE_NONE); } - if (!ble_ctx_get_spp_active(device) && !ble_ctx_get_hid_active(device)) { + if (!ble_spp_get_active(device) && !ble_hid_get_active(device)) { ble_gap_adv_stop(); } return ERROR_NONE; } static error_t midi_send(struct Device* device, const uint8_t* msg, size_t len) { - uint16_t conn = ble_ctx_get_midi_conn_handle(device); + uint16_t conn = ble_midi_get_conn_handle(device); if (conn == BLE_HS_CONN_HANDLE_NONE) return ERROR_INVALID_STATE; // BLE MIDI 2-byte header: [0x80|(ts_high&0x3F)][0x80|(ts_low&0x7F)] uint8_t header[2] = { 0x80, 0x80 }; @@ -138,8 +195,8 @@ static error_t midi_send(struct Device* device, const uint8_t* msg, size_t len) LOG_E(TAG, "midi_send: mbuf append failed"); return ERROR_INVALID_STATE; } - LOG_I(TAG, "midi_send %u bytes (indicate=%d)", (unsigned)len, (int)ble_ctx_get_midi_use_indicate(device)); - int rc = ble_ctx_get_midi_use_indicate(device) + LOG_I(TAG, "midi_send %u bytes (indicate=%d)", (unsigned)len, (int)ble_midi_get_use_indicate(device)); + int rc = ble_midi_get_use_indicate(device) ? ble_gatts_indicate_custom(conn, midi_io_handle, om) : ble_gatts_notify_custom(conn, midi_io_handle, om); if (rc != 0) { @@ -150,7 +207,7 @@ static error_t midi_send(struct Device* device, const uint8_t* msg, size_t len) } static bool midi_is_connected(struct Device* device) { - return ble_ctx_get_midi_conn_handle(device) != BLE_HS_CONN_HANDLE_NONE; + return ble_midi_get_conn_handle(device) != BLE_HS_CONN_HANDLE_NONE; } const BtMidiApi nimble_midi_api = { diff --git a/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_scan.cpp b/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_scan.cpp index 2852fd2b5..be558975f 100644 --- a/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_scan.cpp +++ b/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_scan.cpp @@ -204,10 +204,10 @@ void ble_resolve_next_unnamed_peer(struct Device* device, size_t start_idx) { // Skip if a profile server or HID host connection attempt is active — // initiating a central connection simultaneously would fail (BLE_HS_EALREADY). - if (ble_ctx_get_midi_active(device) || ble_ctx_get_spp_active(device) || - ble_ctx_get_hid_active(device) || ble_ctx_get_hid_host_active(device)) { + if (ble_midi_get_active(device) || ble_spp_get_active(device) || + ble_hid_get_active(device) || ble_hid_get_host_active(device)) { LOG_I(TAG, "Name resolution: skipping (server or HID host active)"); - ble_ctx_set_scan_active(device, false); + ble_set_scan_active(device, false); struct BtEvent e = {}; e.type = BT_EVENT_SCAN_FINISHED; ble_publish_event(device, e); @@ -233,7 +233,7 @@ void ble_resolve_next_unnamed_peer(struct Device* device, size_t start_idx) { if (!found) { LOG_I(TAG, "Name resolution: complete (%u devices)", (unsigned)i); - ble_ctx_set_scan_active(device, false); + ble_set_scan_active(device, false); struct BtEvent e = {}; e.type = BT_EVENT_SCAN_FINISHED; ble_publish_event(device, e); diff --git a/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_spp.cpp b/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_spp.cpp index 3db016f93..909a72e8e 100644 --- a/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_spp.cpp +++ b/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_spp.cpp @@ -91,11 +91,30 @@ void ble_spp_init_gatt_handles(struct Device* serial_child) { nus_chars_with_handle[1].arg = serial_child; } +// ---- SPP field accessor implementations ---- + +bool ble_spp_get_active(struct Device* device) { + BleCtx* ctx = ble_get_ctx(device); + return ctx && ctx->spp_active.load(); +} +void ble_spp_set_active(struct Device* device, bool v) { + BleCtx* ctx = ble_get_ctx(device); + if (ctx) ctx->spp_active.store(v); +} +uint16_t ble_spp_get_conn_handle(struct Device* device) { + BleCtx* ctx = ble_get_ctx(device); + return ctx ? ctx->spp_conn_handle.load() : (uint16_t)BLE_HS_CONN_HANDLE_NONE; +} +void ble_spp_set_conn_handle(struct Device* device, uint16_t h) { + BleCtx* ctx = ble_get_ctx(device); + if (ctx) ctx->spp_conn_handle.store(h); +} + // ---- SPP sub-API implementations ---- // All functions receive the serial child Device*. static error_t spp_start(struct Device* device) { - ble_ctx_set_spp_active(device, true); + ble_spp_set_active(device, true); ble_start_advertising(device, &NUS_SVC_UUID); return ERROR_NONE; } @@ -105,22 +124,22 @@ error_t ble_spp_start_internal(struct Device* serial_child) { } static error_t spp_stop(struct Device* device) { - ble_ctx_set_spp_active(device, false); - uint16_t conn = ble_ctx_get_spp_conn_handle(device); + ble_spp_set_active(device, false); + uint16_t conn = ble_spp_get_conn_handle(device); if (conn != BLE_HS_CONN_HANDLE_NONE) { ble_gap_terminate(conn, BLE_ERR_REM_USER_CONN_TERM); - ble_ctx_set_spp_conn_handle(device, BLE_HS_CONN_HANDLE_NONE); + ble_spp_set_conn_handle(device, BLE_HS_CONN_HANDLE_NONE); } // Do NOT restart advertising after user-initiated stop — restarting name-only // advertising causes bonded Windows hosts to auto-reconnect in a tight loop. - if (!ble_ctx_get_midi_active(device) && !ble_ctx_get_hid_active(device)) { + if (!ble_midi_get_active(device) && !ble_hid_get_active(device)) { ble_gap_adv_stop(); } return ERROR_NONE; } static error_t spp_write(struct Device* device, const uint8_t* data, size_t len, size_t* written) { - uint16_t conn = ble_ctx_get_spp_conn_handle(device); + uint16_t conn = ble_spp_get_conn_handle(device); if (conn == BLE_HS_CONN_HANDLE_NONE) { if (written) *written = 0; return ERROR_INVALID_STATE; @@ -162,7 +181,7 @@ static error_t spp_read(struct Device* device, uint8_t* data, size_t max_len, si } static bool spp_is_connected(struct Device* device) { - return ble_ctx_get_spp_conn_handle(device) != BLE_HS_CONN_HANDLE_NONE; + return ble_spp_get_conn_handle(device) != BLE_HS_CONN_HANDLE_NONE; } const BtSerialApi nimble_serial_api = { From d3694365c634acc5db62ac59771c496cb971a727 Mon Sep 17 00:00:00 2001 From: Shadowtrance Date: Fri, 3 Apr 2026 23:33:40 +1000 Subject: [PATCH 09/12] move back! --- .../private/bluetooth/esp32_ble_internal.h | 58 ------- .../source/drivers/bluetooth/esp32_ble.cpp | 148 +++++++++++++++++- .../drivers/bluetooth/esp32_ble_hid.cpp | 11 -- .../drivers/bluetooth/esp32_ble_midi.cpp | 57 ------- .../drivers/bluetooth/esp32_ble_spp.cpp | 19 --- 5 files changed, 145 insertions(+), 148 deletions(-) diff --git a/Platforms/platform-esp32/private/bluetooth/esp32_ble_internal.h b/Platforms/platform-esp32/private/bluetooth/esp32_ble_internal.h index c74473d21..3dd9a8776 100644 --- a/Platforms/platform-esp32/private/bluetooth/esp32_ble_internal.h +++ b/Platforms/platform-esp32/private/bluetooth/esp32_ble_internal.h @@ -32,64 +32,6 @@ #include #include -// ---- BleCtx ---- -// Private shared state for the ESP32 BLE driver. -// Sub-modules access it exclusively through ble_get_ctx(). - -#define BLE_MAX_CALLBACKS 8 - -struct BleCallbackEntry { - BtEventCallback fn; - void* ctx; -}; - -struct BleCtx { - // Mutexes - SemaphoreHandle_t radio_mutex; // guards radio state transitions - SemaphoreHandle_t cb_mutex; // guards callbacks array - - // Radio / scan state (atomic — read from multiple tasks) - std::atomic radio_state; - std::atomic scan_active; - // Set by Tactility HID host to prevent simultaneous central connection during name resolution - std::atomic hid_host_active; - - // Event callbacks (guarded by cb_mutex) - BleCallbackEntry callbacks[BLE_MAX_CALLBACKS]; - size_t callback_count; - - // Connection handles + active flags (atomic — accessed from multiple tasks) - std::atomic spp_conn_handle; - std::atomic spp_active; - std::atomic midi_conn_handle; - std::atomic midi_active; - std::atomic midi_use_indicate; // true when client subscribed for INDICATE (e.g. Windows) - std::atomic hid_active; - std::atomic link_encrypted; - std::atomic pending_reset_count; - - // Timers - esp_timer_handle_t midi_keepalive_timer; // 2-second periodic Active Sensing - esp_timer_handle_t adv_restart_timer; // one-shot after connect failure (500 ms) - // One-shot timer used to dispatch dispatchDisable off the NimBLE host task. - // nimble_port_stop() must not be called from the NimBLE host task itself. - esp_timer_handle_t disable_timer; - - // BLE device name (set before or after radio enable; applied in dispatch_enable) - char device_name[BLE_DEVICE_NAME_MAX + 1]; - - // Device reference (passed to BtEventCallback) - struct Device* device; - - // Child devices (created by esp32_ble_start_device, destroyed by stop_device) - struct Device* serial_child; - struct Device* midi_child; - struct Device* hid_device_child; -}; - -// Always returns the root BLE device's BleCtx regardless of which device is passed. -BleCtx* ble_get_ctx(struct Device* device); - // ---- General field accessors (defined in esp32_ble.cpp) ---- BtRadioState ble_get_radio_state(struct Device* device); diff --git a/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble.cpp b/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble.cpp index f0e21607b..16e5d6346 100644 --- a/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble.cpp +++ b/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble.cpp @@ -32,18 +32,73 @@ // ble_store_config_init() is not declared in the public header in some IDF versions. extern "C" void ble_store_config_init(void); +// ---- BleCtx ---- +// Private to this translation unit. Sub-modules access BleCtx fields exclusively +// through the accessor functions declared in the per-module headers. + +#define BLE_MAX_CALLBACKS 8 + +struct BleCallbackEntry { + BtEventCallback fn; + void* ctx; +}; + +struct BleCtx { + // Mutexes + SemaphoreHandle_t radio_mutex; // guards radio state transitions + SemaphoreHandle_t cb_mutex; // guards callbacks array + + // Radio / scan state (atomic — read from multiple tasks) + std::atomic radio_state; + std::atomic scan_active; + // Set by Tactility HID host to prevent simultaneous central connection during name resolution + std::atomic hid_host_active; + + // Event callbacks (guarded by cb_mutex) + BleCallbackEntry callbacks[BLE_MAX_CALLBACKS]; + size_t callback_count; + + // Connection handles + active flags (atomic — accessed from multiple tasks) + std::atomic spp_conn_handle; + std::atomic spp_active; + std::atomic midi_conn_handle; + std::atomic midi_active; + std::atomic midi_use_indicate; // true when client subscribed for INDICATE (e.g. Windows) + std::atomic hid_active; + std::atomic link_encrypted; + std::atomic pending_reset_count; + + // Timers + esp_timer_handle_t midi_keepalive_timer; // 2-second periodic Active Sensing + esp_timer_handle_t adv_restart_timer; // one-shot after connect failure (500 ms) + // One-shot timer used to dispatch dispatchDisable off the NimBLE host task. + // nimble_port_stop() must not be called from the NimBLE host task itself. + esp_timer_handle_t disable_timer; + + // BLE device name (set before or after radio enable; applied in dispatch_enable) + char device_name[BLE_DEVICE_NAME_MAX + 1]; + + // Device reference (passed to BtEventCallback) + struct Device* device; + + // Child devices (created by esp32_ble_start_device, destroyed by stop_device) + struct Device* serial_child; + struct Device* midi_child; + struct Device* hid_device_child; +}; + // File-static device pointer used only by NimBLE host callbacks whose signature // is fixed by the NimBLE API (on_sync, on_reset) and cannot carry a Device*. // All other callbacks receive Device* via their void* arg parameter. static struct Device* s_device = nullptr; -// ---- Context accessor ---- +// ---- Context accessor (file-private) ---- // Always returns the root BLE device's BleCtx regardless of which device is passed // (root, child, or grandchild). Using s_device directly avoids device-tree traversal // ambiguity: the root BLE device may itself have a parent in the device tree, and // walking up from it would land on the wrong node. -BleCtx* ble_get_ctx(struct Device* /*device*/) { +static BleCtx* ble_get_ctx(struct Device* /*device*/) { return s_device ? (BleCtx*)device_get_driver_data(s_device) : nullptr; } @@ -55,7 +110,7 @@ static void dispatch_enable(BleCtx* ctx); static void dispatch_disable(BleCtx* ctx); static int gap_event_handler(struct ble_gap_event* event, void* arg); -// ---- General field accessor implementations ---- +// ---- Field accessor implementations ---- BtRadioState ble_get_radio_state(struct Device* device) { BleCtx* ctx = ble_get_ctx(device); @@ -75,6 +130,93 @@ void ble_set_scan_active(struct Device* device, bool v) { if (ctx) ctx->scan_active.store(v); } +// SPP + +bool ble_spp_get_active(struct Device* device) { + BleCtx* ctx = ble_get_ctx(device); + return ctx && ctx->spp_active.load(); +} +void ble_spp_set_active(struct Device* device, bool v) { + BleCtx* ctx = ble_get_ctx(device); + if (ctx) ctx->spp_active.store(v); +} +uint16_t ble_spp_get_conn_handle(struct Device* device) { + BleCtx* ctx = ble_get_ctx(device); + return ctx ? ctx->spp_conn_handle.load() : (uint16_t)BLE_HS_CONN_HANDLE_NONE; +} +void ble_spp_set_conn_handle(struct Device* device, uint16_t h) { + BleCtx* ctx = ble_get_ctx(device); + if (ctx) ctx->spp_conn_handle.store(h); +} + +// MIDI + +bool ble_midi_get_active(struct Device* device) { + BleCtx* ctx = ble_get_ctx(device); + return ctx && ctx->midi_active.load(); +} +void ble_midi_set_active(struct Device* device, bool v) { + BleCtx* ctx = ble_get_ctx(device); + if (ctx) ctx->midi_active.store(v); +} +uint16_t ble_midi_get_conn_handle(struct Device* device) { + BleCtx* ctx = ble_get_ctx(device); + return ctx ? ctx->midi_conn_handle.load() : (uint16_t)BLE_HS_CONN_HANDLE_NONE; +} +void ble_midi_set_conn_handle(struct Device* device, uint16_t h) { + BleCtx* ctx = ble_get_ctx(device); + if (ctx) ctx->midi_conn_handle.store(h); +} +bool ble_midi_get_use_indicate(struct Device* device) { + BleCtx* ctx = ble_get_ctx(device); + return ctx && ctx->midi_use_indicate.load(); +} +void ble_midi_set_use_indicate(struct Device* device, bool v) { + BleCtx* ctx = ble_get_ctx(device); + if (ctx) ctx->midi_use_indicate.store(v); +} + +error_t ble_midi_ensure_keepalive(struct Device* device, esp_timer_cb_t cb, uint64_t period_us) { + BleCtx* ctx = ble_get_ctx(device); + if (!ctx) return ERROR_INVALID_STATE; + if (ctx->midi_keepalive_timer == nullptr) { + esp_timer_create_args_t args = {}; + args.callback = cb; + args.arg = device; + args.dispatch_method = ESP_TIMER_TASK; + args.name = "ble_midi_as"; + int rc = esp_timer_create(&args, &ctx->midi_keepalive_timer); + if (rc != ESP_OK) { + LOG_E(TAG, "midi keepalive timer create failed (rc=%d)", rc); + return ERROR_INVALID_STATE; + } + } + int rc = esp_timer_start_periodic(ctx->midi_keepalive_timer, period_us); + if (rc != ESP_OK) { + LOG_E(TAG, "midi keepalive timer start failed (rc=%d)", rc); + return ERROR_INVALID_STATE; + } + return ERROR_NONE; +} + +void ble_midi_stop_keepalive(struct Device* device) { + BleCtx* ctx = ble_get_ctx(device); + if (ctx && ctx->midi_keepalive_timer != nullptr) { + esp_timer_stop(ctx->midi_keepalive_timer); + } +} + +// HID + +bool ble_hid_get_active(struct Device* device) { + BleCtx* ctx = ble_get_ctx(device); + return ctx && ctx->hid_active.load(); +} +void ble_hid_set_active(struct Device* device, bool v) { + BleCtx* ctx = ble_get_ctx(device); + if (ctx) ctx->hid_active.store(v); +} + // ---- Event publishing ---- void ble_publish_event(struct Device* device, struct BtEvent event) { diff --git a/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_hid.cpp b/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_hid.cpp index f7ab42b0e..65044a0bc 100644 --- a/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_hid.cpp +++ b/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_hid.cpp @@ -299,17 +299,6 @@ static const struct ble_gatt_svc_def gatt_svcs_gamepad[] = { { 0 } }; -// ---- HID field accessor implementations ---- - -bool ble_hid_get_active(struct Device* device) { - BleCtx* ctx = ble_get_ctx(device); - return ctx && ctx->hid_active.load(); -} -void ble_hid_set_active(struct Device* device, bool v) { - BleCtx* ctx = ble_get_ctx(device); - if (ctx) ctx->hid_active.store(v); -} - // ---- GATT profile switch ---- // device must be the HID device child Device*. diff --git a/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_midi.cpp b/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_midi.cpp index 91f07b970..0945a41c9 100644 --- a/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_midi.cpp +++ b/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_midi.cpp @@ -36,63 +36,6 @@ static const ble_uuid128_t MIDI_IO_UUID = BLE_UUID128_INIT( uint16_t midi_io_handle; -// ---- MIDI field accessor implementations ---- - -bool ble_midi_get_active(struct Device* device) { - BleCtx* ctx = ble_get_ctx(device); - return ctx && ctx->midi_active.load(); -} -void ble_midi_set_active(struct Device* device, bool v) { - BleCtx* ctx = ble_get_ctx(device); - if (ctx) ctx->midi_active.store(v); -} -uint16_t ble_midi_get_conn_handle(struct Device* device) { - BleCtx* ctx = ble_get_ctx(device); - return ctx ? ctx->midi_conn_handle.load() : (uint16_t)BLE_HS_CONN_HANDLE_NONE; -} -void ble_midi_set_conn_handle(struct Device* device, uint16_t h) { - BleCtx* ctx = ble_get_ctx(device); - if (ctx) ctx->midi_conn_handle.store(h); -} -bool ble_midi_get_use_indicate(struct Device* device) { - BleCtx* ctx = ble_get_ctx(device); - return ctx && ctx->midi_use_indicate.load(); -} -void ble_midi_set_use_indicate(struct Device* device, bool v) { - BleCtx* ctx = ble_get_ctx(device); - if (ctx) ctx->midi_use_indicate.store(v); -} - -error_t ble_midi_ensure_keepalive(struct Device* device, esp_timer_cb_t cb, uint64_t period_us) { - BleCtx* ctx = ble_get_ctx(device); - if (!ctx) return ERROR_INVALID_STATE; - if (ctx->midi_keepalive_timer == nullptr) { - esp_timer_create_args_t args = {}; - args.callback = cb; - args.arg = device; - args.dispatch_method = ESP_TIMER_TASK; - args.name = "ble_midi_as"; - int rc = esp_timer_create(&args, &ctx->midi_keepalive_timer); - if (rc != ESP_OK) { - LOG_E(TAG, "midi keepalive timer create failed (rc=%d)", rc); - return ERROR_INVALID_STATE; - } - } - int rc = esp_timer_start_periodic(ctx->midi_keepalive_timer, period_us); - if (rc != ESP_OK) { - LOG_E(TAG, "midi keepalive timer start failed (rc=%d)", rc); - return ERROR_INVALID_STATE; - } - return ERROR_NONE; -} - -void ble_midi_stop_keepalive(struct Device* device) { - BleCtx* ctx = ble_get_ctx(device); - if (ctx && ctx->midi_keepalive_timer != nullptr) { - esp_timer_stop(ctx->midi_keepalive_timer); - } -} - // midi_chr_access is called with the midi child Device* (set via ble_midi_init_gatt_handles). static int midi_chr_access(uint16_t conn_handle, uint16_t attr_handle, struct ble_gatt_access_ctxt* ctxt, void* arg) { diff --git a/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_spp.cpp b/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_spp.cpp index 909a72e8e..0eeeacddd 100644 --- a/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_spp.cpp +++ b/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_spp.cpp @@ -91,25 +91,6 @@ void ble_spp_init_gatt_handles(struct Device* serial_child) { nus_chars_with_handle[1].arg = serial_child; } -// ---- SPP field accessor implementations ---- - -bool ble_spp_get_active(struct Device* device) { - BleCtx* ctx = ble_get_ctx(device); - return ctx && ctx->spp_active.load(); -} -void ble_spp_set_active(struct Device* device, bool v) { - BleCtx* ctx = ble_get_ctx(device); - if (ctx) ctx->spp_active.store(v); -} -uint16_t ble_spp_get_conn_handle(struct Device* device) { - BleCtx* ctx = ble_get_ctx(device); - return ctx ? ctx->spp_conn_handle.load() : (uint16_t)BLE_HS_CONN_HANDLE_NONE; -} -void ble_spp_set_conn_handle(struct Device* device, uint16_t h) { - BleCtx* ctx = ble_get_ctx(device); - if (ctx) ctx->spp_conn_handle.store(h); -} - // ---- SPP sub-API implementations ---- // All functions receive the serial child Device*. From 3bf177cf7f2e5bb35d85eadefede277532135078 Mon Sep 17 00:00:00 2001 From: Shadowtrance Date: Fri, 3 Apr 2026 23:37:17 +1000 Subject: [PATCH 10/12] Revert "move back!" This reverts commit d3694365c634acc5db62ac59771c496cb971a727. --- .../private/bluetooth/esp32_ble_internal.h | 58 +++++++ .../source/drivers/bluetooth/esp32_ble.cpp | 148 +----------------- .../drivers/bluetooth/esp32_ble_hid.cpp | 11 ++ .../drivers/bluetooth/esp32_ble_midi.cpp | 57 +++++++ .../drivers/bluetooth/esp32_ble_spp.cpp | 19 +++ 5 files changed, 148 insertions(+), 145 deletions(-) diff --git a/Platforms/platform-esp32/private/bluetooth/esp32_ble_internal.h b/Platforms/platform-esp32/private/bluetooth/esp32_ble_internal.h index 3dd9a8776..c74473d21 100644 --- a/Platforms/platform-esp32/private/bluetooth/esp32_ble_internal.h +++ b/Platforms/platform-esp32/private/bluetooth/esp32_ble_internal.h @@ -32,6 +32,64 @@ #include #include +// ---- BleCtx ---- +// Private shared state for the ESP32 BLE driver. +// Sub-modules access it exclusively through ble_get_ctx(). + +#define BLE_MAX_CALLBACKS 8 + +struct BleCallbackEntry { + BtEventCallback fn; + void* ctx; +}; + +struct BleCtx { + // Mutexes + SemaphoreHandle_t radio_mutex; // guards radio state transitions + SemaphoreHandle_t cb_mutex; // guards callbacks array + + // Radio / scan state (atomic — read from multiple tasks) + std::atomic radio_state; + std::atomic scan_active; + // Set by Tactility HID host to prevent simultaneous central connection during name resolution + std::atomic hid_host_active; + + // Event callbacks (guarded by cb_mutex) + BleCallbackEntry callbacks[BLE_MAX_CALLBACKS]; + size_t callback_count; + + // Connection handles + active flags (atomic — accessed from multiple tasks) + std::atomic spp_conn_handle; + std::atomic spp_active; + std::atomic midi_conn_handle; + std::atomic midi_active; + std::atomic midi_use_indicate; // true when client subscribed for INDICATE (e.g. Windows) + std::atomic hid_active; + std::atomic link_encrypted; + std::atomic pending_reset_count; + + // Timers + esp_timer_handle_t midi_keepalive_timer; // 2-second periodic Active Sensing + esp_timer_handle_t adv_restart_timer; // one-shot after connect failure (500 ms) + // One-shot timer used to dispatch dispatchDisable off the NimBLE host task. + // nimble_port_stop() must not be called from the NimBLE host task itself. + esp_timer_handle_t disable_timer; + + // BLE device name (set before or after radio enable; applied in dispatch_enable) + char device_name[BLE_DEVICE_NAME_MAX + 1]; + + // Device reference (passed to BtEventCallback) + struct Device* device; + + // Child devices (created by esp32_ble_start_device, destroyed by stop_device) + struct Device* serial_child; + struct Device* midi_child; + struct Device* hid_device_child; +}; + +// Always returns the root BLE device's BleCtx regardless of which device is passed. +BleCtx* ble_get_ctx(struct Device* device); + // ---- General field accessors (defined in esp32_ble.cpp) ---- BtRadioState ble_get_radio_state(struct Device* device); diff --git a/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble.cpp b/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble.cpp index 16e5d6346..f0e21607b 100644 --- a/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble.cpp +++ b/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble.cpp @@ -32,73 +32,18 @@ // ble_store_config_init() is not declared in the public header in some IDF versions. extern "C" void ble_store_config_init(void); -// ---- BleCtx ---- -// Private to this translation unit. Sub-modules access BleCtx fields exclusively -// through the accessor functions declared in the per-module headers. - -#define BLE_MAX_CALLBACKS 8 - -struct BleCallbackEntry { - BtEventCallback fn; - void* ctx; -}; - -struct BleCtx { - // Mutexes - SemaphoreHandle_t radio_mutex; // guards radio state transitions - SemaphoreHandle_t cb_mutex; // guards callbacks array - - // Radio / scan state (atomic — read from multiple tasks) - std::atomic radio_state; - std::atomic scan_active; - // Set by Tactility HID host to prevent simultaneous central connection during name resolution - std::atomic hid_host_active; - - // Event callbacks (guarded by cb_mutex) - BleCallbackEntry callbacks[BLE_MAX_CALLBACKS]; - size_t callback_count; - - // Connection handles + active flags (atomic — accessed from multiple tasks) - std::atomic spp_conn_handle; - std::atomic spp_active; - std::atomic midi_conn_handle; - std::atomic midi_active; - std::atomic midi_use_indicate; // true when client subscribed for INDICATE (e.g. Windows) - std::atomic hid_active; - std::atomic link_encrypted; - std::atomic pending_reset_count; - - // Timers - esp_timer_handle_t midi_keepalive_timer; // 2-second periodic Active Sensing - esp_timer_handle_t adv_restart_timer; // one-shot after connect failure (500 ms) - // One-shot timer used to dispatch dispatchDisable off the NimBLE host task. - // nimble_port_stop() must not be called from the NimBLE host task itself. - esp_timer_handle_t disable_timer; - - // BLE device name (set before or after radio enable; applied in dispatch_enable) - char device_name[BLE_DEVICE_NAME_MAX + 1]; - - // Device reference (passed to BtEventCallback) - struct Device* device; - - // Child devices (created by esp32_ble_start_device, destroyed by stop_device) - struct Device* serial_child; - struct Device* midi_child; - struct Device* hid_device_child; -}; - // File-static device pointer used only by NimBLE host callbacks whose signature // is fixed by the NimBLE API (on_sync, on_reset) and cannot carry a Device*. // All other callbacks receive Device* via their void* arg parameter. static struct Device* s_device = nullptr; -// ---- Context accessor (file-private) ---- +// ---- Context accessor ---- // Always returns the root BLE device's BleCtx regardless of which device is passed // (root, child, or grandchild). Using s_device directly avoids device-tree traversal // ambiguity: the root BLE device may itself have a parent in the device tree, and // walking up from it would land on the wrong node. -static BleCtx* ble_get_ctx(struct Device* /*device*/) { +BleCtx* ble_get_ctx(struct Device* /*device*/) { return s_device ? (BleCtx*)device_get_driver_data(s_device) : nullptr; } @@ -110,7 +55,7 @@ static void dispatch_enable(BleCtx* ctx); static void dispatch_disable(BleCtx* ctx); static int gap_event_handler(struct ble_gap_event* event, void* arg); -// ---- Field accessor implementations ---- +// ---- General field accessor implementations ---- BtRadioState ble_get_radio_state(struct Device* device) { BleCtx* ctx = ble_get_ctx(device); @@ -130,93 +75,6 @@ void ble_set_scan_active(struct Device* device, bool v) { if (ctx) ctx->scan_active.store(v); } -// SPP - -bool ble_spp_get_active(struct Device* device) { - BleCtx* ctx = ble_get_ctx(device); - return ctx && ctx->spp_active.load(); -} -void ble_spp_set_active(struct Device* device, bool v) { - BleCtx* ctx = ble_get_ctx(device); - if (ctx) ctx->spp_active.store(v); -} -uint16_t ble_spp_get_conn_handle(struct Device* device) { - BleCtx* ctx = ble_get_ctx(device); - return ctx ? ctx->spp_conn_handle.load() : (uint16_t)BLE_HS_CONN_HANDLE_NONE; -} -void ble_spp_set_conn_handle(struct Device* device, uint16_t h) { - BleCtx* ctx = ble_get_ctx(device); - if (ctx) ctx->spp_conn_handle.store(h); -} - -// MIDI - -bool ble_midi_get_active(struct Device* device) { - BleCtx* ctx = ble_get_ctx(device); - return ctx && ctx->midi_active.load(); -} -void ble_midi_set_active(struct Device* device, bool v) { - BleCtx* ctx = ble_get_ctx(device); - if (ctx) ctx->midi_active.store(v); -} -uint16_t ble_midi_get_conn_handle(struct Device* device) { - BleCtx* ctx = ble_get_ctx(device); - return ctx ? ctx->midi_conn_handle.load() : (uint16_t)BLE_HS_CONN_HANDLE_NONE; -} -void ble_midi_set_conn_handle(struct Device* device, uint16_t h) { - BleCtx* ctx = ble_get_ctx(device); - if (ctx) ctx->midi_conn_handle.store(h); -} -bool ble_midi_get_use_indicate(struct Device* device) { - BleCtx* ctx = ble_get_ctx(device); - return ctx && ctx->midi_use_indicate.load(); -} -void ble_midi_set_use_indicate(struct Device* device, bool v) { - BleCtx* ctx = ble_get_ctx(device); - if (ctx) ctx->midi_use_indicate.store(v); -} - -error_t ble_midi_ensure_keepalive(struct Device* device, esp_timer_cb_t cb, uint64_t period_us) { - BleCtx* ctx = ble_get_ctx(device); - if (!ctx) return ERROR_INVALID_STATE; - if (ctx->midi_keepalive_timer == nullptr) { - esp_timer_create_args_t args = {}; - args.callback = cb; - args.arg = device; - args.dispatch_method = ESP_TIMER_TASK; - args.name = "ble_midi_as"; - int rc = esp_timer_create(&args, &ctx->midi_keepalive_timer); - if (rc != ESP_OK) { - LOG_E(TAG, "midi keepalive timer create failed (rc=%d)", rc); - return ERROR_INVALID_STATE; - } - } - int rc = esp_timer_start_periodic(ctx->midi_keepalive_timer, period_us); - if (rc != ESP_OK) { - LOG_E(TAG, "midi keepalive timer start failed (rc=%d)", rc); - return ERROR_INVALID_STATE; - } - return ERROR_NONE; -} - -void ble_midi_stop_keepalive(struct Device* device) { - BleCtx* ctx = ble_get_ctx(device); - if (ctx && ctx->midi_keepalive_timer != nullptr) { - esp_timer_stop(ctx->midi_keepalive_timer); - } -} - -// HID - -bool ble_hid_get_active(struct Device* device) { - BleCtx* ctx = ble_get_ctx(device); - return ctx && ctx->hid_active.load(); -} -void ble_hid_set_active(struct Device* device, bool v) { - BleCtx* ctx = ble_get_ctx(device); - if (ctx) ctx->hid_active.store(v); -} - // ---- Event publishing ---- void ble_publish_event(struct Device* device, struct BtEvent event) { diff --git a/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_hid.cpp b/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_hid.cpp index 65044a0bc..f7ab42b0e 100644 --- a/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_hid.cpp +++ b/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_hid.cpp @@ -299,6 +299,17 @@ static const struct ble_gatt_svc_def gatt_svcs_gamepad[] = { { 0 } }; +// ---- HID field accessor implementations ---- + +bool ble_hid_get_active(struct Device* device) { + BleCtx* ctx = ble_get_ctx(device); + return ctx && ctx->hid_active.load(); +} +void ble_hid_set_active(struct Device* device, bool v) { + BleCtx* ctx = ble_get_ctx(device); + if (ctx) ctx->hid_active.store(v); +} + // ---- GATT profile switch ---- // device must be the HID device child Device*. diff --git a/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_midi.cpp b/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_midi.cpp index 0945a41c9..91f07b970 100644 --- a/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_midi.cpp +++ b/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_midi.cpp @@ -36,6 +36,63 @@ static const ble_uuid128_t MIDI_IO_UUID = BLE_UUID128_INIT( uint16_t midi_io_handle; +// ---- MIDI field accessor implementations ---- + +bool ble_midi_get_active(struct Device* device) { + BleCtx* ctx = ble_get_ctx(device); + return ctx && ctx->midi_active.load(); +} +void ble_midi_set_active(struct Device* device, bool v) { + BleCtx* ctx = ble_get_ctx(device); + if (ctx) ctx->midi_active.store(v); +} +uint16_t ble_midi_get_conn_handle(struct Device* device) { + BleCtx* ctx = ble_get_ctx(device); + return ctx ? ctx->midi_conn_handle.load() : (uint16_t)BLE_HS_CONN_HANDLE_NONE; +} +void ble_midi_set_conn_handle(struct Device* device, uint16_t h) { + BleCtx* ctx = ble_get_ctx(device); + if (ctx) ctx->midi_conn_handle.store(h); +} +bool ble_midi_get_use_indicate(struct Device* device) { + BleCtx* ctx = ble_get_ctx(device); + return ctx && ctx->midi_use_indicate.load(); +} +void ble_midi_set_use_indicate(struct Device* device, bool v) { + BleCtx* ctx = ble_get_ctx(device); + if (ctx) ctx->midi_use_indicate.store(v); +} + +error_t ble_midi_ensure_keepalive(struct Device* device, esp_timer_cb_t cb, uint64_t period_us) { + BleCtx* ctx = ble_get_ctx(device); + if (!ctx) return ERROR_INVALID_STATE; + if (ctx->midi_keepalive_timer == nullptr) { + esp_timer_create_args_t args = {}; + args.callback = cb; + args.arg = device; + args.dispatch_method = ESP_TIMER_TASK; + args.name = "ble_midi_as"; + int rc = esp_timer_create(&args, &ctx->midi_keepalive_timer); + if (rc != ESP_OK) { + LOG_E(TAG, "midi keepalive timer create failed (rc=%d)", rc); + return ERROR_INVALID_STATE; + } + } + int rc = esp_timer_start_periodic(ctx->midi_keepalive_timer, period_us); + if (rc != ESP_OK) { + LOG_E(TAG, "midi keepalive timer start failed (rc=%d)", rc); + return ERROR_INVALID_STATE; + } + return ERROR_NONE; +} + +void ble_midi_stop_keepalive(struct Device* device) { + BleCtx* ctx = ble_get_ctx(device); + if (ctx && ctx->midi_keepalive_timer != nullptr) { + esp_timer_stop(ctx->midi_keepalive_timer); + } +} + // midi_chr_access is called with the midi child Device* (set via ble_midi_init_gatt_handles). static int midi_chr_access(uint16_t conn_handle, uint16_t attr_handle, struct ble_gatt_access_ctxt* ctxt, void* arg) { diff --git a/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_spp.cpp b/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_spp.cpp index 0eeeacddd..909a72e8e 100644 --- a/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_spp.cpp +++ b/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_spp.cpp @@ -91,6 +91,25 @@ void ble_spp_init_gatt_handles(struct Device* serial_child) { nus_chars_with_handle[1].arg = serial_child; } +// ---- SPP field accessor implementations ---- + +bool ble_spp_get_active(struct Device* device) { + BleCtx* ctx = ble_get_ctx(device); + return ctx && ctx->spp_active.load(); +} +void ble_spp_set_active(struct Device* device, bool v) { + BleCtx* ctx = ble_get_ctx(device); + if (ctx) ctx->spp_active.store(v); +} +uint16_t ble_spp_get_conn_handle(struct Device* device) { + BleCtx* ctx = ble_get_ctx(device); + return ctx ? ctx->spp_conn_handle.load() : (uint16_t)BLE_HS_CONN_HANDLE_NONE; +} +void ble_spp_set_conn_handle(struct Device* device, uint16_t h) { + BleCtx* ctx = ble_get_ctx(device); + if (ctx) ctx->spp_conn_handle.store(h); +} + // ---- SPP sub-API implementations ---- // All functions receive the serial child Device*. From da4200bf105cc9b9d93be4a0281969ba7615976c Mon Sep 17 00:00:00 2001 From: Shadowtrance Date: Sat, 4 Apr 2026 02:41:16 +1000 Subject: [PATCH 11/12] fix some of the things --- Devices/lilygo-tdeck/lilygo,tdeck.dts | 4 +- Devices/m5stack-tab5/m5stack,tab5.dts | 4 +- Firmware/CMakeLists.txt | 1 + .../{esp32,ble-nimble.yaml => esp32,ble.yaml} | 2 +- .../include/tactility/bindings/esp32_ble.h | 7 +++ .../tactility/bindings/esp32_ble_nimble.h | 7 --- .../{esp32_ble_nimble.h => esp32_ble.h} | 0 .../private/bluetooth/esp32_ble_hid.h | 2 +- .../private/bluetooth/esp32_ble_internal.h | 4 -- .../source/drivers/bluetooth/README.md | 48 ----------------- .../source/drivers/bluetooth/esp32_ble.cpp | 6 +-- .../bluetooth/BluetoothPairedDevice.h | 2 +- Tactility/Source/bluetooth/Bluetooth.cpp | 2 +- .../bluetooth/BluetoothPairedDevice.cpp | 2 +- Tactility/Source/bluetooth/README.md | 52 ------------------- .../include/tactility/drivers/bluetooth.h | 2 +- TactilityKernel/source/drivers/bluetooth.cpp | 2 +- TactilityKernel/source/kernel_symbols.c | 2 +- 18 files changed, 23 insertions(+), 126 deletions(-) rename Platforms/platform-esp32/bindings/{esp32,ble-nimble.yaml => esp32,ble.yaml} (75%) create mode 100644 Platforms/platform-esp32/include/tactility/bindings/esp32_ble.h delete mode 100644 Platforms/platform-esp32/include/tactility/bindings/esp32_ble_nimble.h rename Platforms/platform-esp32/include/tactility/drivers/{esp32_ble_nimble.h => esp32_ble.h} (100%) delete mode 100644 Platforms/platform-esp32/source/drivers/bluetooth/README.md delete mode 100644 Tactility/Source/bluetooth/README.md diff --git a/Devices/lilygo-tdeck/lilygo,tdeck.dts b/Devices/lilygo-tdeck/lilygo,tdeck.dts index 361824719..5d0bbd451 100644 --- a/Devices/lilygo-tdeck/lilygo,tdeck.dts +++ b/Devices/lilygo-tdeck/lilygo,tdeck.dts @@ -4,7 +4,7 @@ #include #include #include -#include +#include #include #include @@ -14,7 +14,7 @@ model = "LilyGO T-Deck"; ble0 { - compatible = "esp32,ble-nimble"; + compatible = "esp32,ble"; }; gpio0 { diff --git a/Devices/m5stack-tab5/m5stack,tab5.dts b/Devices/m5stack-tab5/m5stack,tab5.dts index 254b84322..f51735798 100644 --- a/Devices/m5stack-tab5/m5stack,tab5.dts +++ b/Devices/m5stack-tab5/m5stack,tab5.dts @@ -5,7 +5,7 @@ #include #include #include -#include +#include #include #include #include @@ -15,7 +15,7 @@ model = "Tab5"; ble0 { - compatible = "esp32,ble-nimble"; + compatible = "esp32,ble"; }; gpio0 { diff --git a/Firmware/CMakeLists.txt b/Firmware/CMakeLists.txt index a344f7027..bd04a6312 100644 --- a/Firmware/CMakeLists.txt +++ b/Firmware/CMakeLists.txt @@ -19,6 +19,7 @@ endif () set(DEVICETREE_LOCATION "${PROJECT_ROOT}/Devices/${TACTILITY_DEVICE_ID}") # Check if device has Bluetooth enabled +# Fixes the sdkconfig bluetooth enable options from getting nuked on non-P4+C6 builds when idf build runs if (DEFINED ENV{ESP_IDF_VERSION}) file(READ "${DEVICETREE_LOCATION}/device.properties" device_properties_content) if (device_properties_content MATCHES "bluetooth=true") diff --git a/Platforms/platform-esp32/bindings/esp32,ble-nimble.yaml b/Platforms/platform-esp32/bindings/esp32,ble.yaml similarity index 75% rename from Platforms/platform-esp32/bindings/esp32,ble-nimble.yaml rename to Platforms/platform-esp32/bindings/esp32,ble.yaml index f0a09bf61..3663c7405 100644 --- a/Platforms/platform-esp32/bindings/esp32,ble-nimble.yaml +++ b/Platforms/platform-esp32/bindings/esp32,ble.yaml @@ -1,6 +1,6 @@ description: ESP32 BLE driver (NimBLE) -compatible: "esp32,ble-nimble" +compatible: "esp32,ble" properties: _unused: diff --git a/Platforms/platform-esp32/include/tactility/bindings/esp32_ble.h b/Platforms/platform-esp32/include/tactility/bindings/esp32_ble.h new file mode 100644 index 000000000..ef299811a --- /dev/null +++ b/Platforms/platform-esp32/include/tactility/bindings/esp32_ble.h @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include + +DEFINE_DEVICETREE(ble, struct Esp32BleNimbleConfig) diff --git a/Platforms/platform-esp32/include/tactility/bindings/esp32_ble_nimble.h b/Platforms/platform-esp32/include/tactility/bindings/esp32_ble_nimble.h deleted file mode 100644 index 9ae79c184..000000000 --- a/Platforms/platform-esp32/include/tactility/bindings/esp32_ble_nimble.h +++ /dev/null @@ -1,7 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -#pragma once - -#include -#include - -DEFINE_DEVICETREE(ble_nimble, struct Esp32BleNimbleConfig) diff --git a/Platforms/platform-esp32/include/tactility/drivers/esp32_ble_nimble.h b/Platforms/platform-esp32/include/tactility/drivers/esp32_ble.h similarity index 100% rename from Platforms/platform-esp32/include/tactility/drivers/esp32_ble_nimble.h rename to Platforms/platform-esp32/include/tactility/drivers/esp32_ble.h diff --git a/Platforms/platform-esp32/private/bluetooth/esp32_ble_hid.h b/Platforms/platform-esp32/private/bluetooth/esp32_ble_hid.h index c5c3102e4..a495ae1ad 100644 --- a/Platforms/platform-esp32/private/bluetooth/esp32_ble_hid.h +++ b/Platforms/platform-esp32/private/bluetooth/esp32_ble_hid.h @@ -11,7 +11,7 @@ #include #include -enum class BleHidProfile { None, KbConsumer, Mouse, KbMouse, Gamepad }; +enum BleHidProfile { None, KbConsumer, Mouse, KbMouse, Gamepad }; struct BleHidDeviceCtx { std::atomic hid_conn_handle; diff --git a/Platforms/platform-esp32/private/bluetooth/esp32_ble_internal.h b/Platforms/platform-esp32/private/bluetooth/esp32_ble_internal.h index c74473d21..5c9e676c1 100644 --- a/Platforms/platform-esp32/private/bluetooth/esp32_ble_internal.h +++ b/Platforms/platform-esp32/private/bluetooth/esp32_ble_internal.h @@ -32,10 +32,6 @@ #include #include -// ---- BleCtx ---- -// Private shared state for the ESP32 BLE driver. -// Sub-modules access it exclusively through ble_get_ctx(). - #define BLE_MAX_CALLBACKS 8 struct BleCallbackEntry { diff --git a/Platforms/platform-esp32/source/drivers/bluetooth/README.md b/Platforms/platform-esp32/source/drivers/bluetooth/README.md deleted file mode 100644 index b350dd79d..000000000 --- a/Platforms/platform-esp32/source/drivers/bluetooth/README.md +++ /dev/null @@ -1,48 +0,0 @@ -# ESP32 Bluetooth Driver - -Implements the `BluetoothApi` kernel driver interface on top of the NimBLE host stack. -Only compiled when `CONFIG_BT_NIMBLE_ENABLED=y`. - -## Source Files - -| File | Purpose | -|------|---------| -| `esp32_ble.cpp` | Core driver: NimBLE lifecycle, GAP event handler, advertising, radio enable/disable | -| `esp32_ble_scan.cpp` | GAP discovery, name resolution, auto-connect dispatch | -| `esp32_ble_spp.cpp` | NUS (Nordic UART Service) GATT server — `BtSerialApi` | -| `esp32_ble_midi.cpp` | BLE MIDI GATT server — `BtMidiApi` | -| `esp32_ble_hid.cpp` | HID peripheral (keyboard / mouse / gamepad) — `BtHidDeviceApi` | - -Internal shared state is defined in -`Platforms/platform-esp32/private/bluetooth/esp32_ble_internal.h` (`BleCtx`). - -The public C API (`bluetooth_scan_start()` etc.) is implemented in -`TactilityKernel/source/drivers/bluetooth.cpp` as thin wrappers over the `BluetoothApi` -function-pointer struct. External code must only call those public functions — never touch -`BluetoothApi` directly. - -SPP, MIDI, and HID Device profiles are exposed as **child devices** created by -`esp32_ble_start_device()`. Each gets its own `DeviceType` and is found at runtime via -`bluetooth_serial_get_device()`, `bluetooth_midi_get_device()`, or -`bluetooth_hid_device_get_device()`. Their drivers have `start_device=nullptr` since -initialization is handled by the parent driver; they obtain the shared `BleCtx` via `ble_get_ctx(device)`. - -## Profiles - -| Profile | Role | API struct | -|---------|------|------------| -| HID Host | Central | Managed in Tactility layer (`BluetoothHidHost.cpp`) | -| HID Device | Peripheral | `BtHidDeviceApi` (child device: `BLUETOOTH_HID_DEVICE_TYPE`) | -| SPP (NUS) | Peripheral | `BtSerialApi` (child device: `BLUETOOTH_SERIAL_TYPE`) | -| MIDI | Peripheral | `BtMidiApi` (child device: `BLUETOOTH_MIDI_TYPE`) | - -Only one peripheral profile (HID Device, SPP, or MIDI) can advertise at a time. - -## Locking Rules - -- `BleCtx::radio_mutex` — guards radio enable/disable state transitions. -- `BleCtx::data_mutex` — guards scan results and RX queues. Released before any NimBLE call. -- `std::atomic<>` — connection handles and flag bools are atomics (read/written cross-task). -- File I/O must **never** run on the NimBLE host task — dispatch via `getMainDispatcher()`. - -See `bluetooth.puml` for the full thread model diagram. diff --git a/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble.cpp b/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble.cpp index f0e21607b..bcfebfa23 100644 --- a/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble.cpp +++ b/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble.cpp @@ -48,7 +48,7 @@ BleCtx* ble_get_ctx(struct Device* /*device*/) { } // ---- Forward declarations ---- -static void ble_host_task(void* param); +static void host_task(void* param); static void on_sync(); static void on_reset(int reason); static void dispatch_enable(BleCtx* ctx); @@ -460,7 +460,7 @@ static void on_reset(int reason) { } } -static void ble_host_task(void* param) { +static void host_task(void* param) { LOG_I(TAG, "BLE host task started"); nimble_port_run(); // nimble_port_deinit() is called by dispatch_disable() after nimble_port_stop() returns. @@ -667,7 +667,7 @@ static void dispatch_enable(BleCtx* ctx) { ble_att_set_preferred_mtu(BLE_ATT_MTU_MAX); // Start NimBLE host task (on_sync will fire when ready) - nimble_port_freertos_init(ble_host_task); + nimble_port_freertos_init(host_task); } static void dispatch_disable(BleCtx* ctx) { diff --git a/Tactility/Include/Tactility/bluetooth/BluetoothPairedDevice.h b/Tactility/Include/Tactility/bluetooth/BluetoothPairedDevice.h index e4bbf5239..96ee6ebd5 100644 --- a/Tactility/Include/Tactility/bluetooth/BluetoothPairedDevice.h +++ b/Tactility/Include/Tactility/bluetooth/BluetoothPairedDevice.h @@ -17,7 +17,7 @@ struct PairedDevice { std::string addrToHex(const std::array& addr); -bool contains(const std::string& addr_hex); +bool hasFileForDevice(const std::string& addr_hex); bool load(const std::string& addr_hex, PairedDevice& device); diff --git a/Tactility/Source/bluetooth/Bluetooth.cpp b/Tactility/Source/bluetooth/Bluetooth.cpp index 4fe2aad8f..97ebd7136 100644 --- a/Tactility/Source/bluetooth/Bluetooth.cpp +++ b/Tactility/Source/bluetooth/Bluetooth.cpp @@ -166,7 +166,7 @@ static void bt_event_bridge(struct Device* /*device*/, void* /*context*/, struct std::array peer_addr; memcpy(peer_addr.data(), addr_buf, 6); const auto hex = settings::addrToHex(peer_addr); - if (!settings::contains(hex)) { + if (!settings::hasFileForDevice(hex)) { settings::PairedDevice dev; dev.addr = peer_addr; dev.name = ""; diff --git a/Tactility/Source/bluetooth/BluetoothPairedDevice.cpp b/Tactility/Source/bluetooth/BluetoothPairedDevice.cpp index 81802cd06..7cb4402f8 100644 --- a/Tactility/Source/bluetooth/BluetoothPairedDevice.cpp +++ b/Tactility/Source/bluetooth/BluetoothPairedDevice.cpp @@ -55,7 +55,7 @@ static std::string getFilePath(const std::string& addr_hex) { return std::format(DEVICE_SETTINGS_FORMAT, DATA_DIR, addr_hex); } -bool contains(const std::string& addr_hex) { +bool hasFileForDevice(const std::string& addr_hex) { return file::isFile(getFilePath(addr_hex)); } diff --git a/Tactility/Source/bluetooth/README.md b/Tactility/Source/bluetooth/README.md deleted file mode 100644 index aac7cd7d6..000000000 --- a/Tactility/Source/bluetooth/README.md +++ /dev/null @@ -1,52 +0,0 @@ -# Bluetooth Bridge (Tactility Layer) - -Bridges the kernel `BluetoothApi` driver to the Tactility C++ application layer. - -## Architecture - -``` -Apps / Services - │ tt::bluetooth:: public API (Bluetooth.h) - ▼ -Bluetooth.cpp — registers BtEventCallback, translates events, dispatches I/O -BluetoothSpp.cpp — SPP start/stop (uses bluetooth_serial child device) -BluetoothMidi.cpp — MIDI start/stop (uses bluetooth_midi child device) -BluetoothHidDevice.cpp — HID device start/stop (uses bluetooth_hid_device child device) -BluetoothHidHost.cpp — BLE HID central: GATT discovery, LVGL indev management -BluetoothSettings.cpp / BluetoothPairedDevice.cpp — settings persistence - │ bluetooth_*() public C functions - │ (TactilityKernel/drivers/bluetooth.h + bluetooth_serial/midi/hid_device.h) - ▼ -esp32_ble*.cpp — NimBLE kernel driver (Platforms/platform-esp32) -``` - -## Source Files - -| File | Purpose | -|------|---------| -| `Bluetooth.cpp` | Bridge: event callback, scan cache, public C++ API (`tt::bluetooth::`) | -| `BluetoothSpp.cpp` | SPP start/stop, auto-start setting persistence | -| `BluetoothMidi.cpp` | MIDI start/stop, auto-start setting persistence | -| `BluetoothHidDevice.cpp` | HID device start/stop, appearance → mode mapping | -| `BluetoothHidHost.cpp` | HID host: GATT discovery, report parsing, LVGL indev registration | -| `BluetoothMock.cpp` | No-op stubs for non-BLE builds | -| `BluetoothPairedDevice.cpp` | Paired peer persistence (`.device.properties` files) | -| `BluetoothSettings.cpp` | Global BT settings (`enableOnBoot`, `sppAutoStart`, `midiAutoStart`) | - -## Key Design Points - -- **No direct struct access**: external code calls `bluetooth_scan_start(device)` etc. - (public C API in ``), never the `BluetoothApi` struct. - SPP, MIDI, and HID Device profiles use their own child device headers - (`bluetooth_serial.h`, `bluetooth_midi.h`, `bluetooth_hid_device.h`). -- **File I/O off NimBLE task**: the bridge callback (`bt_event_bridge`) runs on the NimBLE - host task (4 KB stack). All `settings::load/save` calls are dispatched to `getMainDispatcher()`. -- **Scan result cache**: `Bluetooth.cpp` maintains a `std::vector` populated from - `BT_EVENT_PEER_FOUND` events, since the kernel driver does not expose a `get_scan_results()` call. -- **Addr-type cache**: parallel cache of `{addr, addr_type}` entries used by `BluetoothHidHost.cpp` - for `ble_gap_connect()`. -- **Settings paths**: stored under `/data/service/bluetooth/` (legacy path kept for - backward compatibility with existing device data). - -See `Platforms/platform-esp32/source/drivers/bluetooth/README.md` for the driver internals -and `bluetooth.puml` for the thread model diagram. diff --git a/TactilityKernel/include/tactility/drivers/bluetooth.h b/TactilityKernel/include/tactility/drivers/bluetooth.h index 6ab48b38a..bba8ed84f 100644 --- a/TactilityKernel/include/tactility/drivers/bluetooth.h +++ b/TactilityKernel/include/tactility/drivers/bluetooth.h @@ -283,7 +283,7 @@ extern const struct DeviceType BLUETOOTH_TYPE; * since data symbols may not be exported by the ELF loader. * @return the first ready Device of BLUETOOTH_TYPE, or NULL if none found. */ -struct Device* bluetooth_get_device(void); +struct Device* bluetooth_find_first_ready_device(void); error_t bluetooth_get_radio_state(struct Device* device, enum BtRadioState* state); error_t bluetooth_set_radio_enabled(struct Device* device, bool enabled); diff --git a/TactilityKernel/source/drivers/bluetooth.cpp b/TactilityKernel/source/drivers/bluetooth.cpp index 76040c9ac..518e3f8fb 100644 --- a/TactilityKernel/source/drivers/bluetooth.cpp +++ b/TactilityKernel/source/drivers/bluetooth.cpp @@ -8,7 +8,7 @@ extern "C" { // ---- Device lookup ---- -struct Device* bluetooth_get_device() { +struct Device* bluetooth_find_first_ready_device() { struct Device* found = nullptr; device_for_each_of_type(&BLUETOOTH_TYPE, &found, [](struct Device* dev, void* ctx) -> bool { if (device_is_ready(dev)) { diff --git a/TactilityKernel/source/kernel_symbols.c b/TactilityKernel/source/kernel_symbols.c index f48caaa70..ce8b6750b 100644 --- a/TactilityKernel/source/kernel_symbols.c +++ b/TactilityKernel/source/kernel_symbols.c @@ -122,7 +122,7 @@ const struct ModuleSymbol KERNEL_SYMBOLS[] = { DEFINE_MODULE_SYMBOL(uart_controller_flush_input), DEFINE_MODULE_SYMBOL(UART_CONTROLLER_TYPE), // drivers/bluetooth - DEFINE_MODULE_SYMBOL(bluetooth_get_device), + DEFINE_MODULE_SYMBOL(bluetooth_find_first_ready_device), DEFINE_MODULE_SYMBOL(bluetooth_get_radio_state), DEFINE_MODULE_SYMBOL(bluetooth_set_radio_enabled), DEFINE_MODULE_SYMBOL(bluetooth_scan_start), From 87277b56c56a428a73936396f05b8cc64e00caa7 Mon Sep 17 00:00:00 2001 From: Shadowtrance Date: Sat, 4 Apr 2026 18:12:13 +1000 Subject: [PATCH 12/12] Addressing feedback? hmm --- .../private/bluetooth/esp32_ble_hid.h | 14 +- .../private/bluetooth/esp32_ble_internal.h | 17 +- .../private/bluetooth/esp32_ble_midi.h | 11 - .../private/bluetooth/esp32_ble_spp.h | 13 -- .../source/drivers/bluetooth/esp32_ble.cpp | 194 +++++------------- .../drivers/bluetooth/esp32_ble_hid.cpp | 62 ++++-- .../drivers/bluetooth/esp32_ble_midi.cpp | 73 +++++-- .../drivers/bluetooth/esp32_ble_scan.cpp | 100 ++++----- .../drivers/bluetooth/esp32_ble_spp.cpp | 60 +++++- Platforms/platform-esp32/source/module.cpp | 9 + .../Include/Tactility/bluetooth/Bluetooth.h | 29 --- Tactility/Source/bluetooth/Bluetooth.cpp | 29 ++- .../Source/bluetooth/BluetoothHidDevice.cpp | 36 +--- Tactility/Source/bluetooth/BluetoothMidi.cpp | 34 +-- Tactility/Source/bluetooth/BluetoothMock.cpp | 9 - Tactility/Source/bluetooth/BluetoothSpp.cpp | 34 +-- 16 files changed, 303 insertions(+), 421 deletions(-) diff --git a/Platforms/platform-esp32/private/bluetooth/esp32_ble_hid.h b/Platforms/platform-esp32/private/bluetooth/esp32_ble_hid.h index a495ae1ad..f908bf462 100644 --- a/Platforms/platform-esp32/private/bluetooth/esp32_ble_hid.h +++ b/Platforms/platform-esp32/private/bluetooth/esp32_ble_hid.h @@ -8,25 +8,19 @@ #include -#include #include enum BleHidProfile { None, KbConsumer, Mouse, KbMouse, Gamepad }; -struct BleHidDeviceCtx { - std::atomic hid_conn_handle; -}; - struct Device; -bool ble_hid_get_active(struct Device* device); -void ble_hid_set_active(struct Device* device, bool v); +bool ble_hid_get_active(struct Device* device); +void ble_hid_set_active(struct Device* device, bool v); +uint16_t ble_hid_get_conn_handle(struct Device* device); +void ble_hid_set_conn_handle(struct Device* device, uint16_t h); // device must be the hid_device child Device*. void ble_hid_init_gatt(); -void ble_hid_init_gatt_handles(); void ble_hid_switch_profile(struct Device* hid_child, BleHidProfile profile); -extern const BtHidDeviceApi nimble_hid_device_api; - #endif // CONFIG_BT_NIMBLE_ENABLED diff --git a/Platforms/platform-esp32/private/bluetooth/esp32_ble_internal.h b/Platforms/platform-esp32/private/bluetooth/esp32_ble_internal.h index 5c9e676c1..5d7dc1a4f 100644 --- a/Platforms/platform-esp32/private/bluetooth/esp32_ble_internal.h +++ b/Platforms/platform-esp32/private/bluetooth/esp32_ble_internal.h @@ -23,8 +23,6 @@ #include #include -#include -#include // ---- Per-module headers (structs, accessors, sub-API externs) ---- @@ -74,6 +72,12 @@ struct BleCtx { // BLE device name (set before or after radio enable; applied in dispatch_enable) char device_name[BLE_DEVICE_NAME_MAX + 1]; + // Scan data (guarded by scan_mutex) + SemaphoreHandle_t scan_mutex; + BtPeerRecord scan_results[64]; + ble_addr_t scan_addrs[64]; + size_t scan_count; + // Device reference (passed to BtEventCallback) struct Device* device; @@ -96,9 +100,7 @@ bool ble_get_scan_active(struct Device* device); void ble_set_scan_active(struct Device* device, bool v); // ---- Scan data management (defined in esp32_ble_scan.cpp) ---- -void ble_scan_init(); -void ble_scan_deinit(); -void ble_scan_clear_results(); +void ble_scan_clear_results(struct Device* device); // ---- Event publishing ---- void ble_publish_event(struct Device* device, struct BtEvent event); @@ -112,6 +114,11 @@ void ble_schedule_adv_restart(struct Device* device, uint64_t delay_us); int ble_gap_disc_event_handler(struct ble_gap_event* event, void* arg); void ble_resolve_next_unnamed_peer(struct Device* device, size_t start_idx); +// ---- Child driver definitions (one per profile sub-module) ---- +extern struct Driver esp32_ble_serial_driver; +extern struct Driver esp32_ble_midi_driver; +extern struct Driver esp32_ble_hid_device_driver; + // ---- SPP GATT (defined in esp32_ble_spp.cpp) ---- // device must be the serial child Device*. void ble_spp_init_gatt_handles(struct Device* serial_child); diff --git a/Platforms/platform-esp32/private/bluetooth/esp32_ble_midi.h b/Platforms/platform-esp32/private/bluetooth/esp32_ble_midi.h index 84377d39f..e8da75998 100644 --- a/Platforms/platform-esp32/private/bluetooth/esp32_ble_midi.h +++ b/Platforms/platform-esp32/private/bluetooth/esp32_ble_midi.h @@ -10,17 +10,7 @@ #include #include -#include -#include - #include -#include -#include - -struct BleMidiCtx { - SemaphoreHandle_t rx_mutex; - std::deque> rx_queue; -}; struct Device; @@ -37,6 +27,5 @@ void ble_midi_set_use_indicate(struct Device* device, bool v); error_t ble_midi_ensure_keepalive(struct Device* device, esp_timer_cb_t cb, uint64_t period_us); void ble_midi_stop_keepalive(struct Device* device); -extern const BtMidiApi nimble_midi_api; #endif // CONFIG_BT_NIMBLE_ENABLED diff --git a/Platforms/platform-esp32/private/bluetooth/esp32_ble_spp.h b/Platforms/platform-esp32/private/bluetooth/esp32_ble_spp.h index a14dd8a0d..73149e18c 100644 --- a/Platforms/platform-esp32/private/bluetooth/esp32_ble_spp.h +++ b/Platforms/platform-esp32/private/bluetooth/esp32_ble_spp.h @@ -7,19 +7,7 @@ #if defined(CONFIG_BT_NIMBLE_ENABLED) #include -#include - -#include -#include - #include -#include -#include - -struct BleSppCtx { - SemaphoreHandle_t rx_mutex; - std::deque> rx_queue; -}; struct Device; @@ -28,6 +16,5 @@ void ble_spp_set_active(struct Device* device, bool v); uint16_t ble_spp_get_conn_handle(struct Device* device); void ble_spp_set_conn_handle(struct Device* device, uint16_t h); -extern const BtSerialApi nimble_serial_api; #endif // CONFIG_BT_NIMBLE_ENABLED diff --git a/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble.cpp b/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble.cpp index bcfebfa23..7d0755adf 100644 --- a/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble.cpp +++ b/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble.cpp @@ -32,19 +32,29 @@ // ble_store_config_init() is not declared in the public header in some IDF versions. extern "C" void ble_store_config_init(void); -// File-static device pointer used only by NimBLE host callbacks whose signature +// Sub-module API structs (defined in their respective cpp files). +extern const BtHidDeviceApi nimble_hid_device_api; +extern const BtSerialApi nimble_serial_api; +extern const BtMidiApi nimble_midi_api; + +// File-static BleCtx pointer used only by NimBLE host callbacks whose signature // is fixed by the NimBLE API (on_sync, on_reset) and cannot carry a Device*. // All other callbacks receive Device* via their void* arg parameter. -static struct Device* s_device = nullptr; +static BleCtx* s_ctx = nullptr; // ---- Context accessor ---- -// Always returns the root BLE device's BleCtx regardless of which device is passed -// (root, child, or grandchild). Using s_device directly avoids device-tree traversal -// ambiguity: the root BLE device may itself have a parent in the device tree, and -// walking up from it would land on the wrong node. - -BleCtx* ble_get_ctx(struct Device* /*device*/) { - return s_device ? (BleCtx*)device_get_driver_data(s_device) : nullptr; +// Returns the BleCtx for any BLE device (root or child). +// If device is the root BLE device (BLUETOOTH_TYPE) its driver_data IS the BleCtx. +// If device is a child BLE device its parent is the root BLE device. +// We must NOT use device_get_parent blindly: the DTS root node is the parent of ble0, +// so device_get_parent(ble0) returns a non-null node whose driver_data is not BleCtx. + +BleCtx* ble_get_ctx(struct Device* device) { + if (device_get_type(device) == &BLUETOOTH_TYPE) { + return (BleCtx*)device_get_driver_data(device); + } + struct Device* parent = device_get_parent(device); + return parent ? (BleCtx*)device_get_driver_data(parent) : nullptr; } // ---- Forward declarations ---- @@ -58,21 +68,17 @@ static int gap_event_handler(struct ble_gap_event* event, void* arg); // ---- General field accessor implementations ---- BtRadioState ble_get_radio_state(struct Device* device) { - BleCtx* ctx = ble_get_ctx(device); - return ctx ? ctx->radio_state.load() : BT_RADIO_STATE_OFF; + return ble_get_ctx(device)->radio_state.load(); } bool ble_hid_get_host_active(struct Device* device) { - BleCtx* ctx = ble_get_ctx(device); - return ctx && ctx->hid_host_active.load(); + return ble_get_ctx(device)->hid_host_active.load(); } bool ble_get_scan_active(struct Device* device) { - BleCtx* ctx = ble_get_ctx(device); - return ctx && ctx->scan_active.load(); + return ble_get_ctx(device)->scan_active.load(); } void ble_set_scan_active(struct Device* device, bool v) { - BleCtx* ctx = ble_get_ctx(device); - if (ctx) ctx->scan_active.store(v); + ble_get_ctx(device)->scan_active.store(v); } // ---- Event publishing ---- @@ -97,11 +103,9 @@ void ble_publish_event(struct Device* device, struct BtEvent event) { static void adv_restart_callback(void* arg) { struct Device* device = (struct Device*)arg; BleCtx* ctx = ble_get_ctx(device); - if (ctx == nullptr || ctx->radio_state.load() != BT_RADIO_STATE_ON) return; + if (ctx->radio_state.load() != BT_RADIO_STATE_ON) return; - BleHidDeviceCtx* hid_ctx = ctx->hid_device_child - ? (BleHidDeviceCtx*)device_get_driver_data(ctx->hid_device_child) : nullptr; - uint16_t hid_conn = hid_ctx ? hid_ctx->hid_conn_handle.load() : (uint16_t)BLE_HS_CONN_HANDLE_NONE; + uint16_t hid_conn = ble_hid_get_conn_handle(ctx->hid_device_child); if (ctx->midi_active.load() && ctx->midi_conn_handle.load() == BLE_HS_CONN_HANDLE_NONE) { ble_start_advertising(device, &MIDI_SVC_UUID); @@ -142,11 +146,6 @@ void ble_schedule_adv_restart(struct Device* device, uint64_t delay_us) { static int gap_event_handler(struct ble_gap_event* event, void* arg) { struct Device* device = (struct Device*)arg; BleCtx* ctx = ble_get_ctx(device); - if (ctx == nullptr) return 0; - - // Resolve HID device child context once for the whole handler. - BleHidDeviceCtx* hid_ctx = ctx->hid_device_child - ? (BleHidDeviceCtx*)device_get_driver_data(ctx->hid_device_child) : nullptr; switch (event->type) { case BLE_GAP_EVENT_CONNECT: @@ -154,7 +153,7 @@ static int gap_event_handler(struct ble_gap_event* event, void* arg) { LOG_I(TAG, "Connected (handle=%u hid_active=%d hid_conn=%u)", event->connect.conn_handle, (int)ctx->hid_active.load(), - (unsigned)(hid_ctx ? hid_ctx->hid_conn_handle.load() : BLE_HS_CONN_HANDLE_NONE)); + (unsigned)ble_hid_get_conn_handle(ctx->hid_device_child)); // Do NOT call ble_gap_security_initiate() here. // Windows BLE MIDI initiates encryption itself; calling here creates a race // with REPEAT_PAIRING+RETRY → two concurrent SM procedures → disconnect. @@ -172,10 +171,10 @@ static int gap_event_handler(struct ble_gap_event* event, void* arg) { uint16_t hdl = event->disconnect.conn.conn_handle; bool was_spp = (ctx->spp_conn_handle.load() == hdl); bool was_midi = (ctx->midi_conn_handle.load() == hdl); - bool was_hid = hid_ctx && (hid_ctx->hid_conn_handle.load() == hdl); + bool was_hid = (ble_hid_get_conn_handle(ctx->hid_device_child) == hdl); if (was_spp) ctx->spp_conn_handle.store(BLE_HS_CONN_HANDLE_NONE); if (was_midi) { ctx->midi_conn_handle.store(BLE_HS_CONN_HANDLE_NONE); ctx->midi_use_indicate.store(false); } - if (was_hid && hid_ctx) hid_ctx->hid_conn_handle.store(BLE_HS_CONN_HANDLE_NONE); + if (was_hid) ble_hid_set_conn_handle(ctx->hid_device_child, BLE_HS_CONN_HANDLE_NONE); ctx->link_encrypted.store(false); // If HID was stopped while connected, switch profile to None now that the // connection is gone. ble_gatts_mutable() is true here (no active connection, @@ -191,7 +190,7 @@ static int gap_event_handler(struct ble_gap_event* event, void* arg) { // and calling ble_gap_adv_start() from within the NimBLE host task while the // controller is shutting down would block the host task and hang nimble_port_stop(). if (ctx->radio_state.load() != BT_RADIO_STATE_OFF_PENDING) { - uint16_t hid_conn_now = hid_ctx ? hid_ctx->hid_conn_handle.load() : (uint16_t)BLE_HS_CONN_HANDLE_NONE; + uint16_t hid_conn_now = ble_hid_get_conn_handle(ctx->hid_device_child); if (ctx->midi_active.load() && ctx->midi_conn_handle.load() == BLE_HS_CONN_HANDLE_NONE) { ble_start_advertising(device, &MIDI_SVC_UUID); } else if (ctx->spp_active.load() && ctx->spp_conn_handle.load() == BLE_HS_CONN_HANDLE_NONE) { @@ -288,8 +287,8 @@ static int gap_event_handler(struct ble_gap_event* event, void* arg) { } LOG_I(TAG, "HID CCCD subscribed: %s (attr=%u conn=%u)", rpt_name, event->subscribe.attr_handle, event->subscribe.conn_handle); - if (hid_ctx && hid_ctx->hid_conn_handle.load() == BLE_HS_CONN_HANDLE_NONE) { - hid_ctx->hid_conn_handle.store(event->subscribe.conn_handle); + if (ble_hid_get_conn_handle(ctx->hid_device_child) == BLE_HS_CONN_HANDLE_NONE) { + ble_hid_set_conn_handle(ctx->hid_device_child, event->subscribe.conn_handle); } } break; @@ -323,9 +322,9 @@ static int gap_event_handler(struct ble_gap_event* event, void* arg) { // Windows only writes HID CCCDs in Phase 2; NimBLE may restore them from NVS // silently (no SUBSCRIBE event). Without this, hid_conn_handle stays NONE // and hid_device_is_connected() returns false for the entire Phase 2 session. - if (ctx->hid_active.load() && hid_ctx && - hid_ctx->hid_conn_handle.load() == BLE_HS_CONN_HANDLE_NONE) { - hid_ctx->hid_conn_handle.store(event->enc_change.conn_handle); + if (ctx->hid_active.load() && + ble_hid_get_conn_handle(ctx->hid_device_child) == BLE_HS_CONN_HANDLE_NONE) { + ble_hid_set_conn_handle(ctx->hid_device_child, event->enc_change.conn_handle); LOG_I(TAG, "HID conn handle set on ENC_CHANGE (conn=%u)", event->enc_change.conn_handle); } @@ -408,7 +407,7 @@ static void on_sync() { nus_tx_handle, midi_io_handle, hid_kb_input_handle, hid_consumer_input_handle, hid_mouse_input_handle, hid_gamepad_input_handle); - BleCtx* ctx = ble_get_ctx(s_device); + BleCtx* ctx = s_ctx; if (ctx == nullptr) return; ctx->pending_reset_count.store(0); @@ -423,18 +422,17 @@ static void on_sync() { // can retrieve child driver data (BleSppCtx / BleMidiCtx) without globals. ble_spp_init_gatt_handles(ctx->serial_child); ble_midi_init_gatt_handles(ctx->midi_child); - ble_hid_init_gatt_handles(); ctx->radio_state.store(BT_RADIO_STATE_ON); struct BtEvent e = {}; e.type = BT_EVENT_RADIO_STATE_CHANGED; e.radio_state = BT_RADIO_STATE_ON; - ble_publish_event(s_device, e); + ble_publish_event(ctx->device, e); // The Tactility bridge handles auto-start (SPP/MIDI) in response to // BT_EVENT_RADIO_STATE_CHANGED(ON). We just start name-only advertising // so the device is visible immediately. - ble_start_advertising(s_device, nullptr); + ble_start_advertising(ctx->device, nullptr); } static void dispatch_disable_timer_cb(void* arg) { @@ -444,7 +442,7 @@ static void dispatch_disable_timer_cb(void* arg) { static void on_reset(int reason) { LOG_W(TAG, "NimBLE host reset (reason=%d)", reason); - BleCtx* ctx = ble_get_ctx(s_device); + BleCtx* ctx = s_ctx; if (ctx == nullptr) return; if (ctx->radio_state.load() == BT_RADIO_STATE_ON_PENDING) { @@ -704,10 +702,7 @@ static void dispatch_disable(BleCtx* ctx) { active_hid_rpt_map_len = 0; // Clear hid_conn_handle in the child device driver data if still alive. - if (ctx->hid_device_child != nullptr) { - BleHidDeviceCtx* hid_ctx = (BleHidDeviceCtx*)device_get_driver_data(ctx->hid_device_child); - if (hid_ctx) hid_ctx->hid_conn_handle.store(BLE_HS_CONN_HANDLE_NONE); - } + ble_hid_set_conn_handle(ctx->hid_device_child, BLE_HS_CONN_HANDLE_NONE); if (ctx->midi_keepalive_timer != nullptr) { esp_timer_stop(ctx->midi_keepalive_timer); @@ -764,7 +759,7 @@ static error_t api_scan_start(struct Device* device) { return ERROR_INVALID_STATE; } - ble_scan_clear_results(); + ble_scan_clear_results(device); struct ble_gap_disc_params disc_params = {}; disc_params.passive = 0; @@ -944,88 +939,10 @@ const BluetoothApi nimble_bluetooth_api = { .fire_event = api_fire_event, }; -// ---- Child device lifecycle functions ---- -// Serial (SPP) and MIDI children create/destroy their driver data (BleSppCtx / BleMidiCtx) -// in their device start/stop lifecycle functions. The HID device child manages its own -// driver data (BleHidDeviceCtx) in the API hid_device_start/stop functions. - -static error_t esp32_ble_serial_start_device(struct Device* device) { - BleSppCtx* sctx = new BleSppCtx(); - sctx->rx_mutex = xSemaphoreCreateMutex(); - device_set_driver_data(device, sctx); - return ERROR_NONE; -} - -static error_t esp32_ble_serial_stop_device(struct Device* device) { - BleSppCtx* sctx = (BleSppCtx*)device_get_driver_data(device); - if (sctx != nullptr) { - vSemaphoreDelete(sctx->rx_mutex); - delete sctx; - device_set_driver_data(device, nullptr); - } - return ERROR_NONE; -} - -static error_t esp32_ble_midi_start_device(struct Device* device) { - BleMidiCtx* mctx = new BleMidiCtx(); - mctx->rx_mutex = xSemaphoreCreateMutex(); - device_set_driver_data(device, mctx); - return ERROR_NONE; -} - -static error_t esp32_ble_midi_stop_device(struct Device* device) { - BleMidiCtx* mctx = (BleMidiCtx*)device_get_driver_data(device); - if (mctx != nullptr) { - vSemaphoreDelete(mctx->rx_mutex); - delete mctx; - device_set_driver_data(device, nullptr); - } - return ERROR_NONE; -} - -static error_t esp32_ble_hid_device_stop_device(struct Device* device) { - // Safety cleanup: free any BleHidDeviceCtx that was not deleted by hid_device_stop() - // (e.g. if the BLE device is stopped while HID is still connected). - BleHidDeviceCtx* hid_ctx = (BleHidDeviceCtx*)device_get_driver_data(device); - delete hid_ctx; // safe even if nullptr - device_set_driver_data(device, nullptr); - return ERROR_NONE; -} - // ---- Driver definitions ---- - -static Driver esp32_ble_serial_driver = { - .name = "esp32-ble-serial", - .compatible = nullptr, - .start_device = esp32_ble_serial_start_device, - .stop_device = esp32_ble_serial_stop_device, - .api = &nimble_serial_api, - .device_type = &BLUETOOTH_SERIAL_TYPE, - .owner = nullptr, - .internal = nullptr, -}; - -static Driver esp32_ble_midi_driver = { - .name = "esp32-ble-midi", - .compatible = nullptr, - .start_device = esp32_ble_midi_start_device, - .stop_device = esp32_ble_midi_stop_device, - .api = &nimble_midi_api, - .device_type = &BLUETOOTH_MIDI_TYPE, - .owner = nullptr, - .internal = nullptr, -}; - -static Driver esp32_ble_hid_device_driver = { - .name = "esp32-ble-hid-device", - .compatible = nullptr, - .start_device = nullptr, - .stop_device = esp32_ble_hid_device_stop_device, - .api = &nimble_hid_device_api, - .device_type = &BLUETOOTH_HID_DEVICE_TYPE, - .owner = nullptr, - .internal = nullptr, -}; +// Child driver structs (esp32_ble_serial_driver, esp32_ble_midi_driver, +// esp32_ble_hid_device_driver) are defined in their respective sub-module files +// and declared extern in esp32_ble_internal.h. // ---- Driver lifecycle ---- @@ -1049,15 +966,6 @@ static void destroy_child_device(struct Device*& child) { } static error_t esp32_ble_start_device(struct Device* device) { - // Construct child drivers once (they are static; DriverInternal persists for process lifetime). - static bool child_drivers_constructed = false; - if (!child_drivers_constructed) { - driver_construct(&esp32_ble_serial_driver); - driver_construct(&esp32_ble_midi_driver); - driver_construct(&esp32_ble_hid_device_driver); - child_drivers_constructed = true; - } - BleCtx* ctx = new BleCtx(); ctx->radio_mutex = xSemaphoreCreateRecursiveMutex(); ctx->cb_mutex = xSemaphoreCreateMutex(); @@ -1085,6 +993,9 @@ static error_t esp32_ble_start_device(struct Device* device) { ctx->serial_child = nullptr; ctx->midi_child = nullptr; ctx->hid_device_child = nullptr; + ctx->scan_mutex = xSemaphoreCreateMutex(); + ctx->scan_count = 0; + memset(ctx->scan_results, 0, sizeof(ctx->scan_results)); // Create the disable timer used to dispatch dispatchDisable off the NimBLE host task. esp_timer_create_args_t disable_args = {}; @@ -1098,9 +1009,7 @@ static error_t esp32_ble_start_device(struct Device* device) { } device_set_driver_data(device, ctx); - s_device = device; - - ble_scan_init(); + s_ctx = ctx; // Create child devices for the serial, MIDI and HID device profiles. // device_start() on each child will invoke start_device (for serial/midi) @@ -1127,7 +1036,10 @@ static error_t esp32_ble_stop_device(struct Device* device) { dispatch_disable(ctx); } - ble_scan_deinit(); + if (ctx->scan_mutex != nullptr) { + vSemaphoreDelete(ctx->scan_mutex); + ctx->scan_mutex = nullptr; + } if (ctx->disable_timer != nullptr) { esp_timer_stop(ctx->disable_timer); @@ -1135,7 +1047,7 @@ static error_t esp32_ble_stop_device(struct Device* device) { ctx->disable_timer = nullptr; } - s_device = nullptr; + s_ctx = nullptr; device_set_driver_data(device, nullptr); delete ctx; return ERROR_NONE; @@ -1143,7 +1055,7 @@ static error_t esp32_ble_stop_device(struct Device* device) { // ---- Driver registration ---- -static const char* esp32_bt_compatible[] = { "esp32,ble-nimble", nullptr }; +static const char* esp32_bt_compatible[] = { "esp32,ble", nullptr }; Driver esp32_bluetooth_driver = { .name = "esp32-bluetooth", diff --git a/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_hid.cpp b/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_hid.cpp index f7ab42b0e..6e3d123e9 100644 --- a/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_hid.cpp +++ b/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_hid.cpp @@ -16,11 +16,20 @@ #define TAG "esp32_ble_hid" #include +#include #include #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wmissing-field-initializers" +// ---- HID device context (stored as driver data of the HID device child) ---- + +#include + +struct BleHidDeviceCtx { + std::atomic hid_conn_handle; +}; + // ---- Module globals ---- BleHidProfile current_hid_profile = BleHidProfile::None; @@ -301,13 +310,25 @@ static const struct ble_gatt_svc_def gatt_svcs_gamepad[] = { // ---- HID field accessor implementations ---- +static BleCtx* hid_root_ctx(struct Device* device) { + return ble_get_ctx(device); +} + bool ble_hid_get_active(struct Device* device) { - BleCtx* ctx = ble_get_ctx(device); - return ctx && ctx->hid_active.load(); + return hid_root_ctx(device)->hid_active.load(); } void ble_hid_set_active(struct Device* device, bool v) { - BleCtx* ctx = ble_get_ctx(device); - if (ctx) ctx->hid_active.store(v); + hid_root_ctx(device)->hid_active.store(v); +} +uint16_t ble_hid_get_conn_handle(struct Device* device) { + if (device == nullptr) return (uint16_t)BLE_HS_CONN_HANDLE_NONE; + BleHidDeviceCtx* hid_ctx = (BleHidDeviceCtx*)device_get_driver_data(device); + return hid_ctx ? hid_ctx->hid_conn_handle.load() : (uint16_t)BLE_HS_CONN_HANDLE_NONE; +} +void ble_hid_set_conn_handle(struct Device* device, uint16_t h) { + if (device == nullptr) return; + BleHidDeviceCtx* hid_ctx = (BleHidDeviceCtx*)device_get_driver_data(device); + if (hid_ctx) hid_ctx->hid_conn_handle.store(h); } // ---- GATT profile switch ---- @@ -387,15 +408,6 @@ void ble_hid_init_gatt() { } } -void ble_hid_init_gatt_handles() { - // val_handle pointers in char arrays are updated by NimBLE at registration time. - // No explicit action needed here; called for symmetry with spp/midi init. - (void)hid_kb_input_handle; - (void)hid_consumer_input_handle; - (void)hid_mouse_input_handle; - (void)hid_gamepad_input_handle; -} - // ---- HID Device sub-API implementations ---- // All functions receive the HID device child Device* and operate on BleHidDeviceCtx // stored as its driver data. @@ -520,7 +532,7 @@ static bool hid_device_is_connected(struct Device* device) { return hid_ctx != nullptr && hid_ctx->hid_conn_handle.load() != BLE_HS_CONN_HANDLE_NONE; } -const BtHidDeviceApi nimble_hid_device_api = { +extern const BtHidDeviceApi nimble_hid_device_api = { .start = hid_device_start, .stop = hid_device_stop, .send_key = hid_device_send_key, @@ -531,6 +543,28 @@ const BtHidDeviceApi nimble_hid_device_api = { .is_connected = hid_device_is_connected, }; +// ---- HID device child driver lifecycle ---- + +static error_t esp32_ble_hid_device_stop_device(struct Device* device) { + // Safety cleanup: free any BleHidDeviceCtx that was not deleted by hid_device_stop() + // (e.g. if the BLE device is stopped while HID is still connected). + BleHidDeviceCtx* hid_ctx = (BleHidDeviceCtx*)device_get_driver_data(device); + delete hid_ctx; // safe even if nullptr + device_set_driver_data(device, nullptr); + return ERROR_NONE; +} + +Driver esp32_ble_hid_device_driver = { + .name = "esp32-ble-hid-device", + .compatible = nullptr, + .start_device = nullptr, + .stop_device = esp32_ble_hid_device_stop_device, + .api = &nimble_hid_device_api, + .device_type = &BLUETOOTH_HID_DEVICE_TYPE, + .owner = nullptr, + .internal = nullptr, +}; + #pragma GCC diagnostic pop #endif // CONFIG_BT_NIMBLE_ENABLED diff --git a/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_midi.cpp b/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_midi.cpp index 91f07b970..01298bd0c 100644 --- a/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_midi.cpp +++ b/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_midi.cpp @@ -8,15 +8,27 @@ #include #include +#include +#include #include #include #include #define TAG "esp32_ble_midi" #include +#include +#include #include +#include #include +// ---- MIDI device context (stored as driver data of the MIDI child device) ---- + +struct BleMidiCtx { + SemaphoreHandle_t rx_mutex; + std::deque> rx_queue; +}; + #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wmissing-field-initializers" @@ -38,34 +50,31 @@ uint16_t midi_io_handle; // ---- MIDI field accessor implementations ---- +static BleCtx* midi_root_ctx(struct Device* device) { + return ble_get_ctx(device); +} + bool ble_midi_get_active(struct Device* device) { - BleCtx* ctx = ble_get_ctx(device); - return ctx && ctx->midi_active.load(); + return midi_root_ctx(device)->midi_active.load(); } void ble_midi_set_active(struct Device* device, bool v) { - BleCtx* ctx = ble_get_ctx(device); - if (ctx) ctx->midi_active.store(v); + midi_root_ctx(device)->midi_active.store(v); } uint16_t ble_midi_get_conn_handle(struct Device* device) { - BleCtx* ctx = ble_get_ctx(device); - return ctx ? ctx->midi_conn_handle.load() : (uint16_t)BLE_HS_CONN_HANDLE_NONE; + return midi_root_ctx(device)->midi_conn_handle.load(); } void ble_midi_set_conn_handle(struct Device* device, uint16_t h) { - BleCtx* ctx = ble_get_ctx(device); - if (ctx) ctx->midi_conn_handle.store(h); + midi_root_ctx(device)->midi_conn_handle.store(h); } bool ble_midi_get_use_indicate(struct Device* device) { - BleCtx* ctx = ble_get_ctx(device); - return ctx && ctx->midi_use_indicate.load(); + return midi_root_ctx(device)->midi_use_indicate.load(); } void ble_midi_set_use_indicate(struct Device* device, bool v) { - BleCtx* ctx = ble_get_ctx(device); - if (ctx) ctx->midi_use_indicate.store(v); + midi_root_ctx(device)->midi_use_indicate.store(v); } error_t ble_midi_ensure_keepalive(struct Device* device, esp_timer_cb_t cb, uint64_t period_us) { - BleCtx* ctx = ble_get_ctx(device); - if (!ctx) return ERROR_INVALID_STATE; + BleCtx* ctx = midi_root_ctx(device); if (ctx->midi_keepalive_timer == nullptr) { esp_timer_create_args_t args = {}; args.callback = cb; @@ -87,8 +96,8 @@ error_t ble_midi_ensure_keepalive(struct Device* device, esp_timer_cb_t cb, uint } void ble_midi_stop_keepalive(struct Device* device) { - BleCtx* ctx = ble_get_ctx(device); - if (ctx && ctx->midi_keepalive_timer != nullptr) { + BleCtx* ctx = midi_root_ctx(device); + if (ctx->midi_keepalive_timer != nullptr) { esp_timer_stop(ctx->midi_keepalive_timer); } } @@ -210,13 +219,43 @@ static bool midi_is_connected(struct Device* device) { return ble_midi_get_conn_handle(device) != BLE_HS_CONN_HANDLE_NONE; } -const BtMidiApi nimble_midi_api = { +extern const BtMidiApi nimble_midi_api = { .start = midi_start, .stop = midi_stop, .send = midi_send, .is_connected = midi_is_connected, }; +// ---- MIDI child driver lifecycle ---- + +static error_t esp32_ble_midi_start_device(struct Device* device) { + BleMidiCtx* mctx = new BleMidiCtx(); + mctx->rx_mutex = xSemaphoreCreateMutex(); + device_set_driver_data(device, mctx); + return ERROR_NONE; +} + +static error_t esp32_ble_midi_stop_device(struct Device* device) { + BleMidiCtx* mctx = (BleMidiCtx*)device_get_driver_data(device); + if (mctx != nullptr) { + vSemaphoreDelete(mctx->rx_mutex); + delete mctx; + device_set_driver_data(device, nullptr); + } + return ERROR_NONE; +} + +Driver esp32_ble_midi_driver = { + .name = "esp32-ble-midi", + .compatible = nullptr, + .start_device = esp32_ble_midi_start_device, + .stop_device = esp32_ble_midi_stop_device, + .api = &nimble_midi_api, + .device_type = &BLUETOOTH_MIDI_TYPE, + .owner = nullptr, + .internal = nullptr, +}; + #pragma GCC diagnostic pop #endif // CONFIG_BT_NIMBLE_ENABLED diff --git a/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_scan.cpp b/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_scan.cpp index be558975f..1abf4bbe8 100644 --- a/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_scan.cpp +++ b/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_scan.cpp @@ -15,50 +15,27 @@ #define TAG "esp32_ble_scan" #include -// ---- Module-static scan data ---- -// Scan results and their associated state are owned entirely by this module. -// esp32_ble.cpp clears them via ble_scan_clear_results() before each new scan. - -#define BLE_SCAN_MAX 64 - -static SemaphoreHandle_t s_scan_mutex = nullptr; -static BtPeerRecord s_scan_results[BLE_SCAN_MAX]; -static ble_addr_t s_scan_addrs[BLE_SCAN_MAX]; // full ble_addr_t (type+val) for connections -static size_t s_scan_count = 0; - -// Module-static device pointer used only by the name-resolution GAP/GATT callbacks -// whose void* arg is already occupied by the peer index (uintptr_t). +// ---- Module-static scan context ---- // Set at the start of ble_resolve_next_unnamed_peer; valid for the duration of // the sequential resolution chain (single-device, single-scan at a time). -static struct Device* s_scan_device = nullptr; - -// ---- Scan data lifecycle ---- +// Using BleCtx* (not Device*) so we avoid keeping a static Device reference. +static BleCtx* s_scan_ctx = nullptr; -void ble_scan_init() { - s_scan_mutex = xSemaphoreCreateMutex(); - s_scan_count = 0; - memset(s_scan_results, 0, sizeof(s_scan_results)); -} - -void ble_scan_deinit() { - if (s_scan_mutex != nullptr) { - vSemaphoreDelete(s_scan_mutex); - s_scan_mutex = nullptr; - } - s_scan_count = 0; -} +// ---- Scan data helpers ---- -void ble_scan_clear_results() { - xSemaphoreTake(s_scan_mutex, portMAX_DELAY); - s_scan_count = 0; - memset(s_scan_results, 0, sizeof(s_scan_results)); - xSemaphoreGive(s_scan_mutex); +void ble_scan_clear_results(struct Device* device) { + BleCtx* ctx = ble_get_ctx(device); + xSemaphoreTake(ctx->scan_mutex, portMAX_DELAY); + ctx->scan_count = 0; + memset(ctx->scan_results, 0, sizeof(ctx->scan_results)); + xSemaphoreGive(ctx->scan_mutex); } // ---- GAP scan callback ---- int ble_gap_disc_event_handler(struct ble_gap_event* event, void* arg) { struct Device* device = (struct Device*)arg; + BleCtx* ctx = ble_get_ctx(device); switch (event->type) { case BLE_GAP_EVENT_DISC: { @@ -81,25 +58,25 @@ int ble_gap_disc_event_handler(struct ble_gap_event* event, void* arg) { } { - xSemaphoreTake(s_scan_mutex, portMAX_DELAY); + xSemaphoreTake(ctx->scan_mutex, portMAX_DELAY); bool found = false; - for (size_t i = 0; i < s_scan_count; ++i) { - if (memcmp(s_scan_results[i].addr, record.addr, BT_ADDR_LEN) == 0) { + for (size_t i = 0; i < ctx->scan_count; ++i) { + if (memcmp(ctx->scan_results[i].addr, record.addr, BT_ADDR_LEN) == 0) { // Deduplicate: merge name from SCAN_RSP without clobbering ADV_IND name if (record.name[0] != '\0') { - memcpy(s_scan_results[i].name, record.name, BT_NAME_MAX + 1); + memcpy(ctx->scan_results[i].name, record.name, BT_NAME_MAX + 1); } - s_scan_results[i].rssi = record.rssi; + ctx->scan_results[i].rssi = record.rssi; found = true; break; } } - if (!found && s_scan_count < BLE_SCAN_MAX) { - s_scan_results[s_scan_count] = record; - s_scan_addrs[s_scan_count] = disc.addr; // full addr (type+val) - s_scan_count++; + if (!found && ctx->scan_count < 64) { + ctx->scan_results[ctx->scan_count] = record; + ctx->scan_addrs[ctx->scan_count] = disc.addr; // full addr (type+val) + ctx->scan_count++; } - xSemaphoreGive(s_scan_mutex); + xSemaphoreGive(ctx->scan_mutex); } struct BtEvent e = {}; @@ -133,7 +110,7 @@ int ble_gap_disc_event_handler(struct ble_gap_event* event, void* arg) { static int name_read_callback(uint16_t conn_handle, const struct ble_gatt_error* error, struct ble_gatt_attr* attr, void* arg) { - struct Device* device = s_scan_device; + BleCtx* ctx = s_scan_ctx; if (error->status == 0 && attr != nullptr) { size_t idx = (size_t)(uintptr_t)arg; @@ -142,19 +119,19 @@ static int name_read_callback(uint16_t conn_handle, const struct ble_gatt_error* char name_buf[BT_NAME_MAX + 1] = {}; os_mbuf_copydata(attr->om, 0, len, name_buf); { - xSemaphoreTake(s_scan_mutex, portMAX_DELAY); - if (idx < s_scan_count && s_scan_results[idx].name[0] == '\0') { - memcpy(s_scan_results[idx].name, name_buf, len); - s_scan_results[idx].name[len] = '\0'; + xSemaphoreTake(ctx->scan_mutex, portMAX_DELAY); + if (idx < ctx->scan_count && ctx->scan_results[idx].name[0] == '\0') { + memcpy(ctx->scan_results[idx].name, name_buf, len); + ctx->scan_results[idx].name[len] = '\0'; LOG_I(TAG, "Name resolved (idx=%u): %s", (unsigned)idx, name_buf); } - BtPeerRecord record = (idx < s_scan_count) ? s_scan_results[idx] : BtPeerRecord{}; - xSemaphoreGive(s_scan_mutex); + BtPeerRecord record = (idx < ctx->scan_count) ? ctx->scan_results[idx] : BtPeerRecord{}; + xSemaphoreGive(ctx->scan_mutex); struct BtEvent e = {}; e.type = BT_EVENT_PEER_FOUND; e.peer = record; - ble_publish_event(device, e); + ble_publish_event(ctx->device, e); } } return 0; // wait for BLE_HS_EDONE @@ -167,7 +144,7 @@ static int name_read_callback(uint16_t conn_handle, const struct ble_gatt_error* static int name_res_gap_callback(struct ble_gap_event* event, void* arg) { size_t idx = (size_t)(uintptr_t)arg; - struct Device* device = s_scan_device; + BleCtx* ctx = s_scan_ctx; switch (event->type) { case BLE_GAP_EVENT_CONNECT: @@ -184,13 +161,13 @@ static int name_res_gap_callback(struct ble_gap_event* event, void* arg) { } } else { LOG_I(TAG, "Name resolution: connect failed (idx=%u status=%d)", (unsigned)idx, event->connect.status); - ble_resolve_next_unnamed_peer(device, idx + 1); + ble_resolve_next_unnamed_peer(ctx->device, idx + 1); } break; case BLE_GAP_EVENT_DISCONNECT: LOG_I(TAG, "Name resolution: disconnected (idx=%u)", (unsigned)idx); - ble_resolve_next_unnamed_peer(device, idx + 1); + ble_resolve_next_unnamed_peer(ctx->device, idx + 1); break; default: @@ -200,7 +177,8 @@ static int name_res_gap_callback(struct ble_gap_event* event, void* arg) { } void ble_resolve_next_unnamed_peer(struct Device* device, size_t start_idx) { - s_scan_device = device; + BleCtx* ctx = ble_get_ctx(device); + s_scan_ctx = ctx; // Skip if a profile server or HID host connection attempt is active — // initiating a central connection simultaneously would fail (BLE_HS_EALREADY). @@ -219,16 +197,16 @@ void ble_resolve_next_unnamed_peer(struct Device* device, size_t start_idx) { ble_addr_t addr = {}; bool found = false; { - xSemaphoreTake(s_scan_mutex, portMAX_DELAY); - while (i < s_scan_count) { - if (s_scan_results[i].name[0] == '\0') { - addr = s_scan_addrs[i]; + xSemaphoreTake(ctx->scan_mutex, portMAX_DELAY); + while (i < ctx->scan_count) { + if (ctx->scan_results[i].name[0] == '\0') { + addr = ctx->scan_addrs[i]; found = true; break; } ++i; } - xSemaphoreGive(s_scan_mutex); + xSemaphoreGive(ctx->scan_mutex); } if (!found) { diff --git a/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_spp.cpp b/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_spp.cpp index 909a72e8e..ec5584994 100644 --- a/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_spp.cpp +++ b/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_spp.cpp @@ -8,14 +8,26 @@ #include #include +#include +#include #include #include #include #define TAG "esp32_ble_spp" +#include +#include #include +#include #include +// ---- SPP device context (stored as driver data of the serial child device) ---- + +struct BleSppCtx { + SemaphoreHandle_t rx_mutex; + std::deque> rx_queue; +}; + #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wmissing-field-initializers" @@ -93,21 +105,21 @@ void ble_spp_init_gatt_handles(struct Device* serial_child) { // ---- SPP field accessor implementations ---- +static BleCtx* spp_root_ctx(struct Device* device) { + return ble_get_ctx(device); +} + bool ble_spp_get_active(struct Device* device) { - BleCtx* ctx = ble_get_ctx(device); - return ctx && ctx->spp_active.load(); + return spp_root_ctx(device)->spp_active.load(); } void ble_spp_set_active(struct Device* device, bool v) { - BleCtx* ctx = ble_get_ctx(device); - if (ctx) ctx->spp_active.store(v); + spp_root_ctx(device)->spp_active.store(v); } uint16_t ble_spp_get_conn_handle(struct Device* device) { - BleCtx* ctx = ble_get_ctx(device); - return ctx ? ctx->spp_conn_handle.load() : (uint16_t)BLE_HS_CONN_HANDLE_NONE; + return spp_root_ctx(device)->spp_conn_handle.load(); } void ble_spp_set_conn_handle(struct Device* device, uint16_t h) { - BleCtx* ctx = ble_get_ctx(device); - if (ctx) ctx->spp_conn_handle.store(h); + spp_root_ctx(device)->spp_conn_handle.store(h); } // ---- SPP sub-API implementations ---- @@ -184,7 +196,7 @@ static bool spp_is_connected(struct Device* device) { return ble_spp_get_conn_handle(device) != BLE_HS_CONN_HANDLE_NONE; } -const BtSerialApi nimble_serial_api = { +extern const BtSerialApi nimble_serial_api = { .start = spp_start, .stop = spp_stop, .write = spp_write, @@ -192,6 +204,36 @@ const BtSerialApi nimble_serial_api = { .is_connected = spp_is_connected, }; +// ---- Serial child driver lifecycle ---- + +static error_t esp32_ble_serial_start_device(struct Device* device) { + BleSppCtx* sctx = new BleSppCtx(); + sctx->rx_mutex = xSemaphoreCreateMutex(); + device_set_driver_data(device, sctx); + return ERROR_NONE; +} + +static error_t esp32_ble_serial_stop_device(struct Device* device) { + BleSppCtx* sctx = (BleSppCtx*)device_get_driver_data(device); + if (sctx != nullptr) { + vSemaphoreDelete(sctx->rx_mutex); + delete sctx; + device_set_driver_data(device, nullptr); + } + return ERROR_NONE; +} + +Driver esp32_ble_serial_driver = { + .name = "esp32-ble-serial", + .compatible = nullptr, + .start_device = esp32_ble_serial_start_device, + .stop_device = esp32_ble_serial_stop_device, + .api = &nimble_serial_api, + .device_type = &BLUETOOTH_SERIAL_TYPE, + .owner = nullptr, + .internal = nullptr, +}; + #pragma GCC diagnostic pop #endif // CONFIG_BT_NIMBLE_ENABLED diff --git a/Platforms/platform-esp32/source/module.cpp b/Platforms/platform-esp32/source/module.cpp index 16235e757..2cd39e52a 100644 --- a/Platforms/platform-esp32/source/module.cpp +++ b/Platforms/platform-esp32/source/module.cpp @@ -21,6 +21,9 @@ extern Driver esp32_spi_driver; extern Driver esp32_uart_driver; #if defined(CONFIG_BT_NIMBLE_ENABLED) extern Driver esp32_bluetooth_driver; +extern Driver esp32_ble_serial_driver; +extern Driver esp32_ble_midi_driver; +extern Driver esp32_ble_hid_device_driver; #endif static error_t start() { @@ -36,6 +39,9 @@ static error_t start() { check(driver_construct_add(&esp32_uart_driver) == ERROR_NONE); #if defined(CONFIG_BT_NIMBLE_ENABLED) check(driver_construct_add(&esp32_bluetooth_driver) == ERROR_NONE); + check(driver_construct_add(&esp32_ble_serial_driver) == ERROR_NONE); + check(driver_construct_add(&esp32_ble_midi_driver) == ERROR_NONE); + check(driver_construct_add(&esp32_ble_hid_device_driver) == ERROR_NONE); #endif return ERROR_NONE; } @@ -44,6 +50,9 @@ static error_t stop() { /* We crash when destruct fails, because if a single driver fails to destruct, * there is no guarantee that the previously destroyed drivers can be recovered */ #if defined(CONFIG_BT_NIMBLE_ENABLED) + check(driver_remove_destruct(&esp32_ble_hid_device_driver) == ERROR_NONE); + check(driver_remove_destruct(&esp32_ble_midi_driver) == ERROR_NONE); + check(driver_remove_destruct(&esp32_ble_serial_driver) == ERROR_NONE); check(driver_remove_destruct(&esp32_bluetooth_driver) == ERROR_NONE); #endif check(driver_remove_destruct(&esp32_gpio_driver) == ERROR_NONE); diff --git a/Tactility/Include/Tactility/bluetooth/Bluetooth.h b/Tactility/Include/Tactility/bluetooth/Bluetooth.h index 630804fe3..d07476e86 100644 --- a/Tactility/Include/Tactility/bluetooth/Bluetooth.h +++ b/Tactility/Include/Tactility/bluetooth/Bluetooth.h @@ -89,35 +89,6 @@ void hidHostDisconnect(); /** @return true when a BLE HID peripheral is fully subscribed and acting as LVGL input device */ bool hidHostIsConnected(); -// ---- BLE HID Device ---- - -/** - * @brief Start advertising as a BLE HID device. - * @param[in] appearance BLE GAP Appearance value: - * 0x03C0=Generic HID, 0x03C1=Keyboard (default), 0x03C2=Mouse, 0x03C4=Gamepad - * @return true on success - */ -bool hidDeviceStart(uint16_t appearance = 0x03C1); - -/** @brief Stop the HID device server and close any active connection. */ -void hidDeviceStop(); - -// ---- BLE SPP (Nordic UART Service) ---- - -/** @brief Start advertising as a NUS (Nordic UART Service) server. @return true on success */ -bool sppStart(); - -/** @brief Stop the SPP server and close any active connection. */ -void sppStop(); - -// ---- BLE MIDI ---- - -/** @brief Start advertising as a BLE MIDI device. @return true on success */ -bool midiStart(); - -/** @brief Stop the MIDI server and close any active connection. */ -void midiStop(); - /** * @brief Initialize the Bluetooth bridge layer and optionally enable the radio. * Called once from Tactility startup (after kernel drivers are ready). diff --git a/Tactility/Source/bluetooth/Bluetooth.cpp b/Tactility/Source/bluetooth/Bluetooth.cpp index 97ebd7136..f95e4f893 100644 --- a/Tactility/Source/bluetooth/Bluetooth.cpp +++ b/Tactility/Source/bluetooth/Bluetooth.cpp @@ -15,6 +15,9 @@ #include #include #include +#include +#include +#include #include #include @@ -128,10 +131,14 @@ static void bt_event_bridge(struct Device* /*device*/, void* /*context*/, struct } } else if (settings::shouldSppAutoStart()) { LOGGER.info("Auto-starting SPP server"); - sppStart(); + if (struct Device* dev = bluetooth_serial_get_device()) { + bluetooth_serial_start(dev); + } } else if (settings::shouldMidiAutoStart()) { LOGGER.info("Auto-starting MIDI server"); - midiStart(); + if (struct Device* dev = bluetooth_midi_get_device()) { + bluetooth_midi_start(dev); + } } }); break; @@ -342,11 +349,19 @@ void connect(const std::array& addr, int profileId) { if (profileId == BT_PROFILE_HID_HOST) { hidHostConnect(addr); } else if (profileId == BT_PROFILE_HID_DEVICE) { - hidDeviceStart(); + if (struct Device* dev = bluetooth_hid_device_get_device()) { + bluetooth_hid_device_start(dev, BT_HID_DEVICE_MODE_KEYBOARD); + } } else if (profileId == BT_PROFILE_SPP) { - sppStart(); + if (struct Device* dev = bluetooth_serial_get_device()) { + bluetooth_serial_start(dev); + settings::setSppAutoStart(true); + } } else if (profileId == BT_PROFILE_MIDI) { - midiStart(); + if (struct Device* dev = bluetooth_midi_get_device()) { + bluetooth_midi_start(dev); + settings::setMidiAutoStart(true); + } } } @@ -355,7 +370,9 @@ void disconnect(const std::array& addr, int profileId) { if (profileId == BT_PROFILE_HID_HOST) { hidHostDisconnect(); } else if (profileId == BT_PROFILE_HID_DEVICE) { - hidDeviceStop(); + if (struct Device* dev = bluetooth_hid_device_get_device()) { + bluetooth_hid_device_stop(dev); + } } else { struct Device* dev = findFirstDevice(); if (dev == nullptr) return; diff --git a/Tactility/Source/bluetooth/BluetoothHidDevice.cpp b/Tactility/Source/bluetooth/BluetoothHidDevice.cpp index 3a373d5bc..252cc8ad5 100644 --- a/Tactility/Source/bluetooth/BluetoothHidDevice.cpp +++ b/Tactility/Source/bluetooth/BluetoothHidDevice.cpp @@ -1,34 +1,2 @@ -#ifdef ESP_PLATFORM -#include -#endif - -#if defined(CONFIG_BT_NIMBLE_ENABLED) - -#include - -#include - -namespace tt::bluetooth { - -bool hidDeviceStart(uint16_t appearance) { - struct Device* dev = bluetooth_hid_device_get_device(); - if (dev == nullptr) return false; - BtHidDeviceMode mode; - switch (appearance) { - case 0x03C2: mode = BT_HID_DEVICE_MODE_MOUSE; break; - case 0x03C4: mode = BT_HID_DEVICE_MODE_GAMEPAD; break; - case 0x03C0: mode = BT_HID_DEVICE_MODE_KEYBOARD_MOUSE; break; - default: mode = BT_HID_DEVICE_MODE_KEYBOARD; break; - } - return bluetooth_hid_device_start(dev, mode) == ERROR_NONE; -} - -void hidDeviceStop() { - struct Device* dev = bluetooth_hid_device_get_device(); - if (dev == nullptr) return; - bluetooth_hid_device_stop(dev); -} - -} // namespace tt::bluetooth - -#endif // CONFIG_BT_NIMBLE_ENABLED +// HID device start/stop are now called directly via bluetooth_hid_device_start/stop +// from TactilityKernel. This file is intentionally empty. diff --git a/Tactility/Source/bluetooth/BluetoothMidi.cpp b/Tactility/Source/bluetooth/BluetoothMidi.cpp index 1738653f0..20076d088 100644 --- a/Tactility/Source/bluetooth/BluetoothMidi.cpp +++ b/Tactility/Source/bluetooth/BluetoothMidi.cpp @@ -1,31 +1,3 @@ -#ifdef ESP_PLATFORM -#include -#endif - -#if defined(CONFIG_BT_NIMBLE_ENABLED) - -#include -#include - -#include - -namespace tt::bluetooth { - -bool midiStart() { - struct Device* dev = bluetooth_midi_get_device(); - if (dev == nullptr) return false; - if (bluetooth_midi_start(dev) != ERROR_NONE) return false; - settings::setMidiAutoStart(true); - return true; -} - -void midiStop() { - struct Device* dev = bluetooth_midi_get_device(); - if (dev == nullptr) return; - settings::setMidiAutoStart(false); - bluetooth_midi_stop(dev); -} - -} // namespace tt::bluetooth - -#endif // CONFIG_BT_NIMBLE_ENABLED +// MIDI start/stop are now called directly via bluetooth_midi_start/stop +// from TactilityKernel, with settings managed inline at call sites. +// This file is intentionally empty. diff --git a/Tactility/Source/bluetooth/BluetoothMock.cpp b/Tactility/Source/bluetooth/BluetoothMock.cpp index 7fa906004..f0a98fa49 100644 --- a/Tactility/Source/bluetooth/BluetoothMock.cpp +++ b/Tactility/Source/bluetooth/BluetoothMock.cpp @@ -37,15 +37,6 @@ void hidHostConnect(const std::array& /*addr*/) {} void hidHostDisconnect() {} bool hidHostIsConnected() { return false; } -bool hidDeviceStart(uint16_t /*appearance*/) { return false; } -void hidDeviceStop() {} - -bool sppStart() { return false; } -void sppStop() {} - -bool midiStart() { return false; } -void midiStop() {} - void systemStart() {} } // namespace tt::bluetooth diff --git a/Tactility/Source/bluetooth/BluetoothSpp.cpp b/Tactility/Source/bluetooth/BluetoothSpp.cpp index 10ac7a045..e8a179c4f 100644 --- a/Tactility/Source/bluetooth/BluetoothSpp.cpp +++ b/Tactility/Source/bluetooth/BluetoothSpp.cpp @@ -1,31 +1,3 @@ -#ifdef ESP_PLATFORM -#include -#endif - -#if defined(CONFIG_BT_NIMBLE_ENABLED) - -#include -#include - -#include - -namespace tt::bluetooth { - -bool sppStart() { - struct Device* dev = bluetooth_serial_get_device(); - if (dev == nullptr) return false; - if (bluetooth_serial_start(dev) != ERROR_NONE) return false; - settings::setSppAutoStart(true); - return true; -} - -void sppStop() { - struct Device* dev = bluetooth_serial_get_device(); - if (dev == nullptr) return; - settings::setSppAutoStart(false); - bluetooth_serial_stop(dev); -} - -} // namespace tt::bluetooth - -#endif // CONFIG_BT_NIMBLE_ENABLED +// SPP start/stop are now called directly via bluetooth_serial_start/stop +// from TactilityKernel, with settings managed inline at call sites. +// This file is intentionally empty.