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/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..5d0bbd451 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"; + }; + gpio0 { compatible = "espressif,esp32-gpio"; gpio-count = <49>; diff --git a/Devices/m5stack-tab5/device.properties b/Devices/m5stack-tab5/device.properties index 541092dce..4c1ecf80f 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" diff --git a/Devices/m5stack-tab5/m5stack,tab5.dts b/Devices/m5stack-tab5/m5stack,tab5.dts index 93fc843dc..f51735798 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"; + }; + gpio0 { compatible = "espressif,esp32-gpio"; gpio-count = <57>; diff --git a/Firmware/CMakeLists.txt b/Firmware/CMakeLists.txt index 27d739be4..bd04a6312 100644 --- a/Firmware/CMakeLists.txt +++ b/Firmware/CMakeLists.txt @@ -18,6 +18,15 @@ 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") + list(APPEND REQUIRES_LIST bt) + endif() +endif() + # # DTS compiler python dependencies # 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/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), 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.yaml b/Platforms/platform-esp32/bindings/esp32,ble.yaml new file mode 100644 index 000000000..3663c7405 --- /dev/null +++ b/Platforms/platform-esp32/bindings/esp32,ble.yaml @@ -0,0 +1,8 @@ +description: ESP32 BLE driver (NimBLE) + +compatible: "esp32,ble" + +properties: + _unused: + type: int + default: 0 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/drivers/esp32_ble.h b/Platforms/platform-esp32/include/tactility/drivers/esp32_ble.h new file mode 100644 index 000000000..c38833e63 --- /dev/null +++ b/Platforms/platform-esp32/include/tactility/drivers/esp32_ble.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_hid.h b/Platforms/platform-esp32/private/bluetooth/esp32_ble_hid.h new file mode 100644 index 000000000..f908bf462 --- /dev/null +++ b/Platforms/platform-esp32/private/bluetooth/esp32_ble_hid.h @@ -0,0 +1,26 @@ +#pragma once + +#ifdef ESP_PLATFORM +#include +#endif + +#if defined(CONFIG_BT_NIMBLE_ENABLED) + +#include + +#include + +enum BleHidProfile { None, KbConsumer, Mouse, KbMouse, Gamepad }; + +struct Device; + +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_switch_profile(struct Device* hid_child, BleHidProfile profile); + +#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 new file mode 100644 index 000000000..5d7dc1a4f --- /dev/null +++ b/Platforms/platform-esp32/private/bluetooth/esp32_ble_internal.h @@ -0,0 +1,156 @@ +#pragma once + +#ifdef ESP_PLATFORM +#include +#endif + +#if defined(CONFIG_BT_NIMBLE_ENABLED) + +#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 + +// ---- Per-module headers (structs, accessors, sub-API externs) ---- + +#include +#include +#include + +#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]; + + // 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; + + // 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); + +bool ble_hid_get_host_active(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_clear_results(struct Device* device); + +// ---- Event publishing ---- +void ble_publish_event(struct Device* device, struct BtEvent event); + +// ---- Advertising helpers (defined in esp32_ble.cpp) ---- +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(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); +error_t ble_spp_start_internal(struct Device* serial_child); + +// ---- MIDI GATT (defined in esp32_ble_midi.cpp) ---- +// 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); + +// ---- 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. +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 +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.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.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..e8da75998 --- /dev/null +++ b/Platforms/platform-esp32/private/bluetooth/esp32_ble_midi.h @@ -0,0 +1,31 @@ +#pragma once + +#ifdef ESP_PLATFORM +#include +#endif + +#if defined(CONFIG_BT_NIMBLE_ENABLED) + +#include +#include + +#include +#include + +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); + + +#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..73149e18c --- /dev/null +++ b/Platforms/platform-esp32/private/bluetooth/esp32_ble_spp.h @@ -0,0 +1,20 @@ +#pragma once + +#ifdef ESP_PLATFORM +#include +#endif + +#if defined(CONFIG_BT_NIMBLE_ENABLED) + +#include +#include + +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); + + +#endif // CONFIG_BT_NIMBLE_ENABLED 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..7d0755adf --- /dev/null +++ b/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble.cpp @@ -0,0 +1,1071 @@ +#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); + +// 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 BleCtx* s_ctx = nullptr; + +// ---- Context accessor ---- +// 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 ---- +static void 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); + +// ---- General field accessor implementations ---- + +BtRadioState ble_get_radio_state(struct Device* device) { + return ble_get_ctx(device)->radio_state.load(); +} + +bool ble_hid_get_host_active(struct Device* device) { + return ble_get_ctx(device)->hid_host_active.load(); +} +bool ble_get_scan_active(struct Device* device) { + return ble_get_ctx(device)->scan_active.load(); +} +void ble_set_scan_active(struct Device* device, bool v) { + ble_get_ctx(device)->scan_active.store(v); +} + +// ---- 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; + 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) { + struct Device* device = (struct Device*)arg; + BleCtx* ctx = ble_get_ctx(device); + if (ctx->radio_state.load() != BT_RADIO_STATE_ON) return; + + 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); + } 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() && hid_conn == BLE_HS_CONN_HANDLE_NONE) { + ble_start_advertising_hid(device, hid_appearance); + } +} + +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(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); + 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) { + struct Device* device = (struct Device*)arg; + BleCtx* ctx = ble_get_ctx(device); + + 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)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. + // 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(device, 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 = (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) 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, + // advertising stopped by hid_device_stop), so switch_profile is safe. + // 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_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. + // 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) { + 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) { + ble_start_advertising(device, &NUS_SVC_UUID); + } else if (ctx->hid_active.load() && hid_conn_now == BLE_HS_CONN_HANDLE_NONE) { + ble_start_advertising_hid(device, 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(device, 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(device, 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 (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; + + 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() && + 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); + } + // 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(device, 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(device, 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 = s_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 — 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); + + 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->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(ctx->device, 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 = s_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 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(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. + 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) { + 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, device); + 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(struct Device* device, uint16_t appearance) { + ble_gap_adv_stop(); + + // Always sync the GAP name from ctx right before building the advertising packet. + 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); + 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) { + 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); + 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, device); + 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->device, 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->device, 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_init_gatt(); + + 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) + nimble_port_freertos_init(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->device, 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_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; + + // Clear hid_conn_handle in the child device driver data if still alive. + 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); + 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->device, 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; + } + + ble_scan_clear_results(device); + + 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, device); + 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(device, 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(device, 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(ctx->hid_device_child, BT_HID_DEVICE_MODE_KEYBOARD); + } else if (profile == BT_PROFILE_SPP) { + return ble_spp_start_internal(ctx->serial_child); + } else if (profile == BT_PROFILE_MIDI) { + return ble_midi_start_internal(ctx->midi_child); + } + // 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(ctx->hid_device_child); + } else if (profile == BT_PROFILE_SPP) { + return nimble_serial_api.stop(ctx->serial_child); + } else if (profile == BT_PROFILE_MIDI) { + return nimble_midi_api.stop(ctx->midi_child); + } + 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 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(device, 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); +} + +static void api_fire_event(struct Device* device, struct BtEvent event) { + BleCtx* ctx = (BleCtx*)device_get_driver_data(device); + if (ctx) ble_publish_event(device, 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_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, +}; + +// ---- Driver definitions ---- +// 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 ---- + +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) { + BleCtx* ctx = new BleCtx(); + ctx->radio_mutex = xSemaphoreCreateRecursiveMutex(); + 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->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_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; + // 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; + 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 = {}; + 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); + 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) + // 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); + + 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. + // 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); + + if (ctx->radio_state.load() != BT_RADIO_STATE_OFF) { + dispatch_disable(ctx); + } + + if (ctx->scan_mutex != nullptr) { + vSemaphoreDelete(ctx->scan_mutex); + ctx->scan_mutex = nullptr; + } + + if (ctx->disable_timer != nullptr) { + esp_timer_stop(ctx->disable_timer); + esp_timer_delete(ctx->disable_timer); + ctx->disable_timer = nullptr; + } + + s_ctx = nullptr; + device_set_driver_data(device, nullptr); + delete ctx; + return ERROR_NONE; +} + +// ---- Driver registration ---- + +static const char* esp32_bt_compatible[] = { "esp32,ble", 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.cpp b/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_hid.cpp new file mode 100644 index 000000000..6e3d123e9 --- /dev/null +++ b/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_hid.cpp @@ -0,0 +1,570 @@ +#ifdef ESP_PLATFORM +#include +#endif + +#if defined(CONFIG_BT_NIMBLE_ENABLED) + +#include + +#include +#include +#include +#include +#include + +#include + +#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; +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 } +}; + +// ---- 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) { + return hid_root_ctx(device)->hid_active.load(); +} +void ble_hid_set_active(struct Device* device, bool 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 ---- +// device must be the HID device child Device*. + +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); + + ble_gap_adv_stop(); + + 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(); + 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_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); + } + } +} + +// ---- 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) { + // 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; + 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_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_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; + 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 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_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) { + 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(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; +} + +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) { + 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(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) { + 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(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) { + 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(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) { + 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(hid_ctx->hid_conn_handle.load(), hid_gamepad_input_handle, buf, sizeof(buf)); +} + +static bool hid_device_is_connected(struct Device* device) { + BleHidDeviceCtx* hid_ctx = (BleHidDeviceCtx*)device_get_driver_data(device); + return hid_ctx != nullptr && hid_ctx->hid_conn_handle.load() != BLE_HS_CONN_HANDLE_NONE; +} + +extern 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, +}; + +// ---- 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 new file mode 100644 index 000000000..01298bd0c --- /dev/null +++ b/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_midi.cpp @@ -0,0 +1,261 @@ +#ifdef ESP_PLATFORM +#include +#endif + +#if defined(CONFIG_BT_NIMBLE_ENABLED) + +#include + +#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" + +// ---- 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; + +// ---- 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) { + return midi_root_ctx(device)->midi_active.load(); +} +void ble_midi_set_active(struct Device* device, bool v) { + midi_root_ctx(device)->midi_active.store(v); +} +uint16_t ble_midi_get_conn_handle(struct Device* device) { + return midi_root_ctx(device)->midi_conn_handle.load(); +} +void ble_midi_set_conn_handle(struct Device* device, uint16_t h) { + midi_root_ctx(device)->midi_conn_handle.store(h); +} +bool ble_midi_get_use_indicate(struct Device* device) { + return midi_root_ctx(device)->midi_use_indicate.load(); +} +void ble_midi_set_use_indicate(struct Device* device, bool 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 = midi_root_ctx(device); + 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 = midi_root_ctx(device); + if (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) { + 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; // 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(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; + ble_publish_event(device, e); + } + } + return 0; +} + +struct ble_gatt_chr_def midi_chars[] = { + { + .uuid = &MIDI_IO_UUID.u, + .access_cb = midi_chr_access, + .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* 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 = 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; // midi child 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_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); +} + +// ---- MIDI sub-API implementations ---- +// All functions receive the midi child Device*. + +static error_t midi_start(struct Device* device) { + 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_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; +} + +error_t ble_midi_start_internal(struct Device* midi_child) { + return midi_start(midi_child); +} + +static error_t midi_stop(struct Device* 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_midi_set_conn_handle(device, BLE_HS_CONN_HANDLE_NONE); + } + 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_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 }; + 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)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) { + 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) { + return ble_midi_get_conn_handle(device) != BLE_HS_CONN_HANDLE_NONE; +} + +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 new file mode 100644 index 000000000..1abf4bbe8 --- /dev/null +++ b/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_scan.cpp @@ -0,0 +1,236 @@ +#ifdef ESP_PLATFORM +#include +#endif + +#if defined(CONFIG_BT_NIMBLE_ENABLED) + +#include + +#include +#include + +#include +#include + +#define TAG "esp32_ble_scan" +#include + +// ---- 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). +// Using BleCtx* (not Device*) so we avoid keeping a static Device reference. +static BleCtx* s_scan_ctx = nullptr; + +// ---- Scan data helpers ---- + +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: { + 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->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) { + // 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 < 64) { + ctx->scan_results[ctx->scan_count] = record; + ctx->scan_addrs[ctx->scan_count] = disc.addr; // full addr (type+val) + ctx->scan_count++; + } + xSemaphoreGive(ctx->scan_mutex); + } + + struct BtEvent e = {}; + e.type = BT_EVENT_PEER_FOUND; + e.peer = record; + ble_publish_event(device, 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(device, 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 = s_scan_ctx; + + 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->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 < 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(ctx->device, 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 = s_scan_ctx; + + 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->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->device, idx + 1); + break; + + default: + break; + } + return 0; +} + +void ble_resolve_next_unnamed_peer(struct Device* device, size_t start_idx) { + 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). + 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_set_scan_active(device, false); + struct BtEvent e = {}; + e.type = BT_EVENT_SCAN_FINISHED; + ble_publish_event(device, e); + return; + } + + size_t i = start_idx; + while (true) { + ble_addr_t addr = {}; + bool found = false; + { + 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(ctx->scan_mutex); + } + + if (!found) { + LOG_I(TAG, "Name resolution: complete (%u devices)", (unsigned)i); + ble_set_scan_active(device, false); + struct BtEvent e = {}; + e.type = BT_EVENT_SCAN_FINISHED; + ble_publish_event(device, 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..ec5584994 --- /dev/null +++ b/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_spp.cpp @@ -0,0 +1,239 @@ +#ifdef ESP_PLATFORM +#include +#endif + +#if defined(CONFIG_BT_NIMBLE_ENABLED) + +#include + +#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" + +// ---- 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; + +// 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; // 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(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; + ble_publish_event(device, e); + } + } + return 0; +} + +struct ble_gatt_chr_def nus_chars_with_handle[] = { + { + .uuid = &NUS_RX_UUID.u, + .access_cb = nus_chr_access, + .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 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* 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 = serial_child; + nus_chars_with_handle[1].arg = 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) { + return spp_root_ctx(device)->spp_active.load(); +} +void ble_spp_set_active(struct Device* device, bool v) { + spp_root_ctx(device)->spp_active.store(v); +} +uint16_t ble_spp_get_conn_handle(struct Device* device) { + return spp_root_ctx(device)->spp_conn_handle.load(); +} +void ble_spp_set_conn_handle(struct Device* device, uint16_t h) { + spp_root_ctx(device)->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_spp_set_active(device, true); + ble_start_advertising(device, &NUS_SVC_UUID); + return ERROR_NONE; +} + +error_t ble_spp_start_internal(struct Device* serial_child) { + return spp_start(serial_child); +} + +static error_t spp_stop(struct Device* 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_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_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_spp_get_conn_handle(device); + if (conn == 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(conn, 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) { + 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(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 = sctx->rx_queue.front(); + size_t copy_len = std::min(front.size(), max_len); + memcpy(data, front.data(), copy_len); + 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) { + return ble_spp_get_conn_handle(device) != BLE_HS_CONN_HANDLE_NONE; +} + +extern const BtSerialApi nimble_serial_api = { + .start = spp_start, + .stop = spp_stop, + .write = spp_write, + .read = spp_read, + .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 84a73aa6c..2cd39e52a 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,12 @@ 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; +extern Driver esp32_ble_serial_driver; +extern Driver esp32_ble_midi_driver; +extern Driver esp32_ble_hid_device_driver; +#endif static error_t start() { /* We crash when construct fails, because if a single driver fails to construct, @@ -26,12 +37,24 @@ 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); + 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; } 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); 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..d07476e86 --- /dev/null +++ b/Tactility/Include/Tactility/bluetooth/Bluetooth.h @@ -0,0 +1,99 @@ +#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(); + +/** + * @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..96ee6ebd5 --- /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 hasFileForDevice(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..09ea2257c --- /dev/null +++ b/Tactility/Source/app/btmanage/BtManage.cpp @@ -0,0 +1,189 @@ +#include +#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) { + // 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); + getMainDispatcher().dispatch([self, event] { + 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..f95e4f893 --- /dev/null +++ b/Tactility/Source/bluetooth/Bluetooth.cpp @@ -0,0 +1,392 @@ +#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 + +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"); + if (struct Device* dev = bluetooth_serial_get_device()) { + bluetooth_serial_start(dev); + } + } else if (settings::shouldMidiAutoStart()) { + LOGGER.info("Auto-starting MIDI server"); + if (struct Device* dev = bluetooth_midi_get_device()) { + bluetooth_midi_start(dev); + } + } + }); + 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::hasFileForDevice(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) { + 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) { + if (struct Device* dev = bluetooth_serial_get_device()) { + bluetooth_serial_start(dev); + settings::setSppAutoStart(true); + } + } else if (profileId == BT_PROFILE_MIDI) { + if (struct Device* dev = bluetooth_midi_get_device()) { + bluetooth_midi_start(dev); + settings::setMidiAutoStart(true); + } + } +} + +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) { + if (struct Device* dev = bluetooth_hid_device_get_device()) { + bluetooth_hid_device_stop(dev); + } + } 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..252cc8ad5 --- /dev/null +++ b/Tactility/Source/bluetooth/BluetoothHidDevice.cpp @@ -0,0 +1,2 @@ +// 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/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..20076d088 --- /dev/null +++ b/Tactility/Source/bluetooth/BluetoothMidi.cpp @@ -0,0 +1,3 @@ +// 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 new file mode 100644 index 000000000..f0a98fa49 --- /dev/null +++ b/Tactility/Source/bluetooth/BluetoothMock.cpp @@ -0,0 +1,44 @@ +#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; } + +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..7cb4402f8 --- /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 hasFileForDevice(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..e8a179c4f --- /dev/null +++ b/Tactility/Source/bluetooth/BluetoothSpp.cpp @@ -0,0 +1,3 @@ +// 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. 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..bba8ed84f --- /dev/null +++ b/TactilityKernel/include/tactility/drivers/bluetooth.h @@ -0,0 +1,306 @@ +#pragma once + +#include +#include +#include + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +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 + +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); + + /** + * 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 + * 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_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); +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); +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); + +#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..518e3f8fb --- /dev/null +++ b/TactilityKernel/source/drivers/bluetooth.cpp @@ -0,0 +1,99 @@ +#include +#include +#include + +#define BT_API(device) ((const struct BluetoothApi*)device_get_driver(device)->api) + +extern "C" { + +// ---- Device lookup ---- + +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)) { + *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); +} + +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) { + 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..ce8b6750b 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,50 @@ 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_find_first_ready_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_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), + // 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..09a7bf576 100644 --- a/device.py +++ b/device.py @@ -307,6 +307,34 @@ 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): + 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") + 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") + # 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. + 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 +353,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)