Bluetooth (#518)

* Bluetooth LE addition

* fixes

* use the psram!

helps a little on S3 (t-deck)

* custom device name

* Update symbols.c

* Feedback + fixes

Fixes external app start/stop server (child devices)
Fixes BtManage causing a full system hang upon disabling bt when a device is connected to the host.

* updoot

* more updoot

* move back!

* Revert "move back!"

This reverts commit d3694365c634acc5db62ac59771c496cb971a727.

* fix some of the things

* Addressing feedback? hmm

* Fixes

Bug 1 — Reconnect loop / Reconnect not working fixed
Bug 2 — Name-only advertising overwrites HID advertising
Bug 3 — BleHidDeviceCtx leak on re-enable
Enhancement — HID device auto-start on radio re-enable

* stuff...

* update for consistency with others

* fix crashes

and some bonus symbols

* a few symbols, i2c speed, cdn message

100kHz i2c speed seems to be more compatible with m5stack modules...and probably in general.
cdn message no longer applies

* Hide BT Settings when bt not enabled

* Addressing things and device fixes

* Missed one!

* stuff
This commit is contained in:
Shadowtrance 2026-05-23 06:49:37 +10:00 committed by GitHub
parent 895e6bc50d
commit 5c78d55b04
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
72 changed files with 7155 additions and 352 deletions

View File

@ -0,0 +1 @@
enableOnBoot=false

View File

@ -29,3 +29,4 @@ CONFIG_ESP_HOSTED_ENABLED=y
CONFIG_ESP_HOSTED_P4_DEV_BOARD_FUNC_BOARD=y
CONFIG_ESP_HOSTED_SDIO_HOST_INTERFACE=y
CONFIG_SLAVE_IDF_TARGET_ESP32C6=y
CONFIG_ESP_HOSTED_USE_MEMPOOL=n

View File

@ -13,6 +13,7 @@ spiRamMode=OCT
spiRamSpeed=120M
tinyUsb=true
esptoolFlashFreq=120M
bluetooth=true
[display]
size=2.8"

View File

@ -4,6 +4,7 @@
#include <tactility/bindings/esp32_gpio.h>
#include <tactility/bindings/esp32_i2c.h>
#include <tactility/bindings/esp32_i2s.h>
#include <tactility/bindings/esp32_ble.h>
#include <tactility/bindings/esp32_spi.h>
#include <tactility/bindings/esp32_uart.h>
@ -12,6 +13,10 @@
compatible = "root";
model = "LilyGO T-Deck";
ble0 {
compatible = "espressif,esp32-ble";
};
gpio0 {
compatible = "espressif,esp32-gpio";
gpio-count = <49>;

View File

@ -27,7 +27,7 @@
bmi270 {
compatible = "bosch,bmi270";
reg = <0x68>;
reg = <0x69>;
};
};

View File

@ -28,7 +28,7 @@
bmi270 {
compatible = "bosch,bmi270";
reg = <0x68>;
reg = <0x69>;
};
bm8563 {

View File

@ -9,9 +9,10 @@ launcherAppId=Launcher
target=ESP32P4
flashSize=16MB
spiRam=true
spiRamMode=OCT
spiRamMode=HEX
spiRamSpeed=200M
esptoolFlashFreq=80M
bluetooth=true
[display]
size=5"
@ -23,9 +24,6 @@ colorDepth=16
fontSize=28
dpi=250
[cdn]
warningMessage=Only the original hardware variant with the ILI9881C display is supported for now
[sdkconfig]
CONFIG_WIFI_PROV_SCAN_MAX_ENTRIES=16
CONFIG_WIFI_PROV_AUTOSTOP_TIMEOUT=30
@ -41,3 +39,5 @@ CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_D1_4BIT_BUS_SLOT_1=10
CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_D2_4BIT_BUS_SLOT_1=9
CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_D3_4BIT_BUS_SLOT_1=8
CONFIG_ESP_HOSTED_SDIO_GPIO_RESET_SLAVE=15
# Fixes recent changes to esp_hosted
CONFIG_ESP_HOSTED_USE_MEMPOOL=n

View File

@ -5,6 +5,7 @@
#include <tactility/bindings/esp32_i2c.h>
#include <tactility/bindings/esp32_i2s.h>
#include <tactility/bindings/esp32_spi.h>
#include <tactility/bindings/esp32_ble.h>
#include <bindings/bmi270.h>
#include <bindings/pi4ioe5v6408.h>
#include <bindings/rx8130ce.h>
@ -13,6 +14,10 @@
compatible = "root";
model = "Tab5";
ble0 {
compatible = "espressif,esp32-ble";
};
gpio0 {
compatible = "espressif,esp32-gpio";
gpio-count = <57>;
@ -49,7 +54,7 @@
i2c_port_a: i2c1 {
compatible = "espressif,esp32-i2c";
port = <I2C_NUM_1>;
clock-frequency = <400000>;
clock-frequency = <100000>;
pin-sda = <&gpio0 53 GPIO_FLAG_NONE>;
pin-scl = <&gpio0 54 GPIO_FLAG_NONE>;
};

View File

@ -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
#

View File

@ -109,6 +109,7 @@ shared_symbol_code_point_names = [
"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 @@ statusbar_symbol_code_point_names = [
"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

View File

@ -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"

View File

@ -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"

View File

@ -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
}
};

View File

@ -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
}
};

View File

@ -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
}
};

View File

@ -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
}
};

View File

@ -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
}
};

