/* * SPDX-FileCopyrightText: 2022-2023 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Unlicense OR CC0-1.0 */ #include #include #include #include #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "freertos/event_groups.h" #include "esp_err.h" #include "esp_log.h" #include "usb/usb_host.h" #include "errno.h" #include "driver/gpio.h" #include "hid_host.h" #include "hid_usage_keyboard.h" #include "hid_usage_mouse.h" #define APP_QUIT_PIN GPIO_NUM_0 #define APP_QUIT_PIN_POLL_MS 500 #define READY_TO_UNINSTALL (HOST_NO_CLIENT | HOST_ALL_FREE) /* Main char symbol for ENTER key */ #define KEYBOARD_ENTER_MAIN_CHAR '\r' /* When set to 1 pressing ENTER will be extending with LineFeed during serial debug output */ #define KEYBOARD_ENTER_LF_EXTEND 1 /** * @brief Application Event from USB Host driver * */ typedef enum { HOST_NO_CLIENT = 0x1, HOST_ALL_FREE = 0x2, DEVICE_CONNECTED = 0x4, DEVICE_DISCONNECTED = 0x8, DEVICE_ADDRESS_MASK = 0xFF0, } app_event_t; /** * @brief Key event * */ typedef struct { enum key_state { KEY_STATE_PRESSED = 0x00, KEY_STATE_RELEASED = 0x01 } state; uint8_t modifier; uint8_t key_code; } key_event_t; #define USB_EVENTS_TO_WAIT (DEVICE_CONNECTED | DEVICE_ADDRESS_MASK | DEVICE_DISCONNECTED) static const char *TAG = "example"; static EventGroupHandle_t usb_flags; static bool hid_device_connected = false; hid_host_interface_handle_t keyboard_handle = NULL; hid_host_interface_handle_t mouse_handle = NULL; /** * @brief Scancode to ascii table */ const uint8_t keycode2ascii [57][2] = { {0, 0}, /* HID_KEY_NO_PRESS */ {0, 0}, /* HID_KEY_ROLLOVER */ {0, 0}, /* HID_KEY_POST_FAIL */ {0, 0}, /* HID_KEY_ERROR_UNDEFINED */ {'a', 'A'}, /* HID_KEY_A */ {'b', 'B'}, /* HID_KEY_B */ {'c', 'C'}, /* HID_KEY_C */ {'d', 'D'}, /* HID_KEY_D */ {'e', 'E'}, /* HID_KEY_E */ {'f', 'F'}, /* HID_KEY_F */ {'g', 'G'}, /* HID_KEY_G */ {'h', 'H'}, /* HID_KEY_H */ {'i', 'I'}, /* HID_KEY_I */ {'j', 'J'}, /* HID_KEY_J */ {'k', 'K'}, /* HID_KEY_K */ {'l', 'L'}, /* HID_KEY_L */ {'m', 'M'}, /* HID_KEY_M */ {'n', 'N'}, /* HID_KEY_N */ {'o', 'O'}, /* HID_KEY_O */ {'p', 'P'}, /* HID_KEY_P */ {'q', 'Q'}, /* HID_KEY_Q */ {'r', 'R'}, /* HID_KEY_R */ {'s', 'S'}, /* HID_KEY_S */ {'t', 'T'}, /* HID_KEY_T */ {'u', 'U'}, /* HID_KEY_U */ {'v', 'V'}, /* HID_KEY_V */ {'w', 'W'}, /* HID_KEY_W */ {'x', 'X'}, /* HID_KEY_X */ {'y', 'Y'}, /* HID_KEY_Y */ {'z', 'Z'}, /* HID_KEY_Z */ {'1', '!'}, /* HID_KEY_1 */ {'2', '@'}, /* HID_KEY_2 */ {'3', '#'}, /* HID_KEY_3 */ {'4', '$'}, /* HID_KEY_4 */ {'5', '%'}, /* HID_KEY_5 */ {'6', '^'}, /* HID_KEY_6 */ {'7', '&'}, /* HID_KEY_7 */ {'8', '*'}, /* HID_KEY_8 */ {'9', '('}, /* HID_KEY_9 */ {'0', ')'}, /* HID_KEY_0 */ {KEYBOARD_ENTER_MAIN_CHAR, KEYBOARD_ENTER_MAIN_CHAR}, /* HID_KEY_ENTER */ {0, 0}, /* HID_KEY_ESC */ {'\b', 0}, /* HID_KEY_DEL */ {0, 0}, /* HID_KEY_TAB */ {' ', ' '}, /* HID_KEY_SPACE */ {'-', '_'}, /* HID_KEY_MINUS */ {'=', '+'}, /* HID_KEY_EQUAL */ {'[', '{'}, /* HID_KEY_OPEN_BRACKET */ {']', '}'}, /* HID_KEY_CLOSE_BRACKET */ {'\\', '|'}, /* HID_KEY_BACK_SLASH */ {'\\', '|'}, /* HID_KEY_SHARP */ // HOTFIX: for NonUS Keyboards repeat HID_KEY_BACK_SLASH {';', ':'}, /* HID_KEY_COLON */ {'\'', '"'}, /* HID_KEY_QUOTE */ {'`', '~'}, /* HID_KEY_TILDE */ {',', '<'}, /* HID_KEY_LESS */ {'.', '>'}, /* HID_KEY_GREATER */ {'/', '?'} /* HID_KEY_SLASH */ }; /** * @brief Makes new line depending on report output protocol type * * @param[in] proto Current protocol to output */ static void hid_print_new_device_report_header(hid_protocol_t proto) { static hid_protocol_t prev_proto_output = HID_PROTOCOL_NONE; if (prev_proto_output != proto) { prev_proto_output = proto; printf("\r\n"); if (proto == HID_PROTOCOL_MOUSE) { printf("Mouse\r\n"); } if (proto == HID_PROTOCOL_KEYBOARD) { printf("Keyboard\r\n"); } fflush(stdout); } } /** * @brief HID Keyboard modifier verification for capitalization application (right or left shift) * * @param[in] modifier * @return true Modifier was pressed (left or right shift) * @return false Modifier was not pressed (left or right shift) * */ static inline bool hid_keyboard_is_modifier_shift(uint8_t modifier) { if ((modifier && HID_LEFT_SHIFT) || (modifier && HID_RIGHT_SHIFT)) { return true; } return false; } /** * @brief HID Keyboard get char symbol from key code * * @param[in] modifier Keyboard modifier data * @param[in] key_code Keyboard key code * @param[in] key_char Pointer to key char data * * @return true Key scancode converted successfully * @return false Key scancode unknown */ static inline bool hid_keyboard_get_char(uint8_t modifier, uint8_t key_code, unsigned char *key_char) { uint8_t mod = (hid_keyboard_is_modifier_shift(modifier)) ? 1 : 0; if ((key_code >= HID_KEY_A) && (key_code <= HID_KEY_SLASH)) { *key_char = keycode2ascii[key_code][mod]; } else { // All other key pressed return false; } return true; } /** * @brief HID Keyboard print char symbol * * @param[in] key_char Keyboard char to stdout */ static inline void hid_keyboard_print_char(unsigned int key_char) { if (!!key_char) { putchar(key_char); #if (KEYBOARD_ENTER_LF_EXTEND) if (KEYBOARD_ENTER_MAIN_CHAR == key_char) { putchar('\n'); } #endif // KEYBOARD_ENTER_LF_EXTEND fflush(stdout); } } /** * @brief Key Event. Key event with the key code, state and modifier. * * @param[in] key_event Pointer to Key Event structure * */ static void key_event_callback(key_event_t *key_event) { unsigned char key_char; hid_print_new_device_report_header(HID_PROTOCOL_KEYBOARD); if (KEY_STATE_PRESSED == key_event->state) { if (hid_keyboard_get_char(key_event->modifier, key_event->key_code, &key_char)) { hid_keyboard_print_char(key_char); } } } /** * @brief Key buffer scan code search. * * @param[in] src Pointer to source buffer where to search * @param[in] key Key scancode to search * @param[in] length Size of the source buffer */ static inline bool key_found(const uint8_t *const src, uint8_t key, unsigned int length) { for (unsigned int i = 0; i < length; i++) { if (src[i] == key) { return true; } } return false; } /** * @brief USB HID Host Keyboard Interface report callback handler * * @param[in] data Pointer to input report data buffer * @param[in] length Length of input report data buffer */ static void hid_host_keyboard_report_callback(const uint8_t *const data, const int length) { hid_keyboard_input_report_boot_t *kb_report = (hid_keyboard_input_report_boot_t *)data; if (length < sizeof(hid_keyboard_input_report_boot_t)) { return; } static uint8_t prev_keys[HID_KEYBOARD_KEY_MAX] = { 0 }; key_event_t key_event; for (int i = 0; i < HID_KEYBOARD_KEY_MAX; i++) { // key has been released verification if (prev_keys[i] > HID_KEY_ERROR_UNDEFINED && !key_found(kb_report->key, prev_keys[i], HID_KEYBOARD_KEY_MAX)) { key_event.key_code = prev_keys[i]; key_event.modifier = 0; key_event.state = KEY_STATE_RELEASED; key_event_callback(&key_event); } // key has been pressed verification if (kb_report->key[i] > HID_KEY_ERROR_UNDEFINED && !key_found(prev_keys, kb_report->key[i], HID_KEYBOARD_KEY_MAX)) { key_event.key_code = kb_report->key[i]; key_event.modifier = kb_report->modifier.val; key_event.state = KEY_STATE_PRESSED; key_event_callback(&key_event); } } memcpy(prev_keys, &kb_report->key, HID_KEYBOARD_KEY_MAX); } /** * @brief USB HID Host Mouse Interface report callback handler * * @param[in] data Pointer to input report data buffer * @param[in] length Length of input report data buffer */ static void hid_host_mouse_report_callback(const uint8_t *const data, const int length) { hid_mouse_input_report_boot_t *mouse_report = (hid_mouse_input_report_boot_t *)data; if (length < sizeof(hid_mouse_input_report_boot_t)) { return; } static int x_pos = 0; static int y_pos = 0; // Calculate absolute position from displacement x_pos += mouse_report->x_displacement; y_pos += mouse_report->y_displacement; hid_print_new_device_report_header(HID_PROTOCOL_MOUSE); printf("X: %06d\tY: %06d\t|%c|%c|\r", x_pos, y_pos, (mouse_report->buttons.button1 ? 'o' : ' '), (mouse_report->buttons.button2 ? 'o' : ' ')); fflush(stdout); } /** * @brief USB HID Host event callback. Handle such event as device connection and removing * * @param[in] event HID device event * @param[in] arg Pointer to arguments, does not used */ static void hid_host_event_callback(const hid_host_event_t *event, void *arg) { if (event->event == HID_DEVICE_CONNECTED) { // Obtained USB device address is placed after application events xEventGroupSetBits(usb_flags, DEVICE_CONNECTED | (event->device.address << 4)); } else if (event->event == HID_DEVICE_DISCONNECTED) { xEventGroupSetBits(usb_flags, DEVICE_DISCONNECTED); } } /** * @brief USB HID Host interface callback * * @param[in] event HID interface event * @param[in] arg Pointer to arguments, does not used */ static void hid_host_interface_event_callback(const hid_host_interface_event_t *event, void *arg) { switch (event->event) { case HID_DEVICE_INTERFACE_INIT: ESP_LOGI(TAG, "Interface number %d, protocol %s", event->interface.num, (event->interface.proto == HID_PROTOCOL_KEYBOARD) ? "Keyboard" : "Mouse"); if (event->interface.proto == HID_PROTOCOL_KEYBOARD) { const hid_host_interface_config_t hid_keyboard_config = { .proto = HID_PROTOCOL_KEYBOARD, .callback = hid_host_keyboard_report_callback, }; hid_host_claim_interface(&hid_keyboard_config, &keyboard_handle); } if (event->interface.proto == HID_PROTOCOL_MOUSE) { const hid_host_interface_config_t hid_mouse_config = { .proto = HID_PROTOCOL_MOUSE, .callback = hid_host_mouse_report_callback, }; hid_host_claim_interface(&hid_mouse_config, &mouse_handle); } break; case HID_DEVICE_INTERFACE_TRANSFER_ERROR: ESP_LOGD(TAG, "Interface number %d, transfer error", event->interface.num); break; case HID_DEVICE_INTERFACE_CLAIM: case HID_DEVICE_INTERFACE_RELEASE: // ... do nothing here for now break; default: ESP_LOGI(TAG, "%s Unhandled event %X, Interface number %d", __FUNCTION__, event->event, event->interface.num); break; } } /** * @brief Handle common USB host library events * * @param[in] args Pointer to arguments, does not used */ static void handle_usb_events(void *args) { while (1) { uint32_t event_flags; usb_host_lib_handle_events(portMAX_DELAY, &event_flags); // Release devices once all clients has deregistered if (event_flags & USB_HOST_LIB_EVENT_FLAGS_NO_CLIENTS) { usb_host_device_free_all(); xEventGroupSetBits(usb_flags, HOST_NO_CLIENT); } // Give ready_to_uninstall_usb semaphore to indicate that USB Host library // can be deinitialized, and terminate this task. if (event_flags & USB_HOST_LIB_EVENT_FLAGS_ALL_FREE) { xEventGroupSetBits(usb_flags, HOST_ALL_FREE); } } vTaskDelete(NULL); } static bool wait_for_event(EventBits_t event, TickType_t timeout) { return xEventGroupWaitBits(usb_flags, event, pdTRUE, pdTRUE, timeout) & event; } void app_main(void) { TaskHandle_t usb_events_task_handle; hid_host_device_handle_t hid_device; BaseType_t task_created; const gpio_config_t input_pin = { .pin_bit_mask = BIT64(APP_QUIT_PIN), .mode = GPIO_MODE_INPUT, .pull_up_en = GPIO_PULLUP_ENABLE, }; ESP_ERROR_CHECK( gpio_config(&input_pin) ); ESP_LOGI(TAG, "HID HOST example"); usb_flags = xEventGroupCreate(); assert(usb_flags); const usb_host_config_t host_config = { .skip_phy_setup = false, .intr_flags = ESP_INTR_FLAG_LEVEL1 }; ESP_ERROR_CHECK( usb_host_install(&host_config) ); task_created = xTaskCreate(handle_usb_events, "usb_events", 4096, NULL, 2, &usb_events_task_handle); assert(task_created); // hid host driver config const hid_host_driver_config_t hid_host_config = { .create_background_task = true, .task_priority = 5, .stack_size = 4096, .core_id = 0, .callback = hid_host_event_callback, .callback_arg = NULL }; ESP_ERROR_CHECK( hid_host_install(&hid_host_config) ); do { EventBits_t event = xEventGroupWaitBits(usb_flags, USB_EVENTS_TO_WAIT, pdTRUE, pdFALSE, pdMS_TO_TICKS(APP_QUIT_PIN_POLL_MS)); if (event & DEVICE_CONNECTED) { xEventGroupClearBits(usb_flags, DEVICE_CONNECTED); hid_device_connected = true; } if (event & DEVICE_ADDRESS_MASK) { xEventGroupClearBits(usb_flags, DEVICE_ADDRESS_MASK); const hid_host_device_config_t hid_host_device_config = { .dev_addr = (event & DEVICE_ADDRESS_MASK) >> 4, .iface_event_cb = hid_host_interface_event_callback, .iface_event_arg = NULL, }; ESP_ERROR_CHECK( hid_host_install_device(&hid_host_device_config, &hid_device) ); } if (event & DEVICE_DISCONNECTED) { xEventGroupClearBits(usb_flags, DEVICE_DISCONNECTED); hid_host_release_interface(keyboard_handle); hid_host_release_interface(mouse_handle); ESP_ERROR_CHECK( hid_host_uninstall_device(hid_device) ); hid_device_connected = false; } } while (gpio_get_level(APP_QUIT_PIN) != 0); if (hid_device_connected) { ESP_LOGI(TAG, "Uninitializing HID Device"); hid_host_release_interface(keyboard_handle); hid_host_release_interface(mouse_handle); ESP_ERROR_CHECK( hid_host_uninstall_device(hid_device) ); hid_device_connected = false; } ESP_LOGI(TAG, "Uninitializing USB"); ESP_ERROR_CHECK( hid_host_uninstall() ); wait_for_event(READY_TO_UNINSTALL, portMAX_DELAY); ESP_ERROR_CHECK( usb_host_uninstall() ); vTaskDelete(usb_events_task_handle); vEventGroupDelete(usb_flags); ESP_LOGI(TAG, "Done"); }