#include "ble_hid.h" #include "board_config.h" #if BOARD_BLE_ENABLED #include "esp_hidd.h" #include "esp_hid_common.h" #include "esp_hid_gap.h" #include "esp_log.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include #include static const char *TAG = "ble_hid"; #define HID_KEYBOARD_REPORT_ID 1 #define HID_CONSUMER_REPORT_ID 2 #define HID_KEYBOARD_REPORT_LEN 8 #define HID_CONSUMER_REPORT_LEN 2 /* Bit positions for ESP-IDF mediaReportMap2 (Windows-compatible). */ #define HID_CC_RPT_VOLUME_BITS 0x3F #define HID_CC_RPT_BUTTON_BITS 0xF0 #define HID_CC_RPT_VOLUME_UP 0x40 #define HID_CC_RPT_VOLUME_DOWN 0x80 #define HID_CC_RPT_MUTE 1 #define HID_CC_RPT_PLAY 5 #define HID_CC_RPT_SCAN_NEXT_TRK 10 #define HID_CC_RPT_SCAN_PREV_TRK 11 /* * Keyboard (Report ID 1) + Consumer Control (Report ID 2, mediaReportMap2). * Windows 11 expects the bit-packed consumer report from ESP-IDF examples, * not a raw 16-bit HID usage in the input report. */ static const unsigned char s_hid_report_map[] = { /* Keyboard - Report ID 1 */ 0x05, 0x01, // Usage Page (Generic Desktop) 0x09, 0x06, // Usage (Keyboard) 0xA1, 0x01, // Collection (Application) 0x85, HID_KEYBOARD_REPORT_ID, 0x05, 0x07, // Usage Page (Keyboard/Keypad) 0x19, 0xE0, // Usage Minimum (0xE0) 0x29, 0xE7, // Usage Maximum (0xE7) 0x15, 0x00, // Logical Minimum (0) 0x25, 0x01, // Logical Maximum (1) 0x75, 0x01, // Report Size (1) 0x95, 0x08, // Report Count (8) 0x81, 0x02, // Input (Data, Variable, Absolute) 0x95, 0x01, // Report Count (1) 0x75, 0x08, // Report Size (8) 0x81, 0x03, // Input (Constant, Variable, Absolute) 0x95, 0x05, // Report Count (5) 0x75, 0x01, // Report Size (1) 0x05, 0x08, // Usage Page (LEDs) 0x19, 0x01, // Usage Minimum (0x01) 0x29, 0x05, // Usage Maximum (0x05) 0x91, 0x02, // Output (Data, Variable, Absolute) 0x95, 0x01, // Report Count (1) 0x75, 0x03, // Report Size (3) 0x91, 0x03, // Output (Constant, Variable, Absolute) 0x95, 0x05, // Report Count (5) 0x75, 0x08, // Report Size (8) 0x15, 0x00, // Logical Minimum (0) 0x25, 0x65, // Logical Maximum (101) 0x05, 0x07, // Usage Page (Keyboard/Keypad) 0x19, 0x00, // Usage Minimum (0x00) 0x29, 0x65, // Usage Maximum (0x65) 0x81, 0x00, // Input (Data, Array, Absolute) 0xC0, // End Collection /* Consumer Control - Report ID 2 (mediaReportMap2 from ESP-IDF) */ 0x05, 0x0C, // Usage Page (Consumer) 0x09, 0x01, // Usage (Consumer Control) 0xA1, 0x01, // Collection (Application) 0x85, HID_CONSUMER_REPORT_ID, 0x09, 0x02, // Usage (Numeric Key Pad) 0xA1, 0x02, // Collection (Logical) 0x05, 0x09, // Usage Page (Button) 0x19, 0x01, // Usage Minimum (0x01) 0x29, 0x0A, // Usage Maximum (0x0A) 0x15, 0x01, // Logical Minimum (1) 0x25, 0x0A, // Logical Maximum (10) 0x75, 0x04, // Report Size (4) 0x95, 0x01, // Report Count (1) 0x81, 0x00, // Input (Data, Array, Absolute) 0xC0, // End Collection 0x05, 0x0C, // Usage Page (Consumer) 0x09, 0x86, // Usage (Channel) 0x15, 0xFF, // Logical Minimum (-1) 0x25, 0x01, // Logical Maximum (1) 0x75, 0x02, // Report Size (2) 0x95, 0x01, // Report Count (1) 0x81, 0x46, // Input (Data, Variable, Relative) 0x09, 0xE9, // Usage (Volume Increment) 0x09, 0xEA, // Usage (Volume Decrement) 0x15, 0x00, // Logical Minimum (0) 0x75, 0x01, // Report Size (1) 0x95, 0x02, // Report Count (2) 0x81, 0x02, // Input (Data, Variable, Absolute) 0x09, 0xE2, // Usage (Mute) 0x09, 0x30, // Usage (Power) 0x09, 0x83, // Usage (Recall Last) 0x09, 0x81, // Usage (Assign Selection) 0x09, 0xB0, // Usage (Play) 0x09, 0xB1, // Usage (Pause) 0x09, 0xB2, // Usage (Record) 0x09, 0xB3, // Usage (Fast Forward) 0x09, 0xB4, // Usage (Rewind) 0x09, 0xB5, // Usage (Scan Next Track) 0x09, 0xB6, // Usage (Scan Previous Track) 0x09, 0xB7, // Usage (Stop) 0x15, 0x01, // Logical Minimum (1) 0x25, 0x0C, // Logical Maximum (12) 0x75, 0x04, // Report Size (4) 0x95, 0x01, // Report Count (1) 0x81, 0x00, // Input (Data, Array, Absolute) 0x09, 0x80, // Usage (Selection) 0xA1, 0x02, // Collection (Logical) 0x05, 0x09, // Usage Page (Button) 0x19, 0x01, // Usage Minimum (0x01) 0x29, 0x03, // Usage Maximum (0x03) 0x15, 0x01, // Logical Minimum (1) 0x25, 0x03, // Logical Maximum (3) 0x75, 0x02, // Report Size (2) 0x81, 0x00, // Input (Data, Array, Absolute) 0xC0, // End Collection 0x81, 0x03, // Input (Constant, Variable, Absolute) 0xC0, // End Collection }; static esp_hid_raw_report_map_t s_report_maps[] = { { .data = s_hid_report_map, .len = sizeof(s_hid_report_map), }, }; static esp_hid_device_config_t s_hid_config = { .vendor_id = 0x16C0, .product_id = 0x05DF, .version = 0x0100, .device_name = CONFIG_ESP_BLE_DEVICE_NAME, .manufacturer_name = "HeavenlyHeroStar", .serial_number = "1234567890", .report_maps = s_report_maps, .report_maps_len = 1, }; static esp_hidd_dev_t *s_hid_dev; static ble_hid_status_cb_t s_status_cb; static bool s_connected; static void ble_hid_cc_set_volume_up(uint8_t *buffer) { buffer[0] &= HID_CC_RPT_VOLUME_BITS; buffer[0] |= HID_CC_RPT_VOLUME_UP; } static void ble_hid_cc_set_volume_down(uint8_t *buffer) { buffer[0] &= HID_CC_RPT_VOLUME_BITS; buffer[0] |= HID_CC_RPT_VOLUME_DOWN; } static void ble_hid_cc_set_button(uint8_t *buffer, uint8_t button) { buffer[1] &= HID_CC_RPT_BUTTON_BITS; buffer[1] |= button; } static bool ble_hid_encode_consumer_report(uint16_t usage, uint8_t buffer[HID_CONSUMER_REPORT_LEN]) { memset(buffer, 0, HID_CONSUMER_REPORT_LEN); switch (usage) { case HID_CONSUMER_VOLUME_UP: ble_hid_cc_set_volume_up(buffer); return true; case HID_CONSUMER_VOLUME_DOWN: ble_hid_cc_set_volume_down(buffer); return true; case HID_CONSUMER_MUTE: ble_hid_cc_set_button(buffer, HID_CC_RPT_MUTE); return true; case HID_CONSUMER_PLAY_PAUSE: ble_hid_cc_set_button(buffer, HID_CC_RPT_PLAY); return true; case HID_CONSUMER_SCAN_NEXT: ble_hid_cc_set_button(buffer, HID_CC_RPT_SCAN_NEXT_TRK); return true; case HID_CONSUMER_SCAN_PREVIOUS: ble_hid_cc_set_button(buffer, HID_CC_RPT_SCAN_PREV_TRK); return true; default: ESP_LOGW(TAG, "unsupported consumer usage 0x%04X", usage); return false; } } static void ble_hid_log_buffer(const char *label, const uint8_t *data, size_t len) { char hex[40]; size_t pos = 0; for (size_t i = 0; i < len && pos + 3 < sizeof(hex); i++) { pos += (size_t)snprintf(hex + pos, sizeof(hex) - pos, "%02x ", data[i]); } ESP_LOGI(TAG, "%s len=%u: %s", label, (unsigned)len, hex); } static int ble_hid_send_input_report(uint8_t report_id, const uint8_t *buffer, size_t len, const char *label) { esp_err_t ret; if (!ble_hid_ready()) { return ESP_ERR_INVALID_STATE; } ble_hid_log_buffer(label, buffer, len); ret = esp_hidd_dev_input_set(s_hid_dev, 0, report_id, (uint8_t *)buffer, len); return (ret == ESP_OK) ? 0 : ret; } static void ble_hid_send_idle_reports(void) { uint8_t keyboard[HID_KEYBOARD_REPORT_LEN] = {0}; uint8_t consumer[HID_CONSUMER_REPORT_LEN] = {0}; if (!ble_hid_ready()) { return; } (void)ble_hid_send_input_report(HID_KEYBOARD_REPORT_ID, keyboard, sizeof(keyboard), "keyboard idle"); (void)ble_hid_send_input_report(HID_CONSUMER_REPORT_ID, consumer, sizeof(consumer), "consumer idle"); } static void ble_hid_idle_task(void *arg) { (void)arg; /* Windows often enables notify a few seconds after connect. */ vTaskDelay(pdMS_TO_TICKS(3500)); ble_hid_send_idle_reports(); vTaskDelete(NULL); } static void ble_hid_hidd_event(void *handler_args, esp_event_base_t base, int32_t id, void *event_data) { esp_hidd_event_t event = (esp_hidd_event_t)id; esp_hidd_event_data_t *param = (esp_hidd_event_data_t *)event_data; (void)handler_args; (void)base; switch (event) { case ESP_HIDD_START_EVENT: ESP_LOGI(TAG, "HID stack started"); esp_hid_ble_gap_adv_start(); break; case ESP_HIDD_CONNECT_EVENT: if (param->connect.status == 0) { s_connected = true; ESP_LOGI(TAG, "HID connected"); if (s_status_cb != NULL) { s_status_cb(true); } xTaskCreate(ble_hid_idle_task, "ble_hid_idle", 2048, NULL, 5, NULL); } else { ESP_LOGW(TAG, "HID connect failed status=%d", param->connect.status); } break; case ESP_HIDD_DISCONNECT_EVENT: s_connected = false; ESP_LOGI(TAG, "HID disconnected reason=%d", param->disconnect.reason); if (s_status_cb != NULL) { s_status_cb(false); } esp_hid_ble_gap_adv_start(); break; case ESP_HIDD_PROTOCOL_MODE_EVENT: ESP_LOGI(TAG, "protocol mode map=%u mode=%s", param->protocol_mode.map_index, param->protocol_mode.protocol_mode ? "REPORT" : "BOOT"); break; case ESP_HIDD_CONTROL_EVENT: ESP_LOGI(TAG, "control map=%u %sSUSPEND", param->control.map_index, param->control.control ? "EXIT_" : ""); break; case ESP_HIDD_OUTPUT_EVENT: ESP_LOGD(TAG, "output map=%u id=%u len=%d", param->output.map_index, param->output.report_id, param->output.length); break; default: break; } } int ble_hid_init(ble_hid_status_cb_t status_cb) { esp_err_t ret; s_status_cb = status_cb; s_connected = false; ret = esp_hidd_dev_init(&s_hid_config, ESP_HID_TRANSPORT_BLE, ble_hid_hidd_event, &s_hid_dev); if (ret != ESP_OK) { ESP_LOGE(TAG, "esp_hidd_dev_init failed: %d", ret); return ret; } ESP_LOGI(TAG, "esp_hidd ready (keyboard=%u bytes, consumer=%u bytes)", (unsigned)HID_KEYBOARD_REPORT_LEN, (unsigned)HID_CONSUMER_REPORT_LEN); return ESP_OK; } void ble_hid_on_input_notify(bool enabled) { if (enabled) { ble_hid_send_idle_reports(); } } bool ble_hid_ready(void) { return s_connected && s_hid_dev != NULL; } int ble_hid_key_press(uint8_t modifier, uint8_t keycode) { uint8_t buffer[HID_KEYBOARD_REPORT_LEN] = {0}; if (!ble_hid_ready()) { return ESP_ERR_INVALID_STATE; } buffer[0] = modifier; buffer[2] = keycode; return ble_hid_send_input_report(HID_KEYBOARD_REPORT_ID, buffer, sizeof(buffer), "press"); } int ble_hid_key_release(void) { uint8_t buffer[HID_KEYBOARD_REPORT_LEN] = {0}; if (!ble_hid_ready()) { return ESP_ERR_INVALID_STATE; } return ble_hid_send_input_report(HID_KEYBOARD_REPORT_ID, buffer, sizeof(buffer), "release"); } int ble_hid_send_combo(uint8_t modifier, uint8_t keycode) { int rc; rc = ble_hid_key_press(modifier, keycode); if (rc != 0) { return rc; } vTaskDelay(pdMS_TO_TICKS(50)); return ble_hid_key_release(); } int ble_hid_consumer_press(uint16_t usage) { uint8_t buffer[HID_CONSUMER_REPORT_LEN] = {0}; if (!ble_hid_ready()) { return ESP_ERR_INVALID_STATE; } if (usage == 0) { return ESP_ERR_INVALID_ARG; } if (!ble_hid_encode_consumer_report(usage, buffer)) { return ESP_ERR_NOT_SUPPORTED; } return ble_hid_send_input_report(HID_CONSUMER_REPORT_ID, buffer, sizeof(buffer), "consumer press"); } int ble_hid_consumer_release(void) { uint8_t buffer[HID_CONSUMER_REPORT_LEN] = {0}; if (!ble_hid_ready()) { return ESP_ERR_INVALID_STATE; } return ble_hid_send_input_report(HID_CONSUMER_REPORT_ID, buffer, sizeof(buffer), "consumer release"); } int ble_hid_send_consumer(uint16_t usage) { int rc; rc = ble_hid_consumer_press(usage); if (rc != 0) { return rc; } vTaskDelay(pdMS_TO_TICKS(100)); return ble_hid_consumer_release(); } #else int ble_hid_init(ble_hid_status_cb_t status_cb) { (void)status_cb; return 0; } bool ble_hid_ready(void) { return false; } int ble_hid_key_press(uint8_t modifier, uint8_t keycode) { (void)modifier; (void)keycode; return -1; } int ble_hid_key_release(void) { return -1; } int ble_hid_send_combo(uint8_t modifier, uint8_t keycode) { (void)modifier; (void)keycode; return -1; } int ble_hid_send_consumer(uint16_t usage) { (void)usage; return -1; } #endif /* BOARD_BLE_ENABLED */