View File

@ -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
}
};

View File

@ -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
}
};

View File

@ -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
}
};

View File

@ -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
}
};

View File

@ -359,6 +359,7 @@ const struct ModuleSymbol lvgl_module_symbols[] = {
DEFINE_MODULE_SYMBOL(lv_indev_active),
DEFINE_MODULE_SYMBOL(lv_indev_get_next),
DEFINE_MODULE_SYMBOL(lv_indev_set_group),
DEFINE_MODULE_SYMBOL(lv_indev_wait_release),
// lv_timer
DEFINE_MODULE_SYMBOL(lv_timer_handler),
DEFINE_MODULE_SYMBOL(lv_timer_handler_run_in_period),
@ -400,6 +401,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),
@ -456,6 +459,9 @@ const struct ModuleSymbol lvgl_module_symbols[] = {
// lv_draw_line
DEFINE_MODULE_SYMBOL(lv_draw_line),
DEFINE_MODULE_SYMBOL(lv_draw_line_dsc_init),
// lv_draw_rect
DEFINE_MODULE_SYMBOL(lv_draw_fill),
DEFINE_MODULE_SYMBOL(lv_draw_fill_dsc_init),
// lv_area
DEFINE_MODULE_SYMBOL(lv_area_get_width),
DEFINE_MODULE_SYMBOL(lv_area_get_height),

View File

@ -8,3 +8,5 @@ idf_component_register(
PRIV_INCLUDE_DIRS "private/"
REQUIRES TactilityKernel driver vfs fatfs
)
idf_component_optional_requires(PRIVATE bt)

View File

@ -0,0 +1,8 @@
description: ESP32 BLE driver (NimBLE)
compatible: "espressif,esp32-ble"
properties:
_unused:
type: int
default: 0

View File

@ -0,0 +1,15 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <tactility/bindings/bindings.h>
#include <tactility/drivers/esp32_ble.h>
#ifdef __cplusplus
extern "C" {
#endif
DEFINE_DEVICETREE(esp32_ble, struct Esp32BleConfig)
#ifdef __cplusplus
}
#endif

View File

@ -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 Esp32BleConfig {
int _unused; /**< Placeholder — driver reads all config from NimBLE Kconfig. */
};
#ifdef __cplusplus
}
#endif

View File

@ -0,0 +1,34 @@
#pragma once
#ifdef ESP_PLATFORM
#include <sdkconfig.h>
#endif
#if defined(CONFIG_BT_NIMBLE_ENABLED)
#include <tactility/drivers/bluetooth_hid_device.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
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();
bool ble_hid_switch_profile(struct Device* hid_child, BleHidProfile profile);
#ifdef __cplusplus
}
#endif
#endif // CONFIG_BT_NIMBLE_ENABLED

View File

@ -0,0 +1,170 @@
#pragma once
#ifdef ESP_PLATFORM
#include <sdkconfig.h>
#endif
#if defined(CONFIG_BT_NIMBLE_ENABLED)
#include <tactility/drivers/bluetooth.h>
#include <tactility/error.h>
// 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 <tactility/log.h>
#include <host/ble_gap.h>
#include <host/ble_gatt.h>
#include <host/ble_hs.h>
#include <host/ble_uuid.h>
#include <esp_timer.h>
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <atomic>
// ---- Per-module headers (structs, accessors, sub-API externs) ----
#include <bluetooth/esp32_ble_spp.h>
#include <bluetooth/esp32_ble_midi.h>
#include <bluetooth/esp32_ble_hid.h>
#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<BtRadioState> radio_state;
std::atomic<bool> scan_active;
// Set by Tactility HID host to prevent simultaneous central connection during name resolution
std::atomic<bool> 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<uint16_t> spp_conn_handle;
std::atomic<bool> spp_active;
std::atomic<uint16_t> midi_conn_handle;
std::atomic<bool> midi_active;
std::atomic<bool> midi_use_indicate; // true when client subscribed for INDICATE (e.g. Windows)
std::atomic<bool> hid_active;
std::atomic<bool> link_encrypted;
std::atomic<int> 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
// ---- HCI gate (ble_hci_gate.c) — P4/esp-hosted only ----
// Controls whether hci_rx_handler forwards packets to NimBLE.
// Set true after nimble_port_init(), false before nimble_port_stop().
#if defined(CONFIG_ESP_HOSTED_ENABLED)
#ifdef __cplusplus
extern "C" {
#endif
void ble_hci_gate_set_active(bool active);
bool ble_hci_gate_wait_idle(int max_ms);
#ifdef __cplusplus
}
#endif
#endif // CONFIG_ESP_HOSTED_ENABLED
#endif // CONFIG_BT_NIMBLE_ENABLED

View File

@ -0,0 +1,38 @@
#pragma once
#ifdef ESP_PLATFORM
#include <sdkconfig.h>
#endif
#if defined(CONFIG_BT_NIMBLE_ENABLED)
#include <tactility/drivers/bluetooth_midi.h>
#include <tactility/error.h>
#include <esp_timer.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
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);
#ifdef __cplusplus
}
#endif
#endif // CONFIG_BT_NIMBLE_ENABLED

View File

@ -0,0 +1,27 @@
#pragma once
#ifdef ESP_PLATFORM
#include <sdkconfig.h>
#endif
#if defined(CONFIG_BT_NIMBLE_ENABLED)
#include <tactility/drivers/bluetooth_serial.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
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);
#ifdef __cplusplus
}
#endif
#endif // CONFIG_BT_NIMBLE_ENABLED

