mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-10-23 14:32:22 +00:00
Migrate ESP32 native USB roles to bounded hardware CDC
Switch eight S3 board defaults and 78 remaining TinyUSB recipes to hardware Serial/JTAG while preserving UART and other platforms. Restore retained S3 PHY ownership after a TinyUSB OTA restart and prepare KISS queues before enabling the driver. Add real recipe resolution, Companion/KISS transport sanitizer coverage, and RF lifecycle reset regressions to CI. Native tests pass 1765/1765; SDK, transport, recipe and USB watchdog checks pass. Board and role cross-compilation and physical OTA qualification continue.
This commit is contained in:
1 parent
4939de4c61
commit
835c7a8e6e
23 files changed
+1491
-39
No files matched your search
@@ -266,6 +266,8 @@ jobs:
|
||||
python3 -B test/test_esp32_tinyusb_nonblocking.py
|
||||
python3 -B test/test_esp32_usb_session_fix.py
|
||||
python3 -B test/test_hwcdc_tx_backport.py -v
|
||||
python3 -B test/test_esp32_hwcdc_recipes.py -v
|
||||
python3 -B test/test_hwcdc_role_transport.py -v
|
||||
python3 -B test/test_hwcdc_write_capacity.py -v
|
||||
python3 -B test/test_esp32_file_buffer_fix.py -v
|
||||
python3 -B test/test_companion_usb_logging_reply.py
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"core": "esp32",
|
||||
"extra_flags": [
|
||||
"-DBOARD_HAS_PSRAM",
|
||||
"-DARDUINO_USB_MODE=0",
|
||||
"-DARDUINO_USB_MODE=1",
|
||||
"-DARDUINO_USB_CDC_ON_BOOT=1",
|
||||
"-DARDUINO_RUNNING_CORE=1",
|
||||
"-DARDUINO_EVENT_RUNNING_CORE=1"
|
||||
@@ -34,8 +34,8 @@
|
||||
"flash_size": "16MB",
|
||||
"maximum_ram_size": 8388608,
|
||||
"maximum_size": 16777216,
|
||||
"use_1200bps_touch": true,
|
||||
"wait_for_upload_port": true,
|
||||
"use_1200bps_touch": false,
|
||||
"wait_for_upload_port": false,
|
||||
"require_upload_port": true,
|
||||
"speed": 921600
|
||||
},
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"core": "esp32",
|
||||
"extra_flags": [
|
||||
"-DBOARD_HAS_PSRAM",
|
||||
"-DARDUINO_USB_MODE=0",
|
||||
"-DARDUINO_USB_MODE=1",
|
||||
"-DARDUINO_USB_CDC_ON_BOOT=1",
|
||||
"-DARDUINO_RUNNING_CORE=1",
|
||||
"-DARDUINO_EVENT_RUNNING_CORE=1"
|
||||
@@ -34,8 +34,8 @@
|
||||
"flash_size": "16MB",
|
||||
"maximum_ram_size": 8388608,
|
||||
"maximum_size": 16777216,
|
||||
"use_1200bps_touch": true,
|
||||
"wait_for_upload_port": true,
|
||||
"use_1200bps_touch": false,
|
||||
"wait_for_upload_port": false,
|
||||
"require_upload_port": true,
|
||||
"speed": 921600
|
||||
},
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"core": "esp32",
|
||||
"extra_flags": [
|
||||
"-DBOARD_HAS_PSRAM",
|
||||
"-DARDUINO_USB_MODE=0",
|
||||
"-DARDUINO_USB_MODE=1",
|
||||
"-DARDUINO_USB_CDC_ON_BOOT=1",
|
||||
"-DARDUINO_RUNNING_CORE=1",
|
||||
"-DARDUINO_EVENT_RUNNING_CORE=1"
|
||||
@@ -34,8 +34,8 @@
|
||||
"flash_size": "16MB",
|
||||
"maximum_ram_size": 8388608,
|
||||
"maximum_size": 16777216,
|
||||
"use_1200bps_touch": true,
|
||||
"wait_for_upload_port": true,
|
||||
"use_1200bps_touch": false,
|
||||
"wait_for_upload_port": false,
|
||||
"require_upload_port": true,
|
||||
"speed": 921600
|
||||
},
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"core": "esp32",
|
||||
"extra_flags": [
|
||||
"-DARDUINO_USB_CDC_ON_BOOT=1",
|
||||
"-DARDUINO_USB_MODE=0",
|
||||
"-DARDUINO_USB_MODE=1",
|
||||
"-DARDUINO_RUNNING_CORE=1",
|
||||
"-DARDUINO_EVENT_RUNNING_CORE=1"
|
||||
],
|
||||
@@ -30,8 +30,8 @@
|
||||
"flash_size": "8MB",
|
||||
"maximum_ram_size": 327680,
|
||||
"maximum_size": 8388608,
|
||||
"use_1200bps_touch": true,
|
||||
"wait_for_upload_port": true,
|
||||
"use_1200bps_touch": false,
|
||||
"wait_for_upload_port": false,
|
||||
"require_upload_port": true,
|
||||
"speed": 921600
|
||||
},
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"core": "esp32",
|
||||
"extra_flags": [
|
||||
"-DARDUINO_USB_CDC_ON_BOOT=1",
|
||||
"-DARDUINO_USB_MODE=0",
|
||||
"-DARDUINO_USB_MODE=1",
|
||||
"-DARDUINO_RUNNING_CORE=1",
|
||||
"-DARDUINO_EVENT_RUNNING_CORE=1"
|
||||
],
|
||||
@@ -30,8 +30,8 @@
|
||||
"flash_size": "8MB",
|
||||
"maximum_ram_size": 327680,
|
||||
"maximum_size": 8388608,
|
||||
"use_1200bps_touch": true,
|
||||
"wait_for_upload_port": true,
|
||||
"use_1200bps_touch": false,
|
||||
"wait_for_upload_port": false,
|
||||
"require_upload_port": true,
|
||||
"speed": 921600
|
||||
},
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"extra_flags": [
|
||||
"-DBOARD_HAS_PSRAM",
|
||||
"-DARDUINO_USB_CDC_ON_BOOT=1",
|
||||
"-DARDUINO_USB_MODE=0",
|
||||
"-DARDUINO_USB_MODE=1",
|
||||
"-DARDUINO_RUNNING_CORE=1",
|
||||
"-DARDUINO_EVENT_RUNNING_CORE=0"
|
||||
],
|
||||
@@ -33,8 +33,8 @@
|
||||
"flash_size": "16MB",
|
||||
"maximum_ram_size": 327680,
|
||||
"maximum_size": 16777216,
|
||||
"use_1200bps_touch": true,
|
||||
"wait_for_upload_port": true,
|
||||
"use_1200bps_touch": false,
|
||||
"wait_for_upload_port": false,
|
||||
"require_upload_port": true,
|
||||
"speed": 921600
|
||||
},
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"extra_flags": [
|
||||
"-DBOARD_HAS_PSRAM",
|
||||
"-DARDUINO_USB_CDC_ON_BOOT=1",
|
||||
"-DARDUINO_USB_MODE=0",
|
||||
"-DARDUINO_USB_MODE=1",
|
||||
"-DARDUINO_RUNNING_CORE=1",
|
||||
"-DARDUINO_EVENT_RUNNING_CORE=0"
|
||||
],
|
||||
@@ -33,8 +33,8 @@
|
||||
"flash_size": "16MB",
|
||||
"maximum_ram_size": 327680,
|
||||
"maximum_size": 16777216,
|
||||
"use_1200bps_touch": true,
|
||||
"wait_for_upload_port": true,
|
||||
"use_1200bps_touch": false,
|
||||
"wait_for_upload_port": false,
|
||||
"require_upload_port": true,
|
||||
"speed": 921600
|
||||
},
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"-DBOARD_HAS_PSRAM",
|
||||
"-DLILYGO_TBEAM_1W",
|
||||
"-DARDUINO_USB_CDC_ON_BOOT=1",
|
||||
"-DARDUINO_USB_MODE=0",
|
||||
"-DARDUINO_USB_MODE=1",
|
||||
"-DARDUINO_RUNNING_CORE=1",
|
||||
"-DARDUINO_EVENT_RUNNING_CORE=1"
|
||||
],
|
||||
@@ -40,6 +40,8 @@
|
||||
],
|
||||
"name": "LilyGo TBeam-1W",
|
||||
"upload": {
|
||||
"use_1200bps_touch": false,
|
||||
"wait_for_upload_port": false,
|
||||
"flash_size": "16MB",
|
||||
"maximum_ram_size": 327680,
|
||||
"maximum_size": 16777216,
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
# Native USB backpressure and radio liveness
|
||||
|
||||
The native USB defaults for eight ESP32-S3 board families now use hardware
|
||||
USB Serial/JTAG (HWCDC). The TinyUSB behavior below remains available to
|
||||
explicitly selected TinyUSB builds. UART and non-ESP32 transports retain their
|
||||
existing backend.
|
||||
|
||||
ESP32-S2/S3 builds using native TinyUSB CDC (`ARDUINO_USB_MODE=0` and
|
||||
CDC-on-boot) must not wait for a computer to read USB output. In the bundled
|
||||
Arduino-ESP32 2.0.17 core, `USBCDC::write()` can wait indefinitely for transmit
|
||||
@@ -159,3 +164,78 @@ describe those earlier binaries, not the current merged source.
|
||||
The two custom qualification builds used the lab profile
|
||||
`909.5 MHz / 500 kHz / SF5 / CR5`. No board was flashed, and no physical
|
||||
disconnect/reconnect or hardware endpoint recovery was claimed by these checks.
|
||||
|
||||
## ESP32 HWCDC migration and upstream findings (2026-10-04)
|
||||
|
||||
The board defaults change 78 native-TinyUSB recipes across Heltec E213, E290,
|
||||
T190, Wireless Tracker V1.1 and V2, Station G2 and G3, and T-Beam 1W. Two G2
|
||||
recipes already used HWCDC. All 80 recipes in these families select
|
||||
`ARDUINO_USB_MODE=1` and `ARDUINO_USB_CDC_ON_BOOT=1`; upload touch and waiting
|
||||
for a replacement upload port are explicitly disabled. Their framework stays
|
||||
on PlatformIO Espressif32 6.11.0 / Arduino-ESP32 2.0.17. Existing ESP32 UART
|
||||
recipes and all non-ESP32 recipes are unchanged.
|
||||
|
||||
HWCDC output uses the existing bounded facade, early RX/TX allocation, shared
|
||||
writer exclusion, and fresh capacity check inside that exclusion. The pinned
|
||||
SDK receives a build-local TX suffix/interrupt backport from upstream
|
||||
[Arduino-ESP32 #12606](https://github.com/espressif/arduino-esp32/pull/12606).
|
||||
KISS now prepares buffers before starting the USB driver and uses this facade.
|
||||
Its debug/logging gates stay closed so diagnostics cannot split a binary frame.
|
||||
A USB bus reset drops partial host commands and old queued frame suffixes while
|
||||
preserving radio transmission state and settings. Radio work continues while
|
||||
USB cleanup retries.
|
||||
|
||||
On S3, the pinned HWCDC initialization also restores RTC PHY ownership to the
|
||||
hardware Serial/JTAG peripheral. A software restart after running TinyUSB can
|
||||
otherwise retain ownership by the OTG controller, including an OTA update to an
|
||||
HWCDC image. When that retained state is detected, initialization first briefly
|
||||
detaches the old OTG pads so the host can enumerate the changed device. Ordinary
|
||||
HWCDC startup does not incur that extra interval.
|
||||
|
||||
HWCDC does not expose the same DTR session boundary as TinyUSB. Closing a host
|
||||
port while leaving USB connected is not proof of a new firmware session; a
|
||||
partial command may survive that close. Bus reset/unplug cleanup and protocol
|
||||
client leases must be tested separately. Hosts should rediscover the port after
|
||||
the backend change: the USB product and `/dev/serial/by-id` path can change.
|
||||
|
||||
Upstream reports checked for this transition:
|
||||
|
||||
| Report | Documented behavior | Relevance and mitigation |
|
||||
| --- | --- | --- |
|
||||
| [ESP-IDF #9826](https://github.com/espressif/esp-idf/issues/9826) | S3 retains TinyUSB PHY ownership across software/watchdog restart, preventing USB Serial/JTAG operation. | Pinned HWCDC initialization now explicitly restores the official S3 hardware PHY route and detaches the former OTG connection first. Executed initialization tests seed the retained RTC state; real OTA transition remains a separate hardware check. |
|
||||
| [Meshtastic #10955](https://github.com/meshtastic/firmware/issues/10955) | Tracker V2 watchdog reboot after a serial reader closes; raw log writes block while the USB bus remains alive. Reproduced on Arduino 2.x and 3.x. | Applies to the same hardware/backend combination. Our bounded writer and diagnostic admission prevent waiting for a host to drain output; synthetic negative controls exercise unsafe raw writes. |
|
||||
| [Meshtastic #10975](https://github.com/meshtastic/firmware/issues/10975) | Tracker V2 binary synchronization corrupted by interleaved diagnostics and abandoned short-write suffixes. | Companion/KISS tests retain suffixes across short writes; KISS disables diagnostics before board initialization. |
|
||||
| [Arduino-ESP32 #12782](https://github.com/espressif/arduino-esp32/issues/12782) | Open report of HWCDC failing to reconnect after about two minutes idle on C3/C6/C61/S3, latest master / IDF 5.5.4. | Reported SDK differs from our pinned Arduino 2.0.17 / IDF 4.4. Synthetic tests cover idle lease expiry and reply admission, not physical enumeration on that SDK. |
|
||||
| [ESP-IDF #18996](https://github.com/espressif/esp-idf/issues/18996) | Open report of S3 USB Serial/JTAG failing after repeated software resets from macOS, on IDF 6.0.2; includes XIAO S3. | Not qualified by Linux port reopen tests and not reproduced on our pinned SDK. CPU reset and host-port reopen are distinct cases. |
|
||||
| [ESP-IDF #13287](https://github.com/espressif/esp-idf/issues/13287) | S3 may remain in ROM after upload when download mode was entered with physical BOOT/RESET. | Our older esptool does not offer the newer `watchdog-reset` option. Press RESET after a manual BOOT-mode upload if the application does not start. |
|
||||
| [Meshtastic #4206](https://github.com/meshtastic/firmware/issues/4206) | Station G2 native USB fails to resume around light sleep. | Keep USB active while a host is present. Espressif documents USB Serial/JTAG sleep limitations; a port disappearing during sleep does not establish a firmware crash. |
|
||||
|
||||
Espressif's [USB Serial/JTAG guide](https://docs.espressif.com/projects/esp-idf/en/stable/esp32s3/api-guides/usb-serial-jtag-console.html)
|
||||
describes the fixed CDC/JTAG peripheral and its light/deep sleep limitations.
|
||||
This migration uses only its serial role; it does not provide TinyUSB composite
|
||||
interfaces such as MSC, HID, or a second CDC.
|
||||
|
||||
Additional regression commands (only one PlatformIO process at a time):
|
||||
|
||||
```sh
|
||||
python3 -B test/test_esp32_hwcdc_recipes.py -v
|
||||
python3 -B test/test_hwcdc_tx_backport.py -v
|
||||
python3 -B test/test_hwcdc_write_capacity.py -v
|
||||
python3 -B test/test_hwcdc_role_transport.py -v
|
||||
pio test -e native_kiss_modem
|
||||
```
|
||||
|
||||
The recipe check resolves all 821 tracked environments using PlatformIO's real
|
||||
inheritance and SCons flag/unflag processing. It checks all 500 ESP32 selectors,
|
||||
including all 78 migrated recipes, and the 321 non-ESP32 recipes. The transport
|
||||
test compiles production Companion framing, KISS output methods, setup/loop,
|
||||
and the bounded facade with the patched SDK against simulated USB registers and
|
||||
RTOS queues under ASan/UBSan. It covers withheld readers, stale capacity,
|
||||
partial frames, queue limits, session reset, idle leases, and cleanup retries.
|
||||
The role fixture injects a confirmed session boundary; separate SDK tests
|
||||
execute the bus-reset callback and the S3 retained-PHY initialization. The
|
||||
setup/loop fixture uses stub transport functions to check ordering and continued
|
||||
radio service rather than real peripheral recovery.
|
||||
Native KISS tests also exercise the full modem's radio lifecycle during reset.
|
||||
These tests do not establish physical USB timing or host compatibility for
|
||||
boards that were not connected to the lab.
|
||||
@@ -77,6 +77,13 @@ void KissModem::begin() {
|
||||
resetOutputQueue();
|
||||
}
|
||||
|
||||
void KissModem::resetHostSession() {
|
||||
_rx_len = 0;
|
||||
_rx_escaped = false;
|
||||
_rx_active = false;
|
||||
resetOutputQueue();
|
||||
}
|
||||
|
||||
void KissModem::resetOutputQueue() {
|
||||
_tx_frame_head = 0;
|
||||
_tx_frame_tail = 0;
|
||||
|
||||
@@ -240,6 +240,9 @@ public:
|
||||
mesh::Radio& radio, mesh::MainBoard& board, SensorManager& sensors);
|
||||
|
||||
void begin();
|
||||
// Drop partial host input and queued frame suffixes at a USB bus reset.
|
||||
// Radio transmissions and profile settings belong to the radio lifecycle.
|
||||
void resetHostSession();
|
||||
void loop();
|
||||
|
||||
void setRadioCallback(SetRadioCallback cb) { _setRadioCallback = cb; }
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <helpers/ArduinoHelpers.h>
|
||||
#include <helpers/IdentityGeneration.h>
|
||||
#include <helpers/IdentityStore.h>
|
||||
#include <helpers/UsbLogging.h>
|
||||
#include "KissModem.h"
|
||||
|
||||
#if defined(ESP32_PLATFORM)
|
||||
@@ -27,14 +28,21 @@
|
||||
|
||||
#define NOISE_FLOOR_CALIB_INTERVAL_MS 2000
|
||||
#define AGC_RESET_INTERVAL_MS 30000
|
||||
#define USB_TX_TIMEOUT_MS 50
|
||||
#define USB_TX_BUFFER_SIZE 1024
|
||||
|
||||
#if MESH_ESP32_HWCDC_SESSION_GUARD && !(defined(KISS_UART_RX) && defined(KISS_UART_TX))
|
||||
#define KISS_HWCDC_TRANSPORT 1
|
||||
#else
|
||||
#define KISS_HWCDC_TRANSPORT 0
|
||||
#endif
|
||||
|
||||
StdRNG rng;
|
||||
mesh::LocalIdentity identity;
|
||||
KissModem* modem;
|
||||
static uint32_t next_noise_floor_calib_ms = 0;
|
||||
static uint32_t next_agc_reset_ms = 0;
|
||||
#if KISS_HWCDC_TRANSPORT
|
||||
static bool usb_host_reset_pending = false;
|
||||
#endif
|
||||
|
||||
void halt() {
|
||||
while (1) ;
|
||||
@@ -136,6 +144,11 @@ void onGetStats(uint32_t* rx, uint32_t* tx, uint32_t* errors) {
|
||||
}
|
||||
|
||||
void setup() {
|
||||
#if KISS_HWCDC_TRANSPORT
|
||||
// KISS owns the entire serial byte stream, including gaps between chunks.
|
||||
mesh::setUsbLoggingEnabled(false);
|
||||
mesh::setUsbDebugEnabled(false);
|
||||
#endif
|
||||
board.begin();
|
||||
|
||||
int radioinit_attempts = 0;
|
||||
@@ -176,15 +189,22 @@ void setup() {
|
||||
#endif
|
||||
modem = new KissModem(Serial1, identity, rng, radio_driver, board, sensors);
|
||||
#else
|
||||
#if KISS_HWCDC_TRANSPORT
|
||||
// Allocate the USB queues before begin() enables the live driver ISR.
|
||||
mesh::prepareUsbLoggingPort();
|
||||
#endif
|
||||
Serial.begin(115200);
|
||||
#if KISS_HWCDC_TRANSPORT
|
||||
mesh::beginUsbLoggingPort();
|
||||
#endif
|
||||
uint32_t start = millis();
|
||||
while (!Serial && millis() - start < 3000) delay(10);
|
||||
delay(100);
|
||||
#if defined(ESP32) && ARDUINO_USB_MODE && ARDUINO_USB_CDC_ON_BOOT
|
||||
Serial.setTxTimeoutMs(USB_TX_TIMEOUT_MS);
|
||||
Serial.setTxBufferSize(USB_TX_BUFFER_SIZE);
|
||||
#endif
|
||||
#if KISS_HWCDC_TRANSPORT
|
||||
modem = new KissModem(mesh::usbCompanionPort(), identity, rng, radio_driver, board, sensors);
|
||||
#else
|
||||
modem = new KissModem(Serial, identity, rng, radio_driver, board, sensors);
|
||||
#endif
|
||||
#endif
|
||||
|
||||
modem->setRadioCallback(onSetRadio);
|
||||
@@ -197,6 +217,18 @@ void setup() {
|
||||
}
|
||||
|
||||
void loop() {
|
||||
#if KISS_HWCDC_TRANSPORT
|
||||
mesh::serviceUsbLoggingPort();
|
||||
if (mesh::takeUsbTerminalSessionReset()) {
|
||||
modem->resetHostSession();
|
||||
usb_host_reset_pending = true;
|
||||
}
|
||||
if (usb_host_reset_pending && mesh::tryCompleteUsbTerminalSessionReset()) {
|
||||
usb_host_reset_pending = false;
|
||||
}
|
||||
// The guarded stream blocks host I/O during cleanup. Continue the modem
|
||||
// loop so an in-flight RF transmission can finish while USB is quarantined.
|
||||
#endif
|
||||
#if defined(NRF52_PLATFORM)
|
||||
board.feedWatchdog();
|
||||
#endif
|
||||
|
||||
@@ -106,7 +106,44 @@ def replace_source(build_env, node):
|
||||
# HWCDC fixes are deliberately limited to the installed Arduino2.0.17 C3/S3
|
||||
# native Serial/JTAG driver. Other versions retain their own framework driver.
|
||||
PINNED_HWCDC_SHA256 = "d0a8ca606c2729c8522a041113285dbf27033c22a5a6af8649a7305ffe84c449"
|
||||
PATCHED_HWCDC_SHA256 = "5107db35dc3c855326dff0a9b93b279ee9bda60b5cc1ebcdce77a9028e3577b4"
|
||||
PATCHED_HWCDC_SHA256 = "7429655b063022e42c677dc79ecf0aab78acf33bba21a8cdb822435400c39719"
|
||||
HWCDC_S3_PHY_GUARD = "#if CONFIG_IDF_TARGET_ESP32S3 && ARDUINO_USB_MODE && ARDUINO_USB_CDC_ON_BOOT\n"
|
||||
HWCDC_S3_PHY_HELPERS = HWCDC_S3_PHY_GUARD + r'''#include "soc/usb_wrap_struct.h"
|
||||
#include "soc/rtc_cntl_struct.h"
|
||||
|
||||
// Exact ESP-IDF 4.4.7 usb_phy_ll.h bodies, renamed locally. The complete HAL
|
||||
// header has unrelated volatile struct copies that cannot compile as C++.
|
||||
static inline void mesh_hwcdc_int_jtag_enable(usb_serial_jtag_dev_t *hw)
|
||||
{
|
||||
// USB_Serial_JTAG use internal PHY
|
||||
hw->conf0.phy_sel = 0;
|
||||
// Disable software control USB D+ D- pullup pulldown (Device FS: dp_pullup = 1)
|
||||
hw->conf0.pad_pull_override = 0;
|
||||
// Enable USB D+ pullup
|
||||
hw->conf0.dp_pullup = 1;
|
||||
// Enable USB pad function
|
||||
hw->conf0.usb_pad_enable = 1;
|
||||
// phy_sel is controlled by the following register value
|
||||
RTCCNTL.usb_conf.sw_hw_usb_phy_sel = 1;
|
||||
// phy_sel=sw_usb_phy_sel=0, USB_Serial_JTAG is connected with internal PHY
|
||||
RTCCNTL.usb_conf.sw_usb_phy_sel = 0;
|
||||
}
|
||||
|
||||
static inline void mesh_hwcdc_usb_wrap_pad_enable(usb_wrap_dev_t *hw, bool pad_en)
|
||||
{
|
||||
hw->otg_conf.pad_enable = pad_en;
|
||||
}
|
||||
#endif
|
||||
'''
|
||||
HWCDC_S3_PHY_CONFIG = r""" // TinyUSB's RTC PHY selection survives a software restart (IDF #9826).
|
||||
// An OTA upgrade must release its old pullup before claiming the shared
|
||||
// PHY, so the host discards the previous USB device's descriptors.
|
||||
if (RTCCNTL.usb_conf.sw_hw_usb_phy_sel && RTCCNTL.usb_conf.sw_usb_phy_sel) {
|
||||
mesh_hwcdc_usb_wrap_pad_enable(&USB_WRAP, false);
|
||||
delay(20);
|
||||
}
|
||||
mesh_hwcdc_int_jtag_enable(&USB_SERIAL_JTAG);
|
||||
"""
|
||||
HWCDC_TX_SUPPORT = r"""
|
||||
// MeshCore pinned HWCDC TX suffix/interrupt backport (upstream #12606).
|
||||
static uint8_t mesh_hwcdc_tx_stash[64] = {0};
|
||||
@@ -262,6 +299,13 @@ def patched_hwcdc_source(source):
|
||||
# Perform exact-shape transforms only after the complete input hash passes.
|
||||
source = source.replace('#include "esp_freertos_hooks.h"\n',
|
||||
'#include "esp_freertos_hooks.h"\n#include <string.h>\n', 1)
|
||||
source = source.replace('#include "hal/usb_serial_jtag_ll.h"\n',
|
||||
'#include "hal/usb_serial_jtag_ll.h"\n' + HWCDC_S3_PHY_HELPERS, 1)
|
||||
begin = source.index('void HWCDC::begin(unsigned long baud)')
|
||||
phy_begin = source.index(' // Configure PHY\n', begin)
|
||||
phy_end = source.index(' usb_serial_jtag_ll_disable_intr_mask(USB_SERIAL_JTAG_LL_INTR_MASK);', phy_begin)
|
||||
source = (source[:phy_begin] + HWCDC_S3_PHY_GUARD + HWCDC_S3_PHY_CONFIG
|
||||
+ '#else\n' + source[phy_begin:phy_end] + '#endif\n' + source[phy_end:])
|
||||
source = source.replace('usb_serial_jtag_ll_ena_intr_mask(USB_SERIAL_JTAG_INTR_SERIAL_IN_EMPTY);',
|
||||
'mesh_hwcdc_enable_tx_intr();')
|
||||
begin = source.index(' if (usbjtag_intr_status & USB_SERIAL_JTAG_INTR_SERIAL_IN_EMPTY) {')
|
||||
|
||||
Vendored
+169
@@ -0,0 +1,169 @@
|
||||
// The runner inserts the production KISS setup and backend selection.
|
||||
#include <Arduino.h>
|
||||
#include <helpers/UsbLogging.h>
|
||||
#include <cassert>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
static std::vector<std::string> calls;
|
||||
static bool usb_logging = true, usb_debug = true;
|
||||
static bool serial_live = false;
|
||||
static bool reset_event = false, cleanup_ready = false;
|
||||
struct TestSerial : Stream {
|
||||
int rx = -1, tx = -1;
|
||||
unsigned baud = 0;
|
||||
explicit operator bool() const { return true; }
|
||||
void begin(unsigned speed) {
|
||||
baud = speed;
|
||||
if (this == primary) { serial_live = true; calls.push_back("serial.begin"); }
|
||||
else calls.push_back("uart.begin");
|
||||
}
|
||||
void setPins(int receive, int transmit) { rx = receive; tx = transmit; }
|
||||
static TestSerial* primary;
|
||||
} Serial, Serial1;
|
||||
TestSerial* TestSerial::primary = &Serial;
|
||||
static Stream guarded_port;
|
||||
namespace mesh {
|
||||
void setUsbLoggingEnabled(bool value) { usb_logging = value; calls.push_back("logging.gate"); }
|
||||
void setUsbDebugEnabled(bool value) { usb_debug = value; calls.push_back("debug.gate"); }
|
||||
void prepareUsbLoggingPort() {
|
||||
assert(!serial_live && "HWCDC ring resize must precede enabling the ISR");
|
||||
calls.push_back("prepare");
|
||||
}
|
||||
void beginUsbLoggingPort() {
|
||||
assert(serial_live);
|
||||
calls.push_back("begin.logging");
|
||||
}
|
||||
Stream& usbCompanionPort() { return guarded_port; }
|
||||
void serviceUsbLoggingPort() { calls.push_back("usb.service"); }
|
||||
bool takeUsbTerminalSessionReset() {
|
||||
calls.push_back("usb.boundary");
|
||||
const bool event = reset_event;
|
||||
reset_event = false;
|
||||
return event;
|
||||
}
|
||||
bool tryCompleteUsbTerminalSessionReset() {
|
||||
calls.push_back("usb.cleanup");
|
||||
return cleanup_ready;
|
||||
}
|
||||
}
|
||||
struct Board {
|
||||
void begin() {
|
||||
#if EXPECT_HWCDC
|
||||
assert(!usb_logging && !usb_debug);
|
||||
#endif
|
||||
calls.push_back("board.begin");
|
||||
}
|
||||
void reboot() { assert(false); }
|
||||
void onBootComplete() { calls.push_back("boot.complete"); }
|
||||
void loop() { calls.push_back("board.loop"); }
|
||||
} board;
|
||||
struct Radio {
|
||||
void begin() { calls.push_back("radio.begin"); }
|
||||
unsigned getRngSeed() const { return 0; }
|
||||
void resetAGC() { calls.push_back("radio.agc"); }
|
||||
int recvRaw(uint8_t*, size_t) { calls.push_back("radio.recv"); return 0; }
|
||||
uint8_t receiveProfile() const { return 0; }
|
||||
float getLastSNR() const { return 0; }
|
||||
float getLastRSSI() const { return 0; }
|
||||
void onReceiveProcessed() {}
|
||||
void triggerNoiseFloorCalibrate(unsigned) { calls.push_back("radio.noise"); }
|
||||
void loop() { calls.push_back("radio.loop"); }
|
||||
} radio_driver;
|
||||
struct Rng { void begin(unsigned) {} } rng;
|
||||
static int identity;
|
||||
struct Sensors {
|
||||
void begin() {}
|
||||
void loop() { calls.push_back("sensors.loop"); }
|
||||
} sensors;
|
||||
static bool radio_init() { return true; }
|
||||
static void loadOrCreateIdentity() {}
|
||||
static void onSetRadio() {}
|
||||
static void onSetTxPower() {}
|
||||
static void onGetCurrentRssi() {}
|
||||
static void onGetStats() {}
|
||||
class KissModem {
|
||||
public:
|
||||
Stream* port;
|
||||
KissModem(Stream& stream, int&, Rng&, Radio&, Board&, Sensors&) : port(&stream) {
|
||||
calls.push_back("modem.construct");
|
||||
}
|
||||
template <typename T> void setRadioCallback(T) {}
|
||||
template <typename T> void setTxPowerCallback(T) {}
|
||||
template <typename T> void setGetCurrentRssiCallback(T) {}
|
||||
template <typename T> void setGetStatsCallback(T) {}
|
||||
void begin() { calls.push_back("modem.begin"); }
|
||||
void loop() { calls.push_back("modem.loop"); }
|
||||
void resetHostSession() { calls.push_back("modem.reset"); }
|
||||
bool isActuallyTransmitting() const { return true; }
|
||||
bool isHostOutputBackedUp() const { return false; }
|
||||
bool isTxBusy() const { return true; }
|
||||
void onPacketReceived(int8_t, int8_t, const uint8_t*, int, uint8_t) {}
|
||||
};
|
||||
static KissModem* modem = nullptr;
|
||||
#define MESH_DEBUG_PRINTLN(...) ((void)0)
|
||||
@TRANSPORT_SELECTION@
|
||||
@LOOP_STATE@
|
||||
@SETUP@
|
||||
@LOOP@
|
||||
|
||||
int main() {
|
||||
setup();
|
||||
assert(modem != nullptr);
|
||||
#if EXPECT_HWCDC
|
||||
assert(modem->port == &guarded_port);
|
||||
const std::vector<std::string> expected = {
|
||||
"logging.gate", "debug.gate", "board.begin", "radio.begin", "prepare",
|
||||
"serial.begin", "begin.logging", "modem.construct", "modem.begin", "boot.complete"};
|
||||
assert(calls == expected);
|
||||
#elif EXPECT_UART
|
||||
assert(modem->port == &Serial1);
|
||||
assert(Serial1.rx == KISS_UART_RX && Serial1.tx == KISS_UART_TX);
|
||||
assert(Serial1.baud == 115200 && !serial_live);
|
||||
const std::vector<std::string> expected = {
|
||||
"board.begin", "radio.begin", "uart.begin", "modem.construct", "modem.begin", "boot.complete"};
|
||||
assert(calls == expected);
|
||||
#else
|
||||
assert(modem->port == &Serial);
|
||||
const std::vector<std::string> expected = {
|
||||
"board.begin", "radio.begin", "serial.begin", "modem.construct", "modem.begin", "boot.complete"};
|
||||
assert(calls == expected);
|
||||
#endif
|
||||
// An in-flight radio send and the outer sketch services must continue while
|
||||
// native USB queue purging retries. The boundary is consumed only once.
|
||||
calls.clear();
|
||||
loop();
|
||||
#if EXPECT_HWCDC
|
||||
assert(!usb_host_reset_pending);
|
||||
const std::vector<std::string> normal = {
|
||||
"usb.service", "usb.boundary", "modem.loop", "sensors.loop", "board.loop", "radio.loop"};
|
||||
assert(calls == normal);
|
||||
reset_event = true;
|
||||
calls.clear();
|
||||
loop();
|
||||
assert(usb_host_reset_pending);
|
||||
const std::vector<std::string> boundary = {
|
||||
"usb.service", "usb.boundary", "modem.reset", "usb.cleanup",
|
||||
"modem.loop", "sensors.loop", "board.loop", "radio.loop"};
|
||||
assert(calls == boundary);
|
||||
calls.clear();
|
||||
loop();
|
||||
assert(usb_host_reset_pending);
|
||||
const std::vector<std::string> retry = {
|
||||
"usb.service", "usb.boundary", "usb.cleanup", "modem.loop",
|
||||
"sensors.loop", "board.loop", "radio.loop"};
|
||||
assert(calls == retry);
|
||||
cleanup_ready = true;
|
||||
calls.clear();
|
||||
loop();
|
||||
assert(!usb_host_reset_pending && calls == retry);
|
||||
calls.clear();
|
||||
loop();
|
||||
assert(calls == normal);
|
||||
#else
|
||||
const std::vector<std::string> normal = {
|
||||
"modem.loop", "sensors.loop", "board.loop", "radio.loop"};
|
||||
assert(calls == normal);
|
||||
#endif
|
||||
delete modem;
|
||||
}
|
||||
+253
@@ -0,0 +1,253 @@
|
||||
// This file is appended to the actual HWCDC facade/SDK host harness.
|
||||
#include <helpers/ArduinoSerialInterface.h>
|
||||
|
||||
@KISS_CONSTANTS@
|
||||
class KissModem {
|
||||
Stream& _serial;
|
||||
@KISS_FIELDS@
|
||||
@KISS_DECLARATIONS@
|
||||
public:
|
||||
explicit KissModem(Stream& serial) : _serial(serial) { resetOutputQueue(); }
|
||||
bool queue(uint8_t type, const std::vector<uint8_t>& bytes) {
|
||||
return queueFrame(type, bytes.data(), bytes.size());
|
||||
}
|
||||
bool service() { return tryFlushFrames(); }
|
||||
bool busy() { return _tx_frame_count != 0 || _tx_busy_error_pending; }
|
||||
void serviceBusy() { queuePendingBusyError(); }
|
||||
};
|
||||
@KISS_METHODS@
|
||||
|
||||
@COMPANION_LEASE_DEFAULTS@
|
||||
static_assert(USB_FRAME_REPLY_GRACE_MS == 2000, "default frame grace");
|
||||
static_assert(USB_CLIENT_IDLE_TIMEOUT == 600000, "default idle lease");
|
||||
static_assert(USB_HOST_LOSS_EDGE_MS == 100, "default host edge");
|
||||
static_assert(USB_HOST_LOSS_GRACE_MS == 2000, "default host grace");
|
||||
static mesh::UsbHostPresenceDebouncer usb_hwcdc_host_presence;
|
||||
static ArduinoSerialInterface usb_serial_interface;
|
||||
struct Board {
|
||||
bool host = true;
|
||||
bool isUsbHostConnected() const { return host; }
|
||||
} board;
|
||||
struct Mesh {
|
||||
bool finite = false;
|
||||
bool hasFiniteDelayedReplyForRoute(const ArduinoSerialInterface*) const {
|
||||
return finite;
|
||||
}
|
||||
} the_mesh;
|
||||
|
||||
// Independent protocol expectations; no production encoder is used here.
|
||||
static std::vector<uint8_t> companionFrame(const std::vector<uint8_t>& payload) {
|
||||
std::vector<uint8_t> result = {'>', uint8_t(payload.size()), uint8_t(payload.size() >> 8)};
|
||||
result.insert(result.end(), payload.begin(), payload.end());
|
||||
return result;
|
||||
}
|
||||
static std::vector<uint8_t> kissFrame(uint8_t type, const std::vector<uint8_t>& payload) {
|
||||
std::vector<uint8_t> result = {0xC0};
|
||||
auto encode = [&](uint8_t byte) {
|
||||
if (byte == 0xC0) result.insert(result.end(), {0xDB, 0xDC});
|
||||
else if (byte == 0xDB) result.insert(result.end(), {0xDB, 0xDD});
|
||||
else result.push_back(byte);
|
||||
};
|
||||
encode(type);
|
||||
for (uint8_t byte : payload) encode(byte);
|
||||
result.push_back(0xC0);
|
||||
return result;
|
||||
}
|
||||
static void append(std::vector<uint8_t>& dest, const std::vector<uint8_t>& bytes) {
|
||||
dest.insert(dest.end(), bytes.begin(), bytes.end());
|
||||
}
|
||||
|
||||
// Consume ring capacity after the protocol's outer availableForWrite sample.
|
||||
// This represents another serial producer winning before the write. It is a
|
||||
// one-shot disturbance, and must never make the guarded SDK wait for space.
|
||||
class StaleCapacityStream : public Stream {
|
||||
Stream& _port;
|
||||
bool _consume_once = true;
|
||||
public:
|
||||
explicit StaleCapacityStream(Stream& port) : _port(port) {}
|
||||
int availableForWrite() override { return _port.availableForWrite(); }
|
||||
size_t write(const uint8_t* data, size_t size) override {
|
||||
if (_consume_once) {
|
||||
_consume_once = false;
|
||||
ring_free = ring_free > 2 ? 2 : ring_free;
|
||||
}
|
||||
return _port.write(data, size);
|
||||
}
|
||||
};
|
||||
|
||||
static void setupCompanion(bool activity_check = false) {
|
||||
usb_serial_interface.begin(@ROLE_STREAM@);
|
||||
usb_serial_interface.enableFlowControl(true);
|
||||
usb_serial_interface.enable();
|
||||
if (activity_check) {
|
||||
@COMPANION_CONNECTION@
|
||||
}
|
||||
}
|
||||
static void drainCompanion(size_t capacity = 7) {
|
||||
for (unsigned pass = 0; pass < 1000 && usb_serial_interface.hasPendingIO(); ++pass) {
|
||||
ring_free = capacity;
|
||||
usb_serial_interface.loop();
|
||||
}
|
||||
assert(!usb_serial_interface.hasPendingIO());
|
||||
}
|
||||
static void drainKiss(KissModem& modem, size_t capacity = 7) {
|
||||
for (unsigned pass = 0; pass < 1000 && modem.busy(); ++pass) {
|
||||
ring_free = capacity;
|
||||
modem.service();
|
||||
modem.serviceBusy();
|
||||
}
|
||||
assert(!modem.busy());
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
assert(argc == 2);
|
||||
const std::string name = argv[1];
|
||||
mesh::serialLogBegin();
|
||||
assert(tx_timeout_ms == 5);
|
||||
if (name == "companion_stall" || name == "companion_short") {
|
||||
setupCompanion();
|
||||
std::vector<uint8_t> payload(MAX_FRAME_SIZE);
|
||||
payload[0] = 0x05; // Required protocol response, rather than a best-effort push.
|
||||
for (size_t i = 1; i < payload.size(); ++i) payload[i] = uint8_t(i);
|
||||
const std::vector<uint8_t> tail = {0x00, 0xC0, 0xDB};
|
||||
ring_free = name == "companion_short" ? 5 : 0;
|
||||
if (name == "companion_short") reject_send = true;
|
||||
assert(usb_serial_interface.writeFrame(payload.data(), payload.size()) == payload.size());
|
||||
assert(usb_serial_interface.writeFrame(tail.data(), tail.size()) == tail.size());
|
||||
for (unsigned poll = 0; poll < 100; ++poll) usb_serial_interface.loop();
|
||||
assert(admitted.empty() && usb_serial_interface.hasPendingIO());
|
||||
reject_send = false;
|
||||
drainCompanion();
|
||||
auto expected = companionFrame(payload);
|
||||
append(expected, companionFrame(tail));
|
||||
assert(admitted == expected && zero_sends == 0 && g_mock_millis == 0);
|
||||
} else if (name == "companion_stale") {
|
||||
StaleCapacityStream stale(@ROLE_STREAM@);
|
||||
usb_serial_interface.begin(stale);
|
||||
usb_serial_interface.enableFlowControl(true);
|
||||
usb_serial_interface.enable();
|
||||
ring_free = 10;
|
||||
const std::vector<uint8_t> payload = {0x05, 1, 2, 3, 4, 5, 6, 7};
|
||||
assert(usb_serial_interface.writeFrame(payload.data(), payload.size()) == payload.size());
|
||||
assert(admitted.size() == 2 && g_mock_millis == 0 && zero_sends == 0);
|
||||
drainCompanion();
|
||||
assert(admitted == companionFrame(payload));
|
||||
} else if (name == "companion_session") {
|
||||
setupCompanion();
|
||||
host_input = {'<', 3, 0, 0x01};
|
||||
uint8_t received[MAX_FRAME_SIZE] = {};
|
||||
assert(usb_serial_interface.checkRecvFrame(received) == 0);
|
||||
assert(usb_serial_interface.isReadBusy());
|
||||
ring_free = 0;
|
||||
const uint8_t stale[] = {0x05, 0xAA};
|
||||
assert(usb_serial_interface.writeFrame(stale, sizeof(stale)) == sizeof(stale));
|
||||
// A confirmed BUS_RESET owner closes access before purging protocol state.
|
||||
access_allowed = false;
|
||||
usb_serial_interface.resetSessionState();
|
||||
host_input.clear();
|
||||
assert(!usb_serial_interface.hasPendingIO());
|
||||
ring_free = 32;
|
||||
assert(mesh::guarded.write(stale, sizeof(stale)) == 0 && admitted.empty());
|
||||
access_allowed = true;
|
||||
host_input = {'<', 1, 0, 0x02};
|
||||
assert(usb_serial_interface.checkRecvFrame(received) == 1 && received[0] == 0x02);
|
||||
const uint8_t fresh[] = {0x00, 0xBB};
|
||||
assert(usb_serial_interface.writeFrame(fresh, sizeof(fresh)) == sizeof(fresh));
|
||||
assert(admitted == companionFrame({0x00, 0xBB}));
|
||||
} else if (name == "companion_activity_lease") {
|
||||
setupCompanion(true);
|
||||
board.host = false;
|
||||
connected = false; // The SDK activity heuristic is not DTR proof.
|
||||
assert(!usb_serial_interface.isConnected());
|
||||
g_mock_millis = 100;
|
||||
host_input = {'<', 1, 0, 0x01};
|
||||
uint8_t received[MAX_FRAME_SIZE] = {};
|
||||
assert(usb_serial_interface.checkRecvFrame(received) == 1 && received[0] == 0x01);
|
||||
assert(usb_serial_interface.isConnected());
|
||||
ring_free = 0;
|
||||
const uint8_t response[] = {0x00, 0x11, 0x22};
|
||||
assert(usb_serial_interface.writeFrame(response, sizeof(response)) == sizeof(response));
|
||||
g_mock_millis += 5; // Transient SOF/activity gap must not discard a queued reply.
|
||||
drainCompanion(2);
|
||||
assert(admitted == companionFrame({0x00, 0x11, 0x22}));
|
||||
board.host = true;
|
||||
assert(usb_serial_interface.isConnected());
|
||||
g_mock_millis = 100 + 120000;
|
||||
assert(usb_serial_interface.isConnected());
|
||||
ring_free = 2;
|
||||
assert(usb_serial_interface.writeFrame(response, sizeof(response)) == sizeof(response));
|
||||
drainCompanion(2);
|
||||
auto after_idle = companionFrame({0x00, 0x11, 0x22});
|
||||
append(after_idle, companionFrame({0x00, 0x11, 0x22}));
|
||||
assert(admitted == after_idle && zero_sends == 0);
|
||||
g_mock_millis = 100 + USB_CLIENT_IDLE_TIMEOUT;
|
||||
assert(!usb_serial_interface.isConnected()); // An idle cable cannot claim USB forever.
|
||||
the_mesh.finite = true;
|
||||
assert(usb_serial_interface.isConnected());
|
||||
the_mesh.finite = false;
|
||||
assert(usb_serial_interface.isConnected()); // One bounded final-frame drain lease.
|
||||
g_mock_millis += USB_FRAME_REPLY_GRACE_MS;
|
||||
assert(!usb_serial_interface.isConnected());
|
||||
usb_serial_interface.resetSessionState();
|
||||
usb_hwcdc_host_presence.reset();
|
||||
assert(!usb_serial_interface.isConnected());
|
||||
host_input = {'<', 1, 0, 0x02};
|
||||
assert(usb_serial_interface.checkRecvFrame(received) == 1 && received[0] == 0x02);
|
||||
assert(usb_serial_interface.isConnected());
|
||||
ring_free = 2;
|
||||
assert(usb_serial_interface.writeFrame(response, sizeof(response)) == sizeof(response));
|
||||
drainCompanion(2);
|
||||
append(after_idle, companionFrame({0x00, 0x11, 0x22}));
|
||||
assert(admitted == after_idle && zero_sends == 0);
|
||||
} else if (name == "kiss_stall" || name == "kiss_short") {
|
||||
KissModem modem(@ROLE_STREAM@);
|
||||
ring_free = name == "kiss_short" ? 7 : 0;
|
||||
reject_send = name == "kiss_short";
|
||||
const std::vector<uint8_t> payload = {0xC0, 0xDB, 1, 2, 3, 4};
|
||||
assert(modem.queue(0, payload));
|
||||
for (unsigned poll = 0; poll < 100; ++poll) modem.service();
|
||||
assert(admitted.empty() && modem.busy());
|
||||
reject_send = false;
|
||||
drainKiss(modem, 2);
|
||||
assert(admitted == kissFrame(0, payload) && zero_sends == 0 && g_mock_millis == 0);
|
||||
} else if (name == "kiss_escaped_max") {
|
||||
KissModem modem(@ROLE_STREAM@);
|
||||
std::vector<uint8_t> payload(KISS_MAX_ESCAPABLE_BYTES);
|
||||
for (size_t i = 0; i < payload.size(); ++i) payload[i] = i % 2 ? 0xDB : 0xC0;
|
||||
// Both the type and each payload byte expand to two bytes. The actual
|
||||
// encoder must reject one more byte without retaining an invalid frame.
|
||||
assert(!modem.queue(0xC0, payload));
|
||||
assert(!modem.busy() && admitted.empty());
|
||||
payload.pop_back();
|
||||
assert(modem.queue(0xC0, payload));
|
||||
assert(modem.busy() && admitted.empty());
|
||||
drainKiss(modem, 3);
|
||||
const auto expected = kissFrame(0xC0, payload);
|
||||
assert(expected.size() == KISS_MAX_ENCODED_FRAME_SIZE);
|
||||
assert(admitted == expected && g_mock_millis == 0 && zero_sends == 0);
|
||||
} else if (name == "kiss_stale") {
|
||||
StaleCapacityStream stale(@ROLE_STREAM@);
|
||||
KissModem modem(stale);
|
||||
ring_free = 10;
|
||||
const std::vector<uint8_t> payload = {0xC0, 0xDB, 1, 2, 3, 4};
|
||||
assert(modem.queue(0, payload));
|
||||
assert(admitted.size() == 2 && g_mock_millis == 0 && zero_sends == 0);
|
||||
drainKiss(modem);
|
||||
assert(admitted == kissFrame(0, payload));
|
||||
} else if (name == "kiss_busy") {
|
||||
KissModem modem(@ROLE_STREAM@);
|
||||
const std::vector<uint8_t> first = {0xC0, 1}, second = {0xDB, 2}, discarded = {3};
|
||||
assert(modem.queue(0, first));
|
||||
assert(modem.queue(0, second));
|
||||
assert(!modem.queue(0, discarded));
|
||||
assert(admitted.empty() && modem.busy());
|
||||
drainKiss(modem, 1);
|
||||
auto expected = kissFrame(0, first);
|
||||
append(expected, kissFrame(0, second));
|
||||
append(expected, kissFrame(0x06, {0xF1, 0x07}));
|
||||
assert(admitted == expected && g_mock_millis == 0 && zero_sends == 0);
|
||||
} else {
|
||||
assert(false && "unknown transport fixture case");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
+8
@@ -8,3 +8,11 @@ The full source is necessary to verify the exact input fingerprint and all
|
||||
producer/ISR/reset replacement sites without a PlatformIO package dependency.
|
||||
Underlying correction: upstream commit2ecad233178bfbba9c584ffd9f67e941d84adbb7,
|
||||
https://github.com/espressif/arduino-esp32/pull/12606.
|
||||
|
||||
usb_phy_ll.h is the exact upstream ESP-IDF v4.4.7 ESP32-S3 HAL header:
|
||||
https://github.com/espressif/esp-idf/blob/v4.4.7/components/hal/esp32s3/include/hal/usb_phy_ll.h
|
||||
SHA256: 07f869755299e96fc73fb913bb0b9fed2ade580f12a22c058bbb07247f5c959c
|
||||
It preserves its upstream copyright and Apache-2.0 notice. The executed
|
||||
HWCDC::begin regression uses these official HAL bodies to test retained RTC
|
||||
PHY ownership after a TinyUSB-to-HWCDC software restart, including a host
|
||||
disconnect before the controller changes. It does not simulate enumeration.
|
||||
+157
@@ -0,0 +1,157 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2015-2023 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include "esp_attr.h"
|
||||
#include "soc/soc.h"
|
||||
#include "soc/system_struct.h"
|
||||
#include "soc/usb_wrap_struct.h"
|
||||
#include "soc/rtc_cntl_struct.h"
|
||||
#include "soc/usb_serial_jtag_struct.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief Configures the internal PHY for USB_OTG
|
||||
*
|
||||
* @param hw Start address of the USB Wrap registers
|
||||
*/
|
||||
static inline void usb_phy_ll_int_otg_enable(usb_wrap_dev_t *hw)
|
||||
{
|
||||
// USB_OTG use internal PHY
|
||||
hw->otg_conf.phy_sel = 0;
|
||||
// phy_sel is controlled by the following register value
|
||||
RTCCNTL.usb_conf.sw_hw_usb_phy_sel = 1;
|
||||
// phy_sel=sw_usb_phy_sel=1, USB_OTG is connected with internal PHY
|
||||
RTCCNTL.usb_conf.sw_usb_phy_sel = 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Configures the external PHY for USB_OTG
|
||||
*
|
||||
* @param hw Start address of the USB Wrap registers
|
||||
*/
|
||||
static inline void usb_phy_ll_ext_otg_enable(usb_wrap_dev_t *hw)
|
||||
{
|
||||
// USB_OTG use external PHY
|
||||
hw->otg_conf.phy_sel = 1;
|
||||
// phy_sel is controlled by the following register value
|
||||
RTCCNTL.usb_conf.sw_hw_usb_phy_sel = 1;
|
||||
// phy_sel=sw_usb_phy_sel=0, USB_OTG is connected with external PHY through GPIO Matrix
|
||||
RTCCNTL.usb_conf.sw_usb_phy_sel = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Configures the internal PHY for USB_Serial_JTAG
|
||||
*
|
||||
* @param hw Start address of the USB Serial_JTAG registers
|
||||
*/
|
||||
static inline void usb_phy_ll_int_jtag_enable(usb_serial_jtag_dev_t *hw)
|
||||
{
|
||||
// USB_Serial_JTAG use internal PHY
|
||||
hw->conf0.phy_sel = 0;
|
||||
// Disable software control USB D+ D- pullup pulldown (Device FS: dp_pullup = 1)
|
||||
hw->conf0.pad_pull_override = 0;
|
||||
// Enable USB D+ pullup
|
||||
hw->conf0.dp_pullup = 1;
|
||||
// Enable USB pad function
|
||||
hw->conf0.usb_pad_enable = 1;
|
||||
// phy_sel is controlled by the following register value
|
||||
RTCCNTL.usb_conf.sw_hw_usb_phy_sel = 1;
|
||||
// phy_sel=sw_usb_phy_sel=0, USB_Serial_JTAG is connected with internal PHY
|
||||
RTCCNTL.usb_conf.sw_usb_phy_sel = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Configures the external PHY for USB_Serial_JTAG
|
||||
*
|
||||
* @param hw Start address of the USB Serial_JTAG registers
|
||||
*/
|
||||
static inline void usb_phy_ll_ext_jtag_enable(usb_serial_jtag_dev_t *hw)
|
||||
{
|
||||
// USB_Serial_JTAG use external PHY
|
||||
hw->conf0.phy_sel = 1;
|
||||
// phy_sel is controlled by the following register value
|
||||
RTCCNTL.usb_conf.sw_hw_usb_phy_sel = 1;
|
||||
// phy_sel=sw_usb_phy_sel=1, USB_Serial_JTAG is connected with external PHY
|
||||
RTCCNTL.usb_conf.sw_usb_phy_sel = 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Configures port loads for the internal PHY
|
||||
*
|
||||
* @param hw Start address of the USB Wrap registers
|
||||
* @param dp_pu D+ pullup load
|
||||
* @param dp_pd D+ pulldown load
|
||||
* @param dm_pu D- pullup load
|
||||
* @param dm_pd D- pulldown load
|
||||
*/
|
||||
static inline void usb_phy_ll_int_load_conf(usb_wrap_dev_t *hw, bool dp_pu, bool dp_pd, bool dm_pu, bool dm_pd)
|
||||
{
|
||||
usb_wrap_otg_conf_reg_t conf = hw->otg_conf;
|
||||
conf.pad_pull_override = 1;
|
||||
conf.dp_pullup = dp_pu;
|
||||
conf.dp_pulldown = dp_pd;
|
||||
conf.dm_pullup = dm_pu;
|
||||
conf.dm_pulldown = dm_pd;
|
||||
hw->otg_conf = conf;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Enable the internal PHY control to D+/D- pad
|
||||
* @param hw Start address of the USB Wrap registers
|
||||
* @param pad_en Enable the PHY control to D+/D- pad
|
||||
*/
|
||||
static inline void usb_phy_ll_usb_wrap_pad_enable(usb_wrap_dev_t *hw, bool pad_en)
|
||||
{
|
||||
hw->otg_conf.pad_enable = pad_en;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Enable the internal PHY's test mode
|
||||
*
|
||||
* @param hw Start address of the USB Wrap registers
|
||||
* @param en Whether to enable the internal PHY's test mode
|
||||
*/
|
||||
static inline void usb_phy_ll_int_enable_test_mode(usb_wrap_dev_t *hw, bool en)
|
||||
{
|
||||
if (en) {
|
||||
// Clear USB_WRAP_TEST_CONF_REG
|
||||
hw->test_conf.val = 0;
|
||||
// Set USB test pad oen
|
||||
hw->test_conf.test_usb_wrap_oe = 1;
|
||||
// Enable USB test mode
|
||||
hw->test_conf.test_enable = 1;
|
||||
} else {
|
||||
hw->test_conf.test_enable = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable the bus clock for USB Wrap module
|
||||
* @param clk_en True if enable the clock of USB Wrap module
|
||||
*/
|
||||
FORCE_INLINE_ATTR void usb_phy_ll_usb_wrap_enable_bus_clock(bool clk_en)
|
||||
{
|
||||
SYSTEM.perip_clk_en0.usb_clk_en = clk_en;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Reset the USB Wrap module
|
||||
*/
|
||||
FORCE_INLINE_ATTR void usb_phy_ll_usb_wrap_reset_register(void)
|
||||
{
|
||||
SYSTEM.perip_rst_en0.usb_rst = 1;
|
||||
SYSTEM.perip_rst_en0.usb_rst = 0;
|
||||
}
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,335 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Resolve ESP32 USB recipes with PlatformIO without starting a build.
|
||||
|
||||
The board manifests select Serial's SDK class and the build-local SDK patch.
|
||||
Exercise both selectors after PlatformIO's real flag/unflag processing. This
|
||||
also checks UART and non-ESP32 recipes, whose transports must not migrate.
|
||||
"""
|
||||
|
||||
from collections import Counter
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import runpy
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
try:
|
||||
import SCons.Script
|
||||
except ImportError:
|
||||
core = Path(os.environ.get("PLATFORMIO_CORE_DIR", Path.home() / ".platformio"))
|
||||
for path in sorted((core / "packages" / "tool-scons").glob("scons-local-*"), reverse=True):
|
||||
sys.path.insert(0, str(path))
|
||||
import SCons.Script
|
||||
|
||||
from platformio.builder.tools.piobuild import ParseFlagsExtended, ProcessFlags, ProcessUnFlags
|
||||
from platformio.project.config import ProjectConfig
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
CORE = Path(os.environ.get("PLATFORMIO_CORE_DIR", Path.home() / ".platformio"))
|
||||
USB_MODE = "ARDUINO_USB_MODE"
|
||||
USB_CDC = "ARDUINO_USB_CDC_ON_BOOT"
|
||||
USB_MACROS = (USB_MODE, USB_CDC)
|
||||
|
||||
# These are the eight native-USB families that used TinyUSB before migration.
|
||||
# G2 already used HWCDC for its observer repeater and KISS roles.
|
||||
MIGRATED_BOARD_COUNTS = {
|
||||
"heltec_e213": 8,
|
||||
"heltec_e290": 9,
|
||||
"heltec_t190": 9,
|
||||
"heltec_tracker_v1_1": 2,
|
||||
"heltec_tracker_v2": 15,
|
||||
"station-g2": 12,
|
||||
"station-g3-esp32": 14,
|
||||
"t_beam_1w": 11,
|
||||
}
|
||||
PREVIOUS_G2_HWCDC = {"Station_G2_repeater_observer_mqtt", "Station_G2_kiss_modem"}
|
||||
LEGACY_DIAGNOSTIC_CDC_OVERRIDES = {
|
||||
"heltec_v4_partition_migrator_test_usb_diagnostic",
|
||||
"xiao_s3_partition_migrator_test_usb_diagnostic",
|
||||
}
|
||||
|
||||
# Pin the unaffected family/transport counts rather than copying a machine's
|
||||
# environment inventory. Mixed families have explicit role rules below.
|
||||
UNCHANGED_COUNTS = {
|
||||
("ESP32-S3-WROOM-1-N4", "uart"): 21,
|
||||
("ebyte_eora-s3", "hwcdc"): 8,
|
||||
("esp32-c3-devkitm-1", "uart"): 19,
|
||||
("esp32-c6-devkitm-1", "hwcdc"): 12,
|
||||
("esp32-c6-devkitm-1", "uart"): 4,
|
||||
("esp32-s3-devkitc-1", "hwcdc"): 22,
|
||||
("esp32-s3-devkitc-1", "uart"): 55,
|
||||
("esp32-s3-zero", "hwcdc"): 17,
|
||||
("esp32dev", "classic_uart"): 5,
|
||||
("esp32doit-devkit-v1", "classic_uart"): 19,
|
||||
("esp32s3box", "hwcdc"): 7,
|
||||
("heltec-rc32", "hwcdc"): 22,
|
||||
("heltec_v4", "hwcdc"): 45,
|
||||
("heltec_v4_migrator", "hwcdc"): 1,
|
||||
("heltec_v4_migrator", "uart"): 4,
|
||||
("heltec_v4_r8", "hwcdc"): 25,
|
||||
("heltec_wifi_lora_32_V2", "classic_uart"): 9,
|
||||
("meshnology_w12", "hwcdc"): 12,
|
||||
("seeed_xiao_esp32c3", "hwcdc"): 7,
|
||||
("seeed_xiao_esp32s3", "hwcdc"): 26,
|
||||
("seeed_xiao_esp32s3_migrator", "hwcdc"): 1,
|
||||
("seeed_xiao_esp32s3_migrator", "uart"): 3,
|
||||
("t-deck", "hwcdc"): 4,
|
||||
("t3_s3_v1_x", "hwcdc"): 19,
|
||||
("t_beams3_supreme", "hwcdc"): 9,
|
||||
("thinknode_m7", "uart"): 10,
|
||||
("thinknode_m9", "uart"): 6,
|
||||
("ttgo-lora32-v1", "classic_uart"): 12,
|
||||
("ttgo-t-beam", "classic_uart"): 16,
|
||||
}
|
||||
|
||||
# Native-only CI does not install the ESP32 platform's standard manifests.
|
||||
# Use installed pinned manifests when present, and retain their 6.11.0 USB
|
||||
# defaults for a test requiring no downloads. Project-owned manifests,
|
||||
# including every migrated board, are always read from the checkout.
|
||||
EXTERNAL_BOARD_DEFAULTS = {
|
||||
"esp32-c3-devkitm-1": ("esp32c3", []),
|
||||
"esp32-c6-devkitm-1": ("esp32c6", []),
|
||||
"esp32-s3-devkitc-1": ("esp32s3", ["-DARDUINO_USB_MODE=1"]),
|
||||
"esp32dev": ("esp32", []),
|
||||
"esp32doit-devkit-v1": ("esp32", []),
|
||||
"esp32s3box": ("esp32s3", ["-DARDUINO_USB_MODE=1", "-DARDUINO_USB_CDC_ON_BOOT=1"]),
|
||||
"heltec_wifi_lora_32_V2": ("esp32", []),
|
||||
"seeed_xiao_esp32c3": ("esp32c3", ["-DARDUINO_USB_MODE=1", "-DARDUINO_USB_CDC_ON_BOOT=1"]),
|
||||
"seeed_xiao_esp32s3": ("esp32s3", ["-DARDUINO_USB_MODE=1", "-DARDUINO_USB_CDC_ON_BOOT=1"]),
|
||||
"ttgo-lora32-v1": ("esp32", []),
|
||||
"ttgo-t-beam": ("esp32", []),
|
||||
}
|
||||
|
||||
|
||||
def flag_environment(board_flags=(), build_flags=(), build_unflags=()):
|
||||
env = SCons.Script.Environment(tools=["gcc"])
|
||||
for function in (ParseFlagsExtended, ProcessFlags, ProcessUnFlags):
|
||||
env.AddMethod(function)
|
||||
# Match ProcessProgramDeps: board, user, then build_unflags.
|
||||
env.ProcessFlags(list(board_flags))
|
||||
env.ProcessFlags(list(build_flags))
|
||||
env.ProcessUnFlags(list(build_unflags))
|
||||
return env
|
||||
|
||||
|
||||
def usb_values(env, *, legacy_diagnostic_cdc_override=False):
|
||||
values = {name: [] for name in USB_MACROS}
|
||||
for definition in env.get("CPPDEFINES", []):
|
||||
if isinstance(definition, (tuple, list)):
|
||||
name, value = definition[:2]
|
||||
else:
|
||||
name, value = definition, "1"
|
||||
if name in values:
|
||||
values[name].append(str(value))
|
||||
for name, history in values.items():
|
||||
definitions = set(history)
|
||||
permitted_override = legacy_diagnostic_cdc_override and name == USB_CDC
|
||||
if (len(definitions) > 1 and not permitted_override) or definitions - {"0", "1"}:
|
||||
raise ValueError(f"ambiguous USB selector {name}: {sorted(definitions)}")
|
||||
# ProcessFlags moves -U options to the end of the compiler command.
|
||||
# The SDK patch selector reads CPPDEFINES, so such an override could
|
||||
# compile a different class from the one whose source was patched.
|
||||
if re.search(r"-U\s*" + re.escape(name) + r"\b", str(env.get("_CPPDEFFLAGS", ""))):
|
||||
raise ValueError(f"USB selector undefinition bypasses SDK selection: {name}")
|
||||
result = {name: history[-1] if history else "0" for name, history in values.items()}
|
||||
if result[USB_CDC] == "1" and not values[USB_MODE]:
|
||||
raise ValueError("native CDC requires an explicit USB backend for SDK selection")
|
||||
return result
|
||||
|
||||
|
||||
def bool_option(value):
|
||||
if isinstance(value, bool):
|
||||
return value
|
||||
if str(value).lower() not in ("true", "false", "1", "0", "yes", "no"):
|
||||
raise ValueError(f"invalid upload boolean: {value}")
|
||||
return str(value).lower() in ("true", "1", "yes")
|
||||
|
||||
|
||||
def expected_unchanged_transport(board, name):
|
||||
choices = {kind for family, kind in UNCHANGED_COUNTS if family == board}
|
||||
if len(choices) == 1:
|
||||
return next(iter(choices))
|
||||
if board == "esp32-s3-devkitc-1":
|
||||
return "hwcdc" if name.startswith(("Heltec_Wireless_Tracker_", "RAK_3112_")) else "uart"
|
||||
if board == "esp32-c6-devkitm-1":
|
||||
if name.startswith("M5Stack_Unit_C6L_"):
|
||||
return "hwcdc" if name == "M5Stack_Unit_C6L_companion_radio_usb" else "uart"
|
||||
return "hwcdc"
|
||||
if board in ("heltec_v4_migrator", "seeed_xiao_esp32s3_migrator"):
|
||||
return "hwcdc" if name.endswith("_test_usb_diagnostic") else "uart"
|
||||
raise AssertionError(f"unreviewed ESP32 board: {board}")
|
||||
|
||||
|
||||
class Esp32HwcdcRecipeTests(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
# Load tracked recipes explicitly. A developer's platformio.local.ini
|
||||
# must not create extra matrix rows or change this regression contract.
|
||||
cls.config = ProjectConfig(str(ROOT / "platformio.ini"), parse_extra=False)
|
||||
for path in sorted((ROOT / "variants").glob("*/platformio.ini")):
|
||||
cls.config.read(str(path), parse_extra=False)
|
||||
cls.sdk = runpy.run_path(str(ROOT / "scripts/esp32_usb_session_fix.py"))
|
||||
cls.rows = {}
|
||||
for section in cls.config.sections():
|
||||
if not section.startswith("env:"):
|
||||
continue
|
||||
build_flags = cls.config.get(section, "build_flags", [])
|
||||
board = cls.config.get(section, "board", "")
|
||||
manifest_path = ROOT / "boards" / f"{board}.json"
|
||||
manifest = json.loads(manifest_path.read_text()) if manifest_path.is_file() else {}
|
||||
esp32 = any(re.search(r"\bESP32_PLATFORM\b", flag) for flag in build_flags)
|
||||
if manifest:
|
||||
mcu = manifest["build"].get("mcu", "")
|
||||
board_flags = manifest["build"].get("extra_flags", [])
|
||||
elif esp32:
|
||||
mcu, board_flags = EXTERNAL_BOARD_DEFAULTS[board]
|
||||
pinned_path = CORE / "platforms" / "espressif32@6.11.0" / "boards" / f"{board}.json"
|
||||
if pinned_path.is_file():
|
||||
pinned = json.loads(pinned_path.read_text())
|
||||
mcu = pinned["build"]["mcu"]
|
||||
board_flags = pinned["build"].get("extra_flags", [])
|
||||
else:
|
||||
mcu, board_flags = "", []
|
||||
if isinstance(board_flags, str):
|
||||
board_flags = [board_flags]
|
||||
env = flag_environment(board_flags, build_flags,
|
||||
cls.config.get(section, "build_unflags", []))
|
||||
try:
|
||||
values = usb_values(env, legacy_diagnostic_cdc_override=(
|
||||
section[4:] in LEGACY_DIAGNOSTIC_CDC_OVERRIDES))
|
||||
except ValueError as error:
|
||||
raise ValueError(f"{section}: {error}") from error
|
||||
if not esp32:
|
||||
kind = "non_esp32"
|
||||
elif mcu == "esp32":
|
||||
kind = "classic_uart"
|
||||
elif values[USB_CDC] == "0":
|
||||
kind = "uart"
|
||||
else:
|
||||
kind = "hwcdc" if values[USB_MODE] == "1" else "tinyusb"
|
||||
cls.rows[section[4:]] = {
|
||||
"board": board, "mcu": mcu, "env": env, "values": values,
|
||||
"esp32": esp32, "kind": kind, "manifest": manifest,
|
||||
}
|
||||
|
||||
def test_all_78_migrated_roles_select_hwcdc_and_its_sdk_patch(self):
|
||||
counts = Counter()
|
||||
migrated = set()
|
||||
for name, row in self.rows.items():
|
||||
if row["board"] not in MIGRATED_BOARD_COUNTS:
|
||||
continue
|
||||
with self.subTest(environment=name):
|
||||
counts[row["board"]] += 1
|
||||
self.assertEqual(row["mcu"], "esp32s3")
|
||||
self.assertEqual(row["values"], {USB_MODE: "1", USB_CDC: "1"})
|
||||
self.assertEqual(row["kind"], "hwcdc")
|
||||
self.assertTrue(self.sdk["hwcdc_primary_enabled"](row["env"]))
|
||||
self.assertFalse(self.sdk["native_primary_enabled"](row["env"]))
|
||||
self.assertEqual(self.config.get(f"env:{name}", "platform"),
|
||||
"platformio/espressif32@6.11.0")
|
||||
self.assertFalse(self.config.get(f"env:{name}", "platform_packages", []))
|
||||
self.assertIn("pre:scripts/esp32_usb_session_fix.py",
|
||||
self.config.get(f"env:{name}", "extra_scripts", []))
|
||||
if name not in PREVIOUS_G2_HWCDC:
|
||||
migrated.add(name)
|
||||
self.assertEqual(counts, MIGRATED_BOARD_COUNTS)
|
||||
self.assertTrue(PREVIOUS_G2_HWCDC <= self.rows.keys())
|
||||
self.assertEqual(len(migrated), 78)
|
||||
|
||||
def test_migrated_uploads_use_hardware_reset_without_touch_or_port_wait(self):
|
||||
for name, row in self.rows.items():
|
||||
if row["board"] not in MIGRATED_BOARD_COUNTS:
|
||||
continue
|
||||
with self.subTest(environment=name):
|
||||
upload = row["manifest"]["upload"]
|
||||
for option in ("use_1200bps_touch", "wait_for_upload_port"):
|
||||
self.assertIn(option, upload, "native HWCDC upload defaults must be explicit")
|
||||
value = self.config.get(f"env:{name}", f"board_upload.{option}", upload[option])
|
||||
self.assertFalse(bool_option(value), option)
|
||||
before_reset = self.config.get(f"env:{name}", "board_upload.before_reset",
|
||||
upload.get("before_reset", "default_reset"))
|
||||
self.assertIn(before_reset, ("default_reset", "usb_reset"))
|
||||
self.assertIn(["0x303A", "0x1001"], row["manifest"]["build"]["hwids"])
|
||||
|
||||
def test_no_native_tinyusb_esp32_role_remains_and_sdk_selectors_agree(self):
|
||||
counts = Counter()
|
||||
for name, row in self.rows.items():
|
||||
if not row["esp32"]:
|
||||
continue
|
||||
with self.subTest(environment=name):
|
||||
counts[row["kind"]] += 1
|
||||
self.assertNotEqual(row["kind"], "tinyusb")
|
||||
self.assertEqual(self.sdk["hwcdc_primary_enabled"](row["env"]),
|
||||
row["kind"] == "hwcdc")
|
||||
self.assertFalse(self.sdk["native_primary_enabled"](row["env"]))
|
||||
self.assertEqual(counts, {"hwcdc": 317, "uart": 122, "classic_uart": 61})
|
||||
|
||||
def test_uart_existing_hwcdc_and_newer_mcu_recipes_keep_their_transports(self):
|
||||
counts = Counter()
|
||||
for name, row in self.rows.items():
|
||||
if not row["esp32"] or row["board"] in MIGRATED_BOARD_COUNTS:
|
||||
continue
|
||||
with self.subTest(environment=name):
|
||||
expected = expected_unchanged_transport(row["board"], name)
|
||||
self.assertEqual(row["kind"], expected)
|
||||
counts[row["board"], row["kind"]] += 1
|
||||
self.assertEqual(counts, UNCHANGED_COUNTS)
|
||||
|
||||
def test_non_esp32_recipes_do_not_acquire_esp32_usb_selectors(self):
|
||||
count = 0
|
||||
for name, row in self.rows.items():
|
||||
if row["esp32"]:
|
||||
continue
|
||||
with self.subTest(environment=name):
|
||||
count += 1
|
||||
self.assertEqual(row["values"], {USB_MODE: "0", USB_CDC: "0"})
|
||||
self.assertFalse(self.sdk["hwcdc_primary_enabled"](row["env"]))
|
||||
self.assertFalse(self.sdk["native_primary_enabled"](row["env"]))
|
||||
self.assertEqual(count, 321)
|
||||
|
||||
def test_only_existing_partition_diagnostics_keep_their_cdc_override(self):
|
||||
# These two pre-existing diagnostic recipes intentionally select CDC1
|
||||
# over a CDC0 recovery board. Keep their compiler/SDK last-definition
|
||||
# behavior while rejecting conflicting selectors in normal images.
|
||||
for name in LEGACY_DIAGNOSTIC_CDC_OVERRIDES:
|
||||
with self.subTest(environment=name):
|
||||
row = self.rows[name]
|
||||
with self.assertRaises(ValueError):
|
||||
usb_values(row["env"])
|
||||
self.assertEqual(row["kind"], "hwcdc")
|
||||
self.assertEqual(row["values"], {USB_MODE: "1", USB_CDC: "1"})
|
||||
|
||||
def test_real_unflags_remove_the_old_selector_and_identical_duplicates_are_safe(self):
|
||||
env = flag_environment(["-DARDUINO_USB_MODE=0", "-DARDUINO_USB_CDC_ON_BOOT=1"],
|
||||
["-D ARDUINO_USB_MODE=1", "-DARDUINO_USB_MODE=1"],
|
||||
["-DARDUINO_USB_MODE=0"])
|
||||
self.assertEqual(usb_values(env), {USB_MODE: "1", USB_CDC: "1"})
|
||||
self.assertTrue(self.sdk["hwcdc_primary_enabled"](env))
|
||||
self.assertFalse(self.sdk["native_primary_enabled"](env))
|
||||
env = flag_environment(["-DARDUINO_USB_MODE=1", "-DARDUINO_USB_CDC_ON_BOOT=0"])
|
||||
self.assertEqual(usb_values(env), {USB_MODE: "1", USB_CDC: "0"})
|
||||
self.assertFalse(self.sdk["hwcdc_primary_enabled"](env))
|
||||
|
||||
def test_conflicting_missing_and_undefined_backend_flags_are_rejected(self):
|
||||
cases = (
|
||||
(["-DARDUINO_USB_MODE=0", "-DARDUINO_USB_CDC_ON_BOOT=1"],
|
||||
["-DARDUINO_USB_MODE=1"], []),
|
||||
(["-DARDUINO_USB_MODE=1", "-DARDUINO_USB_CDC_ON_BOOT=1"],
|
||||
["-DARDUINO_USB_CDC_ON_BOOT=0"], []),
|
||||
(["-DARDUINO_USB_MODE=1", "-DARDUINO_USB_CDC_ON_BOOT=1"],
|
||||
["-UARDUINO_USB_MODE"], []),
|
||||
(["-DARDUINO_USB_MODE=1", "-DARDUINO_USB_CDC_ON_BOOT=1"],
|
||||
[], ["-DARDUINO_USB_MODE"]),
|
||||
(["-DARDUINO_USB_MODE=2", "-DARDUINO_USB_CDC_ON_BOOT=1"], [], []),
|
||||
)
|
||||
for board_flags, flags, unflags in cases:
|
||||
with self.subTest(board=board_flags, flags=flags, unflags=unflags):
|
||||
with self.assertRaises(ValueError):
|
||||
usb_values(flag_environment(board_flags, flags, unflags))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,174 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run Companion and KISS output over the actual bounded HWCDC facade/SDK.
|
||||
|
||||
The USB registers and RTOS ring are synthetic. The frame parser/queue, KISS
|
||||
encoder/output queue, HWCDC facade, and patched SDK write are production code.
|
||||
"""
|
||||
from pathlib import Path
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
import test_hwcdc_write_capacity as capacity
|
||||
from test_hwcdc_tx_backport import body
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
FIXTURE = ROOT / "test/fixtures/hwcdc_role_transport.cpp"
|
||||
SETUP_FIXTURE = ROOT / "test/fixtures/hwcdc_kiss_setup.cpp"
|
||||
|
||||
|
||||
class HwcdcRoleTransportTest(unittest.TestCase):
|
||||
def harness(self, raw=False):
|
||||
source = capacity.HwcdcWriteCapacityTest().harness()
|
||||
source = source[:source.index("int main(int argc, char** argv)")]
|
||||
source = source.replace("#include <functional>",
|
||||
"#include <functional>\n#include <deque>")
|
||||
source = source.replace("static size_t ring_free = 0;",
|
||||
"static std::deque<uint8_t> host_input;\n"
|
||||
"static size_t ring_free = 0;")
|
||||
source = source.replace("int available() override { return 0; }",
|
||||
"int available() override { return host_input.size(); }")
|
||||
source = source.replace("int read() override { return -1; }", """
|
||||
int read() override {
|
||||
if (host_input.empty()) return -1;
|
||||
const int byte = host_input.front(); host_input.pop_front(); return byte;
|
||||
}""")
|
||||
source = source.replace("int peek() override { return -1; }",
|
||||
"int peek() override { return host_input.empty() ? -1 : host_input.front(); }")
|
||||
|
||||
# Compile the production output methods verbatim, with only their
|
||||
# unrelated radio/cryptography owners omitted from this host boundary.
|
||||
header = (ROOT / "examples/kiss_modem/KissModem.h").read_text()
|
||||
kiss = (ROOT / "examples/kiss_modem/KissModem.cpp").read_text()
|
||||
constants = "\n".join(line for line in header.splitlines()
|
||||
if line.startswith("#define KISS_")
|
||||
or line.startswith("#define HW_ERR_TX_BUSY")
|
||||
or line.startswith("#define HW_RESP_ERROR"))
|
||||
fields = header[header.index(" uint8_t _tx_frame_buf["):
|
||||
header.index(" static uint16_t appendEscapedByte")]
|
||||
# The output-only shell retains the exact queue fields and declarations
|
||||
# from the production class; its constructor just selects the stream.
|
||||
names = ("appendEscapedByte", "encodeFrame", "resetOutputQueue",
|
||||
"popTxFrame", "tryFlushFrames", "queueFrame",
|
||||
"queueHardwareFrame", "queuePendingBusyError")
|
||||
declarations = "\n".join(
|
||||
line for line in header.splitlines()
|
||||
if any(re.search(r"\b" + name + r"\(", line) for name in names))
|
||||
# body() starts at the supplied signature; retain each return type.
|
||||
methods = "\n".join(
|
||||
kiss[kiss.rfind("\n", 0, kiss.index("KissModem::" + name + "(")) + 1:
|
||||
kiss.index("KissModem::" + name + "(")]
|
||||
+ body(kiss, "KissModem::" + name + "(")
|
||||
for name in names)
|
||||
main = (ROOT / "examples/companion_radio/main.cpp").read_text()
|
||||
connection = body(main[main.index(" // The ESP32 USB-Serial-JTAG peripheral"):],
|
||||
"usb_serial_interface.setConnectedCheck([]()") + ");"
|
||||
fixture = FIXTURE.read_text().replace("@KISS_CONSTANTS@", constants)
|
||||
fixture = fixture.replace("@KISS_FIELDS@", fields)
|
||||
fixture = fixture.replace("@KISS_DECLARATIONS@", declarations)
|
||||
fixture = fixture.replace("@KISS_METHODS@", methods)
|
||||
fixture = fixture.replace("@COMPANION_CONNECTION@", connection)
|
||||
lease_defaults = []
|
||||
for name in ("USB_FRAME_REPLY_GRACE_MS", "USB_CLIENT_IDLE_TIMEOUT",
|
||||
"USB_HOST_LOSS_EDGE_MS", "USB_HOST_LOSS_GRACE_MS"):
|
||||
definition = re.search(r"^\s*#define " + name + r"\s+(.+)$",
|
||||
main, re.MULTILINE)
|
||||
self.assertIsNotNone(definition, name)
|
||||
lease_defaults.append("#define " + name + " " + definition[1])
|
||||
fixture = fixture.replace("@COMPANION_LEASE_DEFAULTS@", "\n".join(lease_defaults))
|
||||
fixture = fixture.replace("@ROLE_STREAM@", "Serial" if raw else "mesh::guarded")
|
||||
return source + fixture
|
||||
|
||||
def compile(self, directory, raw=False):
|
||||
compiler = os.environ.get("CXX") or shutil.which("g++") or shutil.which("clang++")
|
||||
if compiler is None:
|
||||
self.skipTest("a host C++17 compiler is required")
|
||||
source = Path(directory) / "transport.cpp"
|
||||
source.write_text(self.harness(raw), encoding="ascii")
|
||||
binary = Path(directory) / "transport.exe"
|
||||
sanitizers = (["-fsanitize=address,undefined", "-fno-sanitize-recover=all",
|
||||
"-fno-pie", "-no-pie"] if os.name != "nt" else [])
|
||||
result = subprocess.run([
|
||||
compiler, "-std=c++17", "-Wall", "-Wextra", "-Wno-unused-parameter",
|
||||
"-pthread", *sanitizers, "-DESP32_PLATFORM", "-DESP32=1",
|
||||
"-DARDUINO_USB_MODE=1", "-DARDUINO_USB_CDC_ON_BOOT=1",
|
||||
"-I" + str(ROOT / "test/mocks"), "-I" + str(ROOT / "src"),
|
||||
str(source), str(ROOT / "src/helpers/ArduinoSerialInterface.cpp"),
|
||||
"-o", str(binary)
|
||||
], capture_output=True, text=True, timeout=60)
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
return binary
|
||||
|
||||
def test_actual_hwcdc_companion_and_kiss_queues(self):
|
||||
with tempfile.TemporaryDirectory(prefix="hwcdc-role-transport-") as directory:
|
||||
binary = self.compile(directory)
|
||||
for case in ("companion_stall", "companion_short", "companion_stale",
|
||||
"companion_session", "companion_activity_lease",
|
||||
"kiss_stall", "kiss_escaped_max", "kiss_stale",
|
||||
"kiss_short", "kiss_busy"):
|
||||
with self.subTest(case=case):
|
||||
result = subprocess.run([str(binary), case], capture_output=True,
|
||||
text=True, timeout=10)
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
|
||||
def test_raw_hwcdc_negative_control_waits_on_stale_capacity(self):
|
||||
with tempfile.TemporaryDirectory(prefix="hwcdc-raw-role-") as directory:
|
||||
binary = self.compile(directory, raw=True)
|
||||
for case in ("companion_stale", "kiss_stale"):
|
||||
with self.subTest(case=case):
|
||||
result = subprocess.run([str(binary), case], capture_output=True,
|
||||
text=True, timeout=10)
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
|
||||
def test_actual_kiss_setup_selects_guard_and_prepares_before_driver(self):
|
||||
compiler = os.environ.get("CXX") or shutil.which("g++") or shutil.which("clang++")
|
||||
if compiler is None:
|
||||
self.skipTest("a host C++17 compiler is required")
|
||||
main = (ROOT / "examples/kiss_modem/main.cpp").read_text()
|
||||
marker = "#if MESH_ESP32_HWCDC_SESSION_GUARD &&"
|
||||
start = main.index(marker)
|
||||
end = main.index("#endif", start) + len("#endif")
|
||||
setup = body(main, "void setup()")
|
||||
loop = body(main, "void loop()")
|
||||
source = SETUP_FIXTURE.read_text().replace("@TRANSPORT_SELECTION@", main[start:end])
|
||||
source = source.replace("@SETUP@", setup)
|
||||
state = "\n".join(re.findall(
|
||||
r"^#define (?:NOISE_FLOOR_CALIB_INTERVAL_MS|AGC_RESET_INTERVAL_MS).*|"
|
||||
r"^static uint32_t next_(?:noise_floor_calib|agc_reset)_ms.*", main, re.MULTILINE))
|
||||
reset = re.search(r"^static bool usb_host_reset_pending.*", main, re.MULTILINE)
|
||||
self.assertIsNotNone(reset)
|
||||
state += "\n#if KISS_HWCDC_TRANSPORT\n" + reset[0] + "\n#endif"
|
||||
source = source.replace("@LOOP_STATE@", state).replace("@LOOP@", loop)
|
||||
profiles = {
|
||||
"hwcdc": ["ARDUINO_USB_MODE=1", "ARDUINO_USB_CDC_ON_BOOT=1", "EXPECT_HWCDC=1"],
|
||||
"uart-hwcdc": ["ARDUINO_USB_MODE=1", "ARDUINO_USB_CDC_ON_BOOT=1",
|
||||
"KISS_UART_RX=43", "KISS_UART_TX=44", "EXPECT_UART=1"],
|
||||
"tinyusb": ["ARDUINO_USB_MODE=0", "ARDUINO_USB_CDC_ON_BOOT=1"],
|
||||
"classic-uart": ["ARDUINO_USB_MODE=1", "ARDUINO_USB_CDC_ON_BOOT=0"],
|
||||
}
|
||||
sanitizers = (["-fsanitize=address,undefined", "-fno-sanitize-recover=all",
|
||||
"-fno-pie", "-no-pie"] if os.name != "nt" else [])
|
||||
with tempfile.TemporaryDirectory(prefix="hwcdc-kiss-setup-") as directory:
|
||||
cpp = Path(directory) / "setup.cpp"
|
||||
cpp.write_text(source, encoding="ascii")
|
||||
for name, flags in profiles.items():
|
||||
with self.subTest(profile=name):
|
||||
binary = Path(directory) / name
|
||||
compiled = subprocess.run([
|
||||
compiler, "-std=c++17", *sanitizers, "-DESP32=1", "-DARDUINO=10800",
|
||||
"-DESP32_PLATFORM", *["-D" + flag for flag in flags],
|
||||
"-I" + str(ROOT / "test/mocks"), "-I" + str(ROOT / "src"),
|
||||
str(cpp), "-o", str(binary)
|
||||
], capture_output=True, text=True, timeout=60)
|
||||
self.assertEqual(compiled.returncode, 0, compiled.stdout + compiled.stderr)
|
||||
executed = subprocess.run([str(binary)], capture_output=True,
|
||||
text=True, timeout=10)
|
||||
self.assertEqual(executed.returncode, 0, executed.stdout + executed.stderr)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
@@ -1,5 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
import importlib.util
|
||||
import hashlib
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import tempfile
|
||||
@@ -8,7 +9,10 @@ import unittest
|
||||
ROOT=Path(__file__).resolve().parents[1]
|
||||
CORE=ROOT/'test/fixtures/hwcdc_tx_backport'
|
||||
spec=importlib.util.spec_from_file_location('usb_fix',ROOT/'scripts/esp32_usb_session_fix.py')
|
||||
module=importlib.util.module_from_spec(spec);spec.loader.exec_module(module)
|
||||
module=importlib.util.module_from_spec(spec)
|
||||
# Exercise the current transform after rapid edits without timestamp-based
|
||||
# bytecode caches retaining an earlier same-size fingerprint literal.
|
||||
exec(compile(Path(spec.origin).read_text(),spec.origin,'exec'),module.__dict__)
|
||||
RAW=(CORE/'HWCDC.cpp').read_text()
|
||||
PATCHED=module.patched_hwcdc_source(RAW)
|
||||
|
||||
@@ -201,6 +205,112 @@ class BackportTest(unittest.TestCase):
|
||||
body(source,'bool HWCDC::isCDC_Connected()')).replace('@ISR@',body(source,'static void hw_cdc_isr_handler('))
|
||||
def test_actual_isr_suffix_wakeup_pending_quarantine_and_reset(self):
|
||||
self.compile_run(self.harness())
|
||||
def phy_harness(self, source=PATCHED, *, s3=1, mode=1, cdc=1):
|
||||
hal=(CORE/'usb_phy_ll.h').read_text()
|
||||
self.assertEqual(hashlib.sha256(hal.encode()).hexdigest(),
|
||||
'07f869755299e96fc73fb913bb0b9fed2ade580f12a22c058bbb07247f5c959c')
|
||||
helper_names=[('mesh_hwcdc_int_jtag_enable','usb_phy_ll_int_jtag_enable'),
|
||||
('mesh_hwcdc_usb_wrap_pad_enable','usb_phy_ll_usb_wrap_pad_enable')]
|
||||
hal_functions=[]
|
||||
for local,official in helper_names:
|
||||
helper=body(source,'static inline void '+local+'(')
|
||||
self.assertEqual(helper.replace(local,official,1),
|
||||
body(hal,'static inline void '+official+'('))
|
||||
hal_functions.append(helper)
|
||||
hal_functions='\n'.join(hal_functions)
|
||||
self.assertNotIn('#include "hal/usb_phy_ll.h"',source)
|
||||
harness=r'''
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
@CONFIG@
|
||||
#define USB_SERIAL_JTAG_LL_INTR_MASK 7
|
||||
#define USB_SERIAL_JTAG_INTR_SERIAL_IN_EMPTY 1
|
||||
#define USB_SERIAL_JTAG_INTR_SERIAL_OUT_RECV_PKT 2
|
||||
#define USB_SERIAL_JTAG_INTR_BUS_RESET 4
|
||||
#define ETS_USB_SERIAL_JTAG_INTR_SOURCE 1
|
||||
#define ESP_OK 0
|
||||
#define log_e(...) ((void)0)
|
||||
#define isr_log_e(...) ((void)0)
|
||||
struct usb_serial_jtag_dev_t {
|
||||
struct {unsigned phy_sel=1,pad_pull_override=1,dp_pullup=0,usb_pad_enable=0;} conf0;
|
||||
} USB_SERIAL_JTAG;
|
||||
struct usb_wrap_dev_t {struct {unsigned pad_enable=1;} otg_conf;} USB_WRAP;
|
||||
struct {struct {unsigned sw_hw_usb_phy_sel=1,sw_usb_phy_sel=1;} usb_conf;} RTCCNTL;
|
||||
static unsigned delays=0,interrupt_mask=0,allocations=0;
|
||||
static void delay(unsigned ms){
|
||||
// The old TinyUSB owner must be detached before any PHY ownership change.
|
||||
assert(ms==20&&!USB_WRAP.otg_conf.pad_enable);
|
||||
assert(RTCCNTL.usb_conf.sw_hw_usb_phy_sel==1&&RTCCNTL.usb_conf.sw_usb_phy_sel==1);
|
||||
++delays;
|
||||
}
|
||||
#if CONFIG_IDF_TARGET_ESP32S3 && ARDUINO_USB_MODE && ARDUINO_USB_CDC_ON_BOOT
|
||||
@HAL@
|
||||
#endif
|
||||
static int handle_token;
|
||||
static void* tx_lock=&handle_token;
|
||||
static void* rx_queue=&handle_token;
|
||||
static void* tx_ring_buf=&handle_token;
|
||||
static void* intr_handle=nullptr;
|
||||
static void* xSemaphoreCreateMutex(){return &handle_token;}
|
||||
static void usb_serial_jtag_ll_disable_intr_mask(unsigned){interrupt_mask=0;}
|
||||
static void usb_serial_jtag_ll_ena_intr_mask(unsigned mask){interrupt_mask=mask;}
|
||||
static void hw_cdc_isr_handler(void*){}
|
||||
static int esp_intr_alloc(int,int,void(*)(void*),void*,void** out){
|
||||
++allocations;*out=&handle_token;return ESP_OK;
|
||||
}
|
||||
struct HWCDC {
|
||||
bool setRxBufferSize(size_t){return true;}
|
||||
bool setTxBufferSize(size_t){return true;}
|
||||
void end(){}
|
||||
void begin(unsigned long baud);
|
||||
};
|
||||
@BEGIN@
|
||||
int main(){
|
||||
HWCDC serial;
|
||||
// Simulate RTC USB registers retained across esp_restart from TinyUSB.
|
||||
serial.begin(115200);
|
||||
#if CONFIG_IDF_TARGET_ESP32S3 && ARDUINO_USB_MODE && ARDUINO_USB_CDC_ON_BOOT
|
||||
assert(RTCCNTL.usb_conf.sw_hw_usb_phy_sel==1&&RTCCNTL.usb_conf.sw_usb_phy_sel==0);
|
||||
assert(delays==1&&!USB_WRAP.otg_conf.pad_enable);
|
||||
#else
|
||||
// C3, UART-backed Serial and native TinyUSB do not change RTC ownership.
|
||||
assert(RTCCNTL.usb_conf.sw_hw_usb_phy_sel==1&&RTCCNTL.usb_conf.sw_usb_phy_sel==1);
|
||||
assert(delays==0&&USB_WRAP.otg_conf.pad_enable);
|
||||
#endif
|
||||
assert(USB_SERIAL_JTAG.conf0.phy_sel==0&&!USB_SERIAL_JTAG.conf0.pad_pull_override);
|
||||
assert(USB_SERIAL_JTAG.conf0.dp_pullup==1&&USB_SERIAL_JTAG.conf0.usb_pad_enable==1);
|
||||
assert(interrupt_mask==7&&allocations==1);
|
||||
unsigned prior_delays=delays;
|
||||
serial.begin(115200);assert(delays==prior_delays&&allocations==1);
|
||||
// Normal cold/continued HWCDC startup needs no forced disconnect delay.
|
||||
RTCCNTL.usb_conf.sw_hw_usb_phy_sel=0;RTCCNTL.usb_conf.sw_usb_phy_sel=0;
|
||||
serial.begin(115200);assert(delays==prior_delays);
|
||||
}
|
||||
'''
|
||||
config='\n'.join('#define '+name+' '+str(value) for name,value in [
|
||||
('CONFIG_IDF_TARGET_ESP32S3',s3),('ARDUINO_USB_MODE',mode),('ARDUINO_USB_CDC_ON_BOOT',cdc)])
|
||||
# Suppress unused functions only for branches whose complete HAL is
|
||||
# intentionally unavailable; the real source's begin() still executes.
|
||||
harness=harness.replace('static void delay(unsigned ms)',
|
||||
'static void __attribute__((unused)) delay(unsigned ms)')
|
||||
return harness.replace('@CONFIG@',config).replace('@HAL@',hal_functions).replace(
|
||||
'@BEGIN@',body(source,'void HWCDC::begin(unsigned long baud)'))
|
||||
def test_s3_begin_reclaims_retained_tinyusb_phy_after_host_disconnect(self):
|
||||
self.compile_run(self.phy_harness())
|
||||
def test_s3_legacy_begin_retains_otg_ownership_negative_control(self):
|
||||
# Keep verified production helpers while executing only legacy begin.
|
||||
harness=self.phy_harness().replace(body(PATCHED,'void HWCDC::begin(unsigned long baud)'),
|
||||
body(RAW,'void HWCDC::begin(unsigned long baud)'))
|
||||
self.compile_run(harness,expect_success=False)
|
||||
def test_s3_handoff_requires_disconnecting_the_old_controller_negative_control(self):
|
||||
source=PATCHED.replace(' mesh_hwcdc_usb_wrap_pad_enable(&USB_WRAP, false);\n','',1)
|
||||
self.assertNotEqual(source,PATCHED)
|
||||
self.compile_run(self.phy_harness(source),expect_success=False)
|
||||
def test_phy_handoff_is_excluded_from_c3_uart_and_tinyusb(self):
|
||||
for s3,mode,cdc in [(0,1,1),(1,1,0),(1,0,1)]:
|
||||
with self.subTest(s3=s3,mode=mode,cdc=cdc):
|
||||
self.compile_run(self.phy_harness(s3=s3,mode=mode,cdc=cdc))
|
||||
def test_ignored_fifo_count_negative_control(self):
|
||||
source=PATCHED.replace('sent_size = usb_serial_jtag_ll_write_txfifo(queued_buff, queued_size);',
|
||||
'usb_serial_jtag_ll_write_txfifo(queued_buff, queued_size);sent_size = queued_size;',1)
|
||||
|
||||
@@ -335,6 +335,90 @@ TEST_F(KissModemFixture, RadioTxCompletionAdvancesWhileHostOutputIsBackedUp) {
|
||||
EXPECT_FALSE(modem.isTxBusy());
|
||||
}
|
||||
|
||||
TEST_F(KissModemFixture, HostSessionResetPreservesAnActiveRadioTransmission) {
|
||||
radio.setSendComplete(false);
|
||||
serial.pushRx(dataFrame({0x42}));
|
||||
advanceToTxSending();
|
||||
ASSERT_TRUE(modem.isActuallyTransmitting());
|
||||
ASSERT_EQ(radio.startSendCount(), 1);
|
||||
ASSERT_EQ(radio.sendFinishedCount(), 0);
|
||||
|
||||
serial.setBlockWrites(true);
|
||||
serial.pushRx(hardwareFrame(HW_CMD_PING));
|
||||
modem.loop();
|
||||
ASSERT_TRUE(modem.isHostOutputBackedUp());
|
||||
|
||||
modem.resetHostSession();
|
||||
EXPECT_TRUE(modem.isActuallyTransmitting());
|
||||
EXPECT_TRUE(modem.isTxBusy());
|
||||
EXPECT_FALSE(modem.isHostOutputBackedUp());
|
||||
EXPECT_EQ(radio.startSendCount(), 1);
|
||||
EXPECT_EQ(radio.sendFinishedCount(), 0);
|
||||
serial.setBlockWrites(false);
|
||||
modem.loop();
|
||||
EXPECT_TRUE(modem.isActuallyTransmitting());
|
||||
EXPECT_TRUE(serial.writesSnapshot().empty());
|
||||
|
||||
radio.setSendComplete(true);
|
||||
modem.loop();
|
||||
modem.loop();
|
||||
EXPECT_EQ(radio.sendFinishedCount(), 1);
|
||||
EXPECT_EQ(radio.startSendCount(), 1);
|
||||
EXPECT_FALSE(modem.isTxBusy());
|
||||
const std::vector<uint8_t> expected = {
|
||||
KISS_FEND, KISS_CMD_SETHARDWARE, HW_RESP_TX_DONE, 0x01, KISS_FEND};
|
||||
EXPECT_EQ(serial.writesSnapshot(), expected);
|
||||
}
|
||||
|
||||
TEST_F(KissModemFixture, HostSessionResetReleasesAnAlreadyCompletedRadioResult) {
|
||||
serial.pushRx(dataFrame({0x42}));
|
||||
advanceToTxSending();
|
||||
ASSERT_TRUE(modem.isActuallyTransmitting());
|
||||
serial.setBlockWrites(true);
|
||||
const uint8_t packet[] = {0x11};
|
||||
// Occupy both output slots so the completed RF result waits outside them.
|
||||
modem.onPacketReceived(TEST_SNR, TEST_RSSI, packet, sizeof(packet));
|
||||
modem.loop();
|
||||
modem.loop();
|
||||
ASSERT_EQ(radio.sendFinishedCount(), 1);
|
||||
ASSERT_TRUE(modem.isTxBusy());
|
||||
ASSERT_TRUE(modem.isHostOutputBackedUp());
|
||||
|
||||
modem.resetHostSession();
|
||||
serial.setBlockWrites(false);
|
||||
modem.loop();
|
||||
EXPECT_FALSE(modem.isTxBusy());
|
||||
EXPECT_FALSE(modem.isHostOutputBackedUp());
|
||||
EXPECT_TRUE(serial.writesSnapshot().empty());
|
||||
EXPECT_EQ(radio.sendFinishedCount(), 1);
|
||||
EXPECT_EQ(radio.startSendCount(), 1);
|
||||
|
||||
sendHardware(HW_CMD_PING);
|
||||
const std::vector<uint8_t> expected = {
|
||||
KISS_FEND, KISS_CMD_SETHARDWARE, HW_RESP(HW_CMD_PING), KISS_FEND};
|
||||
EXPECT_EQ(serial.writesSnapshot(), expected);
|
||||
}
|
||||
|
||||
TEST_F(KissModemFixture, HostSessionResetDropsPartialInputAndQueuedOutput) {
|
||||
serial.setBlockWrites(true);
|
||||
sendHardware(HW_CMD_PING);
|
||||
serial.pushRx({KISS_FEND, KISS_CMD_TXDELAY});
|
||||
modem.loop();
|
||||
ASSERT_TRUE(modem.isHostOutputBackedUp());
|
||||
|
||||
modem.resetHostSession();
|
||||
EXPECT_FALSE(modem.isHostOutputBackedUp());
|
||||
serial.setBlockWrites(false);
|
||||
// The tail of the abandoned setter must not replace the 50-unit TX delay.
|
||||
serial.pushRx({100, KISS_FEND});
|
||||
modem.loop();
|
||||
EXPECT_TRUE(serial.writesSnapshot().empty());
|
||||
serial.pushRx(dataFrame({0x42}));
|
||||
advanceToTxSending();
|
||||
EXPECT_EQ(radio.startSendCount(), 1);
|
||||
EXPECT_TRUE(modem.isActuallyTransmitting());
|
||||
}
|
||||
|
||||
TEST_F(KissModemFixture, QueueFullReportsBusyWithoutDroppingQueuedFrames) {
|
||||
static constexpr uint8_t TEST_PACKET_ONE[] = {0x11, 0x12};
|
||||
static constexpr uint8_t TEST_PACKET_TWO[] = {0x21, 0x22};
|
||||
|
||||
@@ -349,19 +349,12 @@ build_flags =
|
||||
${Station_G2.build_flags}
|
||||
-UENABLE_OTA
|
||||
-D DISABLE_LORA_OTA=1
|
||||
-D ARDUINO_USB_MODE=1
|
||||
build_src_filter = ${Station_G2.build_src_filter}
|
||||
+<../examples/kiss_modem/>
|
||||
|
||||
[env:Station_G2_repeater_observer_mqtt]
|
||||
extends = Station_G2
|
||||
; The Full repeater needs a serial console, so use the S3's fixed hardware
|
||||
; CDC/JTAG controller instead of TinyUSB's DTR-driven session quarantine.
|
||||
; Companion and the other G2 roles retain their existing USB backend.
|
||||
build_unflags =
|
||||
${Station_G2.build_unflags}
|
||||
-DARDUINO_USB_MODE=0
|
||||
board_upload.use_1200bps_touch = false
|
||||
; All native USB roles inherit hardware CDC/JTAG from the board manifest.
|
||||
board_build.partitions = default_16MB.csv ; standard 16MB partition table
|
||||
extra_scripts =
|
||||
${esp32_base.extra_scripts}
|
||||
@@ -370,7 +363,6 @@ board_ssl_cert_source = adafruit-full
|
||||
board_build.embed_files = src/certs/x509_crt_bundle.bin
|
||||
build_flags =
|
||||
${Station_G2.build_flags}
|
||||
-D ARDUINO_USB_MODE=1
|
||||
; The base observer disables LoRa OTA; build.sh promotes it to FULL and restores OTA.
|
||||
-UENABLE_OTA
|
||||
-D DISABLE_LORA_OTA=1
|
||||
|
||||
Reference in new issue
Block a user