🥑 meltybrain avocado
0

Configure Feed

Select the types of activity you want to include in your feed.

feat: add RP2350 bench tools for receiver flashing and motor checkout

Two temporary tool firmwares to speed up bring-up, separate from the
flight firmware:

- ELRS passthrough: makes the board act as a Betaflight-style serial
bridge so the ELRS receiver can be flashed or configured over USB
with no adapter, transmitter, or WiFi.
- Motor/channel bench test: spins each motor independently at a low
capped throttle from the keyboard and shows live RC channel values,
so wiring can be verified before trusting the spin-control loop.

💘 Generated with Crush

Assisted-by: Crush:us.anthropic.claude-opus-4-8

+548
+40
firmware/motortest/CMakeLists.txt
··· 1 + cmake_minimum_required(VERSION 3.13) 2 + 3 + set(CMAKE_EXPORT_COMPILE_COMMANDS ON) 4 + 5 + include(${CMAKE_CURRENT_SOURCE_DIR}/../pico_sdk_import.cmake) 6 + 7 + project(motortest C CXX ASM) 8 + 9 + set(CMAKE_C_STANDARD 11) 10 + set(CMAKE_CXX_STANDARD 17) 11 + 12 + pico_sdk_init() 13 + 14 + add_executable(motortest 15 + main.c 16 + ../src/dshot/dshot.c 17 + ../src/crsf/crsf.c 18 + ) 19 + 20 + pico_generate_pio_header(motortest 21 + ${CMAKE_CURRENT_SOURCE_DIR}/../src/dshot/dshot_bidir_600.pio) 22 + 23 + target_include_directories(motortest PRIVATE 24 + ${CMAKE_CURRENT_SOURCE_DIR}/../include # hw_pins.h 25 + ${CMAKE_CURRENT_SOURCE_DIR}/../src # dshot/, crsf/ 26 + ${CMAKE_CURRENT_SOURCE_DIR}/../src/dshot 27 + ${CMAKE_CURRENT_SOURCE_DIR}/../src/crsf 28 + ) 29 + 30 + target_link_libraries(motortest PRIVATE 31 + pico_stdlib 32 + hardware_pio 33 + hardware_uart 34 + ) 35 + 36 + pico_add_extra_outputs(motortest) 37 + 38 + # Console + keyboard over USB CDC. Supports picotool -f to exit. 39 + pico_enable_stdio_usb(motortest 1) 40 + pico_enable_stdio_uart(motortest 0)
+133
firmware/motortest/main.c
··· 1 + /* 2 + * Motor + channel bench test for the XIAO RP2350. 3 + * 4 + * TEMPORARY tool image, not the flight controller. It lets you spin each 5 + * ESC/motor independently at a low, capped throttle and watch the raw CRSF 6 + * channel values, so you can verify wiring before trusting the real 7 + * spin-control firmware. Restore the flight firmware when done: 8 + * picotool load -x -f build/holy_guacamole.uf2 9 + * 10 + * ┌───────────────────────── SAFETY ─────────────────────────┐ 11 + * │ REMOVE THE WEAPON BLADE. Get the wheels off the bench │ 12 + * │ (prop the robot up) so it can't drive itself off the │ 13 + * │ edge. Throttle is capped low but motors still move. │ 14 + * └───────────────────────────────────────────────────────────┘ 15 + * 16 + * Drive it from a serial terminal that sends keys live (screen, not cat): 17 + * screen /dev/cu.usbmodemXXXX 115200 18 + * 19 + * Keys: 20 + * a : spin motor A (DSHOT1, GPIO6) at the cap 21 + * b : spin motor B (DSHOT2, GPIO7) at the cap 22 + * c : spin both 23 + * s / space : stop 24 + * 1..5 : set throttle cap to 5/10/15/20/25 % 25 + * + / - : nudge cap up/down 26 + * 27 + * Any running motor auto-stops after SPIN_TIMEOUT_MS as a dead-man. 28 + */ 29 + 30 + #include <stdio.h> 31 + #include "pico/stdlib.h" 32 + #include "hardware/pio.h" 33 + #include "dshot.h" 34 + #include "crsf.h" 35 + #include "hw_pins.h" 36 + 37 + #define SPIN_TIMEOUT_MS 8000 /* auto-stop a running motor after this */ 38 + #define CAP_DEFAULT 0.10f 39 + #define CAP_MAX 0.30f 40 + #define CAP_STEP 0.05f 41 + 42 + static float thr_a, thr_b; /* commanded throttle fractions */ 43 + static float cap = CAP_DEFAULT; 44 + static uint32_t last_cmd_ms; 45 + 46 + static void stop(void) { thr_a = 0.0f; thr_b = 0.0f; } 47 + 48 + static void apply_key(int ch) { 49 + switch (ch) { 50 + case 'a': thr_a = cap; thr_b = 0.0f; break; 51 + case 'b': thr_b = cap; thr_a = 0.0f; break; 52 + case 'c': thr_a = cap; thr_b = cap; break; 53 + case 's': case ' ': stop(); break; 54 + case '+': cap += CAP_STEP; break; 55 + case '-': cap -= CAP_STEP; break; 56 + case '1': cap = 0.05f; break; 57 + case '2': cap = 0.10f; break; 58 + case '3': cap = 0.15f; break; 59 + case '4': cap = 0.20f; break; 60 + case '5': cap = 0.25f; break; 61 + default: return; 62 + } 63 + if (cap < 0.0f) cap = 0.0f; 64 + if (cap > CAP_MAX) cap = CAP_MAX; 65 + /* Re-apply the cap to whatever is already spinning. */ 66 + if (thr_a > 0.0f) thr_a = cap; 67 + if (thr_b > 0.0f) thr_b = cap; 68 + last_cmd_ms = to_ms_since_boot(get_absolute_time()); 69 + printf("\n>> cap=%.0f%% A=%.0f%% B=%.0f%%\n", 70 + (double)(cap * 100), (double)(thr_a * 100), (double)(thr_b * 100)); 71 + } 72 + 73 + int main(void) { 74 + stdio_init_all(); 75 + sleep_ms(2000); /* let USB CDC enumerate before the banner */ 76 + 77 + printf("\n=== Motor + channel bench test ===\n"); 78 + printf("SAFETY: remove the weapon blade, wheels off the bench.\n"); 79 + printf("keys: a=motorA b=motorB c=both s/space=stop 1-5=cap +/-=nudge\n\n"); 80 + 81 + dshot_esc_t esc1, esc2; 82 + if (!dshot_load_program(pio0)) printf("PIO load failed\n"); 83 + if (!dshot_init(&esc1, pio0, PIN_DSHOT1)) printf("DSHOT1 init failed\n"); 84 + if (!dshot_init(&esc2, pio0, PIN_DSHOT2)) printf("DSHOT2 init failed\n"); 85 + crsf_init(); 86 + crsf_state_t rc = {0}; 87 + 88 + /* Arm the ESCs: stream zero throttle for a couple seconds. */ 89 + for (int i = 0; i < 2000; i++) { 90 + dshot_set_throttle(&esc1, 0.0f); 91 + dshot_set_throttle(&esc2, 0.0f); 92 + sleep_ms(1); 93 + } 94 + printf("ESCs armed (zero throttle sent). Ready.\n"); 95 + 96 + last_cmd_ms = to_ms_since_boot(get_absolute_time()); 97 + uint32_t last_print = 0; 98 + 99 + while (1) { 100 + int ch = getchar_timeout_us(0); 101 + if (ch != PICO_ERROR_TIMEOUT) apply_key(ch); 102 + 103 + uint32_t now = to_ms_since_boot(get_absolute_time()); 104 + 105 + /* Dead-man: stop after inactivity if anything is spinning. */ 106 + if ((thr_a > 0.0f || thr_b > 0.0f) && 107 + (now - last_cmd_ms) > SPIN_TIMEOUT_MS) { 108 + stop(); 109 + printf("\n>> auto-stop (%.1fs timeout)\n", SPIN_TIMEOUT_MS / 1000.0); 110 + } 111 + 112 + /* DSHOT must be refreshed continuously (>200 Hz). */ 113 + dshot_set_throttle(&esc1, thr_a); 114 + dshot_set_throttle(&esc2, thr_b); 115 + dshot_read_telemetry(&esc1); 116 + dshot_read_telemetry(&esc2); 117 + 118 + crsf_poll(&rc); 119 + 120 + if (now - last_print > 250) { 121 + last_print = now; 122 + printf("A=%3.0f%% (rpm %5lu) B=%3.0f%% (rpm %5lu) | " 123 + "ch: %4u %4u %4u %4u %4u %4u %s\n", 124 + (double)(thr_a * 100), (unsigned long)esc1.telemetry.rpm, 125 + (double)(thr_b * 100), (unsigned long)esc2.telemetry.rpm, 126 + rc.channels[0], rc.channels[1], rc.channels[2], 127 + rc.channels[3], rc.channels[4], rc.channels[5], 128 + crsf_link_alive(&rc, 500) ? "RC-OK" : "no-rc"); 129 + } 130 + 131 + sleep_us(500); /* ~2 kHz loop */ 132 + } 133 + }
+36
firmware/passthrough/CMakeLists.txt
··· 1 + cmake_minimum_required(VERSION 3.13) 2 + 3 + set(CMAKE_EXPORT_COMPILE_COMMANDS ON) 4 + 5 + # Reuse the parent project's SDK import shim. 6 + include(${CMAKE_CURRENT_SOURCE_DIR}/../pico_sdk_import.cmake) 7 + 8 + project(elrs_passthrough C CXX ASM) 9 + 10 + set(CMAKE_C_STANDARD 11) 11 + set(CMAKE_CXX_STANDARD 17) 12 + 13 + pico_sdk_init() 14 + 15 + add_executable(elrs_passthrough 16 + main.c 17 + usb_descriptors.c 18 + ) 19 + 20 + target_include_directories(elrs_passthrough PRIVATE 21 + ${CMAKE_CURRENT_SOURCE_DIR} # tusb_config.h 22 + ${CMAKE_CURRENT_SOURCE_DIR}/../include # hw_pins.h 23 + ) 24 + 25 + target_link_libraries(elrs_passthrough PRIVATE 26 + pico_stdlib 27 + hardware_uart 28 + hardware_gpio 29 + tinyusb_device 30 + ) 31 + 32 + pico_add_extra_outputs(elrs_passthrough) 33 + 34 + # We drive TinyUSB directly; no pico stdio on USB or UART. 35 + pico_enable_stdio_usb(elrs_passthrough 0) 36 + pico_enable_stdio_uart(elrs_passthrough 0)
+60
firmware/passthrough/README.md
··· 1 + # ELRS Passthrough (RP2350) 2 + 3 + A **temporary tool firmware** that turns the XIAO RP2350 into a Betaflight-style 4 + serial passthrough bridge, so you can flash or configure the ELRS receiver 5 + wired to `uart0` using the **ExpressLRS Configurator** — no USB-to-serial 6 + adapter, no transmitter, no WiFi. 7 + 8 + The receiver is the only thing on `uart0` (GPIO0 = TX → RX's RX, GPIO1 = RX ← 9 + RX's TX, from `../include/hw_pins.h`), which is exactly where a flight 10 + controller sits. This firmware makes the RP2350 answer like one. 11 + 12 + ## How it works 13 + 14 + 1. Presents a USB CDC serial port (raw, no console). 15 + 2. Emulates the minimal Betaflight CLI that `BFinitPassthrough.py` checks: 16 + `#`, `get serialrx_provider|inverted|halfduplex|rx_spi_protocol`, `serial`, 17 + and `serialpassthrough`. 18 + 3. On `serialpassthrough` it becomes a transparent USB↔`uart0` bridge and 19 + mirrors the host's baud onto `uart0`. 20 + 4. The Configurator then sends the ROM training burst + the CRSF 21 + reboot-to-bootloader command through the bridge, and esptool flashes the 22 + receiver over the same wire. 23 + 24 + Until passthrough is active it does **not** forward `uart0` → USB, so the 25 + receiver's CRSF chatter can't corrupt the CLI handshake. 26 + 27 + ## Build 28 + 29 + ```bash 30 + # from repo root, inside the firmware nix shell 31 + nix develop ./firmware --command bash -c \ 32 + 'cmake -B firmware/passthrough/build -S firmware/passthrough \ 33 + -DPICO_PLATFORM=rp2350 -DPICO_BOARD=pico2 && \ 34 + cmake --build firmware/passthrough/build -j' 35 + ``` 36 + 37 + Output: `firmware/passthrough/build/elrs_passthrough.uf2`. 38 + 39 + ## Use it 40 + 41 + 1. **Flash the tool** (reboots the running board into BOOTSEL automatically): 42 + ```bash 43 + picotool load -x firmware/passthrough/build/elrs_passthrough.uf2 44 + ``` 45 + 2. Open the **ExpressLRS Configurator**, pick your receiver target 46 + (e.g. BetaFPV 2.4GHz RX), Flashing Method = **BetaflightPassthrough**, 47 + select the RP2350's serial port, and Flash. (Or use it just to open the 48 + web/settings if you only want to reconfigure.) 49 + 3. **Restore the flight firmware** when done: 50 + ```bash 51 + picotool load -x firmware/build/holy_guacamole.uf2 52 + ``` 53 + 54 + ## Notes 55 + 56 + - This image is standalone; it never runs the flight-control loop. 57 + - Baud is followed live via `tud_cdc_line_coding_cb`, so esptool baud changes 58 + are handled. 59 + - Verified to build and enumerate; the esptool handshake is best confirmed by 60 + an actual flash attempt against the receiver.
+162
firmware/passthrough/main.c
··· 1 + /* 2 + * ELRS passthrough firmware for the XIAO RP2350. 3 + * 4 + * TEMPORARY tool image, not the flight controller. Flash this, use it to 5 + * flash/configure the ELRS receiver wired to uart0, then flash the real 6 + * firmware back (picotool load -x build/holy_guacamole.uf2). 7 + * 8 + * The ELRS receiver's UART is the only thing on uart0 (GPIO0 = TX -> RX's 9 + * RX, GPIO1 = RX <- RX's TX, per include/hw_pins.h). This firmware makes the 10 + * RP2350 sit exactly where a Betaflight flight controller would, so the 11 + * ExpressLRS Configurator's "BetaflightPassthrough" method works against it 12 + * with no extra hardware: 13 + * 14 + * 1. The Configurator opens our USB CDC port and runs a tiny CLI dialogue 15 + * (#, get serialrx_provider, get serialrx_inverted, get 16 + * serialrx_halfduplex, get rx_spi_protocol, serial, serialpassthrough). 17 + * We answer exactly what BFinitPassthrough.py checks for. 18 + * 2. On "serialpassthrough" we drop into a transparent USB<->uart0 bridge 19 + * and mirror the host's requested baud onto uart0. 20 + * 3. The Configurator then sends the ROM training burst + the CRSF 21 + * reboot-to-bootloader command (ec 04 32 62 6c ..) through the bridge, 22 + * and esptool flashes the receiver over the same wire. 23 + * 24 + * We deliberately do NOT forward uart0 -> USB until passthrough is active, 25 + * so the receiver's normal CRSF chatter can't corrupt the CLI dialogue. 26 + */ 27 + 28 + #include <string.h> 29 + #include <stdlib.h> 30 + #include "pico/stdlib.h" 31 + #include "hardware/uart.h" 32 + #include "hardware/gpio.h" 33 + #include "tusb.h" 34 + #include "hw_pins.h" /* PIN_CRSF_TX/RX, CRSF_UART, CRSF_BAUD */ 35 + 36 + /* Default line rate before the host requests one; ELRS CRSF is 420000. */ 37 + #define DEFAULT_BAUD CRSF_BAUD 38 + 39 + typedef enum { MODE_CLI, MODE_PASS } mode_t; 40 + 41 + static mode_t mode = MODE_CLI; 42 + static char line[128]; 43 + static size_t line_len; 44 + 45 + /* ---- USB CDC helpers ---- */ 46 + 47 + static void cdc_str(const char *s) { 48 + tud_cdc_write(s, strlen(s)); 49 + tud_cdc_write_flush(); 50 + } 51 + 52 + /* ---- Minimal Betaflight CLI emulation (CLI phase only) ---- 53 + * 54 + * Responses are shaped to satisfy the exact substring / regex checks in 55 + * ExpressLRS's BFinitPassthrough.py + SerialHelper.py: 56 + * - _validate_serialrx() reads until "# " and looks for " = <value>". 57 + * - serial detection reads \n-delimited lines and regex-matches 58 + * 'serial (\d+) (\d+) ' with the 2nd field's bit 0x40 (Serial RX) set. 59 + */ 60 + static void handle_line(const char *l) { 61 + if (strncmp(l, "get serialrx_provider", 21) == 0) { 62 + cdc_str("serialrx_provider = CRSF\r\n# "); 63 + } else if (strncmp(l, "get serialrx_inverted", 21) == 0) { 64 + cdc_str("serialrx_inverted = OFF\r\n# "); 65 + } else if (strncmp(l, "get serialrx_halfduplex", 23) == 0) { 66 + cdc_str("serialrx_halfduplex = OFF\r\n# "); 67 + } else if (strncmp(l, "get rx_spi_protocol", 19) == 0) { 68 + /* Must NOT contain " = EXPRESSLRS" or the script assumes SPI RX. */ 69 + cdc_str("rx_spi_protocol = AUTO\r\n# "); 70 + } else if (strncmp(l, "serialpassthrough", 17) == 0) { 71 + /* Enter transparent bridge. Baud is mirrored from CDC line coding 72 + * when the host reopens the port, but honor the requested value now 73 + * too in case it doesn't. */ 74 + long baud = DEFAULT_BAUD; 75 + const char *p = l; 76 + while (*p && (*p < '0' || *p > '9')) p++; /* skip "serialpassthrough" */ 77 + while (*p >= '0' && *p <= '9') p++; /* skip UART index */ 78 + while (*p == ' ') p++; 79 + if (*p) baud = strtol(p, NULL, 10); 80 + if (baud > 0) uart_set_baudrate(CRSF_UART, (uint)baud); 81 + /* Drop stale CRSF bytes so the bridge starts clean. */ 82 + while (uart_is_readable(CRSF_UART)) (void)uart_getc(CRSF_UART); 83 + mode = MODE_PASS; 84 + } else if (strcmp(l, "serial") == 0) { 85 + /* One Serial-RX port: index 0, function bitmask 64 (bit 0x40). The 86 + * trailing space matters: the script's regex requires it. */ 87 + cdc_str("serial 0 64 115200 57600 0 115200\r\n# "); 88 + } 89 + /* Unknown commands: stay quiet; the script only depends on the above. */ 90 + } 91 + 92 + static void cli_rx(uint8_t b) { 93 + if (b == '#' && line_len == 0) { 94 + /* Initial CLI wake: reply with a prompt ending in "# ". */ 95 + cdc_str("\r\n# "); 96 + return; 97 + } 98 + if (b == '\n' || b == '\r') { 99 + if (line_len) { 100 + line[line_len] = '\0'; 101 + handle_line(line); 102 + line_len = 0; 103 + } 104 + return; 105 + } 106 + if (line_len < sizeof(line) - 1) line[line_len++] = (char)b; 107 + } 108 + 109 + /* ---- Transparent bridge ---- */ 110 + 111 + static void bridge(void) { 112 + uint8_t buf[64]; 113 + 114 + /* USB -> uart0 */ 115 + while (tud_cdc_available()) { 116 + uint32_t n = tud_cdc_read(buf, sizeof(buf)); 117 + uart_write_blocking(CRSF_UART, buf, n); 118 + } 119 + 120 + /* uart0 -> USB */ 121 + size_t n = 0; 122 + while (uart_is_readable(CRSF_UART) && n < sizeof(buf)) { 123 + buf[n++] = uart_getc(CRSF_UART); 124 + } 125 + if (n) { 126 + tud_cdc_write(buf, n); 127 + tud_cdc_write_flush(); 128 + } 129 + } 130 + 131 + /* TinyUSB callback: mirror the host's requested baud onto uart0 once we're 132 + * bridging. During the CLI phase the CDC baud is irrelevant to uart0. */ 133 + void tud_cdc_line_coding_cb(uint8_t itf, cdc_line_coding_t const *coding) { 134 + (void)itf; 135 + if (mode == MODE_PASS && coding->bit_rate > 0) { 136 + uart_set_baudrate(CRSF_UART, coding->bit_rate); 137 + } 138 + } 139 + 140 + int main(void) { 141 + /* uart0 to the ELRS receiver. */ 142 + uart_init(CRSF_UART, DEFAULT_BAUD); 143 + gpio_set_function(PIN_CRSF_TX, GPIO_FUNC_UART); 144 + gpio_set_function(PIN_CRSF_RX, GPIO_FUNC_UART); 145 + uart_set_hw_flow(CRSF_UART, false, false); 146 + uart_set_format(CRSF_UART, 8, 1, UART_PARITY_NONE); 147 + uart_set_fifo_enabled(CRSF_UART, true); 148 + 149 + tusb_init(); 150 + 151 + while (1) { 152 + tud_task(); 153 + if (mode == MODE_CLI) { 154 + while (tud_cdc_available()) { 155 + uint8_t b; 156 + if (tud_cdc_read(&b, 1) == 1) cli_rx(b); 157 + } 158 + } else { 159 + bridge(); 160 + } 161 + } 162 + }
+25
firmware/passthrough/tusb_config.h
··· 1 + #pragma once 2 + 3 + /* TinyUSB configuration for the ELRS passthrough CDC device. */ 4 + 5 + #ifndef CFG_TUSB_MCU 6 + #define CFG_TUSB_MCU OPT_MCU_RP2040 /* RP2350 uses the rp2040 port */ 7 + #endif 8 + 9 + #define CFG_TUSB_OS OPT_OS_PICO 10 + #define CFG_TUSB_RHPORT0_MODE OPT_MODE_DEVICE 11 + 12 + #define CFG_TUD_ENABLED 1 13 + #define CFG_TUD_ENDPOINT0_SIZE 64 14 + 15 + /* One CDC ACM interface, nothing else. */ 16 + #define CFG_TUD_CDC 1 17 + #define CFG_TUD_MSC 0 18 + #define CFG_TUD_HID 0 19 + #define CFG_TUD_MIDI 0 20 + #define CFG_TUD_VENDOR 0 21 + 22 + /* Generous buffers: esptool bursts data during flashing. */ 23 + #define CFG_TUD_CDC_RX_BUFSIZE 512 24 + #define CFG_TUD_CDC_TX_BUFSIZE 512 25 + #define CFG_TUD_CDC_EP_BUFSIZE 64
+92
firmware/passthrough/usb_descriptors.c
··· 1 + /* 2 + * USB descriptors for the ELRS passthrough CDC device. 3 + * Single CDC ACM interface (virtual serial port). 4 + */ 5 + 6 + #include "tusb.h" 7 + 8 + #define USB_VID 0xCafe /* TinyUSB example VID; fine for a bench tool */ 9 + #define USB_PID 0x4011 10 + #define USB_BCD 0x0200 11 + 12 + /* ---- Device descriptor ---- */ 13 + 14 + static const tusb_desc_device_t desc_device = { 15 + .bLength = sizeof(tusb_desc_device_t), 16 + .bDescriptorType = TUSB_DESC_DEVICE, 17 + .bcdUSB = USB_BCD, 18 + /* Use IAD so the CDC control + data interfaces are grouped. */ 19 + .bDeviceClass = TUSB_CLASS_MISC, 20 + .bDeviceSubClass = MISC_SUBCLASS_COMMON, 21 + .bDeviceProtocol = MISC_PROTOCOL_IAD, 22 + .bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE, 23 + .idVendor = USB_VID, 24 + .idProduct = USB_PID, 25 + .bcdDevice = 0x0100, 26 + .iManufacturer = 0x01, 27 + .iProduct = 0x02, 28 + .iSerialNumber = 0x03, 29 + .bNumConfigurations = 0x01, 30 + }; 31 + 32 + uint8_t const *tud_descriptor_device_cb(void) { 33 + return (uint8_t const *)&desc_device; 34 + } 35 + 36 + /* ---- Configuration descriptor ---- */ 37 + 38 + enum { ITF_NUM_CDC = 0, ITF_NUM_CDC_DATA, ITF_NUM_TOTAL }; 39 + 40 + #define EPNUM_CDC_NOTIF 0x81 41 + #define EPNUM_CDC_OUT 0x02 42 + #define EPNUM_CDC_IN 0x82 43 + 44 + #define CONFIG_TOTAL_LEN (TUD_CONFIG_DESC_LEN + TUD_CDC_DESC_LEN) 45 + 46 + static const uint8_t desc_configuration[] = { 47 + TUD_CONFIG_DESCRIPTOR(1, ITF_NUM_TOTAL, 0, CONFIG_TOTAL_LEN, 48 + 0x00, 100), 49 + TUD_CDC_DESCRIPTOR(ITF_NUM_CDC, 4, EPNUM_CDC_NOTIF, 8, 50 + EPNUM_CDC_OUT, EPNUM_CDC_IN, 64), 51 + }; 52 + 53 + uint8_t const *tud_descriptor_configuration_cb(uint8_t index) { 54 + (void)index; 55 + return desc_configuration; 56 + } 57 + 58 + /* ---- String descriptors ---- */ 59 + 60 + static const char *string_desc_arr[] = { 61 + (const char[]){0x09, 0x04}, /* 0: English (0x0409) */ 62 + "Holy Guacamole", /* 1: Manufacturer */ 63 + "Guac ELRS Passthrough", /* 2: Product */ 64 + "GUAC-PT-0001", /* 3: Serial */ 65 + "Guac ELRS CDC", /* 4: CDC interface */ 66 + }; 67 + 68 + static uint16_t desc_str[32]; 69 + 70 + uint16_t const *tud_descriptor_string_cb(uint8_t index, uint16_t langid) { 71 + (void)langid; 72 + uint8_t chr_count; 73 + 74 + if (index == 0) { 75 + memcpy(&desc_str[1], string_desc_arr[0], 2); 76 + chr_count = 1; 77 + } else { 78 + if (index >= sizeof(string_desc_arr) / sizeof(string_desc_arr[0])) { 79 + return NULL; 80 + } 81 + const char *str = string_desc_arr[index]; 82 + chr_count = (uint8_t)strlen(str); 83 + if (chr_count > 31) chr_count = 31; 84 + for (uint8_t i = 0; i < chr_count; i++) { 85 + desc_str[1 + i] = str[i]; 86 + } 87 + } 88 + 89 + /* First byte: length (bytes), second: string type. */ 90 + desc_str[0] = (uint16_t)((TUSB_DESC_STRING << 8) | (2 * chr_count + 2)); 91 + return desc_str; 92 + }