View File

@ -0,0 +1,142 @@
// ble_hci_gate.c — Strong override of the weak hci_rx_handler from vhci_drv.c.
// Gates HCI packet delivery so packets are silently dropped when NimBLE is not
// initialised, preventing null-npl_funcs crashes during radio teardown (P4 only).
//
// On ESP32-P4, sdio_process_rx_task runs independently of the NimBLE host and can
// deliver an HCI packet after nimble_port_deinit() has zeroed npl_funcs.
// dispatch_disable() calls ble_hci_gate_set_active(false) + ble_hci_gate_wait_idle()
// before touching NimBLE, ensuring no in-flight or future hci_rx_handler call can
// dereference NimBLE internals.
#include <sdkconfig.h>
#if defined(CONFIG_BT_NIMBLE_ENABLED) && defined(CONFIG_ESP_HOSTED_ENABLED)
#include <stdbool.h>
#include <stdatomic.h>
#include <string.h>
#include <esp_log.h>
#include <esp_err.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
// NimBLE transport + HCI
#include <nimble/transport.h>
#include <nimble/hci_common.h>
#include <os/os_mbuf.h>
// H4 packet type indicators (same values used in vhci_drv.c)
#define HCI_H4_EVT 0x04
#define HCI_H4_ACL 0x02
// Local defines from vhci_drv.c (not exported by any header)
#define BLE_HCI_EVENT_HDR_LEN (2)
static const char* TAG = "ble_hci_gate";
// ---- Gate state (zero-init = gate closed until dispatch_enable opens it) ----
static atomic_bool s_hci_active = ATOMIC_VAR_INIT(false);
static atomic_int s_hci_refcount = ATOMIC_VAR_INIT(0);
void ble_hci_gate_set_active(bool active) {
atomic_store_explicit(&s_hci_active, active, memory_order_seq_cst);
}
// Spin-wait until all in-flight hci_rx_handler calls complete (max_ms timeout).
// Returns true if drained within the timeout.
bool ble_hci_gate_wait_idle(int max_ms) {
for (int elapsed = 0; elapsed < max_ms; elapsed++) {
if (atomic_load_explicit(&s_hci_refcount, memory_order_acquire) == 0)
return true;
vTaskDelay(pdMS_TO_TICKS(1));
}
return (atomic_load_explicit(&s_hci_refcount, memory_order_acquire) == 0);
}
// Strong override — replaces the H_WEAK_REF hci_rx_handler in vhci_drv.c.
// All four esp-hosted transport drivers (SDIO, SPI, SPI-HD, UART) call this symbol.
int hci_rx_handler(uint8_t *buf, size_t buf_len) {
// Fast path: gate closed — NimBLE not ready, drop silently
if (!atomic_load_explicit(&s_hci_active, memory_order_acquire))
return ESP_OK;
// Hold a reference so dispatch_disable() waits for us to finish
atomic_fetch_add_explicit(&s_hci_refcount, 1, memory_order_acquire);
// Double-check: gate may have closed between the first check and the increment
if (!atomic_load_explicit(&s_hci_active, memory_order_acquire)) {
atomic_fetch_sub_explicit(&s_hci_refcount, 1, memory_order_release);
return ESP_OK;
}
// ---- Original vhci_drv.c (NimBLE branch) logic ----
uint8_t *data = buf;
uint32_t len_total_read = buf_len;
int rc;
if (data[0] == HCI_H4_EVT) {
uint8_t *evbuf;
int totlen = BLE_HCI_EVENT_HDR_LEN + data[2];
if (totlen > UINT8_MAX + BLE_HCI_EVENT_HDR_LEN) {
ESP_LOGE(TAG, "Rx: len[%d] > max INT, drop", totlen);
atomic_fetch_sub_explicit(&s_hci_refcount, 1, memory_order_release);
return ESP_FAIL;
}
if (totlen > MYNEWT_VAL(BLE_TRANSPORT_EVT_SIZE)) {
ESP_LOGE(TAG, "Rx: len[%d] > max BLE, drop", totlen);
atomic_fetch_sub_explicit(&s_hci_refcount, 1, memory_order_release);
return ESP_FAIL;
}
if (data[1] == BLE_HCI_EVCODE_HW_ERROR) {
ESP_LOGE(TAG, "Rx: HW_ERROR");
atomic_fetch_sub_explicit(&s_hci_refcount, 1, memory_order_release);
return ESP_FAIL;
}
if ((data[1] == BLE_HCI_EVCODE_LE_META) &&
(data[3] == BLE_HCI_LE_SUBEV_ADV_RPT ||
data[3] == BLE_HCI_LE_SUBEV_EXT_ADV_RPT)) {
evbuf = ble_transport_alloc_evt(1);
if (!evbuf) {
ESP_LOGW(TAG, "Rx: Drop ADV Report: OOM (not fatal)");
atomic_fetch_sub_explicit(&s_hci_refcount, 1, memory_order_release);
return ESP_FAIL;
}
} else {
evbuf = ble_transport_alloc_evt(0);
if (!evbuf) {
ESP_LOGE(TAG, "Rx: transport_alloc_evt(0) failed");
atomic_fetch_sub_explicit(&s_hci_refcount, 1, memory_order_release);
return ESP_FAIL;
}
}
memset(evbuf, 0, sizeof *evbuf);
memcpy(evbuf, &data[1], totlen);
rc = ble_transport_to_hs_evt(evbuf);
if (rc) {
ESP_LOGE(TAG, "Rx: transport_to_hs_evt failed");
atomic_fetch_sub_explicit(&s_hci_refcount, 1, memory_order_release);
return ESP_FAIL;
}
} else if (data[0] == HCI_H4_ACL) {
struct os_mbuf *m = ble_transport_alloc_acl_from_ll();
if (!m) {
ESP_LOGE(TAG, "Rx: alloc_acl_from_ll failed");
atomic_fetch_sub_explicit(&s_hci_refcount, 1, memory_order_release);
return ESP_FAIL;
}
if ((rc = os_mbuf_append(m, &data[1], len_total_read - 1)) != 0) {
ESP_LOGE(TAG, "Rx: os_mbuf_append failed; rc=%d", rc);
os_mbuf_free_chain(m);
atomic_fetch_sub_explicit(&s_hci_refcount, 1, memory_order_release);
return ESP_FAIL;
}
ble_transport_to_hs_acl(m);
}
atomic_fetch_sub_explicit(&s_hci_refcount, 1, memory_order_release);
return ESP_OK;
}
#endif // CONFIG_BT_NIMBLE_ENABLED && CONFIG_ESP_HOSTED_ENABLED

