From d1d0206b3594a7ea34ae4ba51ae7481c4f4d4e4b Mon Sep 17 00:00:00 2001 From: heiso Date: Sat, 13 Apr 2024 13:56:32 +0200 Subject: [PATCH] WIP --- .vscode/settings.json | 5 +- firmware/.settings/language.settings.xml | 4 +- firmware/Core/Inc/main.h | 10 +- firmware/Core/Inc/stm32f4xx_hal_conf.h | 2 +- firmware/Core/Inc/tusb_config.h | 17 +- firmware/Core/Inc/usb_descriptors.h | 25 +- firmware/Core/Src/main.c | 209 +- firmware/Core/Src/stm32f4xx_hal_msp.c | 2 +- firmware/Core/Src/usb_descriptors.c | 162 +- firmware/macrolev.ioc | 4 +- firmware/tinyusb | 2 +- web-app/app/components/serial-provider.tsx | 248 + web-app/app/root.tsx | 47 +- web-app/app/routes/_layout._index.tsx | 2 +- ...ayout.hid.tsx => _layout.configurator.tsx} | 94 +- web-app/app/routes/_layout.usb.tsx | 23 + web-app/jest.config.js | 24 - web-app/package-lock.json | 5033 +++++++++-------- web-app/package.json | 34 +- 19 files changed, 3203 insertions(+), 2744 deletions(-) create mode 100644 web-app/app/components/serial-provider.tsx rename web-app/app/routes/{_layout.hid.tsx => _layout.configurator.tsx} (89%) create mode 100644 web-app/app/routes/_layout.usb.tsx delete mode 100644 web-app/jest.config.js diff --git a/.vscode/settings.json b/.vscode/settings.json index 2e786ef..bcea061 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -6,5 +6,8 @@ "editor.quickSuggestions": { "strings": "on" }, - "tailwindCSS.classAttributes": ["class", "className", ".*Styles", ".*Class"] + "tailwindCSS.classAttributes": ["class", "className", ".*Styles", ".*Class"], + "files.associations": { + "main.h": "c" + } } diff --git a/firmware/.settings/language.settings.xml b/firmware/.settings/language.settings.xml index 0a9e24f..af0d254 100644 --- a/firmware/.settings/language.settings.xml +++ b/firmware/.settings/language.settings.xml @@ -5,7 +5,7 @@ - + @@ -16,7 +16,7 @@ - + diff --git a/firmware/Core/Inc/main.h b/firmware/Core/Inc/main.h index 0ed6c47..bf7361f 100644 --- a/firmware/Core/Inc/main.h +++ b/firmware/Core/Inc/main.h @@ -130,7 +130,7 @@ struct key { struct actuation actuation; }; -struct hid_generic_inout_report_key { +struct serial_key { uint8_t row; uint8_t column; uint16_t idle_value; @@ -140,14 +140,6 @@ struct hid_generic_inout_report_key { enum actuation_status status; }; -struct hid_generic_inout_report { - struct hid_generic_inout_report_key keys[6]; - uint8_t duration; - uint8_t trigger_offset; - uint8_t reset_threshold; - uint8_t rapid_trigger_offset; -}; - struct user_config { uint8_t trigger_offset; uint8_t reset_threshold; diff --git a/firmware/Core/Inc/stm32f4xx_hal_conf.h b/firmware/Core/Inc/stm32f4xx_hal_conf.h index 73c6d88..ab78edc 100644 --- a/firmware/Core/Inc/stm32f4xx_hal_conf.h +++ b/firmware/Core/Inc/stm32f4xx_hal_conf.h @@ -214,7 +214,7 @@ #define MAC_ADDR5 0U /* Definition of the Ethernet driver buffers size and count */ -#define ETH_RX_BUF_SIZE /* buffer size for receive */ +#define ETH_RX_BUF_SIZE ETH_MAX_PACKET_SIZE /* buffer size for receive */ #define ETH_TX_BUF_SIZE ETH_MAX_PACKET_SIZE /* buffer size for transmit */ #define ETH_RXBUFNB 4U /* 4 Rx buffers of size ETH_RX_BUF_SIZE */ #define ETH_TXBUFNB 4U /* 4 Tx buffers of size ETH_TX_BUF_SIZE */ diff --git a/firmware/Core/Inc/tusb_config.h b/firmware/Core/Inc/tusb_config.h index d26c54a..6931a79 100644 --- a/firmware/Core/Inc/tusb_config.h +++ b/firmware/Core/Inc/tusb_config.h @@ -88,14 +88,23 @@ extern "C" { #endif //------------- CLASS -------------// -#define CFG_TUD_HID 2 -#define CFG_TUD_CDC 0 +#define CFG_TUD_HID 1 +#define CFG_TUD_CDC 1 #define CFG_TUD_MSC 0 #define CFG_TUD_MIDI 0 -#define CFG_TUD_VENDOR 0 +#define CFG_TUD_VENDOR 1 // HID buffer size Should be sufficient to hold ID (if any) + Data -#define CFG_TUD_HID_EP_BUFSIZE 64 +#define CFG_TUD_HID_EP_BUFSIZE 16 + +// CDC FIFO size of TX and RX +#define CFG_TUD_CDC_RX_BUFSIZE (TUD_OPT_HIGH_SPEED ? 512 : 64) +#define CFG_TUD_CDC_TX_BUFSIZE (TUD_OPT_HIGH_SPEED ? 512 : 64) + +// Vendor FIFO size of TX and RX +// If not configured vendor endpoints will not be buffered +#define CFG_TUD_VENDOR_RX_BUFSIZE (TUD_OPT_HIGH_SPEED ? 512 : 64) +#define CFG_TUD_VENDOR_TX_BUFSIZE (TUD_OPT_HIGH_SPEED ? 512 : 64) #ifdef __cplusplus } diff --git a/firmware/Core/Inc/usb_descriptors.h b/firmware/Core/Inc/usb_descriptors.h index 89d2c93..f947989 100644 --- a/firmware/Core/Inc/usb_descriptors.h +++ b/firmware/Core/Inc/usb_descriptors.h @@ -25,22 +25,31 @@ #ifndef USB_DESCRIPTORS_H_ #define USB_DESCRIPTORS_H_ -#define HID_GENERIC_INOUT_REPORT_BUFFSIZE 64 - enum { REPORT_ID_KEYBOARD = 1, REPORT_ID_CONSUMER_CONTROL, }; enum { - REPORT_ID_ANALOG = 1, - REPORT_ID_CONFIG, + VENDOR_REQUEST_WEBUSB = 1, + VENDOR_REQUEST_MICROSOFT = 2 +}; + +enum { + ITF_0_NUM_KEYBOARD = 0, + ITF_0_NUM_TOTAL }; enum { - ITF_NUM_KEYBOARD, - ITF_NUM_GENERIC_INOUT, - ITF_NUM_TOTAL + ITF_1_NUM_CDC = 0, + ITF_1_NUM_CDC_DATA, + ITF_1_NUM_VENDOR, + ITF_1_NUM_TOTAL +}; + +enum USB_MODE { + USB_MODE_SERIAL = 0, + USB_MODE_HID }; -#endif /* USB_DESCRIPTORS_H_ */ \ No newline at end of file +#endif /* USB_DESCRIPTORS_H_ */ diff --git a/firmware/Core/Src/main.c b/firmware/Core/Src/main.c index 111bf93..1bb27fa 100644 --- a/firmware/Core/Src/main.c +++ b/firmware/Core/Src/main.c @@ -89,12 +89,22 @@ static uint8_t key_triggered = 0; static uint8_t should_send_consumer_report = 0; static uint8_t should_send_keyboard_report = 0; -static uint8_t should_send_generic_inout_report = 0; static uint8_t modifiers = 0; static uint8_t keycodes[6] = {0}; static uint8_t consumer_report = 0; -static struct hid_generic_inout_report generic_inout_report = {0}; + +extern uint8_t const desc_ms_os_20[]; +#define URL "example.tinyusb.org/webusb-serial/index.html" +const tusb_desc_webusb_url_t desc_url = + { + .bLength = 3 + sizeof(URL) - 1, + .bDescriptorType = 3, // WEBUSB URL type + .bScheme = 1, // 0: http, 1: https + .url = URL}; +static bool web_serial_connected = false; +static enum USB_MODE mode = USB_MODE_HID; + /* USER CODE END PV */ /* Private function prototypes -----------------------------------------------*/ @@ -145,6 +155,17 @@ void writeConfig(uint8_t *buffer, uint16_t size) { } HAL_FLASH_Lock(); } + +enum USB_MODE get_mode(void) { + return mode; +} + +void toggle_mode(void) { + mode = mode == USB_MODE_SERIAL ? USB_MODE_HID : USB_MODE_SERIAL; + web_serial_connected = 0; + tud_deinit(BOARD_TUD_RHPORT); + tud_init(BOARD_TUD_RHPORT); +} /* USER CODE END 0 */ /** @@ -152,6 +173,7 @@ void writeConfig(uint8_t *buffer, uint16_t size) { * @retval int */ int main(void) { + /* USER CODE BEGIN 1 */ /* USER CODE END 1 */ @@ -187,8 +209,6 @@ int main(void) { /* Infinite loop */ /* USER CODE BEGIN WHILE */ while (1) { - uint32_t start = HAL_GetTick(); - tud_task(); key_triggered = 0; @@ -237,25 +257,39 @@ int main(void) { } } - if ((should_send_consumer_report || should_send_keyboard_report) && tud_hid_n_ready(ITF_NUM_KEYBOARD)) { - if (tud_suspended()) { - tud_remote_wakeup(); - } else { - if (should_send_consumer_report) { - should_send_consumer_report = 0; - tud_hid_n_report(ITF_NUM_KEYBOARD, REPORT_ID_CONSUMER_CONTROL, &consumer_report, 2); - } else if (should_send_keyboard_report) { - should_send_keyboard_report = 0; - tud_hid_n_keyboard_report(ITF_NUM_KEYBOARD, REPORT_ID_KEYBOARD, modifiers, keycodes); + if (keys[0][10].actuation.status == STATUS_TRIGGERED && keys[4][0].actuation.status == STATUS_TRIGGERED) { + toggle_mode(); + HAL_Delay(1000); + } + + if (mode == USB_MODE_SERIAL) { + // if (tud_cdc_connected()) { + // if (tud_cdc_available()) { + // // tud_cdc_write_str("test"); + // // tud_cdc_write_flush(); + // } + // } + + // if (web_serial_connected) { + // if (tud_vendor_available()) { + // // tud_vendor_write_str("web-serial test"); + // // tud_vendor_write_flush(); + // } + // } + } else { + if ((should_send_consumer_report || should_send_keyboard_report) && tud_hid_ready()) { + if (tud_suspended()) { + tud_remote_wakeup(); + } else { + if (should_send_consumer_report) { + should_send_consumer_report = 0; + tud_hid_report(REPORT_ID_CONSUMER_CONTROL, &consumer_report, 2); + } else if (should_send_keyboard_report) { + should_send_keyboard_report = 0; + tud_hid_keyboard_report(REPORT_ID_KEYBOARD, modifiers, keycodes); + } } } - } else if (should_send_generic_inout_report && tud_hid_n_ready(ITF_NUM_GENERIC_INOUT)) { - should_send_generic_inout_report = 0; - generic_inout_report.duration = HAL_GetTick() - start; - generic_inout_report.trigger_offset = user_config.trigger_offset; - generic_inout_report.reset_threshold = user_config.reset_threshold; - generic_inout_report.rapid_trigger_offset = user_config.rapid_trigger_offset; - tud_hid_n_report(ITF_NUM_GENERIC_INOUT, 0, &generic_inout_report, HID_GENERIC_INOUT_REPORT_BUFFSIZE); } /* USER CODE END WHILE */ @@ -549,58 +583,6 @@ void remove_from_hid_report(struct key *key, uint8_t layer) { } } -void add_to_hid_generic_inout_report(struct key *key) { - uint8_t added = 0; - for (uint8_t i = 0; i < 6; i++) { - if (generic_inout_report.keys[i].row == key->row && generic_inout_report.keys[i].column == key->column) { - generic_inout_report.keys[i].row = key->row; - generic_inout_report.keys[i].column = key->column; - generic_inout_report.keys[i].idle_value = key->calibration.idle_value; - generic_inout_report.keys[i].max_distance = key->calibration.max_distance; - generic_inout_report.keys[i].value = key->state.value; - generic_inout_report.keys[i].distance_8bits = key->state.distance_8bits; - generic_inout_report.keys[i].status = key->actuation.status; - - added = 1; - should_send_generic_inout_report = 1; - break; - } - } - - if (!added) { - for (uint8_t i = 0; i < 6; i++) { - // check if value is 0, 0 means the report is empty - if (generic_inout_report.keys[i].value == 0) { - generic_inout_report.keys[i].row = key->row; - generic_inout_report.keys[i].column = key->column; - generic_inout_report.keys[i].idle_value = key->calibration.idle_value; - generic_inout_report.keys[i].max_distance = key->calibration.max_distance; - generic_inout_report.keys[i].value = key->state.value; - generic_inout_report.keys[i].distance_8bits = key->state.distance_8bits; - generic_inout_report.keys[i].status = key->actuation.status; - - should_send_generic_inout_report = 1; - break; - } - } - } -} - -void remove_from_hid_generic_inout_report(struct key *key) { - for (uint8_t i = 0; i < 6; i++) { - if (generic_inout_report.keys[i].row == key->row && generic_inout_report.keys[i].column == key->column) { - generic_inout_report.keys[i].row = 0; - generic_inout_report.keys[i].column = 0; - generic_inout_report.keys[i].idle_value = 0; - generic_inout_report.keys[i].max_distance = 0; - generic_inout_report.keys[i].value = 0; - generic_inout_report.keys[i].distance_8bits = 0; - generic_inout_report.keys[i].status = 0; - break; - } - } -} - uint8_t update_key_state(struct key *key) { struct state state; @@ -628,7 +610,6 @@ uint8_t update_key_state(struct key *key) { if (key->state.distance == 0 && state.value >= key->calibration.idle_value - IDLE_VALUE_OFFSET) { if (key->idle_counter >= IDLE_CYCLES_UNTIL_SLEEP) { key->is_idle = 1; - remove_from_hid_generic_inout_report(key); return 0; } key->idle_counter++; @@ -765,8 +746,6 @@ void update_key_actuation(struct key *key) { } break; } - - add_to_hid_generic_inout_report(key); } void update_key(struct key *key) { @@ -814,14 +793,84 @@ uint16_t tud_hid_get_report_cb(uint8_t instance, uint8_t report_id, hid_report_t // 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) { (void)report_id; - if (instance == 1 && report_id == 0) { - writeConfig(buffer, bufsize); - // tud_hid_n_report(ITF_NUM_GENERIC_INOUT, 0, &user_config, HID_GENERIC_INOUT_REPORT_BUFFSIZE); + // if (instance == 1 && report_id == 0) { + // writeConfig(buffer, bufsize); - init_keys(); + // init_keys(); + // } +} + +// Invoked when cdc when line state changed e.g connected/disconnected +void tud_cdc_line_state_cb(uint8_t itf, bool dtr, bool rts) { + (void)itf; + + // connected + if (dtr && rts) { + // print initial message when connected + tud_cdc_write_str("\r\nTinyUSB WebUSB device example\r\n"); } } +// Invoked when CDC interface received data from host +void tud_cdc_rx_cb(uint8_t itf) { + (void)itf; +} + +// Invoked when a control transfer occurred on an interface of this class +// Driver response accordingly to the request and the transfer stage (setup/data/ack) +// return false to stall control endpoint (e.g unsupported request) +bool tud_vendor_control_xfer_cb(uint8_t rhport, uint8_t stage, tusb_control_request_t const *request) { + // nothing to with DATA & ACK stage + if (mode != USB_MODE_SERIAL || stage != CONTROL_STAGE_SETUP) + return true; + + switch (request->bmRequestType_bit.type) { + case TUSB_REQ_TYPE_VENDOR: + switch (request->bRequest) { + case VENDOR_REQUEST_WEBUSB: + // match vendor request in BOS descriptor + // Get landing page url + return tud_control_xfer(rhport, request, (void *)(uintptr_t)&desc_url, desc_url.bLength); + + case VENDOR_REQUEST_MICROSOFT: + if (request->wIndex == 7) { + // Get Microsoft OS 2.0 compatible descriptor + uint16_t total_len; + memcpy(&total_len, desc_ms_os_20 + 8, 2); + + return tud_control_xfer(rhport, request, (void *)(uintptr_t)desc_ms_os_20, total_len); + } else { + return false; + } + + default: + break; + } + break; + + case TUSB_REQ_TYPE_CLASS: + if (request->bRequest == 0x22) { + // Webserial simulate the CDC_REQUEST_SET_CONTROL_LINE_STATE (0x22) to connect and disconnect. + web_serial_connected = (request->wValue != 0); + + if (web_serial_connected) { + tud_vendor_write_str("\r\nWebUSB interface connected\r\n"); + tud_vendor_write_flush(); + } + + // response with status OK + return tud_control_status(rhport, request); + } + break; + + default: + break; + } + + // stall unknown request + return false; +} + /* USER CODE END 4 */ /** diff --git a/firmware/Core/Src/stm32f4xx_hal_msp.c b/firmware/Core/Src/stm32f4xx_hal_msp.c index e8323cd..354018f 100644 --- a/firmware/Core/Src/stm32f4xx_hal_msp.c +++ b/firmware/Core/Src/stm32f4xx_hal_msp.c @@ -20,7 +20,6 @@ /* Includes ------------------------------------------------------------------*/ #include "main.h" - /* USER CODE BEGIN Includes */ /* USER CODE END Includes */ @@ -63,6 +62,7 @@ */ void HAL_MspInit(void) { + /* USER CODE BEGIN MspInit 0 */ /* USER CODE END MspInit 0 */ diff --git a/firmware/Core/Src/usb_descriptors.c b/firmware/Core/Src/usb_descriptors.c index 2ffb668..4069774 100644 --- a/firmware/Core/Src/usb_descriptors.c +++ b/firmware/Core/Src/usb_descriptors.c @@ -39,18 +39,43 @@ //--------------------------------------------------------------------+ // Device Descriptors //--------------------------------------------------------------------+ -tusb_desc_device_t const desc_device = +tusb_desc_device_t const desc_device_0 = { .bLength = sizeof(tusb_desc_device_t), .bDescriptorType = TUSB_DESC_DEVICE, .bcdUSB = 0x0200, - .bDeviceClass = 0x00, - .bDeviceSubClass = 0x00, - .bDeviceProtocol = 0x00, + + .bDeviceClass = 0, + .bDeviceSubClass = 0, + .bDeviceProtocol = 0, .bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE, .idVendor = 0xCafe, - .idProduct = USB_PID + 2, + .idProduct = USB_PID, + .bcdDevice = 0x0100, + + .iManufacturer = 0x01, + .iProduct = 0x02, + .iSerialNumber = 0x03, + + .bNumConfigurations = 0x01, +}; + +tusb_desc_device_t const desc_device_1 = + { + .bLength = sizeof(tusb_desc_device_t), + .bDescriptorType = TUSB_DESC_DEVICE, + .bcdUSB = 0x0210, // at least 2.1 or 3.x for BOS & webUSB + + // 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 = 0xCafe, + .idProduct = USB_PID + 11, .bcdDevice = 0x0100, .iManufacturer = 0x01, @@ -63,7 +88,11 @@ tusb_desc_device_t const desc_device = // 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; + if (get_mode() == USB_MODE_SERIAL) { + return (uint8_t const *)&desc_device_1; + } else { + return (uint8_t const *)&desc_device_0; + } } //--------------------------------------------------------------------+ @@ -76,19 +105,12 @@ uint8_t const desc_hid_keyboard_report[] = TUD_HID_REPORT_DESC_CONSUMER(HID_REPORT_ID(REPORT_ID_CONSUMER_CONTROL)), }; -uint8_t const desc_hid_custom_report[] = - { - TUD_HID_REPORT_DESC_GENERIC_INOUT(HID_GENERIC_INOUT_REPORT_BUFFSIZE), -}; - // Invoked when received GET HID REPORT DESCRIPTOR // Application return pointer to descriptor // Descriptor contents must exist long enough for transfer to complete uint8_t const *tud_hid_descriptor_report_cb(uint8_t instance) { - if (instance == ITF_NUM_KEYBOARD) { + if (instance == ITF_0_NUM_KEYBOARD) { return desc_hid_keyboard_report; - } else if (instance == ITF_NUM_GENERIC_INOUT) { - return desc_hid_custom_report; } return NULL; @@ -98,21 +120,37 @@ uint8_t const *tud_hid_descriptor_report_cb(uint8_t instance) { // Configuration Descriptor //--------------------------------------------------------------------+ -// #define CONFIG_TOTAL_LEN (TUD_CONFIG_DESC_LEN + 2 * TUD_HID_DESC_LEN) -#define CONFIG_TOTAL_LEN (TUD_CONFIG_DESC_LEN + TUD_HID_DESC_LEN + TUD_HID_INOUT_DESC_LEN) +#define CONFIG_0_TOTAL_LEN (TUD_CONFIG_DESC_LEN + TUD_HID_DESC_LEN) -#define EPNUM_KEYBOARD 0x81 -#define EPNUM_HID_CUSTOM_IN 0x82 // 0x80 | EPNUM_HID_CUSTOM_OUT -#define EPNUM_HID_CUSTOM_OUT 0x83 +#define EPNUM_KEYBOARD 1 -uint8_t const desc_configuration[] = +uint8_t const desc_configuration_0[] = { // Config number, interface count, string index, total length, attribute, power in mA - TUD_CONFIG_DESCRIPTOR(1, ITF_NUM_TOTAL, 0, CONFIG_TOTAL_LEN, TUSB_DESC_CONFIG_ATT_REMOTE_WAKEUP, 100), + TUD_CONFIG_DESCRIPTOR(1, ITF_0_NUM_TOTAL, 0, CONFIG_0_TOTAL_LEN, TUSB_DESC_CONFIG_ATT_REMOTE_WAKEUP, 100), // Interface number, string index, protocol, report descriptor len, EP In address, size & polling interval - TUD_HID_DESCRIPTOR(ITF_NUM_KEYBOARD, 4, HID_ITF_PROTOCOL_KEYBOARD, sizeof(desc_hid_keyboard_report), EPNUM_KEYBOARD, CFG_TUD_HID_EP_BUFSIZE, 10), - TUD_HID_INOUT_DESCRIPTOR(ITF_NUM_GENERIC_INOUT, 5, HID_ITF_PROTOCOL_NONE, sizeof(desc_hid_custom_report), EPNUM_HID_CUSTOM_OUT, EPNUM_HID_CUSTOM_IN, HID_GENERIC_INOUT_REPORT_BUFFSIZE, 10), + TUD_HID_DESCRIPTOR(ITF_0_NUM_KEYBOARD, 6, HID_ITF_PROTOCOL_KEYBOARD, sizeof(desc_hid_keyboard_report), 0x80 | EPNUM_KEYBOARD, CFG_TUD_HID_EP_BUFSIZE, 10), +}; + +#define CONFIG_1_TOTAL_LEN (TUD_CONFIG_DESC_LEN + TUD_CDC_DESC_LEN + TUD_VENDOR_DESC_LEN) + +#define EPNUM_CDC_NOTIF 1 +#define EPNUM_CDC_IN 2 +#define EPNUM_CDC_OUT 2 +#define EPNUM_VENDOR_IN 3 +#define EPNUM_VENDOR_OUT 3 + +uint8_t const desc_configuration_1[] = + { + // Config number, interface count, string index, total length, attribute, power in mA + TUD_CONFIG_DESCRIPTOR(1, ITF_1_NUM_TOTAL, 0, CONFIG_1_TOTAL_LEN, 0x00, 100), + + // Interface number, string index, EP notification address and size, EP data address (out, in) and size. + TUD_CDC_DESCRIPTOR(ITF_1_NUM_CDC, 4, 0x80 | EPNUM_CDC_NOTIF, 8, EPNUM_CDC_OUT, 0x80 | EPNUM_CDC_IN, TUD_OPT_HIGH_SPEED ? 512 : 64), + + // Interface number, string index, EP Out & IN address, EP size + TUD_VENDOR_DESCRIPTOR(ITF_1_NUM_VENDOR, 5, EPNUM_VENDOR_OUT, 0x80 | EPNUM_VENDOR_IN, TUD_OPT_HIGH_SPEED ? 512 : 64), }; // Invoked when received GET CONFIGURATION DESCRIPTOR @@ -120,9 +158,80 @@ uint8_t const desc_configuration[] = // 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 - return desc_configuration; + if (get_mode() == USB_MODE_SERIAL) { + return desc_configuration_1; + } else { + return desc_configuration_0; + } +} + +//--------------------------------------------------------------------+ +// BOS Descriptor +//--------------------------------------------------------------------+ + +/* Microsoft OS 2.0 registry property descriptor +Per MS requirements https://msdn.microsoft.com/en-us/library/windows/hardware/hh450799(v=vs.85).aspx +device should create DeviceInterfaceGUIDs. It can be done by driver and +in case of real PnP solution device should expose MS "Microsoft OS 2.0 +registry property descriptor". Such descriptor can insert any record +into Windows registry per device/configuration/interface. In our case it +will insert "DeviceInterfaceGUIDs" multistring property. + +GUID is freshly generated and should be OK to use. + +https://developers.google.com/web/fundamentals/native-hardware/build-for-webusb/ +(Section Microsoft OS compatibility descriptors) +*/ + +#define BOS_TOTAL_LEN (TUD_BOS_DESC_LEN + TUD_BOS_WEBUSB_DESC_LEN + TUD_BOS_MICROSOFT_OS_DESC_LEN) + +#define MS_OS_20_DESC_LEN 0xB2 + +// BOS Descriptor is required for webUSB +uint8_t const desc_bos[] = + { + // total length, number of device caps + TUD_BOS_DESCRIPTOR(BOS_TOTAL_LEN, 2), + + // Vendor Code, iLandingPage + TUD_BOS_WEBUSB_DESCRIPTOR(VENDOR_REQUEST_WEBUSB, 1), + + // Microsoft OS 2.0 descriptor + TUD_BOS_MS_OS_20_DESCRIPTOR(MS_OS_20_DESC_LEN, VENDOR_REQUEST_MICROSOFT)}; + +uint8_t const *tud_descriptor_bos_cb(void) { + return desc_bos; } +uint8_t const desc_ms_os_20[] = + { + // Set header: length, type, windows version, total length + U16_TO_U8S_LE(0x000A), U16_TO_U8S_LE(MS_OS_20_SET_HEADER_DESCRIPTOR), U32_TO_U8S_LE(0x06030000), U16_TO_U8S_LE(MS_OS_20_DESC_LEN), + + // Configuration subset header: length, type, configuration index, reserved, configuration total length + U16_TO_U8S_LE(0x0008), U16_TO_U8S_LE(MS_OS_20_SUBSET_HEADER_CONFIGURATION), 0, 0, U16_TO_U8S_LE(MS_OS_20_DESC_LEN - 0x0A), + + // Function Subset header: length, type, first interface, reserved, subset length + U16_TO_U8S_LE(0x0008), U16_TO_U8S_LE(MS_OS_20_SUBSET_HEADER_FUNCTION), ITF_1_NUM_VENDOR, 0, U16_TO_U8S_LE(MS_OS_20_DESC_LEN - 0x0A - 0x08), + + // MS OS 2.0 Compatible ID descriptor: length, type, compatible ID, sub compatible ID + U16_TO_U8S_LE(0x0014), U16_TO_U8S_LE(MS_OS_20_FEATURE_COMPATBLE_ID), 'W', 'I', 'N', 'U', 'S', 'B', 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // sub-compatible + + // MS OS 2.0 Registry property descriptor: length, type + U16_TO_U8S_LE(MS_OS_20_DESC_LEN - 0x0A - 0x08 - 0x08 - 0x14), U16_TO_U8S_LE(MS_OS_20_FEATURE_REG_PROPERTY), + U16_TO_U8S_LE(0x0007), U16_TO_U8S_LE(0x002A), // wPropertyDataType, wPropertyNameLength and PropertyName "DeviceInterfaceGUIDs\0" in UTF-16 + 'D', 0x00, 'e', 0x00, 'v', 0x00, 'i', 0x00, 'c', 0x00, 'e', 0x00, 'I', 0x00, 'n', 0x00, 't', 0x00, 'e', 0x00, + 'r', 0x00, 'f', 0x00, 'a', 0x00, 'c', 0x00, 'e', 0x00, 'G', 0x00, 'U', 0x00, 'I', 0x00, 'D', 0x00, 's', 0x00, 0x00, 0x00, + U16_TO_U8S_LE(0x0050), // wPropertyDataLength + // bPropertyData: “{975F44D9-0D08-43FD-8B3E-127CA8AFFF9D}”. + '{', 0x00, '9', 0x00, '7', 0x00, '5', 0x00, 'F', 0x00, '4', 0x00, '4', 0x00, 'D', 0x00, '9', 0x00, '-', 0x00, + '0', 0x00, 'D', 0x00, '0', 0x00, '8', 0x00, '-', 0x00, '4', 0x00, '3', 0x00, 'F', 0x00, 'D', 0x00, '-', 0x00, + '8', 0x00, 'B', 0x00, '3', 0x00, 'E', 0x00, '-', 0x00, '1', 0x00, '2', 0x00, '7', 0x00, 'C', 0x00, 'A', 0x00, + '8', 0x00, 'A', 0x00, 'F', 0x00, 'F', 0x00, 'F', 0x00, '9', 0x00, 'D', 0x00, '}', 0x00, 0x00, 0x00, 0x00, 0x00}; + +TU_VERIFY_STATIC(sizeof(desc_ms_os_20) == MS_OS_20_DESC_LEN, "Incorrect size"); + //--------------------------------------------------------------------+ // String Descriptors //--------------------------------------------------------------------+ @@ -142,8 +251,9 @@ char const *string_desc_arr[] = "Heiso", // 1: Manufacturer "Macrolev", // 2: Product "345678", // 3: Serials will use unique ID if possible - "Keyboard Interface", // 4: Interface 1 String - "Custom Interface", // 5: Interface 2 String + "CDC Interface", // 4: Interface 1 String + "WebUSB Interface", // 5: Interface 2 String + "Keyboard Interface", // 6: Interface 3 String }; static uint16_t _desc_str[32 + 1]; diff --git a/firmware/macrolev.ioc b/firmware/macrolev.ioc index c531dd3..989f941 100644 --- a/firmware/macrolev.ioc +++ b/firmware/macrolev.ioc @@ -44,8 +44,8 @@ Mcu.PinsNb=16 Mcu.ThirdPartyNb=0 Mcu.UserConstants= Mcu.UserName=STM32F411CEUx -MxCube.Version=6.10.0 -MxDb.Version=DB.6.0.100 +MxCube.Version=6.11.0 +MxDb.Version=DB.6.0.110 NVIC.BusFault_IRQn=true\:0\:0\:false\:false\:true\:false\:false\:false NVIC.DebugMonitor_IRQn=true\:0\:0\:false\:false\:true\:false\:false\:false NVIC.ForceEnableDMAVector=true diff --git a/firmware/tinyusb b/firmware/tinyusb index 08f9ed6..d816a9b 160000 --- a/firmware/tinyusb +++ b/firmware/tinyusb @@ -1 +1 @@ -Subproject commit 08f9ed67c92421cbd0bc09270d2f363886681866 +Subproject commit d816a9bdf892daeb35b88b9a469599fcda432c55 diff --git a/web-app/app/components/serial-provider.tsx b/web-app/app/components/serial-provider.tsx new file mode 100644 index 0000000..d05583c --- /dev/null +++ b/web-app/app/components/serial-provider.tsx @@ -0,0 +1,248 @@ +import { + createContext, + useContext, + useEffect, + useRef, + useState, + type PropsWithChildren, +} from 'react' + +// RESOURCES: +// https://web.dev/serial/ +// https://reillyeon.github.io/serial/#onconnect-attribute-0 +// https://codelabs.developers.google.com/codelabs/web-serial + +export type PortState = 'closed' | 'closing' | 'open' | 'opening' + +export type SerialMessage = { + value: string + timestamp: number +} + +type SerialMessageCallback = (message: SerialMessage) => void + +export type SerialContextValue = { + canUseSerial: boolean + hasTriedAutoconnect: boolean + portState: PortState + connect(): Promise + disconnect(): void + subscribe(callback: SerialMessageCallback): () => void +} +export const SerialContext = createContext({ + canUseSerial: false, + hasTriedAutoconnect: false, + connect: () => Promise.resolve(false), + disconnect: () => {}, + portState: 'closed', + subscribe: () => () => {}, +}) + +export const useSerial = () => useContext(SerialContext) + +type SerialProviderProps = {} + +export const SerialProvider = ({ children }: PropsWithChildren) => { + const [canUseSerial] = useState(() => 'serial' in navigator) + + const [portState, setPortState] = useState('closed') + const [hasTriedAutoconnect, setHasTriedAutoconnect] = useState(false) + const [hasManuallyDisconnected, setHasManuallyDisconnected] = useState(false) + + const portRef = useRef(null) + const readerRef = useRef(null) + const readerClosedPromiseRef = useRef>(Promise.resolve()) + + const currentSubscriberIdRef = useRef(0) + const subscribersRef = useRef>(new Map()) + /** + * Subscribes a callback function to the message event. + * + * @param callback the callback function to subscribe + * @returns an unsubscribe function + */ + const subscribe = (callback: SerialMessageCallback) => { + const id = currentSubscriberIdRef.current + subscribersRef.current.set(id, callback) + currentSubscriberIdRef.current++ + + return () => { + subscribersRef.current.delete(id) + } + } + + /** + * Reads from the given port until it's been closed. + * + * @param port the port to read from + */ + const readUntilClosed = async (port: SerialPort) => { + if (port.readable) { + const textDecoder = new TextDecoderStream() + const readableStreamClosed = port.readable.pipeTo(textDecoder.writable) + readerRef.current = textDecoder.readable.getReader() + + try { + while (true) { + const { value, done } = await readerRef.current.read() + if (done) { + break + } + const timestamp = Date.now() + Array.from(subscribersRef.current).forEach(([name, callback]) => { + callback({ value, timestamp }) + }) + } + } catch (error) { + console.error(error) + } finally { + readerRef.current.releaseLock() + } + + await readableStreamClosed.catch(() => {}) // Ignore the error + } + } + + /** + * Attempts to open the given port. + */ + const openPort = async (port: SerialPort) => { + try { + await port.open({ baudRate: 9600 }) + portRef.current = port + setPortState('open') + setHasManuallyDisconnected(false) + } catch (error) { + setPortState('closed') + console.error('Could not open port') + } + } + + const manualConnectToPort = async () => { + if (canUseSerial && portState === 'closed') { + setPortState('opening') + // const filters = [ + // // Can identify the vendor and product IDs by plugging in the device and visiting: chrome://device-log/ + // // the IDs will be labeled `vid` and `pid`, respectively + // // { + // // usbVendorId: , + // // }, + // ] + try { + const port = await navigator.serial.requestPort({ filters: [] }) + await openPort(port) + return true + } catch (error) { + setPortState('closed') + console.error('User did not select port') + } + } + return false + } + + const autoConnectToPort = async () => { + if (canUseSerial && portState === 'closed') { + setPortState('opening') + const availablePorts = await navigator.serial.getPorts() + if (availablePorts.length) { + const port = availablePorts[0] + await openPort(port) + return true + } else { + setPortState('closed') + } + setHasTriedAutoconnect(true) + } + return false + } + + const manualDisconnectFromPort = async () => { + if (canUseSerial && portState === 'open') { + const port = portRef.current + if (port) { + setPortState('closing') + + // Cancel any reading from port + readerRef.current?.cancel() + await readerClosedPromiseRef.current + readerRef.current = null + + // Close and nullify the port + await port.close() + portRef.current = null + + // Update port state + setHasManuallyDisconnected(true) + setHasTriedAutoconnect(false) + setPortState('closed') + } + } + } + + /** + * Event handler for when the port is disconnected unexpectedly. + */ + const onPortDisconnect = async () => { + // Wait for the reader to finish it's current loop + await readerClosedPromiseRef.current + // Update state + readerRef.current = null + readerClosedPromiseRef.current = Promise.resolve() + portRef.current = null + setHasTriedAutoconnect(false) + setPortState('closed') + } + + // Handles attaching the reader and disconnect listener when the port is open + useEffect(() => { + const port = portRef.current + if (portState === 'open' && port) { + // When the port is open, read until closed + const aborted = { current: false } + readerRef.current?.cancel() + readerClosedPromiseRef.current.then(() => { + if (!aborted.current) { + readerRef.current = null + readerClosedPromiseRef.current = readUntilClosed(port) + } + }) + + // Attach a listener for when the device is disconnected + navigator.serial.addEventListener('disconnect', onPortDisconnect) + + return () => { + aborted.current = true + navigator.serial.removeEventListener('disconnect', onPortDisconnect) + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [portState]) + + // Tries to auto-connect to a port, if possible + useEffect(() => { + if ( + canUseSerial && + !hasManuallyDisconnected && + !hasTriedAutoconnect && + portState === 'closed' + ) { + autoConnectToPort() + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [canUseSerial, hasManuallyDisconnected, hasTriedAutoconnect, portState]) + + return ( + + {children} + + ) +} diff --git a/web-app/app/root.tsx b/web-app/app/root.tsx index a829476..290cb3e 100644 --- a/web-app/app/root.tsx +++ b/web-app/app/root.tsx @@ -1,28 +1,36 @@ // import { type LinksFunction, type MetaFunction } from '@remix-run/node' -import { Links, LiveReload, Meta, Outlet, Scripts, ScrollRestoration } from '@remix-run/react' +import type { LinksFunction } from '@remix-run/node' +import { + Links, + Meta, + Outlet, + Scripts, + ScrollRestoration, + type MetaFunction, +} from '@remix-run/react' import type { PropsWithChildren } from 'react' import './font.css' import './tailwind.css' -// export const meta: MetaFunction = () => { -// return [{ title: 'Macrolev' }] -// } +export const meta: MetaFunction = () => { + return [{ title: 'Macrolev' }] +} -// export const links: LinksFunction = () => { -// return [ -// { -// rel: 'alternate icon', -// type: 'image/png', -// href: '/favicon-32x32.png', -// }, -// { rel: 'apple-touch-icon', href: '/apple-touch-icon.png' }, -// { -// rel: 'manifest', -// href: '/site.webmanifest', -// crossOrigin: 'use-credentials', -// } as const, // necessary to make typescript happy -// ] -// } +export const links: LinksFunction = () => { + return [ + { + rel: 'alternate icon', + type: 'image/png', + href: 'favicon-32x32.png', + }, + { rel: 'apple-touch-icon', href: 'apple-touch-icon.png' }, + { + rel: 'manifest', + href: 'site.webmanifest', + crossOrigin: 'use-credentials', + } as const, // necessary to make typescript happy + ] +} type DocumentProps = PropsWithChildren function Document({ children }: DocumentProps) { @@ -41,7 +49,6 @@ function Document({ children }: DocumentProps) { {children} - ) diff --git a/web-app/app/routes/_layout._index.tsx b/web-app/app/routes/_layout._index.tsx index 19b327c..45bccdb 100644 --- a/web-app/app/routes/_layout._index.tsx +++ b/web-app/app/routes/_layout._index.tsx @@ -33,7 +33,7 @@ export default function Index() {