USB networking (CDC-ECM)
Status: WORKING on macOS (verified on hardware). Plug the XIAO into USB,
the host gets a DHCP lease from the device, and an IRC client connects to
the on-device gateway at 10.97.83.1:6667 over the cable. Implemented in
firmware/src/usbnet/usbnet.cpp (env xiao_wio_usbnet).
What actually worked (the plan below assumed NCM; the real path was ECM):
- Class: CDC-ECM, not NCM. macOS ships a host CDC-ECM driver that
auto-binds; it has no driver that binds a generic NCM gadget. (Linux does
both; Windows wants RNDIS — same vendored
ecm_rndis_device.c.) - ECM-only, not composite. Dropping the USB-CDC console
(
ARDUINO_USB_CDC_ON_BOOT=0+ a manualUSB.begin()) was required: the ACM+ECM composite made macOS load the legacyAppleUSBCDCCompositeDevicedriver, which never activated the link. Cost: no USB console on this build (use the LED status code / a UART probe). - Subnet
10.97.83.1/28, not10.10.10.x.10.10.10.xcollided with a local Docker/VM bridge that owned the whole/24. Pick an obscure range. The DHCP pool is.2–.9(8 leases — the prebuilt lwIP server caps atCONFIG_LWIP_DHCPS_MAX_STATION_NUM=8, so 8 is the real ceiling even though the/28has room for more). This is a point-to-point link, so only one lease is ever used; the pool is headroom, not concurrency. - The killer bug: handing the TinyUSB class driver’s static receive
buffer to
esp_netif_receive()— esp_netif callsheap_caps_free()on it, asserting “free target outside heap areas” and rebooting the device on the first received frame. That one bug presented as a boot-loop,en15flapping, and a macOSAppleUserECMEXC_GUARD (device vanished mid-transaction). Fix: copy each frame into amalloc’d buffer and pass that (with adriver_free_rx_buffercallback) so esp_netif frees heap memory. - esptool’s reset does not boot the app on this chip’s USB-Serial-JTAG, and the DebugMate keeps the XIAO powered — so flashing needs BOOT-button download mode and a full power cycle (disconnect the DebugMate too) to cold-boot the app.
The original NCM-based plan and IDF-hybrid notes are kept below for reference / the Windows-RNDIS and Linux paths.
Original plan (NCM/IDF-hybrid — superseded by the ECM route above)
Section titled “Original plan (NCM/IDF-hybrid — superseded by the ECM route above)”Why this isn’t a one-evening patch
Section titled “Why this isn’t a one-evening patch”The Arduino-ESP32 core exposes TinyUSB CDC/MSC/HID, not the NCM/ECM
network class, and its precompiled libtinyusb is built without
CFG_TUD_NCM. Getting an NCM interface requires building TinyUSB with the
network class enabled and gluing its packet interface into lwIP — i.e. an
ESP-IDF-level build, not an Arduino library drop-in.
The path
Section titled “The path”- Switch
firmware/toframework = arduino, espidfhybrid (PlatformIO supports this; Meshtastic stays pure-Arduino which is why they ship CDC only). Pin ESP-IDF 5.x. Keep all existing code — Arduino API remains available. - Bring in
esp_tinyusb(Espressif component) withCFG_TUD_NCM=1via sdkconfig:CONFIG_TINYUSB_NET_MODE_NCM=y. The S3’s native USB-OTG peripheral handles it; we lose the USB-Serial/JTAG console and gain a TinyUSB CDC console as part of the composite device (NCM + CDC). - lwIP glue: esp_tinyusb provides
tinyusb_net_init()with rx/tx callbacks; attach a lwIP netif (esp_netifwith a custom driver — Espressif ships an example:examples/peripherals/usb/device/tusb_ncm). - DHCP server:
esp_netifDHCPS on the NCM netif, pool of exactly one address (10.10.10.10/30 to host, device = 10.10.10.11). Both addresses configurable (net.usb.addr, ARCHITECTURE.md §Network presentation). - Bind the existing gateway: the IRC listener already accepts on all
netifs (lwIP socket bound to INADDR_ANY) — WiFi and NCM serve the same
lrc::Nodewith zero gateway changes. Telnet TUI :2323 and metrics :8462 come along for free. - nRF52: Adafruit TinyUSB exposes raw class registration; ECM (CDC-ECM works on macOS/Linux; Windows needs RNDIS or WinUSB+driver) via the same step structure. Defer until the ESP32-S3 path is proven.
Acceptance tests
Section titled “Acceptance tests”- macOS/Linux host enumerates a composite NCM+CDC device;
ifconfigshows the interface with a 10.10.10.10 DHCP lease. nc 10.10.10.11 6667+ NICK/USER reaches the gateway (the existing tests/smoke_lrcd.py client script, pointed at the device IP).- Throughput sanity: ≥ 5 Mbit/s iperf through NCM (the radio is the bottleneck after that, as designed).
- Sleep interplay: ACTIVE pinned while the host link is up (POWER.md).
- IDF/Arduino hybrid builds pin you to specific (platform, IDF, Arduino) version triples; choose Tasmota-style frozen versions and record them in platformio.ini comments.
- The native USB port becomes the NCM+CDC composite — flashing falls back to the ROM bootloader over the same port (esptool handles this; document the BOOT-button recovery path for bricked images).