View File

@ -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<BtEvent> and dispatches I/O.
end note
@enduml

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,589 @@
#ifdef ESP_PLATFORM
#include <sdkconfig.h>
#endif
#if defined(CONFIG_BT_NIMBLE_ENABLED)
#include <bluetooth/esp32_ble_internal.h>
#include <host/ble_gap.h>
#include <host/ble_gatt.h>
#include <host/ble_hs_mbuf.h>
#include <services/gap/ble_svc_gap.h>
#include <services/gatt/ble_svc_gatt.h>
#include <cstring>
constexpr auto* TAG = "esp32_ble_hid";
#include <tactility/device.h>
#include <tactility/driver.h>
#include <tactility/log.h>
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
// ---- HID device context (stored as driver data of the HID device child) ----
#include <atomic>
struct BleHidDeviceCtx {
std::atomic<uint16_t> 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*.
bool ble_hid_switch_profile(struct Device* device, BleHidProfile profile) {
if (profile == current_hid_profile) return true;
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 false; // don't update profile — GATT state is inconsistent
}
} else {
LOG_E(TAG, "switchGattProfile: gatts_count_cfg failed rc=%d", rc);
return false;
}
// 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 false;
}
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);
// Do NOT call ble_svc_gatt_changed(0, 0xFFFF) here.
// switchGattProfile() is always called while disconnected (ble_gatts_mutable()
// requires no active connection). ble_gatts_chr_updated() would persist a
// "value_changed=1" flag in NVS for every bonded-but-disconnected peer.
// When a bonded peer (e.g. Windows HID host) reconnects, NimBLE immediately
// sends a Service Changed indication; Windows disconnects to re-discover GATT
// without ACKing the indication; NimBLE re-sends on the next reconnect → loop.
// GATT handles are deterministic (same registration order each time), so
// bonded peers can reuse their cached handles and no indication is needed.
current_hid_profile = profile;
return true;
}
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) {
// Clean up any leftover context from a previous session (e.g. BLE was disabled
// while connected: dispatch_disable() skips the normal hid_device_stop cleanup).
BleHidDeviceCtx* old_ctx = (BleHidDeviceCtx*)device_get_driver_data(device);
delete old_ctx; // no-op if nullptr
// 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;
if (!ble_hid_switch_profile(device, profile)) {
delete (BleHidDeviceCtx*)device_get_driver_data(device);
device_set_driver_data(device, nullptr);
return ERROR_INVALID_STATE;
}
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) {
if (!ble_hid_switch_profile(device, BleHidProfile::None)) {
LOG_E(TAG, "hid_device_stop: switch to None failed — GATT state inconsistent");
}
}
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

View File

@ -0,0 +1,263 @@
#ifdef ESP_PLATFORM
#include <sdkconfig.h>
#endif
#if defined(CONFIG_BT_NIMBLE_ENABLED)
#include <bluetooth/esp32_ble_internal.h>
#include <algorithm>
#include <cstring>
#include <deque>
#include <vector>
#include <host/ble_gap.h>
#include <host/ble_gatt.h>
#include <host/ble_hs_mbuf.h>
constexpr auto* TAG = "esp32_ble_midi";
#include <esp_timer.h>
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <tactility/device.h>
#include <tactility/driver.h>
#include <tactility/log.h>
// ---- MIDI device context (stored as driver data of the MIDI child device) ----
struct BleMidiCtx {
SemaphoreHandle_t rx_mutex;
std::deque<std::vector<uint8_t>> 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;
}
}
// Stop first in case timer is already running
esp_timer_stop(ctx->midi_keepalive_timer);
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<uint8_t> 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

View File

@ -0,0 +1,236 @@
#ifdef ESP_PLATFORM
#include <sdkconfig.h>
#endif
#if defined(CONFIG_BT_NIMBLE_ENABLED)
#include <bluetooth/esp32_ble_internal.h>
#include <host/ble_gap.h>
#include <host/ble_hs_mbuf.h>
#include <algorithm>
#include <cstring>
constexpr auto* TAG = "esp32_ble_scan";
#include <tactility/log.h>
// ---- 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<size_t>(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

View File

