diff --git a/docs/usb-ncm-phone-api-handoff.md b/docs/usb-ncm-phone-api-handoff.md index 9345ea06d..855285ffb 100644 --- a/docs/usb-ncm-phone-api-handoff.md +++ b/docs/usb-ncm-phone-api-handoff.md @@ -26,6 +26,17 @@ Verified on hardware: | Board stays flashable | ✅ many flash cycles | | **Real API session completes** | ✅ `TCPInterface` + `getMyNodeInfo()`, twice in a row, node stays up | +## Enabling on other boards + +The enablement is a reusable fragment: `[usbnet]` in `variants/esp32/usbnet.ini` +carries the build flags, managed components, and sdkconfig any adopter needs, +and documents the adoption recipe (a `_usbnet` env that splices the +fragments in). `[env:meshnology_w12_usbnet]` is the reference consumer. +Eligibility - a native USB-OTG SoC (ESP32-S2/S3/P4) and PSRAM +(`-D BOARD_HAS_PSRAM`) - is enforced at compile time by `#error` guards in +`USBNetEsp32.cpp`, so an ineligible variant fails the build with an +explanation instead of misbehaving on hardware. + ## The crash that blocked this branch - root cause and fix **Symptom:** the node rebooted within seconds whenever a client opened a real diff --git a/src/mesh/usbnet/USBNetEsp32.cpp b/src/mesh/usbnet/USBNetEsp32.cpp index c3abf75c7..d8fdffe9f 100644 --- a/src/mesh/usbnet/USBNetEsp32.cpp +++ b/src/mesh/usbnet/USBNetEsp32.cpp @@ -2,6 +2,21 @@ #if HAS_USB_NET && defined(ARCH_ESP32) +#include + +// Hardware eligibility, enforced here so a variant that sets HAS_USB_NET=1 +// without the prerequisites fails at compile time with an explanation instead +// of misbehaving on hardware (see variants/esp32/usbnet.ini for the adoption +// recipe). +#if !SOC_USB_OTG_SUPPORTED +#error \ + "HAS_USB_NET requires a native USB-OTG SoC (ESP32-S2/S3/P4). C3/C6/H2 only have USB-Serial-JTAG and cannot present a USB device." +#endif +#ifndef BOARD_HAS_PSRAM +#error \ + "HAS_USB_NET requires PSRAM (-D BOARD_HAS_PSRAM). Without PSRAM in the heap the first phone-API session's ~15KB contiguous allocation aborts the node with std::bad_alloc." +#endif + #include "USBNetPolicy.h" #include "configuration.h" #include "mesh/api/WiFiServerAPI.h" diff --git a/variants/esp32/usbnet.ini b/variants/esp32/usbnet.ini new file mode 100644 index 000000000..026ad5a6e --- /dev/null +++ b/variants/esp32/usbnet.ini @@ -0,0 +1,115 @@ +; --------------------------------------------------------------------------- +; USB-Ethernet (CDC-NCM) gadget + phone API over USB ("usbnet") +; +; Presents the node to a USB host as a standard USB Ethernet adaptor so the +; Meshtastic apps can reach the existing TCP phone API (port 4403) over a +; plain USB-C cable - including on iOS/iPadOS in airplane mode, where USB +; serial is not an option (Apple's own drivers win IOKit matching). Apple's +; built-in NCM driver binds it; no dext, no MFi +; (Apple DTS: developer.apple.com/forums/thread/772812). +; +; This section is a reusable fragment, not an env. To enable usbnet for a +; board, add a `_usbnet` env to the board's platformio.ini that +; splices these fragments in. Reference consumer: +; [env:meshnology_w12_usbnet] in variants/esp32s3/meshnology-w12/platformio.ini. +; +; [env:myboard_usbnet] +; extends = env:myboard +; board_level = extra +; extra_scripts = +; ${esp32_common.extra_scripts} +; ${usbnet.extra_scripts} +; build_flags = +; ${env:myboard.build_flags} +; ${usbnet.build_flags} +; custom_component_add = ${usbnet.custom_component_add} +; custom_sdkconfig = +; ${esp32s3_base.custom_sdkconfig} +; ${usbnet.custom_sdkconfig} +; +; Hardware requirements, enforced at compile time in USBNetEsp32.cpp: +; * a native USB-OTG SoC (ESP32-S2/S3/P4) with D+/D- wired to the USB +; connector. C3/C6/H2 only have USB-Serial-JTAG and physically cannot do +; this; boards whose USB-C goes through a CP2102/CH9102 UART bridge +; (e.g. Heltec V3) are equally out. Proxy check: ARDUINO_USB_CDC_ON_BOOT=1 +; in the board's build flags. +; * PSRAM, declared with -D BOARD_HAS_PSRAM. The gadget stack (TinyUSB task, +; NCM transfer buffers, esp_netif, DHCP server, API server) lives in +; internal SRAM; without PSRAM in the heap the first phone-API session's +; ~15KB contiguous allocation aborts the node with std::bad_alloc. +; +; Operational notes for adopters: +; * Once the gadget claims the USB pads the serial console and esptool's +; auto-reset disappear until the next power-on reset. USB_NET_START_DELAY_MS +; (default 8000 in USBNetThread.cpp) is the reflash window after reset; +; raise it per-env while iterating (the W12 pilot uses 30000). +; * A panic reboot re-enters the gadget without ever enumerating +; USB-Serial-JTAG, so panics are invisible on the console. Enable +; coredump-to-flash in the env's custom_sdkconfig if your partition table +; carries a coredump partition (see the W12 env), and mind that +; CONFIG_ESP_COREDUMP_ENABLE_TO_NONE=n must be set explicitly. +; * custom_component_add rewrites the SHARED +; framework-arduinoespressif32/idf_component.yml (backed up once as .orig). +; The platform restores it after the build; bin/restore-idf-component-yml.sh +; is the manual fallback before building other ESP32 envs. +; --------------------------------------------------------------------------- +[usbnet] +build_flags = + -D HAS_USB_NET=1 + ; esp_tinyusb is built as an IDF component, but PlatformIO compiles src/ with + ; its own CPPPATH which does not pick up the project's managed_components. + ; Ours must come first: a stale arduino_tinyusb copy on the path would supply + ; a tusb_config.h where CFG_TUD_NCM is 0, silently disagreeing with the + ; component the app is linked against. USBNetEsp32.cpp static-asserts + ; CFG_TUD_NCM to turn that into a build failure rather than a runtime mystery. + -I managed_components/espressif__esp_tinyusb/include + -I managed_components/espressif__tinyusb/src + ; HybridCompile builds libespressif__{esp_,}tinyusb.a but never adds them to + ; pioarduino-build.py's LIBS, so the app links without them. Pull them in + ; explicitly, inside a group because esp_tinyusb and tinyusb reference each + ; other. The -L search path for those archives is added by + ; extra_scripts/usbnet_link.py -- a relative -L here is silently dropped by + ; PlatformIO. + -Wl,--start-group + -lespressif__esp_tinyusb + -lespressif__tinyusb + -Wl,--end-group +extra_scripts = extra_scripts/usbnet_link.py +custom_component_add = + espressif/esp_tinyusb@^2.2.0 + ; Pin TinyUSB >= 0.21.0 explicitly. esp_tinyusb only requires ">=0.17.0~2", + ; but PR #3630 (first released in 0.21.0) is what makes NCM work on + ; iOS/iPadOS 26 -- without it, SET_ETHERNET_PACKET_FILTER (0x43) and + ; SET/GET_NTB_INPUT_SIZE (0x85/0x86) STALL, iOS deactivates the interface, + ; and DHCP succeeds only ~30% of the time (hathach/tinyusb#3505). + espressif/tinyusb@^0.21.0 +custom_sdkconfig = + CONFIG_TINYUSB_NET_MODE_NCM=y + ; NOTE: do not add CONFIG_TINYUSB_TASK_STACK_SIZE / CONFIG_TINYUSB_TASK_AFFINITY_CPU0 + ; here. Those Kconfig symbols do not exist in esp_tinyusb 2.x - task size, + ; priority and core affinity moved into tinyusb_config_t.task, and anything + ; set here is silently ignored. USBNetEsp32.cpp sets them via + ; TINYUSB_TASK_CUSTOM(). + CONFIG_LWIP_DHCPS=y + ; Suppress DHCP option 6 (DNS). dhcpserver.c only emits it when the DNS offer + ; flag is set -- EXCEPT that with CONFIG_LWIP_DHCPS_ADD_DNS=y (the default) + ; the else-branch vends the server's own address as the DNS server. That + ; would point the host at 192.168.7.1 for name resolution, which answers + ; nothing. Apple's guidance for an accessory is to vend neither a router nor + ; a DNS - vending either makes iOS elect the link, fail its captive-network + ; probe, and kill the interface. + CONFIG_LWIP_DHCPS_ADD_DNS=n + ; KCONFIG NAMESPACE COLLISION -- do not set these to 'y'. + ; Two different components share the CONFIG_TINYUSB_* prefix: + ; * arduino_tinyusb -- the Arduino core's own, NOT built by Meshtastic + ; (HybridCompile omits it), owns CONFIG_TINYUSB_ENABLED + ; * espressif/esp_tinyusb -- the managed component we add above + ; The core's USBCDC.cpp gates on CONFIG_TINYUSB_CDC_ENABLED and USBMSC.cpp on + ; CONFIG_TINYUSB_MSC_ENABLED. Enabling either esp_tinyusb symbol un-gates the + ; matching Arduino file, which then fails to compile because it needs + ; arduino_tinyusb's esp32-hal-tinyusb.h helpers that are not in this build. + ; esp_tinyusb defaults MSC to ON, so it must be turned off explicitly. + ; Consequence: NCM-only for now; a composite CDC-ACM console needs a + ; different mechanism. + CONFIG_TINYUSB_CDC_ENABLED=n + CONFIG_TINYUSB_MSC_ENABLED=n diff --git a/variants/esp32s3/meshnology-w12/platformio.ini b/variants/esp32s3/meshnology-w12/platformio.ini index f75681d31..65b8a85dd 100644 --- a/variants/esp32s3/meshnology-w12/platformio.ini +++ b/variants/esp32s3/meshnology-w12/platformio.ini @@ -34,75 +34,36 @@ build_flags = -I variants/esp32s3/meshnology-w12 ; --------------------------------------------------------------------------- -; USB-Ethernet (CDC-NCM) gadget bring-up target. +; USB-Ethernet (CDC-NCM) gadget target - reference consumer of the shared +; [usbnet] fragment (variants/esp32/usbnet.ini; requirements and the adoption +; recipe are documented there). ; -; Presents the node to a USB-C iPad/iPhone as a standard USB Ethernet adaptor -; so the Meshtastic iOS app can reach the existing TCP phone API (port 4403) -; over a wire, including in airplane mode. Apple's built-in NCM driver binds -; it; no dext, no MFi (Apple DTS: developer.apple.com/forums/thread/772812). -; -; Requires native USB on the connector (S3 D+/D- = GPIO19/20). The W12 -; qualifies: ARDUINO_USB_CDC_ON_BOOT=1 above, and it is already -; requires_dfu = true, so taking over USB-OTG regresses no auto-reset circuit. -; -; NOTE: custom_component_add rewrites the SHARED -; framework-arduinoespressif32/idf_component.yml (backed up once as .orig) and -; is NOT part of the HybridCompile cache key. Run bin/restore-idf-component-yml.sh -; after building this env before building any other ESP32 env. +; The W12 qualifies: native S3 USB on the connector (ARDUINO_USB_CDC_ON_BOOT=1 +; above), PSRAM (BOARD_HAS_PSRAM above), and it is already requires_dfu = true, +; so taking over USB-OTG regresses no auto-reset circuit. ; --------------------------------------------------------------------------- [env:meshnology_w12_usbnet] extends = env:meshnology_w12 board_level = extra extra_scripts = ${esp32_common.extra_scripts} - extra_scripts/usbnet_link.py + ${usbnet.extra_scripts} custom_meshtastic_hw_model_slug = MESHNOLOGY_W12_USBNET custom_meshtastic_display_name = Meshnology W12 (USB-NCM) custom_meshtastic_actively_supported = false build_flags = ${env:meshnology_w12.build_flags} - -D HAS_USB_NET=1 - ; 30s, not 8s: once the gadget claims the USB pads the serial port disappears - ; and esptool has no RTS line to auto-reset with, so every reflash would need a - ; manual BOOT+RST. A 30s window is long enough to start `pio run -t upload` - ; after a reset and keep the dev loop self-service. Lower it for a real build. + ${usbnet.build_flags} + ; 30s, not the 8s code default: once the gadget claims the USB pads the serial + ; port disappears and esptool has no RTS line to auto-reset with, so every + ; reflash would need a manual BOOT+RST. A 30s window is long enough to start + ; `pio run -t upload` after a reset and keep the dev loop self-service. + ; Lower it for a real build. -D USB_NET_START_DELAY_MS=30000 - ; esp_tinyusb is built as an IDF component, but PlatformIO compiles src/ with - ; its own CPPPATH which does not pick up the project's managed_components. - ; Ours must come first: a stale arduino_tinyusb copy on the path would supply a - ; tusb_config.h where CFG_TUD_NCM is 0, silently disagreeing with the component - ; the app is linked against. USBNetEsp32.cpp static-asserts CFG_TUD_NCM to turn - ; that into a build failure rather than a runtime mystery. - -I managed_components/espressif__esp_tinyusb/include - -I managed_components/espressif__tinyusb/src - ; HybridCompile builds libespressif__{esp_,}tinyusb.a but never adds them to - ; pioarduino-build.py's LIBS, so the app links without them. Pull them in - ; explicitly, inside a group because esp_tinyusb and tinyusb reference each - ; other. Two -L paths because the archives land in different places depending - ; on whether the IDF libs were rebuilt for this env (project build dir) or - ; reused from the shared package (framework-libs lib dir, already on LIBPATH). - ; The search path for those archives is added by extra_scripts/usbnet_link.py - ; below -- a relative -L here is silently dropped by PlatformIO. - -Wl,--start-group - -lespressif__esp_tinyusb - -lespressif__tinyusb - -Wl,--end-group -custom_component_add = - espressif/esp_tinyusb@^2.2.0 - ; Pin TinyUSB >= 0.21.0 explicitly. esp_tinyusb only requires ">=0.17.0~2", - ; but PR #3630 (first released in 0.21.0) is what makes NCM work on - ; iOS/iPadOS 26 -- without it, SET_ETHERNET_PACKET_FILTER (0x43) and - ; SET/GET_NTB_INPUT_SIZE (0x85/0x86) STALL, iOS deactivates the interface, - ; and DHCP succeeds only ~30% of the time (hathach/tinyusb#3505). - espressif/tinyusb@^0.21.0 +custom_component_add = ${usbnet.custom_component_add} custom_sdkconfig = ${esp32s3_base.custom_sdkconfig} - CONFIG_TINYUSB_NET_MODE_NCM=y - ; NOTE: do not add CONFIG_TINYUSB_TASK_STACK_SIZE / CONFIG_TINYUSB_TASK_AFFINITY_CPU0 - ; here. Those Kconfig symbols do not exist in esp_tinyusb 2.x - task size, - ; priority and core affinity moved into tinyusb_config_t.task, and anything set - ; here is silently ignored. USBNetEsp32.cpp sets them via TINYUSB_TASK_CUSTOM(). - CONFIG_LWIP_DHCPS=y + ${usbnet.custom_sdkconfig} ; Panic backtraces are otherwise unrecoverable on this build: the console is ; HWCDC on USB-Serial-JTAG, and the gadget takes those pads, so a crash after ; the 30s mark prints into the void. default_16MB.csv already carries a 64KB @@ -118,24 +79,3 @@ custom_sdkconfig = CONFIG_ESP_COREDUMP_DATA_FORMAT_ELF=y CONFIG_ESP_COREDUMP_CHECKSUM_CRC32=y CONFIG_ESP_COREDUMP_MAX_TASKS_NUM=64 - ; Suppress DHCP option 6 (DNS). dhcpserver.c only emits it when the DNS offer - ; flag is set -- EXCEPT that with CONFIG_LWIP_DHCPS_ADD_DNS=y (the default) the - ; else-branch vends the server's own address as the DNS server. That would - ; point the iPad at 192.168.7.1 for name resolution, which answers nothing. - ; Apple's guidance for an accessory is to vend neither a router nor a DNS. - CONFIG_LWIP_DHCPS_ADD_DNS=n - ; KCONFIG NAMESPACE COLLISION -- do not set these to 'y'. - ; Two different components share the CONFIG_TINYUSB_* prefix: - ; * arduino_tinyusb -- the Arduino core's own, NOT built by Meshtastic - ; (HybridCompile omits it), owns CONFIG_TINYUSB_ENABLED - ; * espressif/esp_tinyusb -- the managed component we add above - ; The core's USBCDC.cpp gates on CONFIG_TINYUSB_CDC_ENABLED and USBMSC.cpp on - ; CONFIG_TINYUSB_MSC_ENABLED. Enabling either esp_tinyusb symbol un-gates the - ; matching Arduino file, which then fails to compile because it needs - ; arduino_tinyusb's esp32-hal-tinyusb.h helpers (tinyusb_add_string_descriptor, - ; TUD_CDC_DESCRIPTOR, TU_VERIFY, ...) that are not in this build. - ; esp_tinyusb defaults MSC to ON, so it must be turned off explicitly. - ; Consequence: NCM-only for now; the composite CDC-ACM console needs a - ; different mechanism (see P4). Console stays on UART0 until then. - CONFIG_TINYUSB_CDC_ENABLED=n - CONFIG_TINYUSB_MSC_ENABLED=n