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..3e450af 100644 --- a/firmware/Core/Inc/tusb_config.h +++ b/firmware/Core/Inc/tusb_config.h @@ -88,14 +88,19 @@ extern "C" { #endif //------------- CLASS -------------// -#define CFG_TUD_HID 2 +#define CFG_TUD_HID 1 #define CFG_TUD_CDC 0 #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 + +// 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..bd4536f 100644 --- a/firmware/Core/Inc/usb_descriptors.h +++ b/firmware/Core/Inc/usb_descriptors.h @@ -25,22 +25,20 @@ #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_NUM_KEYBOARD, - ITF_NUM_GENERIC_INOUT, + ITF_NUM_KEYBOARD = 0, + ITF_NUM_VENDOR, ITF_NUM_TOTAL }; -#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..9743161 100644 --- a/firmware/Core/Src/main.c +++ b/firmware/Core/Src/main.c @@ -89,12 +89,21 @@ 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 "heiso.github.io/macrolev/configurator" +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; + /* USER CODE END PV */ /* Private function prototypes -----------------------------------------------*/ @@ -152,6 +161,7 @@ void writeConfig(uint8_t *buffer, uint16_t size) { * @retval int */ int main(void) { + /* USER CODE BEGIN 1 */ /* USER CODE END 1 */ @@ -187,8 +197,7 @@ int main(void) { /* Infinite loop */ /* USER CODE BEGIN WHILE */ while (1) { - uint32_t start = HAL_GetTick(); - + // MARK: Main loop tud_task(); key_triggered = 0; @@ -237,26 +246,53 @@ int main(void) { } } - if ((should_send_consumer_report || should_send_keyboard_report) && tud_hid_n_ready(ITF_NUM_KEYBOARD)) { + // if (mode == USB_MODE_SERIAL) { + // if (tud_cdc_connected()) { + // if (tud_cdc_available()) { + // uint8_t buffer[CFG_TUD_CDC_RX_BUFSIZE] = {0}; + // uint32_t count = tud_cdc_read(buffer, sizeof(buffer)); + // // tud_cdc_write(buffer, count); + // // tud_cdc_write_str("_cdc_\r\n"); + // // tud_cdc_write_flush(); + + // writeConfig(buffer, count); + + // init_keys(); + // tud_cdc_write(&user_config, 3); + // tud_cdc_write_str('EOT'); + // tud_cdc_write_flush(); + // } else { + // // tud_cdc_write(&user_config, sizeof(user_config)); + // // tud_cdc_write_flush(); + // } + // } + + if (web_serial_connected) { + if (tud_vendor_available()) { + uint8_t buffer[CFG_TUD_VENDOR_RX_BUFSIZE]; + uint32_t count = tud_vendor_read(buffer, sizeof(buffer)); + writeConfig(buffer, 3); + + init_keys(); + tud_vendor_write(&user_config, 3); + 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_n_report(ITF_NUM_KEYBOARD, REPORT_ID_CONSUMER_CONTROL, &consumer_report, 2); + tud_hid_report(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); + 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 */ /* USER CODE BEGIN 3 */ @@ -549,58 +585,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 +612,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 +748,6 @@ void update_key_actuation(struct key *key) { } break; } - - add_to_hid_generic_inout_report(key); } void update_key(struct key *key) { @@ -777,6 +758,8 @@ void update_key(struct key *key) { update_key_actuation(key); } +// MARK: tud_* functions + // Invoked when received SET_PROTOCOL request // protocol is either HID_PROTOCOL_BOOT (0) or HID_PROTOCOL_REPORT (1) void tud_hid_set_protocol_cb(uint8_t instance, uint8_t protocol) { @@ -814,12 +797,85 @@ 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(&user_config, 3); +// tud_cdc_write_str('\r\n'); +// tud_cdc_write_flush(); +// } +// } + +// // 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 (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) { + // MARK: WIP + tud_vendor_write(&user_config, 3); + 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..0ab79f9 100644 --- a/firmware/Core/Src/usb_descriptors.c +++ b/firmware/Core/Src/usb_descriptors.c @@ -43,14 +43,17 @@ tusb_desc_device_t const desc_device = { .bLength = sizeof(tusb_desc_device_t), .bDescriptorType = TUSB_DESC_DEVICE, - .bcdUSB = 0x0200, - .bDeviceClass = 0x00, - .bDeviceSubClass = 0x00, - .bDeviceProtocol = 0x00, + .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 + 2, + .idProduct = USB_PID + 11, .bcdDevice = 0x0100, .iManufacturer = 0x01, @@ -76,19 +79,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) { return desc_hid_keyboard_report; - } else if (instance == ITF_NUM_GENERIC_INOUT) { - return desc_hid_custom_report; } return NULL; @@ -98,12 +94,11 @@ 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_TOTAL_LEN (TUD_CONFIG_DESC_LEN + TUD_HID_DESC_LEN + TUD_VENDOR_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 +#define EPNUM_VENDOR_IN 2 +#define EPNUM_VENDOR_OUT 2 uint8_t const desc_configuration[] = { @@ -111,8 +106,10 @@ uint8_t const desc_configuration[] = TUD_CONFIG_DESCRIPTOR(1, ITF_NUM_TOTAL, 0, CONFIG_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_NUM_KEYBOARD, 6, HID_ITF_PROTOCOL_KEYBOARD, sizeof(desc_hid_keyboard_report), 0x80 | EPNUM_KEYBOARD, CFG_TUD_HID_EP_BUFSIZE, 10), + + // Interface number, string index, EP Out & IN address, EP size + TUD_VENDOR_DESCRIPTOR(ITF_NUM_VENDOR, 5, EPNUM_VENDOR_OUT, 0x80 | EPNUM_VENDOR_IN, TUD_OPT_HIGH_SPEED ? 512 : 64), }; // Invoked when received GET CONFIGURATION DESCRIPTOR @@ -123,6 +120,73 @@ uint8_t const *tud_descriptor_configuration_cb(uint8_t index) { return desc_configuration; } +//--------------------------------------------------------------------+ +// 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_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 +206,8 @@ 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 + "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..f4c7002 --- /dev/null +++ b/web-app/app/components/serial-provider.tsx @@ -0,0 +1,298 @@ +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 const EOT = '\r\n' + +export type PortState = 'closed' | 'closing' | 'open' | 'opening' + +export type SerialMessage = { + value: string + timestamp: number +} + +type SerialMessageCallback = (message: SerialMessage) => void + +export interface SerialContextValue { + canUseSerial: boolean + hasTriedAutoconnect: boolean + portState: PortState + connect(): Promise + disconnect(): void + subscribe(callback: SerialMessageCallback): () => void + write(buffer: Uint8Array): void +} +export const SerialContext = createContext({ + canUseSerial: false, + hasTriedAutoconnect: false, + connect: () => Promise.resolve(false), + disconnect: () => {}, + portState: 'closed', + subscribe: () => () => {}, + write: (buffer: Uint8Array) => {}, +}) + +export const useSerial = () => useContext(SerialContext) + +interface 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 reader = port.readable.getReader() + let chunks = '' + + try { + while (true) { + const { value, done } = await reader.read() + const decoded = new TextDecoder().decode(value) + // console.log({ value, done, decoded }) + + chunks += decoded + + if (done || decoded.includes(EOT)) { + console.log('Reading done.') + reader.releaseLock() + break + } + } + } catch (error) { + console.error(error) + throw error + } finally { + reader.releaseLock() + } + + Array.from(subscribersRef.current).forEach(([id, callback]) => { + callback({ value: chunks, timestamp: Date.now() }) + }) + + // const textDecoder = new TextDecoderStream() + // const readableStreamClosed = port.readable.pipeTo(textDecoder.writable) + // readerRef.current = textDecoder.readable.getReader() + + // let messageLeftover = '' + + // try { + // while (true) { + // const { value, done } = await readerRef.current.read() + // if (done) { + // break + // } + + // // Split the given value by the delimiter + // const messages = (messageLeftover + value).split(MESSAGE_DELIMITER) + // // Store any leftover/broken messages for next read + // messageLeftover = messages.pop() ?? '' + + // const timestamp = Date.now() + // Array.from(subscribersRef.current).forEach(([id, callback]) => { + // messages.forEach((value) => { + // 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) { + portRef.current = null + setPortState('closed') + console.error('Could not open port') + } + } + + const manualConnectToPort = async () => { + if (canUseSerial && portState === 'closed') { + setPortState('opening') + const filters = [ + { + usbVendorId: 0xcafe, + }, + ] + 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') + } + + async function writeToPort(buffer: Uint8Array) { + const writer = portRef.current?.writable?.getWriter() + if (writer) { + await writer.write(buffer) + writer.releaseLock() + } + } + + // 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, close the last read and wait for that to finish + const aborted = { current: false } + readerRef.current?.cancel() + readerClosedPromiseRef.current.then(() => { + readerRef.current = null + // Then if the effect hasn't rerun, start reading again + if (!aborted.current) { + 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/devices.context.tsx b/web-app/app/devices.context.tsx deleted file mode 100644 index 1799ef8..0000000 --- a/web-app/app/devices.context.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createContext } from 'react' - -const devices: HIDDevice[] = [] -export const DevicesContext = createContext(devices) diff --git a/web-app/app/root.tsx b/web-app/app/root.tsx index a829476..0fdd69c 100644 --- a/web-app/app/root.tsx +++ b/web-app/app/root.tsx @@ -1,28 +1,35 @@ -// 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 +48,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() {