@ -0,0 +1,239 @@
#ifdef ESP_PLATFORM
#include <sdkconfig.h>
#endif
#if defined(CONFIG_BT_NIMBLE_ENABLED)
#include <bluetooth/esp32_ble_internal.h>
#include <algorithm>
#include <cstring>
#include <deque>
#include <vector>
#include <host/ble_gap.h>
#include <host/ble_gatt.h>
#include <host/ble_hs_mbuf.h>
constexpr auto* TAG = "esp32_ble_spp";
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <tactility/device.h>
#include <tactility/driver.h>
#include <tactility/log.h>
// ---- SPP device context (stored as driver data of the serial child device) ----
struct BleSppCtx {
SemaphoreHandle_t rx_mutex;
std::deque<std::vector<uint8_t>> 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<uint8_t> 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

View File

@ -1,4 +1,9 @@
#ifdef ESP_PLATFORM
#include <sdkconfig.h>
#endif
#include <tactility/check.h>
#include <tactility/device.h>
#include <tactility/driver.h>
#include <tactility/module.h>
@ -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);

View File

@ -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 ()

View File

@ -0,0 +1,9 @@
#pragma once
#include <Tactility/app/App.h>
namespace tt::app::btmanage {
LaunchId start();
} // namespace tt::app::btmanage

View File

