From 0aebadf8296af6abc07dc18a4d108630a5e3a032 Mon Sep 17 00:00:00 2001 From: kenji Date: Sat, 23 Aug 2025 15:06:10 -0400 Subject: [PATCH] complete refactor to directly pass reports and interfaces --- CMakeLists.txt | 6 +- build/hyperx_kb_rgb.uf2 | Bin 172032 -> 174592 bytes hyperx_elite2.c | 208 +++++----------------------- hyperx_elite2.h | 33 +---- main.c | 24 ++++ main_device.c | 93 ------------- main_host.c | 117 ---------------- main_host.h | 7 - tusb_config.h | 2 +- usb_descriptors.c | 175 ----------------------- usb_descriptors.h | 65 --------- usb_device.c | 229 ++++++++++++++++++++++++++++++ usb_device.h | 37 +++++ usb_host.c | 300 ++++++++++++++++++++++++++++++++++++++++ usb_host.h | 45 ++++++ 15 files changed, 681 insertions(+), 660 deletions(-) create mode 100644 main.c delete mode 100644 main_device.c delete mode 100644 main_host.c delete mode 100644 main_host.h delete mode 100644 usb_descriptors.c delete mode 100644 usb_descriptors.h create mode 100644 usb_device.c create mode 100644 usb_device.h create mode 100644 usb_host.c create mode 100644 usb_host.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 5fed187..af7482d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,10 +11,10 @@ add_subdirectory(${PICO_PIO_USB_PATH} pico_pio_usb) add_executable(${PROJECT}) target_sources(${PROJECT} PRIVATE - main_device.c - main_host.c - usb_descriptors.c hyperx_elite2.c + main.c + usb_host.c + usb_device.c ) # print memory usage, enable all warnings diff --git a/build/hyperx_kb_rgb.uf2 b/build/hyperx_kb_rgb.uf2 index 1766647303925ba8121f902567242e0c17d28011..fce9bd029a1c2060fd61d30964cd0eae791baeb3 100644 GIT binary patch delta 32357 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 W=&&$+ZSK3U!GRN~H0$v@-J2D2t}$d9%zZ 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 400) { - backlight = false; - } else { - backlight = true; + if ( absolute_time_diff_us(lastRead, get_absolute_time()) >= 500000) { + adc_value = adc_read(); + if (backlight && adc_value >= LDR_OFF_THRESHOLD) { + backlight = false; + } else if (!backlight && adc_value <= LDR_ON_THRESHOLD) { + backlight = true; + } } } -void rgb_task(uint8_t dev_addr, uint8_t instance, uint8_t report_id) { +void rgb_task(uint8_t dev_addr) { // the RGB protocol used by HyperX sends individual key RGB data in // multiple packets // the code here will determine if we are in the middle of sending @@ -51,26 +46,26 @@ void rgb_task(uint8_t dev_addr, uint8_t instance, uint8_t report_id) { // packet if so at a rate of one packet every 20ms // otherwise, wait 1s before sending the next set of color packets if (sending) { - if ( absolute_time_diff_us(lastTime, get_absolute_time()) > 20000) { + if ( absolute_time_diff_us(lastSend, get_absolute_time()) >= 20000) { if( backlight) { - send_color(dev_addr, instance, report_id, 0x20, 0x20, 0x20); + send_color(dev_addr, 0x20, 0x20, 0x20); // send a dim white color (#202020) for all keys } else{ - send_color(dev_addr, instance, report_id, 0x00, 0x00, 0x00); + send_color(dev_addr, 0x00, 0x00, 0x00); // turn off all lighting by sending (#000000) for all keys } - lastTime = get_absolute_time(); + lastSend = get_absolute_time(); } } else { - if ( absolute_time_diff_us(lastTime, get_absolute_time()) > 500000) { - send_initial(dev_addr, instance, report_id); - lastTime = get_absolute_time(); + if ( absolute_time_diff_us(lastSend, get_absolute_time()) >= 500000) { + send_initial(dev_addr); + lastSend = get_absolute_time(); } } } // send an individual color packett with the desired RGB color -void send_color(uint8_t dev_addr, uint8_t instance, uint8_t report_id, uint8_t red, uint8_t green, uint8_t blue) { +static void send_color(uint8_t dev_addr, uint8_t red, uint8_t green, uint8_t blue) { memset(buf, 0x00, BUF_SIZE); buf_idx = 0; @@ -129,7 +124,7 @@ void send_color(uint8_t dev_addr, uint8_t instance, uint8_t report_id, uint8_t r buf_idx += 4; } - if(tuh_hid_set_report(dev_addr, instance, report_id, HID_REPORT_TYPE_FEATURE, buf, BUF_SIZE)) + if(tuh_hid_set_report(dev_addr, 0, 0, HID_REPORT_TYPE_FEATURE, buf, BUF_SIZE)) { // packet sent successfully, increment packets_sent++; @@ -140,7 +135,7 @@ void send_color(uint8_t dev_addr, uint8_t instance, uint8_t report_id, uint8_t r // all keys have been sent, but the protocol expects NUM_PACKETS // packets to be sent in total; if we have not sent enough packets, // send extra packets of all 0x00 until done - if(tuh_hid_set_report(dev_addr, instance, report_id, HID_REPORT_TYPE_FEATURE, buf, BUF_SIZE)) + if(tuh_hid_set_report(dev_addr, 0, 0, HID_REPORT_TYPE_FEATURE, buf, BUF_SIZE)) { packets_sent++; } @@ -153,7 +148,7 @@ void send_color(uint8_t dev_addr, uint8_t instance, uint8_t report_id, uint8_t r // send the special initialization packet that tells the keyboard to expect // color packets to follow -void send_initial(uint8_t dev_addr, uint8_t instance, uint8_t report_id) { +static void send_initial(uint8_t dev_addr) { memset(buf, 0x00, BUF_SIZE); color_idx = 0; @@ -164,7 +159,7 @@ void send_initial(uint8_t dev_addr, uint8_t instance, uint8_t report_id) { buf[0x00] = 0x04; buf[0x01] = 0xf2; - if (tuh_hid_set_report(dev_addr, instance, report_id, HID_REPORT_TYPE_FEATURE, buf, BUF_SIZE)) + if (tuh_hid_set_report(dev_addr, 0, 0, HID_REPORT_TYPE_FEATURE, buf, BUF_SIZE)) { // we have begun sending packets, so set flag to continue sending // packets at regular intervals @@ -177,148 +172,15 @@ void send_initial(uint8_t dev_addr, uint8_t instance, uint8_t report_id) { void startADC() { stdio_init_all(); adc_init(); - adc_gpio_init(28); - adc_select_input(2); + adc_gpio_init(LDR_PIN); + adc_select_input(LDR_ADC); } -// process media key presses and prepare HID Consumer Control byte -void send_media(uint8_t const* report, uint16_t len) { - (void) len; - if ( report[0]==0x05 ) { - switch(report[2]) { - case 0xB0: // next - mediabit = 0; - break; - case 0xB1: // prev - mediabit = 1; - break; - case 0xB3: // play/pause - mediabit = 2; - break; - case 0xB5: // mute - mediabit = 3; - if (report[3]) { - mute = !mute; - } - break; - default: - return; - } - if (report[3]) { - SET_KEYBIT(mediakeys, mediabit); - } else { - CLEAR_KEYBIT(mediakeys, mediabit); - } - // set flag to send media keys during the next sending round - sendmedia = true; - } else if ( report[0]==0x03 ) { - switch(report[1]) { - case 0xE9: // vol up - SET_KEYBIT(mediakeys, 4); - break; - case 0xEA: // vol down - SET_KEYBIT(mediakeys, 5); - break; - case 0x00: // none - CLEAR_KEYBIT(mediakeys, 4); - CLEAR_KEYBIT(mediakeys, 5); - break; - default: - return; - } - sendmedia = true; +// forward HID report after processing +bool forward_report(uint8_t instance, uint8_t const* report, uint16_t len) { + if (instance == 0x01 && report[0] == 0x03 && report[1] == 0xE2) { + mute = !mute; } + + return tud_hid_n_report(instance, 0, report, len); } - -// process new HID reports from the keyboard and set them up to forward to host -void process_report(uint8_t instance, uint8_t const* report, uint16_t len) { - if (instance==HYPERX_ITF_KEYBOARD || instance==HYPERX_ITF_NKRO) { - // received regular keyboard key events - if (sendkeys) { - // keyboard is currently sending to the host, add to queue - key_buf[buf_end]=len; - key_buf[buf_end+2]=instance; - memcpy(&key_buf[buf_end+2], report, len); - buf_end += (len + 2); - } else { - // immediately process keys and send new HID report to host - updatekeys(instance, report, len); - } - } else if (instance==HYPERX_ITF_KEYPRESS) { - // received media key events - send_media(report, len); - } -} - -// merge key states from boot keyboard and NKRO keyboard interfaces into a -// single packet to send to the host -void updatekeys(uint8_t instance, uint8_t const* report, uint16_t len) { - if(instance==HYPERX_ITF_KEYBOARD) { - boot2nkro(report, nkro_buf1, len); - sendkeys=true; - } else if (instance==HYPERX_ITF_NKRO) { - memcpy(nkro_buf2, report, len); - sendkeys=true; - } - merge_bitmap(nkro_buf, nkro_buf1, nkro_buf2); -} - -// set initial state of keyboard -void reset_keyboard() { - memset(nkro_buf, 0x00, NKRO_BUF_SIZE); - memset(nkro_buf1, 0x00, NKRO_BUF_SIZE); - memset(nkro_buf2, 0x00, NKRO_BUF_SIZE); - memset(key_buf, 0x00, KEY_BUF_SIZE); - buf_start=0; - buf_end=0; - sendkeys=false; - sendmedia=false; -} - -// check if there are any key events waiting to be sent to the host and send -void keyboard_task() { - if (sendkeys) { - // send the current keyboard state to the host - if (tud_hid_report(REPORT_ID_KEYBOARD, nkro_buf, NKRO_BUF_SIZE)) { - sendkeys=false; - } - } else if (sendmedia) { - // send the media keys state to the the host - if (tud_hid_report(REPORT_ID_CONSUMER_CONTROL, mediakeys, 1)) { - sendmedia=false; - } - } else if (buf_start != buf_end) { - // previous key state has been send, but a new key state is waiting - // in the buffer - // load from buffer and setup for sending in the next round - updatekeys(key_buf[buf_start+1], &key_buf[buf_start+2], key_buf[buf_start]); - buf_start += (key_buf[buf_start] + 2); - } else if (buf_start == buf_end) { - // the buffer has been cleared, so reset the buffer position to front - buf_start = 0; - buf_end = 0; - } -} - -// convert a boot keyboard packet into the NKRO bitmap format used by HyperX -void boot2nkro(uint8_t const* boot_report, uint8_t* nkro_report, uint16_t len) { - memset(nkro_report, 0x00, NKRO_BUF_SIZE); - (void) len; - // copy modifiers - nkro_report[0] = boot_report[0]; - - // set regular keyboard keys - for (pos=2; pos<8; pos++) { - if (boot_report[pos] > 0) { - SET_KEYBIT(nkro_report, boot_report[pos]+8); - } - } -} - -// merge two NKRO keyboard bitmaps into a single NKRO keyboard bitmap -void merge_bitmap(uint8_t* nkro_report, uint8_t const* nkro_report1, uint8_t const* nkro_report2){ - for (pos=0; pos < NKRO_BUF_SIZE; pos++) { - nkro_report[pos] = nkro_report1[pos] | nkro_report2[pos]; - } -} - diff --git a/hyperx_elite2.h b/hyperx_elite2.h index d125425..f5a30b6 100644 --- a/hyperx_elite2.h +++ b/hyperx_elite2.h @@ -1,6 +1,11 @@ #ifndef HYPERX_ELITE2_H_ #define HYPERX_ELITE2_H_ +#define LDR_PIN 28 +#define LDR_ADC 2 +#define LDR_OFF_THRESHOLD 500 +#define LDR_ON_THRESHOLD 400 + enum { HYPERX_KEYBOARD_VID = 0x0951, @@ -8,35 +13,11 @@ enum NUM_KEYS = 128, BUF_SIZE = 64, NUM_PACKETS = 10, - NKRO_BUF_SIZE = 15, - KEY_BUF_SIZE = 16*16, -}; - -enum -{ - HYPERX_ITF_KEYBOARD = 0, - HYPERX_ITF_KEYPRESS, - HYPERX_ITF_NKRO }; void get_light(); -void rgb_task(uint8_t dev_addr, uint8_t instance, uint8_t report_id); -void send_color(uint8_t dev_addr, uint8_t instance, uint8_t report_id, - uint8_t red, uint8_t green, uint8_t blue); -void send_initial(uint8_t dev_addr, uint8_t instance, uint8_t report_id); +void rgb_task(uint8_t dev_addr); void startADC(); -void send_media(uint8_t const* report, uint16_t len); -void keyboard_task(); -void reset_keyboard(); -void updatekeys(uint8_t instance, uint8_t const* report, uint16_t len); -void process_report(uint8_t instance, uint8_t const* report, uint16_t len); - - -void merge_bitmap(uint8_t* nkro_report, uint8_t const* nkro_report1, uint8_t const* nkro_report2); -void boot2nkro(uint8_t const* boot_report, uint8_t* nkro_report, uint16_t len); - -#define SET_KEYBIT(array, index) do { (array)[(index) / 8] |= 1 << ((index) % 8); } while(0) -#define CLEAR_KEYBIT(array, index) do { (array)[(index) / 8] &= ~(1 << ((index) % 8)); } while(0) - +bool forward_report(uint8_t instance, uint8_t const* report, uint16_t len); #endif diff --git a/main.c b/main.c new file mode 100644 index 0000000..ec738be --- /dev/null +++ b/main.c @@ -0,0 +1,24 @@ +#include "pico/stdlib.h" +#include "pico/multicore.h" +#include "pico/bootrom.h" +#include "hardware/clocks.h" + +#include "usb_device.h" +#include "usb_host.h" + +// main loop +int main(void) { + // default 125MHz is not appropreate. Sysclock should be multiple of 12MHz. + set_sys_clock_khz(144000, true); + + sleep_ms(10); + + // run usb host on core 1 + multicore_reset_core1(); + multicore_launch_core1(usb_host_main); + + // run usb device on core 0 + usb_device_main(); + + return 0; +} diff --git a/main_device.c b/main_device.c deleted file mode 100644 index 06a7170..0000000 --- a/main_device.c +++ /dev/null @@ -1,93 +0,0 @@ -#include -#include -#include - -#include "pico/stdlib.h" -#include "pico/multicore.h" -#include "pico/bootrom.h" -#include "hardware/clocks.h" - -#include "tusb.h" -#include "usb_descriptors.h" - -#include "hyperx_elite2.h" -#include "main_host.h" - -static absolute_time_t lastRead; - -extern void core1_main(); - -int main(void) { - // default 125MHz is not appropreate. Sysclock should be multiple of 12MHz. - set_sys_clock_khz(144000, true); - - // start ADC for reading LDR - startADC(); - - sleep_ms(10); - - multicore_reset_core1(); - // run TinyUSB host on core 1 - multicore_launch_core1(core1_main); - - // run TinyUSB device on core 0 - tud_init(0); - - while (true) { - if ( absolute_time_diff_us(lastRead, get_absolute_time()) > 500000) { - // read the ADC every 500ms to determine RGB lighting - get_light(); - } - // check keyboard packet queue and send to TinyUSB device - keyboard_task(); - // TinyUSB device task - tud_task(); - } - - return 0; -} - - -// Invoked when received SET_REPORT control request or -// received data on OUT endpoint ( Report ID = 0, Type = 0 ) - -void tud_hid_set_report_cb(uint8_t instance, uint8_t report_id, hid_report_type_t report_type, uint8_t const* buffer, uint16_t bufsize) -{ - - - if (instance == 0 && report_type==HID_REPORT_TYPE_OUTPUT && report_id==REPORT_ID_KEYBOARD) { - // received keyboard indicator LED status from host, so update on - // the keyboard - set_indicator(buffer,bufsize); - } - - // echo the HID report to CDC for debugging - char tempbuf[128]; - size_t count; - if(bufsize>0) { - count = sprintf(tempbuf, "\nReceived device set report type %u on interface %u with ID %u (%u)\n", report_type, instance, report_id, bufsize); - tud_cdc_write(tempbuf, count); - for(int i=0;i -#include -#include - -#include "pico/stdlib.h" -#include "pico/multicore.h" -#include "pico/bootrom.h" - -#include "pio_usb.h" -#include "tusb.h" -#include "usb_descriptors.h" -#include "hyperx_elite2.h" - -#include "main_host.h" - -static bool enabled=false; -static uint8_t kb_addr=0; - -// main process for core 1 to handle USB host events -void core1_main() { - sleep_ms(10); - - // configure PIO USB for use as TinyUSB host - pio_usb_configuration_t pio_cfg = PIO_USB_DEFAULT_CONFIG; - pio_cfg.alarm_pool = (void*) alarm_pool_create(2,1); - tuh_configure(1, TUH_CFGID_RPI_PIO_USB_CONFIGURATION, &pio_cfg); - tuh_hid_set_default_protocol(HID_PROTOCOL_REPORT); - tuh_init(1); - - - while (true) { - if (enabled) { - // keyboard is plugged in, so send RGB states on regular intervals - rgb_task(kb_addr, HYPERX_ITF_KEYBOARD, REPORT_ID_KEYBOARD); - } - tuh_task(); // tinyusb host task - } -} - -// Invoked when device with hid interface is mounted -void tuh_hid_mount_cb(uint8_t dev_addr, uint8_t instance, uint8_t const* desc_report, uint16_t desc_len) -{ - (void)desc_report; - (void)desc_len; - - uint16_t vid, pid; - tuh_vid_pid_get(dev_addr, &vid, &pid); - const char* protocol_str[] = { "None", "Keyboard", "Mouse" }; - uint8_t const itf_protocol = tuh_hid_interface_protocol(dev_addr, instance); - - // send device vid:pid information to CDC for debugging - char tempbuf[128]; - size_t count = sprintf(tempbuf, "[%04x:%04x][%u] HID Interface %u, Protocol = %s \n", vid, pid, dev_addr, instance, protocol_str[itf_protocol]); - tud_cdc_write(tempbuf, count); - tud_cdc_write_flush(); - - // request to receive report if mounted device matches HyperX Alloy Elite 2 - if (vid==HYPERX_KEYBOARD_VID && pid==HYPERX_ELITE2_PID) - { - enabled = true; - kb_addr = dev_addr; - reset_keyboard(); - if ( !tuh_hid_receive_report(dev_addr, instance) ) - { - //tud_cdc_write_str("Error: cannot request report\r\n"); - } - } - -} - -// Invoked when device with hid interface is un-mounted -void tuh_hid_umount_cb(uint8_t dev_addr, uint8_t instance) -{ - (void) instance; - - if(dev_addr==kb_addr){ - kb_addr=0; - enabled=false; - } -} - -// Invoked when received report from device via interrupt endpoint -void tuh_hid_report_received_cb(uint8_t dev_addr, uint8_t instance, uint8_t const* report, uint16_t len) -{ - char tempbuf[64]; - size_t count; - if(len>0) { - // echo HID report to CDC for debugging - count = sprintf(tempbuf, "\n[%u] Received message on interface %u (%u)\n", dev_addr, instance, len); - tud_cdc_write(tempbuf, count); - for(int i=0;i 31 ) chr_count = 31; - - // Convert ASCII string into UTF-16 - for(uint8_t i=0; i +#include +#include +#include + +#include "pico/stdlib.h" +#include "bsp/board_api.h" +#include "tusb.h" + +#include "hyperx_elite2.h" +#include "usb_host.h" + +#include "usb_device.h" + +char cdc_buf[64]; +uint16_t cdc_len; +size_t cdc_count; +device_state_t device_state; + +static uint8_t desc_configuration[DESC_CFG_MAX]; +static uint16_t _desc_str[32+1]; + +static tusb_desc_device_t const desc_device = +{ + .bLength = sizeof(tusb_desc_device_t), + .bDescriptorType = TUSB_DESC_DEVICE, + .bcdUSB = USB_BCD, + + // Use Interface Association Descriptor (IAD) for CDC + // As required by USB Specs IAD's subclass must be common class (2) and protocol must be IAD (1) + .bDeviceClass = TUSB_CLASS_MISC, + .bDeviceSubClass = MISC_SUBCLASS_COMMON, + .bDeviceProtocol = MISC_PROTOCOL_IAD, + + .bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE, + + .idVendor = USB_VID, + .idProduct = USB_PID, + .bcdDevice = 0x0100, + + .iManufacturer = 0x01, + .iProduct = 0x02, + .iSerialNumber = 0x03, + + .bNumConfigurations = 0x01 +}; +static char const* string_desc_arr [] = +{ + (const char[]) { 0x09, 0x04 }, // 0: is supported language is English (0x0409) + "Raspberry Pi", // 1: Manufacturer + "Pico HyperX Elite 2 RGB Controller", // 2: Product + NULL, // 3: Serials, should use chip ID + "Pico HyperX Elite 2 CDC", // 4: CDC + "Pico HyperX Elite 2 HID", // 5: HID +}; + +static void usb_device_init(void); + +// main task for USB device +void usb_device_main(void) { + // start ADC for reading LDR + startADC(); + + // initialize TinyUSB device + device_state = DEVICE_INACTIVE; + usb_device_init(); + + while (true) { + switch ( device_state ) { + case DEVICE_ACTIVE: + break; + case DEVICE_INACTIVE: + break; + case DEVICE_RESTART: + if (tud_disconnect()) { + sleep_ms(100); + if (tud_connect()) { + if ( host_state == HOST_INACTIVE ) { + device_state = DEVICE_INACTIVE; + } else { + device_state = DEVICE_ACTIVE; + } + } + } + break; + default: + break; + } + get_light(); // get the ADC LDR reading + tud_task(); + tud_cdc_write_flush(); + } +} + +// initialize the TinyUSB device +static void usb_device_init(void) { + // run TinyUSB device + tusb_rhport_init_t dev_init = { + .role = TUSB_ROLE_DEVICE, + .speed = TUSB_SPEED_AUTO, + }; + tusb_init(BOARD_TUH_RHPORT, &dev_init); +} + +// Invoked when received GET DEVICE DESCRIPTOR +// Application return pointer to descriptor +uint8_t const * tud_descriptor_device_cb(void) +{ + return (uint8_t const *) &desc_device; +} + +// Invoked when received GET CONFIGURATION DESCRIPTOR +// Application return pointer to descriptor +// Descriptor contents must exist long enough for transfer to complete +uint8_t const * tud_descriptor_configuration_cb(uint8_t index) +{ + (void) index; // for multiple configurations + + memset(desc_configuration, 0, sizeof(desc_configuration)); + uint8_t desc_initial[TUD_CONFIG_DESC_LEN+TUD_CDC_DESC_LEN+1] = { + TUD_CONFIG_DESCRIPTOR(1, 2+num_mounted, 0, TUD_CONFIG_DESC_LEN+TUD_CDC_DESC_LEN+num_mounted*TUD_HID_DESC_LEN, 0x00, 100), + TUD_CDC_DESCRIPTOR(ITF_NUM_CDC, 4, EPNUM_CDC_NOTIF, 8, EPNUM_CDC_OUT, EPNUM_CDC_IN, 64) + }; + memcpy(desc_configuration, desc_initial, TUD_CONFIG_DESC_LEN+TUD_CDC_DESC_LEN); + + if ( descriptors != NULL) { + struct report_desc *descriptor; + for (uint8_t i=0; idev_addr, i); + uint8_t hid_desc[TUD_HID_DESC_LEN+1] = { + TUD_HID_DESCRIPTOR(ITF_NUM_HID+i, 5, HID_ITF_PROTOCOL_NONE, descriptor->desc_len, EPNUM_HID+i, CFG_TUD_HID_EP_BUFSIZE, 1) + }; + memcpy(&desc_configuration[TUD_CONFIG_DESC_LEN+TUD_CDC_DESC_LEN+i*TUD_HID_DESC_LEN], hid_desc, TUD_HID_DESC_LEN); + } + } + + return desc_configuration; +} + +// Invoked when received GET STRING DESCRIPTOR request +// Application return pointer to descriptor, whose contents must exist long enough for transfer to complete +uint16_t const* tud_descriptor_string_cb(uint8_t index, uint16_t langid) +{ + (void) langid; + + uint8_t chr_count; + + switch (index) { + case 0: // langid + memcpy(&_desc_str[1], string_desc_arr[0], 2); + chr_count = 1; + break; + case 3: // serial + chr_count = board_usb_get_serial(_desc_str+1, 32); + break; + default: + // Note: the 0xEE index string is a Microsoft OS 1.0 Descriptors. + // https://docs.microsoft.com/en-us/windows-hardware/drivers/usbcon/microsoft-defined-usb-descriptors + + if ( !(index < sizeof(string_desc_arr)/sizeof(string_desc_arr[0])) ) return NULL; + + char* str = string_desc_arr[index]; + + // Cap at max char + chr_count = (uint8_t) strlen(str); + if ( chr_count > 31 ) chr_count = 31; + + // Convert ASCII string into UTF-16 + for(uint8_t i=0; idev_addr, itf); + if ( descriptor != NULL ) { + return descriptor->descriptor; + } + } + + return NULL; +} + + +// Invoked when received SET_REPORT control request or +// received data on OUT endpoint ( Report ID = 0, Type = 0 ) +void tud_hid_set_report_cb(uint8_t instance, uint8_t report_id, hid_report_type_t report_type, uint8_t const* buffer, uint16_t bufsize) { + // forward to device connected to host + if (descriptors != NULL) { + tuh_hid_set_report(descriptors->dev_addr, instance, report_id, report_type, buffer, bufsize); + } +} + +// Invoked when received GET_REPORT control request +// Application must fill buffer report's content and return its length. +// Return zero will cause the stack to STALL request +uint16_t tud_hid_get_report_cb(uint8_t instance, uint8_t report_id, hid_report_type_t report_type, uint8_t* buffer, uint16_t reqlen) +{ + (void) instance; + (void) report_id; + (void) report_type; + (void) buffer; + (void) reqlen; + + return 0; +} + +// print message to CDC in raw hex +void cdc_print_hex(uint8_t const* msg, uint16_t msg_len) { + (void) msg; + (void) msg_len; + for (int i=0; i +#include +#include +#include + +#include "pico/stdlib.h" +#include "pio_usb.h" +#include "tusb.h" + +#include "hyperx_elite2.h" +#include "usb_device.h" + +#include "usb_host.h" + +host_state_t host_state; +struct report_desc *descriptors; +uint8_t num_mounted=0; +static absolute_time_t request_time; +static struct report_data *report; +static bool enabled=false; +static uint8_t kb_addr=0; + +static void usb_host_init(void); +static void host_ready(void); +static bool request_hid_report(uint8_t dev_addr, uint8_t instance); +static bool stop_hid_report(uint8_t dev_addr, uint8_t instance); +static bool request_hid_reports_all(void); +static bool stop_hid_reports_all(void); +static struct report_desc* report_desc_alloc(void); +static void report_desc_init(struct report_desc *descriptor); +static void report_desc_free(struct report_desc *descriptor); +static bool add_descriptor(uint8_t dev_addr, uint8_t instance, uint8_t const* desc_report, uint16_t desc_len); +static void remove_instance(uint8_t dev_addr, uint8_t instance); + +// initialize usb host +static void usb_host_init(void) { + // configure PIO USB for TinyUSB host + pio_usb_configuration_t pio_cfg = PIO_USB_DEFAULT_CONFIG; + pio_cfg.alarm_pool = (void*) alarm_pool_create(2,1); + tuh_configure(1, TUH_CFGID_RPI_PIO_USB_CONFIGURATION, &pio_cfg); + + // run TinyUSB host + tusb_rhport_init_t host_init = { + .role = TUSB_ROLE_HOST, + .speed = TUSB_SPEED_AUTO, + }; + tuh_hid_set_default_protocol(HID_PROTOCOL_REPORT); + tusb_init(BOARD_TUH_RHPORT, &host_init); + + host_state=HOST_INACTIVE; +} + +void usb_host_main(void) { + usb_host_init(); + + while (true) { + switch ( host_state ) { + case HOST_MOUNTED: + device_state = DEVICE_RESTART; + host_ready(); + break; + case HOST_UNMOUNTED: + host_state = HOST_MOUNTED; + break; + case HOST_START_LISTEN: + request_hid_reports_all(); + break; + case HOST_STOP_LISTEN: + stop_hid_reports_all(); + break; + case HOST_LISTENING: + if (enabled) { + rgb_task(kb_addr); + } + break; + default: + break; + } + tuh_task(); + } +} + +// Invoked when device with hid interface is mounted +void tuh_hid_mount_cb(uint8_t dev_addr, uint8_t instance, uint8_t const* desc_report, uint16_t desc_len) +{ + uint16_t vid, pid; + tuh_vid_pid_get(dev_addr, &vid, &pid); + uint8_t const itf_protocol = tuh_hid_interface_protocol(dev_addr, instance); + + /// send device vid:pid information to CDC for debugging + cdc_count = sprintf(cdc_buf, "Mount: [%04x:%04x][%u:%u] Protocol = %u\n", vid, pid, dev_addr, instance, itf_protocol); + tud_cdc_write(cdc_buf, cdc_count); + + + if (vid==HYPERX_KEYBOARD_VID && pid==HYPERX_ELITE2_PID) { + kb_addr = dev_addr; + enabled = true; + } + + // add to HID report descriptor + if ( add_descriptor(dev_addr, instance, desc_report, desc_len)) { + num_mounted++; + host_state=HOST_MOUNTED; + request_time=get_absolute_time(); + } +} + +// Invoked when device with hid interface is un-mounted +void tuh_hid_umount_cb(uint8_t dev_addr, uint8_t instance) +{ + // send device address:instance to CDC for debugging + cdc_count = sprintf(cdc_buf, "Unmount: [%u:%u]\n", dev_addr, instance); + tud_cdc_write(cdc_buf, cdc_count); + + if (stop_hid_report(dev_addr, instance)) { + tud_cdc_write_str("Successfully stopped receiving reports\n"); + } + + remove_instance(dev_addr, instance); + if (dev_addr == kb_addr) { + enabled = false; + } + num_mounted--; + host_state=HOST_UNMOUNTED; + request_time=get_absolute_time(); +} + +// Invoked when received report from device via interrupt endpoint +void tuh_hid_report_received_cb(uint8_t dev_addr, uint8_t instance, uint8_t const* report, uint16_t len) +{ + if (len > 0 ) { + cdc_count = sprintf(cdc_buf, "[%u:%u](%u) ", dev_addr, instance, len); + tud_cdc_write(cdc_buf,cdc_count); + cdc_print_hex(report, len); + } + + forward_report(instance, report, len); + + // continue to request to receive report + if ( !tuh_hid_receive_report(dev_addr, instance) ) + { + tud_cdc_write_str("Error: cannot request report\r\n"); + } +} + +// start listening on host for HID events +static void host_ready(void) { + if (absolute_time_diff_us(request_time, get_absolute_time()) >= 500000){ + if( descriptors != NULL ) { + host_state = HOST_START_LISTEN; + } else { + host_state = HOST_INACTIVE; + } + } +} + +// request HID input reports on specified device address and instance +static bool request_hid_report(uint8_t dev_addr, uint8_t instance) { + // request to receive reports HID devices + if ( !tuh_hid_receive_report(dev_addr, instance) ) { + cdc_count = sprintf(cdc_buf, "Error: cannot request report on [%u:%u]\n", dev_addr, instance); + tud_cdc_write(cdc_buf, cdc_count); + return false; + } + return true; +} + +// stop receiving HID input reports on specified device address and instance +static bool stop_hid_report(uint8_t dev_addr, uint8_t instance) { + if (!tuh_hid_receive_abort(dev_addr, instance)) { + tud_cdc_write_str("Error: could not stop receiving reports\n"); + return false; + } + + return true; +} + +// start listening to HID input reports on all mounted devices +static bool request_hid_reports_all(void) { + // send request to receive reports on all mounted devices + struct report_desc * current; + for (current=descriptors; current != NULL; current=current->next) { + if (! current->listening) { + if(request_hid_report(current->dev_addr, current->instance)) { + cdc_count = sprintf(cdc_buf, "Listening to input reports on [%u:%u]\n", current->dev_addr, current->instance); + tud_cdc_write(cdc_buf, cdc_count); + current->listening = true; + } else { + tud_cdc_write_str("Error listening to input report(s)\n"); + stop_hid_reports_all(); + return false; + } + } + } + + host_state = HOST_LISTENING; + return true; +} + +// stop listening to HID input reports on all mounted devices +static bool stop_hid_reports_all(void) { + // send request to stop reports on all mounted devices + struct report_desc * current; + for (current=descriptors; current != NULL; current=current->next) { + if (current->listening) { + if(stop_hid_report(current->dev_addr, current->instance)) { + cdc_count = sprintf(cdc_buf, "Stopping input reports on [%u:%u]\n", current->dev_addr, current->instance); + tud_cdc_write(cdc_buf, cdc_count); + current->listening = false; + } else { + tud_cdc_write_str("Error stopping input report(s)\n"); + return false; + } + } + } + + host_state = HOST_INACTIVE; + return true; +} + +// allocate memory for USB interface report descriptor +static struct report_desc * report_desc_alloc(void) { + struct report_desc *ret = REPORT_DESC_ALLOC(); + + if (ret != NULL) { + report_desc_init(ret); + if (descriptors == NULL) { + descriptors = ret; + } else { + struct report_desc *last; + for (last = descriptors; last->next != NULL; last=last->next); + last->next = ret; + } + } + + return ret; +} + +// initialize report descriptor struct +static void report_desc_init(struct report_desc *descriptor) { + memset(descriptor, 0, sizeof(struct report_desc)); + descriptor->next = NULL; +} + +// free memory and teardown usb->bt report ID mappings for report descriptor struct +static void report_desc_free(struct report_desc *descriptor) { + if (descriptor != NULL) { + if (descriptors == descriptor) { + descriptors = descriptor->next; + } else { + struct report_desc *last; + for (last = descriptors; last->next != NULL; last = last->next) { + if ((last->next) == descriptor) { + last->next = descriptor->next; + break; + } + } + } + free(descriptor); + } +} + +// add report descriptor for new HID interface +static bool add_descriptor(uint8_t dev_addr, uint8_t instance, uint8_t const* desc_report, uint16_t desc_len) { + struct report_desc * descriptor = report_desc_alloc(); + if (descriptor == NULL) { + return false; + } + + memcpy(descriptor->descriptor, desc_report, desc_len); + descriptor->desc_len = desc_len; + descriptor->dev_addr = dev_addr; + descriptor->instance = instance; + descriptor->listening = false; + + return true; + +} + +// remove report descriptor for HID interface +static void remove_instance(uint8_t dev_addr, uint8_t instance) { + struct report_desc *descriptor = report_desc_find(dev_addr, instance); + + if (descriptor != NULL) { + report_desc_free(descriptor); + } +} + +// find report descriptor by device address and instance +struct report_desc * report_desc_find(uint8_t dev_addr, uint8_t instance) { + struct report_desc *descriptor; + for (descriptor = descriptors; descriptor != NULL; descriptor = descriptor->next) { + if (descriptor->dev_addr==dev_addr && descriptor->instance==instance) { + break; + } + } + + return descriptor; +} + diff --git a/usb_host.h b/usb_host.h new file mode 100644 index 0000000..b37dba3 --- /dev/null +++ b/usb_host.h @@ -0,0 +1,45 @@ +#ifndef USB_HOST_H_ +#define USB_HOST_H_ + +#define REPORT_MAX_SIZE 64 +#define REPORT_BUF_SIZE 256 +#define DESCRIPTOR_MAX_SIZE 256 + +typedef enum { + HOST_INACTIVE=0, + HOST_MOUNTED, + HOST_UNMOUNTED, + HOST_START_LISTEN, + HOST_LISTENING, + HOST_STOP_LISTEN, +} host_state_t; + +struct report_desc { + uint8_t dev_addr; + uint8_t instance; + uint8_t descriptor[DESCRIPTOR_MAX_SIZE]; + uint16_t desc_len; + struct report_desc *next; + bool listening; +}; + +struct report_data { + uint8_t dev_addr; + uint8_t instance; + uint8_t report[REPORT_MAX_SIZE]; + uint16_t len; +}; + +#define REPORT_DESC_ALLOC() (struct report_desc *)malloc(sizeof(struct report_desc)) +#define REPORT_DATA_ALLOC() (struct report_data *)malloc(sizeof(struct report_data)) + +extern host_state_t host_state; +extern struct report_desc *descriptors; +extern uint8_t num_mounted; + +void usb_host_main(void); +struct report_desc * report_desc_find(uint8_t dev_addr, uint8_t instance); + + +#endif +