v2 Phase 3: on-screen soundpad sending HID F13..F18

Add a Soundpad app screen: a 2x3 grid of colored pads that each emit one of
F13..F18 over the existing BLE HID keyboard, for mapping to a host soundboard.
Key release is deferred via a one-shot LVGL timer so the UI thread never blocks.
Raise the HID report-map keycode ceiling from 101 (0x65) to 115 (0x73) in
ble.cpp so F13..F24 (0x68..0x73) are within the declared key-array range — they
were silently undeliverable before. Wire SCREEN_SOUNDPAD to the real screen
(was a stub).

NOTE: the HID report descriptor changed, so the host must re-pair to refresh its
cached map before F13..F18 are recognized.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
wenil
2026-06-20 14:49:15 +03:00
co-authored by Claude Opus 4.8
parent d5d80e81c0
commit abe72eca0a
2 changed files with 89 additions and 6 deletions
+2 -2
View File
@@ -46,10 +46,10 @@ static const uint8_t HID_REPORT_MAP[] = {
0x95, 0x06, // Report Count (6) 0x95, 0x06, // Report Count (6)
0x75, 0x08, // Report Size (8) 0x75, 0x08, // Report Size (8)
0x15, 0x00, // Logical Minimum (0) 0x15, 0x00, // Logical Minimum (0)
0x25, 0x65, // Logical Maximum (101) 0x25, 0x73, // Logical Maximum (115) - raised from 101 to cover F13..F24 (0x68..0x73)
0x05, 0x07, // Usage Page (Key Codes) 0x05, 0x07, // Usage Page (Key Codes)
0x19, 0x00, // Usage Minimum (0) 0x19, 0x00, // Usage Minimum (0)
0x29, 0x65, // Usage Maximum (101) 0x29, 0x73, // Usage Maximum (115) - the v2 soundpad sends F13..F18 (0x68..0x6D)
0x81, 0x00, // Input (Data, Array) - Key array (6 keys) 0x81, 0x00, // Input (Data, Array) - Key array (6 keys)
0xC0, // End Collection 0xC0, // End Collection
}; };
+84 -1
View File
@@ -595,6 +595,87 @@ static void stub_show_for(screen_t s) {
} }
} }
// ---- Soundpad: 6 pads → HID F13..F18 (the host maps them to a soundboard) ----
// F13..F24 are HID usage IDs 0x68..0x73; the report-map keycode ceiling was
// raised to 0x73 in ble.cpp so these transmit.
struct PadDef { uint8_t keycode; const char* label; uint32_t color; };
static const PadDef PADS[6] = {
{ 0x68, "F13", 0xD85A30 }, // coral
{ 0x69, "F14", 0xBA7517 }, // amber
{ 0x6A, "F15", 0x639922 }, // green
{ 0x6B, "F16", 0x1D9E75 }, // teal
{ 0x6C, "F17", 0x534AB7 }, // purple
{ 0x6D, "F18", 0xD4537E }, // pink
};
static lv_obj_t* soundpad_container;
static void release_timer_cb(lv_timer_t* t) {
(void)t;
ble_keyboard_release();
}
static void pad_click_cb(lv_event_t* e) {
uint8_t key = (uint8_t)(intptr_t)lv_event_get_user_data(e);
ble_keyboard_press(key, 0);
// Release shortly after so the host sees a discrete keypress, without
// blocking the UI thread. The one-shot timer auto-deletes once it fires.
lv_timer_t* t = lv_timer_create(release_timer_cb, 40, NULL);
lv_timer_set_repeat_count(t, 1);
}
static lv_obj_t* make_pad(lv_obj_t* parent, const PadDef* pad, int w, int h) {
lv_obj_t* btn = lv_obj_create(parent);
lv_obj_set_size(btn, w, h);
lv_obj_set_style_bg_color(btn, lv_color_hex(pad->color), 0);
lv_obj_set_style_bg_opa(btn, LV_OPA_COVER, 0);
lv_obj_set_style_radius(btn, 16, 0);
lv_obj_set_style_border_width(btn, 0, 0);
lv_obj_set_style_border_width(btn, 3, LV_STATE_PRESSED); // white ring lights on press
lv_obj_set_style_border_color(btn, lv_color_white(), LV_STATE_PRESSED);
lv_obj_clear_flag(btn, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_add_flag(btn, LV_OBJ_FLAG_CLICKABLE);
lv_obj_add_event_cb(btn, pad_click_cb, LV_EVENT_CLICKED, (void*)(intptr_t)pad->keycode);
lv_obj_t* lbl = lv_label_create(btn);
lv_label_set_text(lbl, pad->label);
lv_obj_set_style_text_font(lbl, &font_styrene_28, 0);
lv_obj_set_style_text_color(lbl, lv_color_white(), 0);
lv_obj_center(lbl);
return btn;
}
static void init_soundpad_screen(lv_obj_t* scr) {
soundpad_container = lv_obj_create(scr);
lv_obj_set_size(soundpad_container, L.scr_w, L.scr_h);
lv_obj_set_pos(soundpad_container, 0, 0);
lv_obj_set_style_bg_opa(soundpad_container, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(soundpad_container, 0, 0);
lv_obj_set_style_pad_all(soundpad_container, 0, 0);
lv_obj_clear_flag(soundpad_container, LV_OBJ_FLAG_SCROLLABLE);
make_screen_title(soundpad_container, "Soundpad");
lv_obj_t* grid = lv_obj_create(soundpad_container);
int avail_h = L.scr_h - L.content_y - L.margin;
lv_obj_set_size(grid, L.content_w, avail_h);
lv_obj_set_pos(grid, L.margin, L.content_y);
lv_obj_set_style_bg_opa(grid, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(grid, 0, 0);
lv_obj_set_style_pad_all(grid, 0, 0);
lv_obj_set_style_pad_row(grid, 12, 0);
lv_obj_set_style_pad_column(grid, 12, 0);
lv_obj_set_flex_flow(grid, LV_FLEX_FLOW_ROW_WRAP);
lv_obj_set_flex_align(grid, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_START);
lv_obj_clear_flag(grid, LV_OBJ_FLAG_SCROLLABLE);
int pad_w = (L.content_w - 16) / 2; // 2 columns, 4px slack
int pad_h = (avail_h - 2 * 12) / 3 - 1; // 3 rows
for (int i = 0; i < 6; i++)
make_pad(grid, &PADS[i], pad_w, pad_h);
lv_obj_add_flag(soundpad_container, LV_OBJ_FLAG_HIDDEN);
}
// ======== Public API ======== // ======== Public API ========
void ui_init(void) { void ui_init(void) {
@@ -618,6 +699,7 @@ void ui_init(void) {
init_menu_screen(scr); init_menu_screen(scr);
init_stub_screen(scr); init_stub_screen(scr);
init_bt_screen(scr); init_bt_screen(scr);
init_soundpad_screen(scr);
logo_img = lv_image_create(scr); logo_img = lv_image_create(scr);
lv_image_set_src(logo_img, &logo_dsc); lv_image_set_src(logo_img, &logo_dsc);
@@ -771,14 +853,15 @@ void ui_show_screen(screen_t screen) {
if (menu_container) lv_obj_add_flag(menu_container, LV_OBJ_FLAG_HIDDEN); if (menu_container) lv_obj_add_flag(menu_container, LV_OBJ_FLAG_HIDDEN);
if (stub_container) lv_obj_add_flag(stub_container, LV_OBJ_FLAG_HIDDEN); if (stub_container) lv_obj_add_flag(stub_container, LV_OBJ_FLAG_HIDDEN);
if (bt_container) lv_obj_add_flag(bt_container, LV_OBJ_FLAG_HIDDEN); if (bt_container) lv_obj_add_flag(bt_container, LV_OBJ_FLAG_HIDDEN);
if (soundpad_container) lv_obj_add_flag(soundpad_container, LV_OBJ_FLAG_HIDDEN);
splash_hide(); splash_hide();
switch (screen) { switch (screen) {
case SCREEN_SPLASH: splash_show(); break; case SCREEN_SPLASH: splash_show(); break;
case SCREEN_USAGE: lv_obj_clear_flag(usage_container, LV_OBJ_FLAG_HIDDEN); break; case SCREEN_USAGE: lv_obj_clear_flag(usage_container, LV_OBJ_FLAG_HIDDEN); break;
case SCREEN_MENU: lv_obj_clear_flag(menu_container, LV_OBJ_FLAG_HIDDEN); break; case SCREEN_MENU: lv_obj_clear_flag(menu_container, LV_OBJ_FLAG_HIDDEN); break;
case SCREEN_SOUNDPAD: lv_obj_clear_flag(soundpad_container, LV_OBJ_FLAG_HIDDEN); break;
case SCREEN_BLUETOOTH: bt_refresh(); lv_obj_clear_flag(bt_container, LV_OBJ_FLAG_HIDDEN); break; case SCREEN_BLUETOOTH: bt_refresh(); lv_obj_clear_flag(bt_container, LV_OBJ_FLAG_HIDDEN); break;
case SCREEN_SOUNDPAD:
case SCREEN_SESSION: case SCREEN_SESSION:
case SCREEN_NOWPLAYING: case SCREEN_NOWPLAYING:
case SCREEN_HOMEASSIST: case SCREEN_HOMEASSIST: