diff --git a/zephcore/CMakeLists.txt b/zephcore/CMakeLists.txt index 6624557..1d466c6 100644 --- a/zephcore/CMakeLists.txt +++ b/zephcore/CMakeLists.txt @@ -493,11 +493,11 @@ if(CONFIG_ZEPHCORE_ROLE_REPEATER) helpers/TransportKeyStore.cpp helpers/CommonCLI.cpp ) - # USB CDC with 1200 baud touch detection (nRF boards with USB CDC ACM only). - # ESP32 boards use usb_serial (no UDC/CDC ACM), so skip custom USB init. + # Shared USBD CDC ACM init + 1200-baud DFU + DTR event/callback module. + # ESP32 boards use usb_serial (no UDC/CDC ACM), so skip when not present. if(NOT CONFIG_CDC_ACM_SERIAL_INITIALIZE_AT_BOOT AND (CONFIG_USB_CDC_ACM OR CONFIG_USBD_CDC_ACM_CLASS)) target_sources(app PRIVATE - adapters/usb/ZephyrRepeaterUSB.cpp + adapters/usb/ZephyrUSBCDC.cpp ) endif() target_include_directories(app PRIVATE @@ -563,11 +563,17 @@ else() target_include_directories(app PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/adapters/transport ) - # USB CDC companion transport (only in debug builds with logging) + # USB CDC companion transport (only in debug builds with logging). + # Pulls in the shared ZephyrUSBCDC module too (USBD lifecycle + DTR callback). if(CONFIG_LOG) target_sources(app PRIVATE adapters/usb/ZephyrCompanionUSB.cpp ) + if(NOT CONFIG_CDC_ACM_SERIAL_INITIALIZE_AT_BOOT AND (CONFIG_USB_CDC_ACM OR CONFIG_USBD_CDC_ACM_CLASS)) + target_sources(app PRIVATE + adapters/usb/ZephyrUSBCDC.cpp + ) + endif() endif() target_include_directories(app PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/adapters/usb diff --git a/zephcore/adapters/usb/ZephyrCompanionUSB.cpp b/zephcore/adapters/usb/ZephyrCompanionUSB.cpp index bd11ef2..9226a1f 100644 --- a/zephcore/adapters/usb/ZephyrCompanionUSB.cpp +++ b/zephcore/adapters/usb/ZephyrCompanionUSB.cpp @@ -4,6 +4,10 @@ * * V3-framed USB CDC for companion mode. Extracted from main_companion.cpp. * Only compiled when CONFIG_LOG is enabled (debug builds). + * + * USBD lifecycle + 1200-baud DFU detection + DTR state tracking live in + * the shared ZephyrUSBCDC module; this file just runs the V3 frame parser + * on top of the CDC ACM UART and reacts to DTR-drop events from there. */ #include @@ -16,14 +20,13 @@ LOG_MODULE_REGISTER(zephcore_usb, CONFIG_ZEPHCORE_USB_LOG_LEVEL); #include -#include #include #include "ZephyrCompanionUSB.h" +#include "ZephyrUSBCDC.h" /* MAX_FRAME_SIZE defined in CompanionMesh.h */ -#define USB_DTR_CHECK_MS 10000 /* USB DTR poll interval - 10s for power savings */ #define USB_RING_BUF_SIZE 512 /* USB RX ring buffer size */ #define USB_FRAME_TIMEOUT_MS 2000 /* Partial frame timeout - reset parser after 2s of no completion */ @@ -35,31 +38,16 @@ static uint8_t usb_rx_buf[MAX_FRAME_SIZE + 2]; /* +2 for length prefix */ static uint16_t usb_rx_idx; static uint16_t usb_frame_len; /* Expected frame length (0 = waiting for header) */ static uint32_t usb_frame_start_time; /* Timestamp of first byte in current frame */ -static bool usb_dtr_active; /* Pointers to mesh event infrastructure (set by init) */ static struct k_event *s_mesh_events; static struct k_work *s_rx_work; static uint32_t s_mesh_event_ble_rx; -static mesh::ZephyrBoard *s_board; /* Work items */ static void usb_rx_work_fn(struct k_work *work); -static void usb_dtr_check_work_fn(struct k_work *work); K_WORK_DEFINE(usb_rx_work, usb_rx_work_fn); -K_WORK_DELAYABLE_DEFINE(usb_dtr_check_work, usb_dtr_check_work_fn); - -/** - * Reboot into the Adafruit nRF52 bootloader DFU mode. - * Triggered by host opening USB CDC at 1200 baud then dropping DTR. - */ -static void reboot_to_bootloader_dfu(void) -{ - LOG_INF("*** REBOOTING TO BOOTLOADER DFU ***"); - s_board->rebootToBootloader(); - CODE_UNREACHABLE; -} /* USB CDC UART interrupt callback - puts bytes in ring buffer */ static void usb_uart_isr(const struct device *dev, void *user_data) @@ -160,51 +148,21 @@ static void usb_rx_work_fn(struct k_work *work) } } -/* USB DTR check - monitors DTR signal for disconnect detection - * AND detects 1200bps baud rate for bootloader DFU trigger. */ -static void usb_dtr_check_work_fn(struct k_work *work) +/* DTR-transition callback from the shared ZephyrUSBCDC module. + * On drop: host closed the port → reset parser, hand control back to BLE. */ +static void on_dtr_change(bool dtr_active) { - ARG_UNUSED(work); - - if (!usb_dev) { + if (dtr_active) { return; } - - uint32_t dtr = 0; - int err = uart_line_ctrl_get(usb_dev, UART_LINE_CTRL_DTR, &dtr); - if (err) { - /* Line control not supported or error */ - k_work_schedule(&usb_dtr_check_work, K_MSEC(USB_DTR_CHECK_MS)); - return; + LOG_INF("usb_dtr: DTR dropped, USB disconnected"); + if (zephcore_ble_get_active_iface() == ZEPHCORE_IFACE_USB) { + zephcore_ble_set_active_iface(ZEPHCORE_IFACE_NONE); + LOG_INF("usb_dtr: active_iface = IFACE_NONE"); } - - bool dtr_now = (dtr != 0); - - /* 1200bps DFU trigger: when host opens port at 1200 baud then - * drops DTR, reboot into bootloader DFU mode. - * This is the Arduino convention used by Adafruit nRF52 boards. */ - if (usb_dtr_active && !dtr_now) { - uint32_t baud = 0; - uart_line_ctrl_get(usb_dev, UART_LINE_CTRL_BAUD_RATE, &baud); - if (baud == 1200) { - reboot_to_bootloader_dfu(); - /* Never returns */ - } - - /* Normal DTR drop - USB disconnected */ - LOG_INF("usb_dtr: DTR dropped, USB disconnected"); - if (zephcore_ble_get_active_iface() == ZEPHCORE_IFACE_USB) { - zephcore_ble_set_active_iface(ZEPHCORE_IFACE_NONE); - LOG_INF("usb_dtr: active_iface = IFACE_NONE"); - } - /* Reset RX state */ - ring_buf_reset(&usb_ring_buf); - usb_frame_len = 0; - usb_rx_idx = 0; - } - - usb_dtr_active = dtr_now; - k_work_schedule(&usb_dtr_check_work, K_MSEC(USB_DTR_CHECK_MS)); + ring_buf_reset(&usb_ring_buf); + usb_frame_len = 0; + usb_rx_idx = 0; } /* Send frame over USB with V3 length prefix */ @@ -242,10 +200,11 @@ void zephcore_usb_companion_init(struct k_event *mesh_events, uint32_t mesh_event_ble_rx, void *board) { + ARG_UNUSED(board); + s_mesh_events = mesh_events; s_rx_work = rx_work; s_mesh_event_ble_rx = mesh_event_ble_rx; - s_board = static_cast(board); #if DT_HAS_COMPAT_STATUS_OKAY(zephyr_cdc_acm_uart) usb_dev = DEVICE_DT_GET_ONE(zephyr_cdc_acm_uart); @@ -257,8 +216,9 @@ void zephcore_usb_companion_init(struct k_event *mesh_events, uart_irq_callback_set(usb_dev, usb_uart_isr); uart_irq_rx_enable(usb_dev); - /* Start DTR monitoring to detect terminal disconnect */ - k_work_schedule(&usb_dtr_check_work, K_MSEC(USB_DTR_CHECK_MS)); + /* DTR state changes (including disconnect) reach us via the + * shared usbd_msg_callback — no polling work needed. */ + zephcore_usbd_set_dtr_cb(on_dtr_change); } else { LOG_WRN("USB CDC device not ready"); usb_dev = NULL; diff --git a/zephcore/adapters/usb/ZephyrRepeaterUSB.cpp b/zephcore/adapters/usb/ZephyrRepeaterUSB.cpp deleted file mode 100644 index f68d10a..0000000 --- a/zephcore/adapters/usb/ZephyrRepeaterUSB.cpp +++ /dev/null @@ -1,203 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * ZephCore - USB CDC ACM with 1200 baud touch detection (Repeater) - * - * This module initializes USB CDC ACM with a message callback to detect - * 1200 baud touch for entering UF2 bootloader mode. - */ - -#include - -#include -#include -#include -#include -#include -#include - -#include -LOG_MODULE_REGISTER(zephcore_usb_repeater, CONFIG_ZEPHCORE_BOARD_LOG_LEVEL); - -#include - -#include "ZephyrRepeaterUSB.h" - -/* - * USB Device Configuration - * Since we're not using CDC_ACM_SERIAL_INITIALIZE_AT_BOOT, we define our own values. - */ -#define ZEPHCORE_USB_VID 0x2fe3 /* Zephyr Project VID */ -#define ZEPHCORE_USB_PID 0x0004 /* CDC ACM PID */ -#define ZEPHCORE_USB_MAX_POWER 125 /* 250mA in 2mA units */ - -/* - * USB Device Context - based on Zephyr's cdc_acm_serial.c - * but with message callback for 1200 baud touch detection. - */ -USBD_DEVICE_DEFINE(zephcore_usbd, - DEVICE_DT_GET(DT_NODELABEL(zephyr_udc0)), - ZEPHCORE_USB_VID, ZEPHCORE_USB_PID); - -USBD_DESC_LANG_DEFINE(zephcore_lang); -USBD_DESC_MANUFACTURER_DEFINE(zephcore_mfr, "ZephCore"); -USBD_DESC_PRODUCT_DEFINE(zephcore_product, "ZephCore Repeater"); -USBD_DESC_SERIAL_NUMBER_DEFINE(zephcore_sn); - -USBD_DESC_CONFIG_DEFINE(zephcore_fs_cfg_desc, "FS Configuration"); -USBD_DESC_CONFIG_DEFINE(zephcore_hs_cfg_desc, "HS Configuration"); - -USBD_CONFIGURATION_DEFINE(zephcore_fs_config, - 0, /* attributes: bus-powered */ - ZEPHCORE_USB_MAX_POWER, &zephcore_fs_cfg_desc); - -USBD_CONFIGURATION_DEFINE(zephcore_hs_config, - 0, /* attributes: bus-powered */ - ZEPHCORE_USB_MAX_POWER, &zephcore_hs_cfg_desc); - -/* ZephyrBoard instance for bootloader entry */ -static mesh::ZephyrBoard usb_board; - -/* Track DTR state for Arduino-style 1200 baud touch detection. - * Arduino triggers on DTR drop (high→low) while baud rate is 1200. - * adafruit-nrfutil --touch 1200 opens port at 1200 baud (DTR high), - * then closes it (DTR low). We detect both the baud rate change and - * the DTR drop to cover all host tool implementations. */ -static bool dtr_was_active; - -static void enter_bootloader(void) -{ - LOG_WRN("Entering bootloader (1200 baud touch)"); - /* Small delay to let USB disconnect cleanly */ - k_msleep(100); - usb_board.rebootToBootloader(); - /* Never returns */ -} - -/* - * USB message callback - detect 1200 baud touch for bootloader entry. - * Two detection methods (matching Arduino behavior): - * 1. Baud rate set to 1200 (SET_LINE_CODING) - * 2. DTR drop while baud is 1200 (SET_CONTROL_LINE_STATE) - */ -static void usbd_msg_callback(struct usbd_context *const ctx, const struct usbd_msg *msg) -{ - if (msg->type == USBD_MSG_CDC_ACM_LINE_CODING) { - uint32_t baudrate = 0; - int ret = uart_line_ctrl_get(msg->dev, UART_LINE_CTRL_BAUD_RATE, &baudrate); - if (ret == 0) { - LOG_DBG("CDC ACM baud rate: %u", baudrate); - if (baudrate == 1200) { - enter_bootloader(); - } - } - } else if (msg->type == USBD_MSG_CDC_ACM_CONTROL_LINE_STATE) { - uint32_t dtr = 0; - uint32_t baudrate = 0; - uart_line_ctrl_get(msg->dev, UART_LINE_CTRL_DTR, &dtr); - uart_line_ctrl_get(msg->dev, UART_LINE_CTRL_BAUD_RATE, &baudrate); - LOG_DBG("CDC ACM DTR=%u baud=%u", dtr, baudrate); - /* Arduino method: DTR drop (high→low) while baud is 1200 */ - if (dtr_was_active && !dtr && baudrate == 1200) { - enter_bootloader(); - } - dtr_was_active = (dtr != 0); - } -} - -static int register_cdc_acm(struct usbd_context *const uds_ctx, - const enum usbd_speed speed) -{ - struct usbd_config_node *cfg_nd; - int err; - - if (speed == USBD_SPEED_HS) { - cfg_nd = &zephcore_hs_config; - } else { - cfg_nd = &zephcore_fs_config; - } - - err = usbd_add_configuration(uds_ctx, speed, cfg_nd); - if (err) { - LOG_ERR("Failed to add configuration"); - return err; - } - - err = usbd_register_class(&zephcore_usbd, "cdc_acm_0", speed, 1); - if (err) { - LOG_ERR("Failed to register CDC ACM class"); - return err; - } - - return usbd_device_set_code_triple(uds_ctx, speed, - USB_BCC_MISCELLANEOUS, 0x02, 0x01); -} - -extern "C" int zephcore_usbd_init(void) -{ - int err; - - LOG_INF("Initializing ZephCore USB CDC with 1200 baud touch detection"); - - /* Add string descriptors */ - err = usbd_add_descriptor(&zephcore_usbd, &zephcore_lang); - if (err) { - LOG_ERR("Failed to add language descriptor (%d)", err); - return err; - } - - err = usbd_add_descriptor(&zephcore_usbd, &zephcore_mfr); - if (err) { - LOG_ERR("Failed to add manufacturer descriptor (%d)", err); - return err; - } - - err = usbd_add_descriptor(&zephcore_usbd, &zephcore_product); - if (err) { - LOG_ERR("Failed to add product descriptor (%d)", err); - return err; - } - - err = usbd_add_descriptor(&zephcore_usbd, &zephcore_sn); - if (err) { - LOG_ERR("Failed to add serial number descriptor (%d)", err); - return err; - } - - /* Register configurations */ - if (USBD_SUPPORTS_HIGH_SPEED && - usbd_caps_speed(&zephcore_usbd) == USBD_SPEED_HS) { - err = register_cdc_acm(&zephcore_usbd, USBD_SPEED_HS); - if (err) { - return err; - } - } - - err = register_cdc_acm(&zephcore_usbd, USBD_SPEED_FS); - if (err) { - return err; - } - - /* Register message callback for 1200 baud detection */ - err = usbd_msg_register_cb(&zephcore_usbd, usbd_msg_callback); - if (err) { - LOG_ERR("Failed to register message callback (%d)", err); - return err; - } - - /* Initialize USB device */ - err = usbd_init(&zephcore_usbd); - if (err) { - LOG_ERR("Failed to initialize USB device (%d)", err); - return err; - } - - /* Enable USB device */ - err = usbd_enable(&zephcore_usbd); - if (err) { - LOG_ERR("Failed to enable USB device (%d)", err); - return err; - } - - LOG_INF("ZephCore USB CDC initialized"); - return 0; -} diff --git a/zephcore/adapters/usb/ZephyrRepeaterUSB.h b/zephcore/adapters/usb/ZephyrRepeaterUSB.h deleted file mode 100644 index 4634acf..0000000 --- a/zephcore/adapters/usb/ZephyrRepeaterUSB.h +++ /dev/null @@ -1,32 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * ZephCore - USB CDC ACM with 1200 baud touch detection (Repeater) - * - * Replaces Zephyr's CDC_ACM_SERIAL_INITIALIZE_AT_BOOT with custom init - * that includes a message callback for detecting 1200 baud touch to - * enter UF2 bootloader mode. - */ - -#ifndef ZEPHCORE_ZEPHYR_REPEATER_USB_H -#define ZEPHCORE_ZEPHYR_REPEATER_USB_H - -#ifdef __cplusplus -extern "C" { -#endif - -/** - * @brief Initialize USB CDC ACM with 1200 baud touch detection. - * - * This replaces Zephyr's default CDC_ACM_SERIAL_INITIALIZE_AT_BOOT - * initialization with our own that includes a message callback for - * detecting 1200 baud touch to enter bootloader mode. - * - * @return 0 on success, negative error code on failure. - */ -int zephcore_usbd_init(void); - -#ifdef __cplusplus -} -#endif - -#endif /* ZEPHCORE_ZEPHYR_REPEATER_USB_H */ diff --git a/zephcore/adapters/usb/ZephyrUSBCDC.cpp b/zephcore/adapters/usb/ZephyrUSBCDC.cpp new file mode 100644 index 0000000..12293ab --- /dev/null +++ b/zephcore/adapters/usb/ZephyrUSBCDC.cpp @@ -0,0 +1,255 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * ZephCore — Unified USB CDC ACM init (companion + repeater + observer) + * + * See ZephyrUSBCDC.h for the contract. This implementation: + * - Defines a single USBD context + CDC ACM class + * - Registers usbd_msg_callback so the host's SET_CONTROL_LINE_STATE and + * SET_LINE_CODING requests reach us as events (no DTR polling) + * - Emits a k_event when DTR transitions high — the boot path waits on it + * instead of a fixed sleep + * - Fires a user callback on every DTR transition (companion: reset RX + + * flip active_iface on drop) + * - Handles the Arduino-style 1200-baud touch → reboot-to-bootloader flow + * for nRF52 boards + */ + +#include "ZephyrUSBCDC.h" + +#include +#include + +#include +#include +#include +#include +#include + +#include +LOG_MODULE_REGISTER(zephcore_usbd, CONFIG_ZEPHCORE_BOARD_LOG_LEVEL); + +#include + +/* ===== Per-role descriptor strings ====== */ +#define ZEPHCORE_USB_VID 0x2fe3 /* Zephyr Project VID */ + +#if defined(ZEPHCORE_REPEATER) +#define ZEPHCORE_USB_PID 0x0004 +#define ZEPHCORE_USB_PRODUCT "ZephCore Repeater" +#elif defined(ZEPHCORE_OBSERVER) +#define ZEPHCORE_USB_PID 0x0005 +#define ZEPHCORE_USB_PRODUCT "ZephCore Observer" +#else +#define ZEPHCORE_USB_PID 0x0006 +#define ZEPHCORE_USB_PRODUCT "ZephCore Companion" +#endif + +#define ZEPHCORE_USB_MAX_POWER 125 /* 250 mA in 2 mA units */ + +/* ===== USBD device + descriptors ===== */ +USBD_DEVICE_DEFINE(zephcore_usbd, + DEVICE_DT_GET(DT_NODELABEL(zephyr_udc0)), + ZEPHCORE_USB_VID, ZEPHCORE_USB_PID); + +USBD_DESC_LANG_DEFINE(zephcore_lang); +USBD_DESC_MANUFACTURER_DEFINE(zephcore_mfr, "ZephCore"); +USBD_DESC_PRODUCT_DEFINE(zephcore_product, ZEPHCORE_USB_PRODUCT); +USBD_DESC_SERIAL_NUMBER_DEFINE(zephcore_sn); + +USBD_DESC_CONFIG_DEFINE(zephcore_fs_cfg_desc, "FS Configuration"); +USBD_DESC_CONFIG_DEFINE(zephcore_hs_cfg_desc, "HS Configuration"); + +USBD_CONFIGURATION_DEFINE(zephcore_fs_config, + 0, ZEPHCORE_USB_MAX_POWER, &zephcore_fs_cfg_desc); +USBD_CONFIGURATION_DEFINE(zephcore_hs_config, + 0, ZEPHCORE_USB_MAX_POWER, &zephcore_hs_cfg_desc); + +/* ===== Module state ===== */ +#define USB_CDC_DTR_BIT BIT(0) +static K_EVENT_DEFINE(usb_cdc_events); + +static bool s_dtr_active; +static bool s_initialized; +static zephcore_usbd_cdc_dtr_cb_t s_dtr_cb; + +/* ZephyrBoard for bootloader-magic write before reset. The class is stateless + * (only static methods over GPREGRET + sys_reboot), so a local instance here + * is fine even when other roles also have their own instance. */ +static mesh::ZephyrBoard usb_board; + +static void enter_bootloader(void) +{ + LOG_WRN("Entering bootloader (1200 baud touch)"); + /* Let USB disconnect cleanly before the reset */ + k_msleep(100); + usb_board.rebootToBootloader(); + CODE_UNREACHABLE; +} + +/* ===== USBD message callback ===== */ +static void usbd_msg_callback(struct usbd_context *const ctx, + const struct usbd_msg *msg) +{ + ARG_UNUSED(ctx); + + if (msg->type == USBD_MSG_CDC_ACM_LINE_CODING) { + uint32_t baudrate = 0; + if (uart_line_ctrl_get(msg->dev, UART_LINE_CTRL_BAUD_RATE, + &baudrate) == 0) { + LOG_DBG("CDC ACM baud rate: %u", baudrate); + if (baudrate == 1200) { + /* Host explicitly set 1200 baud — DFU intent. + * Adafruit nrfutil --touch 1200 closes the + * port immediately after; do not wait for + * the DTR drop to fire. */ + enter_bootloader(); + } + } + } else if (msg->type == USBD_MSG_CDC_ACM_CONTROL_LINE_STATE) { + uint32_t dtr = 0; + uint32_t baudrate = 0; + uart_line_ctrl_get(msg->dev, UART_LINE_CTRL_DTR, &dtr); + uart_line_ctrl_get(msg->dev, UART_LINE_CTRL_BAUD_RATE, + &baudrate); + LOG_DBG("CDC ACM DTR=%u baud=%u", dtr, baudrate); + + const bool dtr_now = (dtr != 0); + + /* Arduino-style DFU: DTR drop while baud is 1200. Some host + * tools change baud then close (handled by LINE_CODING above), + * others just toggle DTR with baud still at 1200 — catch both. */ + if (s_dtr_active && !dtr_now && baudrate == 1200) { + enter_bootloader(); + } + + s_dtr_active = dtr_now; + + if (dtr_now) { + /* Wake the boot-time waiter; subsequent transitions + * leave the bit set, which is what callers expect + * (a "have we ever seen DTR?" gate). */ + k_event_set(&usb_cdc_events, USB_CDC_DTR_BIT); + } + + if (s_dtr_cb) { + s_dtr_cb(dtr_now); + } + } +} + +/* ===== Setup helpers ===== */ +static int register_cdc_acm(struct usbd_context *const uds_ctx, + const enum usbd_speed speed) +{ + struct usbd_config_node *cfg_nd; + int err; + + if (speed == USBD_SPEED_HS) { + cfg_nd = &zephcore_hs_config; + } else { + cfg_nd = &zephcore_fs_config; + } + + err = usbd_add_configuration(uds_ctx, speed, cfg_nd); + if (err) { + LOG_ERR("Failed to add configuration"); + return err; + } + + err = usbd_register_class(&zephcore_usbd, "cdc_acm_0", speed, 1); + if (err) { + LOG_ERR("Failed to register CDC ACM class"); + return err; + } + + return usbd_device_set_code_triple(uds_ctx, speed, + USB_BCC_MISCELLANEOUS, 0x02, 0x01); +} + +/* ===== Public API ===== */ +extern "C" int zephcore_usbd_init(void) +{ + if (s_initialized) { + return 0; + } + + int err; + + LOG_INF("Initializing ZephCore USB CDC ACM (" ZEPHCORE_USB_PRODUCT ")"); + + err = usbd_add_descriptor(&zephcore_usbd, &zephcore_lang); + if (err) { + LOG_ERR("usbd_add_descriptor(lang) failed: %d", err); + return err; + } + err = usbd_add_descriptor(&zephcore_usbd, &zephcore_mfr); + if (err) { + LOG_ERR("usbd_add_descriptor(mfr) failed: %d", err); + return err; + } + err = usbd_add_descriptor(&zephcore_usbd, &zephcore_product); + if (err) { + LOG_ERR("usbd_add_descriptor(product) failed: %d", err); + return err; + } + err = usbd_add_descriptor(&zephcore_usbd, &zephcore_sn); + if (err) { + LOG_ERR("usbd_add_descriptor(sn) failed: %d", err); + return err; + } + + if (USBD_SUPPORTS_HIGH_SPEED && + usbd_caps_speed(&zephcore_usbd) == USBD_SPEED_HS) { + err = register_cdc_acm(&zephcore_usbd, USBD_SPEED_HS); + if (err) { + return err; + } + } + + err = register_cdc_acm(&zephcore_usbd, USBD_SPEED_FS); + if (err) { + return err; + } + + err = usbd_msg_register_cb(&zephcore_usbd, usbd_msg_callback); + if (err) { + LOG_ERR("usbd_msg_register_cb failed: %d", err); + return err; + } + + err = usbd_init(&zephcore_usbd); + if (err) { + LOG_ERR("usbd_init failed: %d", err); + return err; + } + + err = usbd_enable(&zephcore_usbd); + if (err) { + LOG_ERR("usbd_enable failed: %d", err); + return err; + } + + s_initialized = true; + LOG_INF("ZephCore USB CDC initialized"); + return 0; +} + +extern "C" int zephcore_usbd_wait_dtr(uint32_t timeout_ms) +{ + if (!s_initialized) { + return -ENODEV; + } + uint32_t got = k_event_wait(&usb_cdc_events, USB_CDC_DTR_BIT, + false, K_MSEC(timeout_ms)); + return got ? 0 : -EAGAIN; +} + +extern "C" bool zephcore_usbd_is_dtr_active(void) +{ + return s_dtr_active; +} + +extern "C" void zephcore_usbd_set_dtr_cb(zephcore_usbd_cdc_dtr_cb_t cb) +{ + s_dtr_cb = cb; +} diff --git a/zephcore/adapters/usb/ZephyrUSBCDC.h b/zephcore/adapters/usb/ZephyrUSBCDC.h new file mode 100644 index 0000000..5372640 --- /dev/null +++ b/zephcore/adapters/usb/ZephyrUSBCDC.h @@ -0,0 +1,47 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * ZephCore — Unified USB CDC ACM init (companion + repeater + observer) + * + * Owns the USBD device + CDC ACM class registration, the usbd_msg_callback + * for 1200-baud touch DFU + DTR state tracking, and the boot-waiter event. + * Replaces CONFIG_CDC_ACM_SERIAL_INITIALIZE_AT_BOOT auto-init so we control + * the message callback path and can deliver event-driven DTR notifications + * to consumers (no DTR polling work). + */ + +#ifndef ZEPHCORE_ZEPHYR_USB_CDC_H +#define ZEPHCORE_ZEPHYR_USB_CDC_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* Per-transition DTR callback. Fires on every host CONTROL_LINE_STATE change, + * value = current DTR level. NULL is allowed (cb cleared). */ +typedef void (*zephcore_usbd_cdc_dtr_cb_t)(bool dtr_active); + +/* Initialize USB device + CDC ACM class and enumerate. + * Idempotent; subsequent calls are no-ops. + * Returns 0 on success or a negative errno. */ +int zephcore_usbd_init(void); + +/* Block until the host opens the CDC ACM port (DTR transitions high) or the + * timeout elapses. Returns 0 on DTR-high, -EAGAIN on timeout, -ENODEV if + * zephcore_usbd_init() has not been called. */ +int zephcore_usbd_wait_dtr(uint32_t timeout_ms); + +/* Last-known DTR state from the most recent CONTROL_LINE_STATE message. */ +bool zephcore_usbd_is_dtr_active(void); + +/* Register a callback fired on every DTR transition. + * Companion uses this to reset its RX state and flip active_iface on drop. */ +void zephcore_usbd_set_dtr_cb(zephcore_usbd_cdc_dtr_cb_t cb); + +#ifdef __cplusplus +} +#endif + +#endif /* ZEPHCORE_ZEPHYR_USB_CDC_H */ diff --git a/zephcore/boards/common/nrf52_common.conf b/zephcore/boards/common/nrf52_common.conf index c8cbc27..eb0ce10 100644 --- a/zephcore/boards/common/nrf52_common.conf +++ b/zephcore/boards/common/nrf52_common.conf @@ -9,9 +9,13 @@ CONFIG_BUILD_OUTPUT_UF2=y CONFIG_USE_DT_CODE_PARTITION=y # ========== USB CDC ACM (nRF52 USB) ========== +# We own usbd init via adapters/usb/ZephyrUSBCDC.cpp so the same +# usbd_msg_callback can handle 1200-baud DFU detection and signal DTR +# transitions to the boot-waiter event + companion disconnect callback. +# Without =n here the Zephyr sample-style auto-init runs at SYS_INIT +# with no public hook for us. CONFIG_USB_DEVICE_STACK_NEXT=y -CONFIG_CDC_ACM_SERIAL_INITIALIZE_AT_BOOT=y -CONFIG_CDC_ACM_SERIAL_PRODUCT_STRING="ZephCore" +CONFIG_CDC_ACM_SERIAL_INITIALIZE_AT_BOOT=n CONFIG_UART_LINE_CTRL=y # ========== BLE: nRF52-specific controller settings ========== diff --git a/zephcore/boards/common/repeater.conf b/zephcore/boards/common/repeater.conf index d629f9c..3170e2c 100644 --- a/zephcore/boards/common/repeater.conf +++ b/zephcore/boards/common/repeater.conf @@ -18,11 +18,6 @@ CONFIG_BT=n # for Ed25519 key generation, LoRa random delays, etc. CONFIG_ENTROPY_GENERATOR=y -# ========== USB CDC ========== -# Custom USB init with 1200 baud touch detection for UF2 bootloader (nRF52 only). -# ESP32 boards use usb_serial — this setting is ignored there. -CONFIG_CDC_ACM_SERIAL_INITIALIZE_AT_BOOT=n - # ========== Optional ESP32 repeater uplink ========== # Observer-style WiFi+MQTT reporting from repeater role. # Runtime-configured and reboot-applied. diff --git a/zephcore/src/main_companion.cpp b/zephcore/src/main_companion.cpp index 5f73c4f..11be111 100644 --- a/zephcore/src/main_companion.cpp +++ b/zephcore/src/main_companion.cpp @@ -35,6 +35,7 @@ LOG_MODULE_REGISTER(zephcore_main, CONFIG_ZEPHCORE_MAIN_LOG_LEVEL); #if IS_ENABLED(CONFIG_LOG) #include +#include #endif #if IS_ENABLED(CONFIG_ZEPHCORE_UI_DESIGN_JOYSTICK) @@ -525,8 +526,15 @@ int main(void) /* Clear any stale bootloader magic from previous sessions */ zephyr_board.clearBootloaderMagic(); - /* Brief settle — deferred logging will catch up once USB CDC enumerates */ - k_sleep(K_MSEC(100)); + /* USB CDC init up front so the host can enumerate, then block until the + * host opens the port (DTR-high) — event-driven via the shared usbd + * message callback. Unplugged → 1 s timeout, banner is buffered for any + * later attach. CONFIG_LOG=n prod builds skip the whole stack. */ +#if IS_ENABLED(CONFIG_LOG) && DT_HAS_COMPAT_STATUS_OKAY(zephyr_cdc_acm_uart) && \ + !IS_ENABLED(CONFIG_CDC_ACM_SERIAL_INITIALIZE_AT_BOOT) + zephcore_usbd_init(); + zephcore_usbd_wait_dtr(1000); +#endif LOG_INF("=== ZephCore starting ==="); /* Log reset reason so we can diagnose random reboots */ diff --git a/zephcore/src/main_repeater.cpp b/zephcore/src/main_repeater.cpp index a1a55b0..c3158e4 100644 --- a/zephcore/src/main_repeater.cpp +++ b/zephcore/src/main_repeater.cpp @@ -33,9 +33,9 @@ extern "C" void bt_ctlr_assert_handle(char *file, uint32_t line) } #endif -/* USB CDC with 1200 baud touch detection (when not using auto-init) */ +/* USB CDC ACM init + 1200-baud DFU + DTR callbacks (shared with companion) */ #if !IS_ENABLED(CONFIG_CDC_ACM_SERIAL_INITIALIZE_AT_BOOT) -#include +#include #endif #include @@ -384,8 +384,16 @@ int main(void) zephyr_board.clearBootloaderMagic(); #endif - /* Wait for USB CDC to enumerate before any logging */ - k_sleep(K_MSEC(2000)); + /* USB CDC init up front so the host can enumerate, then wait for the + * host to open the port (DTR asserted) — event-driven via the usbd + * message callback. Unplugged → 2 s timeout, no banner; attached → + * banner reaches the user the moment the port opens. */ +#if !IS_ENABLED(CONFIG_CDC_ACM_SERIAL_INITIALIZE_AT_BOOT) && DT_HAS_COMPAT_STATUS_OKAY(zephyr_cdc_acm_uart) + zephcore_usbd_init(); +#endif +#if DT_HAS_COMPAT_STATUS_OKAY(zephyr_cdc_acm_uart) + zephcore_usbd_wait_dtr(2000); +#endif LOG_INF("=== ZephCore Repeater starting ==="); #if IS_ENABLED(CONFIG_ZEPHCORE_WIFI_OTA) @@ -515,10 +523,8 @@ int main(void) k_work_schedule(&initial_advert_work, K_SECONDS(10)); #endif - /* Initialize USB serial for CLI */ -#if !IS_ENABLED(CONFIG_CDC_ACM_SERIAL_INITIALIZE_AT_BOOT) && DT_HAS_COMPAT_STATUS_OKAY(zephyr_cdc_acm_uart) - zephcore_usbd_init(); -#endif + /* USB CDC was initialized earlier (right after clearBootloaderMagic). + * Just acquire the device handle for the CLI's UART IRQ binding below. */ #if DT_HAS_COMPAT_STATUS_OKAY(zephyr_cdc_acm_uart) usb_dev = DEVICE_DT_GET_ONE(zephyr_cdc_acm_uart); #else