diff --git a/firmware/src/ble.cpp b/firmware/src/ble.cpp index b9a4642..c1773e4 100644 --- a/firmware/src/ble.cpp +++ b/firmware/src/ble.cpp @@ -46,10 +46,10 @@ static const uint8_t HID_REPORT_MAP[] = { 0x95, 0x06, // Report Count (6) 0x75, 0x08, // Report Size (8) 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) 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) 0xC0, // End Collection }; diff --git a/firmware/src/ui.cpp b/firmware/src/ui.cpp index 39c9902..2d41cce 100644 --- a/firmware/src/ui.cpp +++ b/firmware/src/ui.cpp @@ -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 ======== void ui_init(void) { @@ -618,6 +699,7 @@ void ui_init(void) { init_menu_screen(scr); init_stub_screen(scr); init_bt_screen(scr); + init_soundpad_screen(scr); logo_img = lv_image_create(scr); lv_image_set_src(logo_img, &logo_dsc); @@ -768,17 +850,18 @@ static void update_nav_chrome(screen_t screen) { void ui_show_screen(screen_t screen) { lv_obj_add_flag(usage_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 (bt_container) lv_obj_add_flag(bt_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 (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(); switch (screen) { case SCREEN_SPLASH: splash_show(); 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_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_SOUNDPAD: case SCREEN_SESSION: case SCREEN_NOWPLAYING: case SCREEN_HOMEASSIST: