From 30464b022d67214b4d6ea014c5f475e2dced1756 Mon Sep 17 00:00:00 2001 From: mikecarper Date: Mon, 5 Oct 2026 13:08:55 -0700 Subject: [PATCH] Guard startup resets in the pinned Arduino 3.1.3 HWCDC driver --- .github/workflows/run-unit-tests.yml | 1 + scripts/esp32_usb_session_fix.py | 45 +- test/fixtures/hwcdc_313/HWCDC.cpp | 613 ++++++++++++++++++ test/fixtures/hwcdc_313/README.txt | 16 + test/fixtures/hwcdc_313/esp_arduino_version.h | 51 ++ test/test_esp32_usb_session_fix.py | 53 +- test/test_hwcdc_313_startup_session.py | 20 + test/test_hwcdc_3311_startup_session.py | 81 ++- 8 files changed, 829 insertions(+), 51 deletions(-) create mode 100644 test/fixtures/hwcdc_313/HWCDC.cpp create mode 100644 test/fixtures/hwcdc_313/README.txt create mode 100644 test/fixtures/hwcdc_313/esp_arduino_version.h create mode 100644 test/test_hwcdc_313_startup_session.py diff --git a/.github/workflows/run-unit-tests.yml b/.github/workflows/run-unit-tests.yml index a28883e09..22eea1db8 100644 --- a/.github/workflows/run-unit-tests.yml +++ b/.github/workflows/run-unit-tests.yml @@ -283,6 +283,7 @@ jobs: python3 -B test/test_esp32_usb_session_fix.py python3 -B test/test_hwcdc_tx_backport.py -v python3 -B test/test_hwcdc_startup_session.py -v + python3 -B test/test_hwcdc_313_startup_session.py -v python3 -B test/test_hwcdc_3311_startup_session.py -v python3 -B test/test_esp32_hwcdc_recipes.py -v python3 -B test/test_hwcdc_role_transport.py -v diff --git a/scripts/esp32_usb_session_fix.py b/scripts/esp32_usb_session_fix.py index df9f14416..c30a4326a 100644 --- a/scripts/esp32_usb_session_fix.py +++ b/scripts/esp32_usb_session_fix.py @@ -104,7 +104,7 @@ def replace_source(build_env, node): # HWCDC TX/PHY fixes are limited to the installed Arduino2.0.17 C3/S3 driver. -# The separately pinned3.3.11 transform below only classifies startup resets. +# The separately pinned 3.x transforms below only classify startup resets. # Other framework versions retain their own driver unchanged. PINNED_HWCDC_SHA256 = "d0a8ca606c2729c8522a041113285dbf27033c22a5a6af8649a7305ffe84c449" PATCHED_HWCDC_SHA256 = "0969a94ae32edcfeeedb3daab19e5697e6c7647dc31578cdb0c3a0cb29e7c6d6" @@ -359,33 +359,41 @@ def hwcdc_primary_enabled(build_env): and defines.get("ARDUINO_USB_MODE") == "1") -# Arduino3.3.11 already supplies its own TX suffix and interrupt fixes. Only -# classify the startup BUS_RESET event at capture; leave that driver intact. +# Keep each reviewed 3.x driver's own TX/FIFO/PHY implementation intact. Only +# classify the startup BUS_RESET event at capture, under an exact source pin. +PINNED_HWCDC_313_SHA256 = "a09e99f1a37931269625019f51f1541a1185cce83f686d7aca4204ecdfea91dd" +PATCHED_HWCDC_313_SHA256 = "a2efe89e363908baf955e60751985edf1411fa930f2971e2d98b69fda3a1d19c" PINNED_HWCDC_3311_SHA256 = "c5ed5fdd05aa0df9b74d390812643599223f96b459256718d0ad328aeaba6a8a" PATCHED_HWCDC_3311_SHA256 = "7b693bf5d72df97336360969ff429c67f3783d472123aeb79b3aabc8154b51ba" -HWCDC_3311_INCLUDE = '#include "hal/usb_serial_jtag_ll.h"\n' -HWCDC_3311_STARTUP_HOOK = 'extern "C" bool meshEsp32HwcdcShouldReportBusReset() __attribute__((weak));\n' -HWCDC_3311_RESET_POST = ( +HWCDC_STARTUP_PINS = { + (3, 1, 3): (PINNED_HWCDC_313_SHA256, PATCHED_HWCDC_313_SHA256), + (3, 3, 11): (PINNED_HWCDC_3311_SHA256, PATCHED_HWCDC_3311_SHA256), +} +HWCDC_STARTUP_INCLUDE = '#include "hal/usb_serial_jtag_ll.h"\n' +HWCDC_STARTUP_HOOK = 'extern "C" bool meshEsp32HwcdcShouldReportBusReset() __attribute__((weak));\n' +HWCDC_RESET_POST = ( ' arduino_hw_cdc_event_post(ARDUINO_HW_CDC_EVENTS, ARDUINO_HW_CDC_BUS_RESET_EVENT, ' '&event, sizeof(arduino_hw_cdc_event_data_t), &xTaskWoken);') -HWCDC_3311_RESET_CAPTURE = ( +HWCDC_RESET_CAPTURE = ( ' // Snapshot startup before the framework event task can delay delivery.\n' ' if (!meshEsp32HwcdcShouldReportBusReset || meshEsp32HwcdcShouldReportBusReset()) {\n' - + ' ' + HWCDC_3311_RESET_POST + '\n' + + ' ' + HWCDC_RESET_POST + '\n' ' }') -def patched_hwcdc_startup_source(source): +def patched_hwcdc_startup_source(source, *, version=(3, 3, 11)): import hashlib + original_digest, patched_digest = HWCDC_STARTUP_PINS[version] source = source.replace("\r\n", "\n") digest = hashlib.sha256(source.encode()).hexdigest() - if digest == PATCHED_HWCDC_3311_SHA256: + if digest == patched_digest: return source - if digest != PINNED_HWCDC_3311_SHA256: - raise RuntimeError("ESP32 HWCDC startup fix: changed pinned3.3.11 source; review SDK update") - return source.replace(HWCDC_3311_INCLUDE, - HWCDC_3311_INCLUDE + HWCDC_3311_STARTUP_HOOK, 1).replace( - HWCDC_3311_RESET_POST, HWCDC_3311_RESET_CAPTURE, 1) + if digest != original_digest: + label = ".".join(str(part) for part in version) + raise RuntimeError("ESP32 HWCDC startup fix: changed pinned" + label + " source; review SDK update") + return source.replace(HWCDC_STARTUP_INCLUDE, + HWCDC_STARTUP_INCLUDE + HWCDC_STARTUP_HOOK, 1).replace( + HWCDC_RESET_POST, HWCDC_RESET_CAPTURE, 1) def hwcdc_framework_version(source): @@ -408,12 +416,11 @@ def replace_hwcdc_source(build_env, node): source = Path(node.srcnode().get_abspath()) version = hwcdc_framework_version(source) if version == (2, 0, 17): - patcher = patched_hwcdc_source - elif version == (3, 3, 11): - patcher = patched_hwcdc_startup_source + patched = patched_hwcdc_source(source.read_text(encoding="utf-8")) + elif version in HWCDC_STARTUP_PINS: + patched = patched_hwcdc_startup_source(source.read_text(encoding="utf-8"), version=version) else: return node - patched = patcher(source.read_text(encoding="utf-8")) destination = Path(build_env.subst("$BUILD_DIR")) / "patched-esp32-usb" / source.name destination.parent.mkdir(parents=True, exist_ok=True) if not destination.exists() or destination.read_text(encoding="utf-8") != patched: diff --git a/test/fixtures/hwcdc_313/HWCDC.cpp b/test/fixtures/hwcdc_313/HWCDC.cpp new file mode 100644 index 000000000..170e323a0 --- /dev/null +++ b/test/fixtures/hwcdc_313/HWCDC.cpp @@ -0,0 +1,613 @@ +// Copyright 2015-2024 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +#include "USB.h" +#if SOC_USB_SERIAL_JTAG_SUPPORTED + +#include "esp32-hal.h" +#include "esp32-hal-periman.h" +#include "HWCDC.h" +#include "freertos/FreeRTOS.h" +#include "freertos/semphr.h" +#include "freertos/queue.h" +#include "freertos/ringbuf.h" +#include "esp_intr_alloc.h" +#include "soc/periph_defs.h" +#include "soc/io_mux_reg.h" +#include "soc/usb_serial_jtag_struct.h" +#pragma GCC diagnostic ignored "-Wvolatile" +#include "hal/usb_serial_jtag_ll.h" +#pragma GCC diagnostic warning "-Wvolatile" +#include "rom/ets_sys.h" + +ESP_EVENT_DEFINE_BASE(ARDUINO_HW_CDC_EVENTS); + +static RingbufHandle_t tx_ring_buf = NULL; +static QueueHandle_t rx_queue = NULL; +static uint8_t rx_data_buf[64] = {0}; +static intr_handle_t intr_handle = NULL; +static SemaphoreHandle_t tx_lock = NULL; +static volatile bool connected = false; + +// SOF in ISR causes problems for uploading firmware +//static volatile unsigned long lastSOF_ms; +//static volatile uint8_t SOF_TIMEOUT; + +// timeout has no effect when USB CDC is unplugged +static uint32_t tx_timeout_ms = 100; + +static esp_event_loop_handle_t arduino_hw_cdc_event_loop_handle = NULL; + +static esp_err_t + arduino_hw_cdc_event_post(esp_event_base_t event_base, int32_t event_id, void *event_data, size_t event_data_size, BaseType_t *task_unblocked) { + if (arduino_hw_cdc_event_loop_handle == NULL) { + return ESP_FAIL; + } + return esp_event_isr_post_to(arduino_hw_cdc_event_loop_handle, event_base, event_id, event_data, event_data_size, task_unblocked); +} + +static esp_err_t + arduino_hw_cdc_event_handler_register_with(esp_event_base_t event_base, int32_t event_id, esp_event_handler_t event_handler, void *event_handler_arg) { + if (!arduino_hw_cdc_event_loop_handle) { + esp_event_loop_args_t event_task_args = { + .queue_size = 5, .task_name = "arduino_hw_cdc_events", .task_priority = 5, .task_stack_size = 2048, .task_core_id = tskNO_AFFINITY + }; + if (esp_event_loop_create(&event_task_args, &arduino_hw_cdc_event_loop_handle) != ESP_OK) { + log_e("esp_event_loop_create failed"); + } + } + if (arduino_hw_cdc_event_loop_handle == NULL) { + return ESP_FAIL; + } + return esp_event_handler_register_with(arduino_hw_cdc_event_loop_handle, event_base, event_id, event_handler, event_handler_arg); +} + +static void hw_cdc_isr_handler(void *arg) { + portBASE_TYPE xTaskWoken = 0; + uint32_t usbjtag_intr_status = 0; + arduino_hw_cdc_event_data_t event = {0}; + usbjtag_intr_status = usb_serial_jtag_ll_get_intsts_mask(); + + if (usbjtag_intr_status & USB_SERIAL_JTAG_INTR_SERIAL_IN_EMPTY) { + // Interrupt tells us the host picked up the data we sent. + if (!HWCDC::isPlugged()) { + connected = false; + usb_serial_jtag_ll_clr_intsts_mask(USB_SERIAL_JTAG_INTR_SERIAL_IN_EMPTY); + // USB is unplugged, nothing to be done here + return; + } else { + connected = true; + } + if (tx_ring_buf != NULL && usb_serial_jtag_ll_txfifo_writable() == 1) { + // We disable the interrupt here so that the interrupt won't be triggered if there is no data to send. + usb_serial_jtag_ll_disable_intr_mask(USB_SERIAL_JTAG_INTR_SERIAL_IN_EMPTY); + size_t queued_size = 0; + uint8_t *queued_buff = (uint8_t *)xRingbufferReceiveUpToFromISR(tx_ring_buf, &queued_size, 64); + // If the hardware fifo is available, write in it. Otherwise, do nothing. + if (queued_buff != NULL) { //Although tx_queued_bytes may be larger than 0. We may have interrupt before xRingbufferSend() was called. + //Copy the queued buffer into the TX FIFO + usb_serial_jtag_ll_clr_intsts_mask(USB_SERIAL_JTAG_INTR_SERIAL_IN_EMPTY); + usb_serial_jtag_ll_write_txfifo(queued_buff, queued_size); + usb_serial_jtag_ll_txfifo_flush(); + vRingbufferReturnItemFromISR(tx_ring_buf, queued_buff, &xTaskWoken); + if (connected) { + usb_serial_jtag_ll_ena_intr_mask(USB_SERIAL_JTAG_INTR_SERIAL_IN_EMPTY); + } + //send event? + //ets_printf("TX:%u\n", queued_size); + event.tx.len = queued_size; + arduino_hw_cdc_event_post(ARDUINO_HW_CDC_EVENTS, ARDUINO_HW_CDC_TX_EVENT, &event, sizeof(arduino_hw_cdc_event_data_t), &xTaskWoken); + } + } else { + usb_serial_jtag_ll_clr_intsts_mask(USB_SERIAL_JTAG_INTR_SERIAL_IN_EMPTY); + } + } + + if (usbjtag_intr_status & USB_SERIAL_JTAG_INTR_SERIAL_OUT_RECV_PKT) { + // read rx buffer(max length is 64), and send available data to ringbuffer. + // Ensure the rx buffer size is larger than RX_MAX_SIZE. + usb_serial_jtag_ll_clr_intsts_mask(USB_SERIAL_JTAG_INTR_SERIAL_OUT_RECV_PKT); + uint32_t rx_fifo_len = usb_serial_jtag_ll_read_rxfifo(rx_data_buf, 64); + uint32_t i = 0; + for (i = 0; i < rx_fifo_len; i++) { + if (rx_queue == NULL || !xQueueSendFromISR(rx_queue, rx_data_buf + i, &xTaskWoken)) { + break; + } + } + event.rx.len = i; + arduino_hw_cdc_event_post(ARDUINO_HW_CDC_EVENTS, ARDUINO_HW_CDC_RX_EVENT, &event, sizeof(arduino_hw_cdc_event_data_t), &xTaskWoken); + connected = true; + } + + if (usbjtag_intr_status & USB_SERIAL_JTAG_INTR_BUS_RESET) { + usb_serial_jtag_ll_clr_intsts_mask(USB_SERIAL_JTAG_INTR_BUS_RESET); + arduino_hw_cdc_event_post(ARDUINO_HW_CDC_EVENTS, ARDUINO_HW_CDC_BUS_RESET_EVENT, &event, sizeof(arduino_hw_cdc_event_data_t), &xTaskWoken); + connected = false; + } + + // SOF ISR is causing esptool to be unable to upload firmware to the board + // if (usbjtag_intr_status & USB_SERIAL_JTAG_INTR_SOF) { + // usb_serial_jtag_ll_clr_intsts_mask(USB_SERIAL_JTAG_INTR_SOF); + // lastSOF_ms = millis(); + // } + + if (xTaskWoken == pdTRUE) { + portYIELD_FROM_ISR(); + } +} + +// Moved to header file as inline function. Kept just as future reference. +//inline bool HWCDC::isPlugged(void) { +// SOF ISR is causing esptool to be unable to upload firmware to the board +// Timer test for SOF seems to work when uploading firmware +// return usb_serial_jtag_is_connected();//(lastSOF_ms + SOF_TIMEOUT) >= millis(); +//} + +bool HWCDC::isCDC_Connected() { + static bool running = false; + + // USB may be unplugged + if (!isPlugged()) { + connected = false; + running = false; + // SOF in ISR causes problems for uploading firmware + //SOF_TIMEOUT = 5; // SOF timeout when unplugged + return false; + } + //else { + // SOF_TIMEOUT = 50; // SOF timeout when plugged + //} + + if (connected) { + running = false; + return true; + } + + if (running == false && !connected) { // enables it only once! + usb_serial_jtag_ll_ena_intr_mask(USB_SERIAL_JTAG_INTR_SERIAL_IN_EMPTY); + } + + // this will feed CDC TX FIFO to trigger IN_EMPTY + usb_serial_jtag_ll_txfifo_flush(); + running = true; + return false; +} + +static void flushTXBuffer(const uint8_t *buffer, size_t size) { + if (!tx_ring_buf) { + return; + } + UBaseType_t uxItemsWaiting = 0; + vRingbufferGetInfo(tx_ring_buf, NULL, NULL, NULL, NULL, &uxItemsWaiting); + size_t freeSpace = xRingbufferGetCurFreeSize(tx_ring_buf); + size_t ringbufferLength = freeSpace + uxItemsWaiting; + + if (buffer == NULL) { + // just flush the whole ring buffer and exit - used by HWCDC::flush() + size_t queued_size = 0; + uint8_t *queued_buff = (uint8_t *)xRingbufferReceiveUpTo(tx_ring_buf, &queued_size, 0, ringbufferLength); + if (queued_size && queued_buff != NULL) { + vRingbufferReturnItem(tx_ring_buf, (void *)queued_buff); + } + return; + } + if (size == 0) { + return; // nothing to do + } + if (freeSpace >= size) { + // there is enough space, just add the data to the ring buffer + if (xRingbufferSend(tx_ring_buf, (void *)buffer, size, 0) != pdTRUE) { + return; + } + } else { + // how many byte should be flushed to make space for the new data + size_t to_flush = size - freeSpace; + if (to_flush > ringbufferLength) { + to_flush = ringbufferLength; + } + size_t queued_size = 0; + uint8_t *queued_buff = (uint8_t *)xRingbufferReceiveUpTo(tx_ring_buf, &queued_size, 0, to_flush); + if (queued_size && queued_buff != NULL) { + vRingbufferReturnItem(tx_ring_buf, (void *)queued_buff); + } + // now add the new data that fits into the ring buffer + uint8_t *bptr = (uint8_t *)buffer; + if (size >= ringbufferLength) { + size = ringbufferLength; + bptr = (uint8_t *)buffer + (size - ringbufferLength); + } + if (xRingbufferSend(tx_ring_buf, (void *)bptr, size, 0) != pdTRUE) { + return; + } + } + // flushes CDC FIFO + usb_serial_jtag_ll_txfifo_flush(); +} + +static void ARDUINO_ISR_ATTR cdc0_write_char(char c) { + if (tx_ring_buf == NULL) { + return; + } + if (!HWCDC::isConnected()) { + // just pop/push RingBuffer and apply FIFO policy + flushTXBuffer((const uint8_t *)&c, 1); + return; + } + if (xPortInIsrContext()) { + xRingbufferSendFromISR(tx_ring_buf, (void *)(&c), 1, NULL); + } else { + xRingbufferSend(tx_ring_buf, (void *)(&c), 1, tx_timeout_ms / portTICK_PERIOD_MS); + } + usb_serial_jtag_ll_txfifo_flush(); + usb_serial_jtag_ll_ena_intr_mask(USB_SERIAL_JTAG_INTR_SERIAL_IN_EMPTY); +} + +HWCDC::HWCDC() { + perimanSetBusDeinit(ESP32_BUS_TYPE_USB_DM, HWCDC::deinit); + perimanSetBusDeinit(ESP32_BUS_TYPE_USB_DP, HWCDC::deinit); + // SOF in ISR causes problems for uploading firmware + // lastSOF_ms = 0; + // SOF_TIMEOUT = 5; +} + +HWCDC::~HWCDC() { + end(); +} + +// It should return just when USB is plugged and CDC is connected. +HWCDC::operator bool() const { + return HWCDC::isCDC_Connected(); +} + +void HWCDC::onEvent(esp_event_handler_t callback) { + onEvent(ARDUINO_HW_CDC_ANY_EVENT, callback); +} + +void HWCDC::onEvent(arduino_hw_cdc_event_t event, esp_event_handler_t callback) { + arduino_hw_cdc_event_handler_register_with(ARDUINO_HW_CDC_EVENTS, event, callback, this); +} + +bool HWCDC::deinit(void *busptr) { + // avoid any recursion issue with Peripheral Manager perimanSetPinBus() call + static bool running = false; + if (running) { + return true; + } + running = true; + // Setting USB D+ D- pins + bool retCode = true; + retCode &= perimanClearPinBus(USB_INT_PHY0_DM_GPIO_NUM); + retCode &= perimanClearPinBus(USB_INT_PHY0_DP_GPIO_NUM); + if (retCode) { + // Force the host to re-enumerate (BUS_RESET) + pinMode(USB_INT_PHY0_DM_GPIO_NUM, OUTPUT_OPEN_DRAIN); + pinMode(USB_INT_PHY0_DP_GPIO_NUM, OUTPUT_OPEN_DRAIN); + digitalWrite(USB_INT_PHY0_DM_GPIO_NUM, LOW); + digitalWrite(USB_INT_PHY0_DP_GPIO_NUM, LOW); + } + // release the flag + running = false; + return retCode; +} + +void HWCDC::begin(unsigned long baud) { + if (tx_lock == NULL) { + tx_lock = xSemaphoreCreateMutex(); + } + //RX Buffer default has 256 bytes if not preset + if (rx_queue == NULL) { + if (!setRxBufferSize(256)) { + log_e("HW CDC RX Buffer error"); + } + } + //TX Buffer default has 256 bytes if not preset + if (tx_ring_buf == NULL) { + if (!setTxBufferSize(256)) { + log_e("HW CDC TX Buffer error"); + } + } + + // the HW Serial pins needs to be first deinited in order to allow `if(Serial)` to work :-( + // But this is also causing terminal to hang, so they are disabled + // deinit(NULL); + // delay(10); // USB Host has to enumerate it again + + // Peripheral Manager setting for USB D+ D- pins + uint8_t pin = USB_INT_PHY0_DM_GPIO_NUM; + if (!perimanSetPinBus(pin, ESP32_BUS_TYPE_USB_DM, (void *)this, -1, -1)) { + goto err; + } + pin = USB_INT_PHY0_DP_GPIO_NUM; + if (!perimanSetPinBus(pin, ESP32_BUS_TYPE_USB_DP, (void *)this, -1, -1)) { + goto err; + } + + // Configure PHY + // USB_Serial_JTAG use internal PHY + USB_SERIAL_JTAG.conf0.phy_sel = 0; + // Disable software control USB D+ D- pullup pulldown (Device FS: dp_pullup = 1) + USB_SERIAL_JTAG.conf0.pad_pull_override = 0; + // Enable USB D+ pullup + USB_SERIAL_JTAG.conf0.dp_pullup = 1; + // Enable USB pad function + USB_SERIAL_JTAG.conf0.usb_pad_enable = 1; + usb_serial_jtag_ll_disable_intr_mask(USB_SERIAL_JTAG_LL_INTR_MASK); + usb_serial_jtag_ll_ena_intr_mask(USB_SERIAL_JTAG_INTR_SERIAL_IN_EMPTY | USB_SERIAL_JTAG_INTR_SERIAL_OUT_RECV_PKT | USB_SERIAL_JTAG_INTR_BUS_RESET); + // SOF ISR is causing esptool to be unable to upload firmware to the board + // usb_serial_jtag_ll_ena_intr_mask( + // USB_SERIAL_JTAG_INTR_SERIAL_IN_EMPTY | USB_SERIAL_JTAG_INTR_SERIAL_OUT_RECV_PKT | USB_SERIAL_JTAG_INTR_BUS_RESET | USB_SERIAL_JTAG_INTR_SOF + // ); + if (!intr_handle && esp_intr_alloc(ETS_USB_SERIAL_JTAG_INTR_SOURCE, 0, hw_cdc_isr_handler, NULL, &intr_handle) != ESP_OK) { + isr_log_e("HW USB CDC failed to init interrupts"); + end(); + return; + } + return; + +err: + log_e("Serial JTAG Pin %u can't be set into Peripheral Manager.", pin); + end(); +} + +void HWCDC::end() { + //Disable/clear/free tx/rx interrupt. + usb_serial_jtag_ll_disable_intr_mask(USB_SERIAL_JTAG_LL_INTR_MASK); + usb_serial_jtag_ll_clr_intsts_mask(USB_SERIAL_JTAG_LL_INTR_MASK); + esp_intr_free(intr_handle); + intr_handle = NULL; + if (tx_lock != NULL) { + vSemaphoreDelete(tx_lock); + tx_lock = NULL; + } + setRxBufferSize(0); + setTxBufferSize(0); + if (arduino_hw_cdc_event_loop_handle) { + esp_event_loop_delete(arduino_hw_cdc_event_loop_handle); + arduino_hw_cdc_event_loop_handle = NULL; + } + HWCDC::deinit(this); + setDebugOutput(false); + connected = false; +} + +void HWCDC::setTxTimeoutMs(uint32_t timeout) { + tx_timeout_ms = timeout; +} + +/* + * WRITING +*/ + +size_t HWCDC::setTxBufferSize(size_t tx_queue_len) { + if (tx_ring_buf) { + vRingbufferDelete(tx_ring_buf); + tx_ring_buf = NULL; + } + if (!tx_queue_len) { + return 0; + } + tx_ring_buf = xRingbufferCreate(tx_queue_len, RINGBUF_TYPE_BYTEBUF); + if (!tx_ring_buf) { + return 0; + } + return tx_queue_len; +} + +int HWCDC::availableForWrite(void) { + if (tx_ring_buf == NULL || tx_lock == NULL) { + return 0; + } + if (xSemaphoreTake(tx_lock, tx_timeout_ms / portTICK_PERIOD_MS) != pdPASS) { + return 0; + } + size_t a = xRingbufferGetCurFreeSize(tx_ring_buf); + xSemaphoreGive(tx_lock); + return a; +} + +size_t HWCDC::write(const uint8_t *buffer, size_t size) { + if (buffer == NULL || size == 0 || tx_ring_buf == NULL || tx_lock == NULL) { + return 0; + } + if (xSemaphoreTake(tx_lock, tx_timeout_ms / portTICK_PERIOD_MS) != pdPASS) { + return 0; + } + if (!isCDC_Connected()) { + // just pop/push RingBuffer and apply FIFO policy + flushTXBuffer(buffer, size); + } else { + size_t space = xRingbufferGetCurFreeSize(tx_ring_buf); + size_t to_send = size, so_far = 0; + + if (space > size) { + space = size; + } + // Non-Blocking method, Sending data to ringbuffer, and handle the data in ISR. + if (space > 0 && xRingbufferSend(tx_ring_buf, (void *)(buffer), space, 0) != pdTRUE) { + size = 0; + } else { + to_send -= space; + so_far += space; + // Now trigger the ISR to read data from the ring buffer. + usb_serial_jtag_ll_txfifo_flush(); + if (connected) { + usb_serial_jtag_ll_ena_intr_mask(USB_SERIAL_JTAG_INTR_SERIAL_IN_EMPTY); + } + // tracks CDC transmission progress to avoid hanging if CDC is unplugged while still sending data + size_t last_toSend = to_send; + uint32_t tries = tx_timeout_ms; // waits 1ms per sending data attempt, in case CDC is unplugged + while (connected && to_send) { + space = xRingbufferGetCurFreeSize(tx_ring_buf); + if (space > to_send) { + space = to_send; + } + // Blocking method, Sending data to ringbuffer, and handle the data in ISR. + if (xRingbufferSend(tx_ring_buf, (void *)(buffer + so_far), space, tx_timeout_ms / portTICK_PERIOD_MS) != pdTRUE) { + size = so_far; + log_w("write failed due to ring buffer full - timeout"); + break; + } + so_far += space; + to_send -= space; + // Now trigger the ISR to read data from the ring buffer. + usb_serial_jtag_ll_txfifo_flush(); + if (connected) { + usb_serial_jtag_ll_ena_intr_mask(USB_SERIAL_JTAG_INTR_SERIAL_IN_EMPTY); + } + if (last_toSend == to_send) { + // no progress in sending data... USB CDC is probably unplugged + tries--; + delay(1); + } else { + last_toSend = to_send; + tries = tx_timeout_ms; // reset the timeout + } + if (tries == 0) { // CDC isn't connected anymore... + size = so_far; + log_w("write failed due to waiting USB Host - timeout"); + connected = false; + } + } + } + // CDC was disconnected while sending data ==> flush the TX buffer keeping the last data + if (to_send && !usb_serial_jtag_ll_txfifo_writable()) { + connected = false; + flushTXBuffer(buffer + so_far, to_send); + } + } + xSemaphoreGive(tx_lock); + return size; +} + +size_t HWCDC::write(uint8_t c) { + return write(&c, 1); +} + +void HWCDC::flush(void) { + if (tx_ring_buf == NULL || tx_lock == NULL) { + return; + } + if (xSemaphoreTake(tx_lock, tx_timeout_ms / portTICK_PERIOD_MS) != pdPASS) { + return; + } + if (!isCDC_Connected()) { + flushTXBuffer(NULL, 0); + } else { + UBaseType_t uxItemsWaiting = 0; + vRingbufferGetInfo(tx_ring_buf, NULL, NULL, NULL, NULL, &uxItemsWaiting); + if (uxItemsWaiting) { + // Now trigger the ISR to read data from the ring buffer. + usb_serial_jtag_ll_txfifo_flush(); + if (connected) { + usb_serial_jtag_ll_ena_intr_mask(USB_SERIAL_JTAG_INTR_SERIAL_IN_EMPTY); + } + } + uint32_t tries = tx_timeout_ms; // waits 1ms per ISR sending data attempt, in case CDC is unplugged + while (connected && tries && uxItemsWaiting) { + delay(1); + UBaseType_t lastUxItemsWaiting = uxItemsWaiting; + vRingbufferGetInfo(tx_ring_buf, NULL, NULL, NULL, NULL, &uxItemsWaiting); + if (lastUxItemsWaiting == uxItemsWaiting) { + tries--; + } + if (connected) { + usb_serial_jtag_ll_ena_intr_mask(USB_SERIAL_JTAG_INTR_SERIAL_IN_EMPTY); + } + } + if (tries == 0) { // CDC isn't connected anymore... + connected = false; + flushTXBuffer(NULL, 0); // flushes all TX Buffer + } + } + xSemaphoreGive(tx_lock); +} + +/* + * READING +*/ + +size_t HWCDC::setRxBufferSize(size_t rx_queue_len) { + if (rx_queue) { + vQueueDelete(rx_queue); + rx_queue = NULL; + } + if (!rx_queue_len) { + return 0; + } + rx_queue = xQueueCreate(rx_queue_len, sizeof(uint8_t)); + if (!rx_queue) { + return 0; + } + return rx_queue_len; +} + +int HWCDC::available(void) { + if (rx_queue == NULL) { + return -1; + } + return uxQueueMessagesWaiting(rx_queue); +} + +int HWCDC::peek(void) { + if (rx_queue == NULL) { + return -1; + } + uint8_t c; + if (xQueuePeek(rx_queue, &c, 0)) { + return c; + } + return -1; +} + +int HWCDC::read(void) { + if (rx_queue == NULL) { + return -1; + } + uint8_t c = 0; + if (xQueueReceive(rx_queue, &c, 0)) { + return c; + } + return -1; +} + +size_t HWCDC::read(uint8_t *buffer, size_t size) { + if (rx_queue == NULL) { + return -1; + } + uint8_t c = 0; + size_t count = 0; + while (count < size && xQueueReceive(rx_queue, &c, 0)) { + buffer[count++] = c; + } + return count; +} + +/* + * DEBUG +*/ + +void HWCDC::setDebugOutput(bool en) { + if (en) { + uartSetDebug(NULL); + ets_install_putc2((void (*)(char)) & cdc0_write_char); + } else { + ets_install_putc2(NULL); + } +} + +#if ARDUINO_USB_MODE && ARDUINO_USB_CDC_ON_BOOT // Hardware JTAG CDC selected +// USBSerial is always available to be used +HWCDC HWCDCSerial; +#endif + +#endif /* SOC_USB_SERIAL_JTAG_SUPPORTED */ diff --git a/test/fixtures/hwcdc_313/README.txt b/test/fixtures/hwcdc_313/README.txt new file mode 100644 index 000000000..d5e847cd2 --- /dev/null +++ b/test/fixtures/hwcdc_313/README.txt @@ -0,0 +1,16 @@ +The original Arduino-ESP32 3.1.3 HWCDC.cpp and version header are copied from +the framework source resolved by the Xiao_C6_repeater_ build: +framework-arduinoespressif32, ESP_ARDUINO_VERSION 3.1.3 + +Upstream repository: https://github.com/espressif/arduino-esp32 +The original source retains its Espressif copyright and Apache-2.0 header. + +HWCDC.cpp SHA256: +a09e99f1a37931269625019f51f1541a1185cce83f686d7aca4204ecdfea91dd + +Tests use the real ISR and application reset helpers against host peripheral +stubs. The 3.1.3 transform only adds the startup predicate declaration and +wraps BUS_RESET event posting. Existing TX, FIFO, PHY, and pin-manager code is +kept byte-for-byte. Unlike 3.3.11, this driver has no partial-TX suffix stash or +TX interrupt spinlock; these tests retain those existing behavior limits. +These tests do not substitute for physical C6 testing. diff --git a/test/fixtures/hwcdc_313/esp_arduino_version.h b/test/fixtures/hwcdc_313/esp_arduino_version.h new file mode 100644 index 000000000..f8b00d3a1 --- /dev/null +++ b/test/fixtures/hwcdc_313/esp_arduino_version.h @@ -0,0 +1,51 @@ +// Copyright 2019 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +/** Major version number (X.x.x) */ +#define ESP_ARDUINO_VERSION_MAJOR 3 +/** Minor version number (x.X.x) */ +#define ESP_ARDUINO_VERSION_MINOR 1 +/** Patch version number (x.x.X) */ +#define ESP_ARDUINO_VERSION_PATCH 3 + +/** + * Macro to convert ARDUINO version number into an integer + * + * To be used in comparisons, such as ESP_ARDUINO_VERSION >= ESP_ARDUINO_VERSION_VAL(2, 0, 0) + */ +#define ESP_ARDUINO_VERSION_VAL(major, minor, patch) ((major << 16) | (minor << 8) | (patch)) + +/** + * Current ARDUINO version, as an integer + * + * To be used in comparisons, such as ESP_ARDUINO_VERSION >= ESP_ARDUINO_VERSION_VAL(2, 0, 0) + */ +#define ESP_ARDUINO_VERSION ESP_ARDUINO_VERSION_VAL(ESP_ARDUINO_VERSION_MAJOR, ESP_ARDUINO_VERSION_MINOR, ESP_ARDUINO_VERSION_PATCH) + +/** + * Current ARDUINO version, as string + */ +#define df2xstr(s) #s +#define df2str(s) df2xstr(s) +#define ESP_ARDUINO_VERSION_STR df2str(ESP_ARDUINO_VERSION_MAJOR) "." df2str(ESP_ARDUINO_VERSION_MINOR) "." df2str(ESP_ARDUINO_VERSION_PATCH) + +#ifdef __cplusplus +} +#endif diff --git a/test/test_esp32_usb_session_fix.py b/test/test_esp32_usb_session_fix.py index 276db3774..40d82f944 100644 --- a/test/test_esp32_usb_session_fix.py +++ b/test/test_esp32_usb_session_fix.py @@ -38,6 +38,20 @@ void tud_umount_cb(void){ ''' +def installed_tested_tinyusb_cores(packages): + # This optional installed-source qualification covers the 2.0.17 layout + # used by the golden anchors. A C6 HWCDC build can replace the default + # package with 3.x; it must not silently change which TinyUSB SDK is tested. + result = [] + for package in sorted(packages.glob("framework-arduinoespressif32*")): + core = package / "cores/esp32" + if not (core / "USBCDC.cpp").is_file() or not (core / "esp_arduino_version.h").is_file(): + continue + if FIX.hwcdc_framework_version(core / "USBCDC.cpp") == (2, 0, 17): + result.append(core) + return result + + class Esp32UsbSessionFixTests(unittest.TestCase): def test_owner_hooks_precede_state_changes_and_rx_queue(self): patched = FIX.patched_cdc_source(CDC) @@ -92,17 +106,44 @@ class Esp32UsbSessionFixTests(unittest.TestCase): [("ARDUINO_USB_CDC_ON_BOOT", 0), ("ARDUINO_USB_MODE", 0)]): env["CPPDEFINES"] = definitions self.assertIs(FIX.replace_source(env, node), node) + # Selecting a newer HWCDC SDK for optional qualification must + # never relax the actual mode0 build's changed-layout rejection. + env["CPPDEFINES"] = [("ARDUINO_USB_CDC_ON_BOOT", 1), ("ARDUINO_USB_MODE", 0)] + source.write_text(CDC.replace(FIX.RX, "void USBCDC::_onRX() {\n"), encoding="utf-8") + with self.assertRaises(RuntimeError): + FIX.replace_source(env, node) + + def test_installed_sdk_selection_uses_exact_version_and_alternate_packages(self): + with tempfile.TemporaryDirectory(prefix="meshcore-tinyusb-sdk-selection-") as directory: + packages = Path(directory) + cores = {} + for name, version in (("framework-arduinoespressif32", (3, 1, 3)), + ("framework-arduinoespressif32@src-old", (2, 0, 17)), + ("framework-arduinoespressif32@src-new", (3, 3, 11))): + core = packages / name / "cores/esp32" + core.mkdir(parents=True) + (core / "USBCDC.cpp").write_text(CDC, encoding="ascii") + (core / "esp_arduino_version.h").write_text( + "\n".join("#define ESP_ARDUINO_VERSION_" + key + " " + str(value) + for key, value in zip(("MAJOR", "MINOR", "PATCH"), version)) + "\n", + encoding="ascii") + cores[version] = core + self.assertEqual(installed_tested_tinyusb_cores(packages), [cores[(2, 0, 17)]]) + (cores[(2, 0, 17)] / "USBCDC.cpp").unlink() + self.assertEqual(installed_tested_tinyusb_cores(packages), []) def test_installed_sdk_compatibility_when_available(self): # Package-free CI still exercises the golden anchors above. Check an # installed SDK on Windows and Unix, including a custom PIO core dir. core = Path(os.environ.get("PLATFORMIO_CORE_DIR", str(Path.home() / ".platformio"))) - package = core / "packages/framework-arduinoespressif32/cores/esp32" - if not (package / "USBCDC.cpp").is_file(): - self.skipTest("installed Arduino-ESP32 SDK unavailable") - for name, patcher in (("USBCDC.cpp", FIX.patched_cdc_source), ("USB.cpp", FIX.patched_usb_source)): - original = (package / name).read_text(encoding="utf-8") - self.assertIn("meshEsp32TinyUsb", patcher(original)) + packages = installed_tested_tinyusb_cores(core / "packages") + if not packages: + self.skipTest("installed Arduino-ESP32 2.0.17 TinyUSB SDK unavailable") + for package in packages: + for name, patcher in (("USBCDC.cpp", FIX.patched_cdc_source), ("USB.cpp", FIX.patched_usb_source)): + with self.subTest(package=package, source=name): + original = (package / name).read_text(encoding="utf-8") + self.assertIn("meshEsp32TinyUsb", patcher(original)) def test_all_esp32_profiles_inherit_hook(self): config = (ROOT / "platformio.ini").read_text() diff --git a/test/test_hwcdc_313_startup_session.py b/test/test_hwcdc_313_startup_session.py new file mode 100644 index 000000000..86b39307a --- /dev/null +++ b/test/test_hwcdc_313_startup_session.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python3 +"""Qualify the capture-only startup hook against Arduino3.1.3's actual ISR.""" +from pathlib import Path +import unittest + +import test_hwcdc_3311_startup_session as reviewed_v3 + +CORE = Path(__file__).resolve().parent / "fixtures/hwcdc_313" +RAW = (CORE / "HWCDC.cpp").read_text() +PATCHED = reviewed_v3.FIX.patched_hwcdc_startup_source(RAW, version=(3, 1, 3)) + + +class Hwcdc313StartupSessionTests(reviewed_v3.Hwcdc3311StartupSessionTests): + # Same production owner gate/session assertions and negative controls, + # with 3.1.3's own ISR and application non-backport cleanup path. + core, raw, patched, version = CORE, RAW, PATCHED, (3, 1, 3) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/test/test_hwcdc_3311_startup_session.py b/test/test_hwcdc_3311_startup_session.py index c17577662..fb0b709cf 100644 --- a/test/test_hwcdc_3311_startup_session.py +++ b/test/test_hwcdc_3311_startup_session.py @@ -17,10 +17,10 @@ RAW = (CORE / "HWCDC.cpp").read_text() PATCHED = FIX.patched_hwcdc_startup_source(RAW) -def harness(**controls): +def harness(patched=PATCHED, **controls): state, functions, owner = startup.application_sources(**controls) # Reuse only the peripheral boundary. All driver methods below come from - # the original 3.3.11 implementation with its capture-only startup patch. + # the reviewed 3.x implementation with its capture-only startup patch. prefix = backport.HARNESS.split("@SUPPORT@", 1)[0] prefix = prefix.replace("static std::vector event_lengths;", "static std::vector event_lengths;\n" @@ -29,17 +29,24 @@ def harness(**controls): " if(event==ARDUINO_HW_CDC_TX_EVENT)event_lengths.push_back(data->tx.len);", " queued_events.push_back(event);\n" " if(event==ARDUINO_HW_CDC_TX_EVENT)event_lengths.push_back(data->tx.len);") + has_stash = "static volatile size_t tx_stash_len" in patched fields = "\n".join(re.search(r"^static [^\n]*\b" + name + r"[^\n]*;", - PATCHED, re.MULTILINE)[0] - for name in ("tx_stash_buf", "tx_stash_len", "hw_cdc_tx_mux")) - driver = "\n".join(backport.body(PATCHED, signature) for signature in ( + patched, re.MULTILINE)[0] + for name in ("tx_stash_buf", "tx_stash_len", "hw_cdc_tx_mux")) if has_stash else "" + signatures = ( "static inline void hw_cdc_enable_tx_intr(void)", "static inline void hw_cdc_enable_tx_intr_from_isr(void)", "static inline void hw_cdc_disable_tx_intr_from_isr(void)", "static inline void hw_cdc_clear_tx_stash(void)", "bool HWCDC::isCDC_Connected()", "static void hw_cdc_isr_handler(", - )) + ) + if not has_stash: + # The 3.1.3 driver has no TX stash/mux helpers and directly updates + # interrupts. Do not invent the newer driver's locking guarantees. + signatures = signatures[-2:] + prefix = prefix.replace("assert(critical_depth);", "") + driver = "\n".join(backport.body(patched, signature) for signature in signatures) service = r''' static void pulse(unsigned limit=64) { fifo_limit=limit; intr_status=USB_SERIAL_JTAG_INTR_SERIAL_IN_EMPTY; @@ -47,29 +54,41 @@ static void pulse(unsigned limit=64) { } static void reset() { ring=Ring{}; staged.clear(); event_lengths.clear(); queued_events.clear(); - tx_stash_len=0; irq_enabled=true; plugged=false; connected=false; + @RESET_STASH@ irq_enabled=true; plugged=false; connected=false; before_empty_return=nullptr; flushes=0; } ''' + service = service.replace("@RESET_STASH@", "tx_stash_len=0;" if has_stash else "") extra = startup.EXTRA.replace("#define MESH_HWCDC_PINNED_TX_BACKPORT 1", "#define MESH_HWCDC_PINNED_TX_BACKPORT 0") extra = extra.replace("mesh_hwcdc_tx_stash_len", "tx_stash_len") extra = extra.replace(" && mesh_hwcdc_fifo_pending", "") extra = extra.replace(" && !mesh_hwcdc_fifo_pending", "") extra = extra.replace(" && mesh_hwcdc_tx_allowed", "") + if not has_stash: + # 3.1.3 does not retain partially accepted FIFO suffixes. Exercise its + # real boot TX event with a fully accepted packet, then enumeration. + extra = extra.replace( + "// Real ISR stages a partial boot diagnostic before the host enumerates.", + "// Real 3.1.3 ISR stages a boot diagnostic before the host enumerates.") + extra = extra.replace("pulse(4); assert(tx_stash_len != 0);", + "pulse(); assert(staged.size() == 11 && ring.data.empty());") + extra = extra.replace(" assert(tx_stash_len == 0);\n", "") extra = extra.replace("@STARTUP_STATE@", state).replace( "@FUNCTIONS@", functions).replace("@OWNER_RESET_HOOK@", owner) - return prefix + FIX.HWCDC_3311_STARTUP_HOOK + fields + "\n" + driver + service + extra + return prefix + FIX.HWCDC_STARTUP_HOOK + fields + "\n" + driver + service + extra class Hwcdc3311StartupSessionTests(startup.HwcdcStartupSessionTests): # Run the exact same client/session assertions and negative controls with # the actual 3.3.11 ISR and non-backport application cleanup branches. + core, raw, patched, version = CORE, RAW, PATCHED, (3, 3, 11) + def run_case(self, case, *, expect_success=True, **controls): with tempfile.TemporaryDirectory(prefix="hwcdc-3311-startup-") as directory: directory = Path(directory) cpp, binary = directory / "startup.cpp", directory / "startup" - cpp.write_text(harness(**controls), encoding="ascii") + cpp.write_text(harness(self.patched, **controls), encoding="ascii") compiled = subprocess.run([ "g++", "-std=c++17", "-pthread", "-Wall", "-Wextra", "-Wno-unused-parameter", "-Wno-sign-compare", @@ -85,20 +104,22 @@ class Hwcdc3311StartupSessionTests(startup.HwcdcStartupSessionTests): self.assertNotEqual(result.returncode, 0, "Negative control did not fail") def test_only_reset_event_posting_and_hook_declaration_change(self): - self.assertEqual(hashlib.sha256(RAW.encode()).hexdigest(), FIX.PINNED_HWCDC_3311_SHA256) - self.assertEqual(hashlib.sha256(PATCHED.encode()).hexdigest(), FIX.PATCHED_HWCDC_3311_SHA256) - self.assertEqual(PATCHED.count(FIX.HWCDC_3311_STARTUP_HOOK), 1) - self.assertEqual(PATCHED.count(FIX.HWCDC_3311_RESET_CAPTURE), 1) - reverted = PATCHED.replace(FIX.HWCDC_3311_STARTUP_HOOK, "", 1).replace( - FIX.HWCDC_3311_RESET_CAPTURE, FIX.HWCDC_3311_RESET_POST, 1) - self.assertEqual(reverted, RAW, "Unrelated driver code changed") - self.assertEqual(FIX.patched_hwcdc_startup_source(PATCHED), PATCHED) - self.assertEqual(FIX.patched_hwcdc_startup_source(RAW.replace("\n", "\r\n")), PATCHED) - for changed in (RAW + "\n", PATCHED + "\n", - RAW.replace("tx_stash_len = 0;", "tx_stash_len = 1;", 1)): + raw_pin, patched_pin = FIX.HWCDC_STARTUP_PINS[self.version] + self.assertEqual(hashlib.sha256(self.raw.encode()).hexdigest(), raw_pin) + self.assertEqual(hashlib.sha256(self.patched.encode()).hexdigest(), patched_pin) + self.assertEqual(self.patched.count(FIX.HWCDC_STARTUP_HOOK), 1) + self.assertEqual(self.patched.count(FIX.HWCDC_RESET_CAPTURE), 1) + reverted = self.patched.replace(FIX.HWCDC_STARTUP_HOOK, "", 1).replace( + FIX.HWCDC_RESET_CAPTURE, FIX.HWCDC_RESET_POST, 1) + self.assertEqual(reverted, self.raw, "Unrelated driver code changed") + patch = lambda source: FIX.patched_hwcdc_startup_source(source, version=self.version) + self.assertEqual(patch(self.patched), self.patched) + self.assertEqual(patch(self.raw.replace("\n", "\r\n")), self.patched) + for changed in (self.raw + "\n", self.patched + "\n", + self.raw.replace("connected = false;", "connected = true;", 1)): with self.subTest(changed=hashlib.sha256(changed.encode()).hexdigest()): with self.assertRaises(RuntimeError): - FIX.patched_hwcdc_startup_source(changed) + patch(changed) def test_build_local_patch_has_exact_version_and_mode_scope(self): class Node: @@ -118,8 +139,8 @@ class Hwcdc3311StartupSessionTests(startup.HwcdcStartupSessionTests): directory = Path(directory) sdk = directory / "sdk"; sdk.mkdir() path, version = sdk / "HWCDC.cpp", sdk / "esp_arduino_version.h" - path.write_text(RAW, encoding="ascii") - version.write_text((CORE / "esp_arduino_version.h").read_text(), encoding="ascii") + path.write_text(self.raw, encoding="ascii") + version.write_text((self.core / "esp_arduino_version.h").read_text(), encoding="ascii") node = Node(path) for definitions in ([], [("ARDUINO_USB_CDC_ON_BOOT", 0), ("ARDUINO_USB_MODE", 1)], [("ARDUINO_USB_CDC_ON_BOOT", 1), ("ARDUINO_USB_MODE", 0)]): @@ -129,13 +150,21 @@ class Hwcdc3311StartupSessionTests(startup.HwcdcStartupSessionTests): env = Environment([("ARDUINO_USB_CDC_ON_BOOT", 1), ("ARDUINO_USB_MODE", 1)], directory / "build") result = Path(FIX.replace_hwcdc_source(env, node)) - self.assertEqual(result.read_text(), PATCHED) - self.assertEqual(path.read_text(), RAW) + self.assertEqual(result.read_text(), self.patched) + self.assertEqual(path.read_text(), self.raw) self.assertEqual(env.paths, [str(sdk)]) before = result.stat().st_mtime_ns FIX.replace_hwcdc_source(env, node) self.assertEqual(result.stat().st_mtime_ns, before) - path.write_text(RAW + "\n", encoding="ascii") + path.write_text(self.raw + "\n", encoding="ascii") + with self.assertRaises(RuntimeError): + FIX.replace_hwcdc_source(env, node) + # Reviewed versions cannot borrow another version's source pin. + path.write_text(self.raw, encoding="ascii") + other = (3, 1, 3) if self.version == (3, 3, 11) else (3, 3, 11) + version.write_text("\n".join("#define ESP_ARDUINO_VERSION_" + name + " " + str(value) + for name, value in zip(("MAJOR", "MINOR", "PATCH"), other)) + "\n", + encoding="ascii") with self.assertRaises(RuntimeError): FIX.replace_hwcdc_source(env, node) # An unreviewed framework version keeps its existing driver.