@ -0,0 +1,99 @@
#pragma once
#include <array>
#include <string>
#include <vector>
#include <cstdint>
struct Device;
namespace tt::bluetooth {
enum class RadioState {
Off,
OnPending,
On,
OffPending,
};
struct PeerRecord {
std::array<uint8_t, 6> 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<PeerRecord> getScanResults();
/** @return the list of currently paired peers */
std::vector<PeerRecord> 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<uint8_t, 6>& addr);
/**
* @brief Remove a previously paired peer.
* @param[in] addr the peer address
*/
void unpair(const std::array<uint8_t, 6>& 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<uint8_t, 6>& 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<uint8_t, 6>& 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<uint8_t, 6>& 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

View File

@ -0,0 +1,30 @@
#pragma once
#include <array>
#include <cstdint>
#include <string>
#include <vector>
namespace tt::bluetooth::settings {
struct PairedDevice {
std::string name;
std::array<uint8_t, 6> 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<uint8_t, 6>& 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<PairedDevice> loadAll();
} // namespace tt::bluetooth::settings

View File

@ -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

View File

@ -0,0 +1,24 @@
#pragma once
#include <array>
#include <cstdint>
namespace tt::app::btmanage {
typedef void (*OnBtToggled)(bool enable);
typedef void (*OnScanToggled)(bool enable);
typedef void (*OnConnectPeer)(const std::array<uint8_t, 6>& addr, int profileId);
typedef void (*OnDisconnectPeer)(const std::array<uint8_t, 6>& addr, int profileId);
typedef void (*OnPairPeer)(const std::array<uint8_t, 6>& addr);
typedef void (*OnForgetPeer)(const std::array<uint8_t, 6>& addr);
struct Bindings {
OnBtToggled onBtToggled;
OnScanToggled onScanToggled;
OnConnectPeer onConnectPeer;
OnDisconnectPeer onDisconnectPeer;
OnPairPeer onPairPeer;
OnForgetPeer onForgetPeer;
};
} // namespace tt::app::btmanage

View File

@ -0,0 +1,40 @@
#pragma once
#include "./View.h"
#include "./State.h"
#include <Tactility/app/App.h>
#include <Tactility/Mutex.h>
#include <Tactility/bluetooth/Bluetooth.h>
#include <tactility/drivers/bluetooth.h>
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

View File

@ -0,0 +1,41 @@
#pragma once
#include <Tactility/bluetooth/Bluetooth.h>
#include <Tactility/RecursiveMutex.h>
namespace tt::app::btmanage {
class State final {
mutable RecursiveMutex mutex;
bool scanning = false;
bluetooth::RadioState radioState = bluetooth::RadioState::Off;
std::vector<bluetooth::PeerRecord> scanResults;
std::vector<bluetooth::PeerRecord> 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<bluetooth::PeerRecord> getScanResults() const {
auto lock = mutex.asScopedLock();
lock.lock();
return scanResults;
}
std::vector<bluetooth::PeerRecord> getPairedPeers() const {
auto lock = mutex.asScopedLock();
lock.lock();
return pairedPeers;
}
};
} // namespace tt::app::btmanage

View File

@ -0,0 +1,41 @@
#pragma once
#include "./Bindings.h"
#include "./State.h"
#include <Tactility/app/AppContext.h>
#include <Tactility/app/AppPaths.h>
#include <lvgl.h>
namespace tt::app::btmanage {
class View final {
Bindings* bindings;
State* state;
std::unique_ptr<AppPaths> 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

View File

@ -0,0 +1,9 @@
#pragma once
#include <string>
namespace tt::app::btpeersettings {
void start(const std::string& addrHex);
} // namespace tt::app::btpeersettings

View File

@ -0,0 +1,29 @@
#pragma once
#include <Tactility/bluetooth/Bluetooth.h>
#include <tactility/drivers/bluetooth.h>
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<uint8_t, 6>& addr_out);
} // namespace tt::bluetooth

View File

@ -38,6 +38,8 @@
#include <Tactility/InitEsp.h>
#endif
#include <Tactility/bluetooth/Bluetooth.h>
namespace tt {
static auto LOGGER = Logger("Tactility");
@ -106,6 +108,8 @@ namespace app {
namespace touchcalibration { 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; }
@ -191,6 +195,11 @@ static void registerInternalApps() {
if (hal::hasDevice(hal::Device::Type::Power)) {
addAppManifest(app::power::manifest);
}
#if defined(CONFIG_BT_ENABLED) && CONFIG_BT_ENABLED
addAppManifest(app::btmanage::manifest);
addAppManifest(app::btpeersettings::manifest);
#endif
}
static void registerInstalledApp(std::string path) {
@ -335,6 +344,7 @@ void run(const Configuration& config, Module* dtsModules[], DtsDevice dtsDevices
settings::initTimeZone();
hal::init(*config.hardware);
network::ntp::init();
bluetooth::systemStart();
registerAndStartPrimaryServices();

View File

@ -0,0 +1,189 @@
#include <Tactility/app/btmanage/BtManagePrivate.h>
#include <Tactility/app/btmanage/View.h>
#include <Tactility/app/AppContext.h>
#include <Tactility/app/AppManifest.h>
#include <Tactility/Logger.h>
#include <Tactility/LogMessages.h>
#include <Tactility/lvgl/LvglSync.h>
#include <Tactility/Tactility.h>
#include <tactility/lvgl_icon_shared.h>
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<uint8_t, 6>& addr, int profileId) {
bluetooth::connect(addr, profileId);
}
static void onDisconnectPeer(const std::array<uint8_t, 6>& addr, int profileId) {
bluetooth::disconnect(addr, profileId);
}
static void onPairPeer(const std::array<uint8_t, 6>& 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<uint8_t, 6>& 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<BtManage*>(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<BtManage>
};
LaunchId start() {
return app::start(manifest.appId);
}
} // namespace tt::app::btmanage

View File

@ -0,0 +1,44 @@
#include <Tactility/app/btmanage/BtManagePrivate.h>
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

View File

@ -0,0 +1,246 @@
#include <format>
#include <string>
#include <tactility/lvgl_module.h>
#include <Tactility/app/btmanage/View.h>
#include <Tactility/app/btmanage/BtManagePrivate.h>
#include <Tactility/app/btpeersettings/BtPeerSettings.h>
#include <Tactility/Logger.h>
#include <Tactility/lvgl/Style.h>
#include <Tactility/lvgl/Toolbar.h>
#include <Tactility/bluetooth/Bluetooth.h>
#include <Tactility/bluetooth/BluetoothSettings.h>
#include <Tactility/bluetooth/BluetoothPairedDevice.h>
#include <Tactility/Tactility.h>
namespace tt::app::btmanage {
static const auto LOGGER = Logger("BtManageView");
static void onEnableSwitchChanged(lv_event_t* event) {
auto* enable_switch = static_cast<lv_obj_t*>(lv_event_get_target(event));
bool is_on = lv_obj_has_state(enable_switch, LV_STATE_CHECKED);
auto bt = std::static_pointer_cast<BtManage>(getCurrentApp());
bt->getBindings().onBtToggled(is_on);
}
static void onEnableOnBootSwitchChanged(lv_event_t* event) {
auto* enable_switch = static_cast<lv_obj_t*>(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_obj_t*>(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<BtManage>(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<PeerListItemData*>(lv_event_get_user_data(event));
auto bt = std::static_pointer_cast<BtManage>(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<uint8_t>((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<PeerListItemData*>(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

View File

@ -0,0 +1,227 @@
#include <Tactility/app/btpeersettings/BtPeerSettings.h>
#include <Tactility/Logger.h>
#include <Tactility/LogMessages.h>
#include <Tactility/app/App.h>
#include <Tactility/app/AppContext.h>
#include <Tactility/app/AppManifest.h>
#include <Tactility/app/alertdialog/AlertDialog.h>
#include <Tactility/lvgl/LvglSync.h>
#include <Tactility/lvgl/Style.h>
#include <Tactility/lvgl/Toolbar.h>
#include <Tactility/bluetooth/Bluetooth.h>
#include <Tactility/bluetooth/BluetoothPairedDevice.h>
#include <tactility/check.h>
#include <tactility/drivers/bluetooth.h>
#include <lvgl.h>
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>();
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<uint8_t, 6> 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<BtPeerSettings*>(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<BtPeerSettings*>(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<std::string> choices = { "Yes", "No" };
alertdialog::start("Confirmation", "Forget this device?", choices);
}
static void onToggleAutoConnect(lv_event_t* event) {
auto* self = static_cast<BtPeerSettings*>(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<BtPeerSettings*>(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> 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<BtPeerSettings>
};
} // namespace tt::app::btpeersettings

View File

@ -0,0 +1,401 @@
#ifdef ESP_PLATFORM
#include <sdkconfig.h>
#endif
#if defined(CONFIG_BT_NIMBLE_ENABLED)
#include <Tactility/bluetooth/Bluetooth.h>
#include <Tactility/bluetooth/BluetoothPairedDevice.h>
#include <Tactility/bluetooth/BluetoothSettings.h>
#include <Tactility/bluetooth/BluetoothPrivate.h>
#include <Tactility/Logger.h>
#include <Tactility/Mutex.h>
#include <Tactility/Tactility.h>
#include <tactility/check.h>
#include <tactility/device.h>
#include <tactility/drivers/bluetooth.h>
#include <tactility/drivers/bluetooth_hid_device.h>
#include <tactility/drivers/bluetooth_midi.h>
#include <tactility/drivers/bluetooth_serial.h>
#include <array>
#include <cstring>
#include <vector>
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<PeerRecord> scan_results_cache;
struct CachedAddr {
uint8_t addr[6];
uint8_t addr_type;
};
static std::vector<CachedAddr> 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<struct Device**>(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_host_auto = false;
bool has_hid_device_auto = false;
for (const auto& p : peers) {
if (!p.autoConnect) continue;
if (p.profileId == BT_PROFILE_HID_HOST) has_hid_host_auto = true;
if (p.profileId == BT_PROFILE_HID_DEVICE) has_hid_device_auto = true;
}
if (has_hid_host_auto) {
LOGGER.info("HID host auto-connect peer found — starting scan");
if (struct Device* dev = findFirstDevice()) {
bluetooth_scan_start(dev);
}
} else if (has_hid_device_auto) {
LOGGER.info("HID device auto-start (bonded peer found)");
if (struct Device* dev = bluetooth_hid_device_get_device()) {
bluetooth_hid_device_start(dev, BT_HID_DEVICE_MODE_KEYBOARD);
}
} else {
if (settings::shouldSppAutoStart()) {
LOGGER.info("Auto-starting SPP server");
if (struct Device* dev = bluetooth_serial_get_device()) {
bluetooth_serial_start(dev);
}
}
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<uint8_t, 6> 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<uint8_t, 6> 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<uint8_t, 6> 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);
}
});
// TODO: Fix auto reconnect if user manually disconnects
} 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<PeerRecord> getScanResults() {
auto lock = scan_cache_mutex.asScopedLock();
lock.lock();
return scan_results_cache;
}
std::vector<PeerRecord> getPairedPeers() {
auto stored = settings::loadAll();
std::vector<PeerRecord> result;
result.reserve(stored.size());
std::array<uint8_t, 6> 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<uint8_t, 6>& /*addr*/) {
// Pairing is handled automatically during connection by NimBLE SM.
}
void unpair(const std::array<uint8_t, 6>& addr) {
struct Device* dev = findFirstDevice();
if (dev != nullptr) {
bluetooth_unpair(dev, addr.data());
}
settings::remove(settings::addrToHex(addr));
}
void connect(const std::array<uint8_t, 6>& 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<uint8_t, 6>& 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

View File

@ -0,0 +1,885 @@
#ifdef ESP_PLATFORM
#include <sdkconfig.h>
#endif
#if defined(CONFIG_BT_NIMBLE_ENABLED)
#include <Tactility/bluetooth/Bluetooth.h>
#include <Tactility/bluetooth/BluetoothPairedDevice.h>
#include <Tactility/bluetooth/BluetoothPrivate.h>
#include <Tactility/Assets.h>
#include <Tactility/Logger.h>
#include <Tactility/Tactility.h>
#include <Tactility/lvgl/Keyboard.h>
#include <Tactility/lvgl/LvglSync.h>
#include <host/ble_gap.h>
#include <host/ble_gatt.h>
#include <host/ble_hs.h>
#include <host/ble_uuid.h>
#include <esp_timer.h>
#include <freertos/FreeRTOS.h>
#include <freertos/queue.h>
#include <lvgl.h>
#include <algorithm>
#include <array>
#include <atomic>
#include <cstring>
#include <memory>
#include <vector>
#define TAG "BtHidHost"
#include <esp_log.h>
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<HidHostInputRpt> inputRpts;
std::vector<uint16_t> allChrDefHandles;
int subscribeIdx = 0;
int dscDiscIdx = 0;
int rptRefReadIdx = 0;
uint16_t rptMapHandle = 0;
std::vector<uint8_t> 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<uint8_t, 6> peerAddr = {};
};
// ---- Globals ----
static std::unique_ptr<HidHostCtx> 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<int32_t> hid_host_mouse_x{0};
static std::atomic<int32_t> hid_host_mouse_y{0};
static std::atomic<bool> hid_host_mouse_btn{false};
static std::atomic<bool> 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<uint32_t>('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<uint32_t>(snums[i]) : static_cast<uint32_t>(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<Entry> typeMap;
std::vector<HidReportType> 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,
&notify_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<uint8_t, 6>& 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<HidHostCtx>();
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<uint8_t, 6>& 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

View File

@ -0,0 +1,44 @@
#ifdef ESP_PLATFORM
#include <sdkconfig.h>
#endif
#if !defined(CONFIG_BT_NIMBLE_ENABLED)
#include <Tactility/bluetooth/Bluetooth.h>
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<PeerRecord> getScanResults() { return {}; }
std::vector<PeerRecord> getPairedPeers() { return {}; }
void pair(const std::array<uint8_t, 6>& /*addr*/) {}
void unpair(const std::array<uint8_t, 6>& /*addr*/) {}
void connect(const std::array<uint8_t, 6>& /*addr*/, int /*profileId*/) {}
void disconnect(const std::array<uint8_t, 6>& /*addr*/, int /*profileId*/) {}
bool isProfileSupported(int /*profileId*/) { return false; }
void hidHostConnect(const std::array<uint8_t, 6>& /*addr*/) {}
void hidHostDisconnect() {}
bool hidHostIsConnected() { return false; }
void systemStart() {}
} // namespace tt::bluetooth
#endif // !CONFIG_BT_NIMBLE_ENABLED

View File

@ -0,0 +1,118 @@
#include <Tactility/bluetooth/BluetoothPairedDevice.h>
#include <Tactility/file/File.h>
#include <Tactility/file/PropertiesFile.h>
#include <Tactility/Logger.h>
#include <dirent.h>
#include <format>
#include <iomanip>
#include <sstream>
#include <string>
#include <cstdio>
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<uint8_t, 6>& addr) {
std::stringstream stream;
stream << std::hex;
for (int i = 0; i < 6; ++i) {
stream << std::setw(2) << std::setfill('0') << static_cast<int>(addr[i]);
}
return stream.str();
}
static bool hexToAddr(const std::string& hex, std::array<uint8_t, 6>& 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<uint8_t>(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<std::string, std::string> 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)) {
// TODO: Handle incorrect parsing input
device.profileId = std::stoi(map[KEY_PROFILE_ID]);
}
return true;
}
bool save(const PairedDevice& device) {
const auto addr_hex = addrToHex(device.addr);
std::map<std::string, std::string> 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<PairedDevice> loadAll() {
std::vector<dirent> 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<PairedDevice> 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

View File

@ -0,0 +1,103 @@
#include <Tactility/bluetooth/BluetoothSettings.h>
#include <Tactility/file/File.h>
#include <Tactility/file/PropertiesFile.h>
#include <Tactility/Logger.h>
#include <Tactility/Mutex.h>
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<std::string, std::string> 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<std::string, std::string> 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

View File

@ -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);
}
}
}

View File

@ -11,6 +11,10 @@
#include <Tactility/service/ServiceContext.h>
#include <Tactility/service/ServicePaths.h>
#include <Tactility/service/ServiceRegistration.h>
#include <Tactility/bluetooth/Bluetooth.h>
#include <tactility/drivers/bluetooth.h>
#include <tactility/drivers/bluetooth_serial.h>
#include <tactility/drivers/bluetooth_midi.h>
#include <Tactility/service/gps/GpsService.h>
#include <Tactility/service/wifi/Wifi.h>
#include <tactility/check.h>
@ -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<Timer> 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 @@ public:
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 @@ public:
~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);
}

View File

@ -447,6 +447,8 @@ 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),
ESP_ELFSYM_EXPORT(esp_timer_get_time),
// delimiter
ESP_ELFSYM_END
};

View File

@ -0,0 +1,307 @@
#pragma once
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <tactility/error.h>
#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_get_paired_peers(struct Device* device, struct BtPeerRecord* out, size_t* count);
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

View File

@ -0,0 +1,124 @@
#pragma once
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <tactility/error.h>
#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

View File

@ -0,0 +1,65 @@
#pragma once
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <tactility/error.h>
#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

View File

@ -0,0 +1,77 @@
#pragma once
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <tactility/error.h>
#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

View File

@ -0,0 +1,103 @@
#include <tactility/drivers/bluetooth.h>
#include <tactility/device.h>
#include <tactility/driver.h>
#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<struct Device**>(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);
}
error_t bluetooth_get_paired_peers(struct Device* device, struct BtPeerRecord* out, size_t* count) {
return BT_API(device)->get_paired_peers(device, out, count);
}
// ---- 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"

View File

@ -0,0 +1,57 @@
#include <tactility/drivers/bluetooth_hid_device.h>
#include <tactility/device.h>
#include <tactility/driver.h>
#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<struct Device**>(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"

View File

@ -0,0 +1,41 @@
#include <tactility/drivers/bluetooth_midi.h>
#include <tactility/device.h>
#include <tactility/driver.h>
#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<struct Device**>(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"

View File

@ -0,0 +1,45 @@
#include <tactility/drivers/bluetooth_serial.h>
#include <tactility/device.h>
#include <tactility/driver.h>
#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<struct Device**>(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"

View File

@ -4,6 +4,10 @@
#include <tactility/concurrent/timer.h>
#include <tactility/device.h>
#include <tactility/driver.h>
#include <tactility/drivers/bluetooth.h>
#include <tactility/drivers/bluetooth_serial.h>
#include <tactility/drivers/bluetooth_midi.h>
#include <tactility/drivers/bluetooth_hid_device.h>
#include <tactility/drivers/gpio_controller.h>
#include <tactility/drivers/i2c_controller.h>
#include <tactility/drivers/i2s_controller.h>
@ -50,6 +54,7 @@ const struct ModuleSymbol KERNEL_SYMBOLS[] = {
DEFINE_MODULE_SYMBOL(device_for_each_child),
DEFINE_MODULE_SYMBOL(device_for_each_of_type),
DEFINE_MODULE_SYMBOL(device_exists_of_type),
DEFINE_MODULE_SYMBOL(device_find_by_name),
// driver
DEFINE_MODULE_SYMBOL(driver_construct),
DEFINE_MODULE_SYMBOL(driver_destruct),
@ -117,6 +122,51 @@ 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_get_paired_peers),
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),

View File

@ -307,6 +307,39 @@ 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("CONFIG_BT_NIMBLE_TRANSPORT_UART=n\n")
output_file.write("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")
# Persist bond/CCCD data to NVS so that bonds survive BLE disable+re-enable
# cycles (nimble_port_deinit clears RAM-only state). Without this, bonded peers
# (e.g. Windows HID host) lose their LTK match on re-enable and enter a
# rapid connect/disconnect/re-pair loop.
output_file.write("CONFIG_BT_NIMBLE_NVS_PERSIST=y\n")
def write_custom_sdkconfig(output_file, device_properties: ConfigParser):
if "sdkconfig" in device_properties.sections():
output_file.write("# Custom\n")
@ -325,6 +358,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)