Skip to content

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 manual USB.begin()) was required: the ACM+ECM composite made macOS load the legacy AppleUSBCDCCompositeDevice driver, 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, not 10.10.10.x. 10.10.10.x collided 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 at CONFIG_LWIP_DHCPS_MAX_STATION_NUM=8, so 8 is the real ceiling even though the /28 has 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 calls heap_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, en15 flapping, and a macOS AppleUserECM EXC_GUARD (device vanished mid-transaction). Fix: copy each frame into a malloc’d buffer and pass that (with a driver_free_rx_buffer callback) 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)”

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.

  1. Switch firmware/ to framework = arduino, espidf hybrid (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.
  2. Bring in esp_tinyusb (Espressif component) with CFG_TUD_NCM=1 via 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).
  3. lwIP glue: esp_tinyusb provides tinyusb_net_init() with rx/tx callbacks; attach a lwIP netif (esp_netif with a custom driver — Espressif ships an example: examples/peripherals/usb/device/tusb_ncm).
  4. DHCP server: esp_netif DHCPS 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).
  5. 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::Node with zero gateway changes. Telnet TUI :2323 and metrics :8462 come along for free.
  6. 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.
  • macOS/Linux host enumerates a composite NCM+CDC device; ifconfig shows 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).