From 602dbfe3d9ad43630d9b4b6e8d9b6a73f4bc48b8 Mon Sep 17 00:00:00 2001 From: mikecarper Date: Thu, 10 Sep 2026 02:37:15 -0700 Subject: [PATCH] Fix inherited nRF52 USB READY hang in build-local TinyUSB driver --- .github/workflows/run-unit-tests.yml | 1 + docs/nrf52-usb-ready-fix.md | 41 +++++ platformio.ini | 1 + scripts/nrf52_usb_power_fix.py | 97 ++++++++++ test/fixtures/nrf52_usb_power_original.c | 188 ++++++++++++++++++++ test/nrf5x_power_harness.py | 217 +++++++++++++++++++++++ test/test_nrf52_usb_power.py | 100 +++++++++++ 7 files changed, 645 insertions(+) create mode 100644 docs/nrf52-usb-ready-fix.md create mode 100644 scripts/nrf52_usb_power_fix.py create mode 100644 test/fixtures/nrf52_usb_power_original.c create mode 100644 test/nrf5x_power_harness.py create mode 100644 test/test_nrf52_usb_power.py diff --git a/.github/workflows/run-unit-tests.yml b/.github/workflows/run-unit-tests.yml index 78da97cd..2d6f9645 100644 --- a/.github/workflows/run-unit-tests.yml +++ b/.github/workflows/run-unit-tests.yml @@ -88,6 +88,7 @@ jobs: python3 -B test/test_cascade_release_package.py python3 -B test/test_t096_full_memory.py python3 -B test/test_nrf52_ble_startup.py + python3 -B test/test_nrf52_usb_power.py python3 -B test/test_shared_mota_queue.py - name: Verify Companion Bluetooth settings and WebConfig diff --git a/docs/nrf52-usb-ready-fix.md b/docs/nrf52-usb-ready-fix.md new file mode 100644 index 00000000..0a6c1a40 --- /dev/null +++ b/docs/nrf52-usb-ready-fix.md @@ -0,0 +1,41 @@ +# nRF52 USB READY hang + +The RAK3401 on Mercer was found stuck in the framework's TinyUSB power handler: +USBD was enabled and attached, but its write-one-to-clear READY event was +already consumed. The duplicate-event guard also required HFCLK to be running. +If that guard missed the completed initialization, the following unbounded +READY wait could stop application startup. A bootloader update alone cannot +replace this USB driver in an already installed MeshCore application. + +The wait and guard were inherited through the pinned Adafruit nRF52 Arduino +framework, not introduced by the recent OTAFIX upstream merge. TinyUSB's wait +dates back at least to 2019; the guard was added in +[upstream commit 7d9efd0697](https://github.com/hathach/tinyusb/commit/7d9efd06979f6cdde2a4093f0c26e8100312c92c). +OTAFIX's August 24 HFCLK backport addressed a later clock wait, leaving this +earlier READY wait uncovered. The exact live interrupt ordering is not known; +the stalled program counter and consumed event were read directly over SWD. + +All nRF52 environments inherit `pre:scripts/nrf52_usb_power_fix.py`. It compiles +a build-local replacement for the matching framework driver, without changing +PlatformIO's shared SDK or affecting ESP32 builds. A changed/unrecognized +framework implementation stops the build for review. The backport: + +- requests HFCLK before checking the consumed READY event; +- rechecks attachment while waiting, including completion by another callback; +- bounds both waits with one 100,000-poll budget, independent of OS ticks; +- ignores stale READY on a disabled peripheral and exits if it is removed. + +The poll budget is an iteration limit, not a promised elapsed-time timeout. +If the clock/peripheral never becomes ready, the caller returns instead of +freezing the application. A later power event can retry; permanent electrical +failure can still require reconnecting USB. This does not establish or fix the +separate Pi hub/controller fault. + +Run `python3 -B test/test_nrf52_usb_power.py -v`. The tests compile the actual +patched handler from the pinned-framework fixture, model W1C event semantics, +and cover duplicate/nested callbacks, delayed/missing clocks and READY, removal, +retry, and detached USB. Restoring the inherited READY prefix must reproduce +the infinite wait under a subprocess deadline. Additional tests check SDK +isolation, idempotence, fail-closed patching, and all nRF52 environment hooks. +The harness is shared with OTAFIX's TinyUSB fork at +`test/otafix/nrf5x_power_test.py`; keep the two copies in sync when extending it. diff --git a/platformio.ini b/platformio.ini index fd80a126..f7e4fc1f 100644 --- a/platformio.ini +++ b/platformio.ini @@ -137,6 +137,7 @@ platform_packages = framework-arduinoadafruitnrf52 @ https://github.com/meshcore-dev/Adafruit_nRF52_Arduino#d541301 platformio/toolchain-gccarmnoneeabi@^1.140201.0 extra_scripts = create-uf2.py + pre:scripts/nrf52_usb_power_fix.py pre:scripts/nrf52_internal_bootloader_link.py post:scripts/check_firmware_ram.py build_flags = ${arduino_base.build_flags} diff --git a/scripts/nrf52_usb_power_fix.py b/scripts/nrf52_usb_power_fix.py new file mode 100644 index 00000000..52252a49 --- /dev/null +++ b/scripts/nrf52_usb_power_fix.py @@ -0,0 +1,97 @@ +"""Compile a build-local fix for the framework's inherited nRF52 USB READY hang. + +Never modify PlatformIO's shared framework package. Fail closed if a framework +update changes the code this narrow backport expects. The same handler policy +is carried by OTAFIX's mikecarper/tinyusb fork. +""" + +from pathlib import Path + + +OLD_READY = """ case USB_EVT_READY: + // Skip if pull-up is enabled and HCLK is already running. + // Application probably call this more than necessary. + if (NRF_USBD->USBPULLUP && hfclk_running()) break; + + // Waiting for USBD peripheral enabled + while (!(USBD_EVENTCAUSE_READY_Msk & NRF_USBD->EVENTCAUSE)) {} + +""" +READY = """ case USB_EVT_READY: + { + // READY is a consumed event, not a persistent status bit. In particular, + // a post-SoftDevice callback may find USB already attached but HFCLK no + // longer requested by this context. Restore the clock before testing + // completion; do not wait for a second peripheral READY event. + uint32_t remaining = 100000; + if ( !NRF_USBD->ENABLE ) break; + hfclk_enable(); + while ( !hfclk_running() ) + { + if ( !NRF_USBD->ENABLE || !--remaining ) return; + } + + // Recheck attachment inside the wait: another READY handler can preempt + // this one and consume EVENTCAUSE before it attaches. Both waits share + // a finite poll budget, independent of ticks/interrupts being available. + while ( !(USBD_EVENTCAUSE_READY_Msk & NRF_USBD->EVENTCAUSE) ) + { + if ( NRF_USBD->USBPULLUP || !NRF_USBD->ENABLE || !--remaining ) return; + } + if ( NRF_USBD->USBPULLUP || !NRF_USBD->ENABLE ) break; + +""" +OLD_ATTACH = """ // Wait for HFCLK + while (!hfclk_running()) {} + + // Enable pull up + NRF_USBD->USBPULLUP = 1; + __ISB(); + __DSB(); // for sync + break; + + case USB_EVT_REMOVED:""" +ATTACH = """ // Enable pull up + if ( NRF_USBD->ENABLE ) NRF_USBD->USBPULLUP = 1; + __ISB(); + __DSB(); // for sync + } + break; + + case USB_EVT_REMOVED:""" + + +def patched_source(source): + source = source.replace("\r\n", "\n") + if READY in source and ATTACH in source and OLD_READY not in source and OLD_ATTACH not in source: + return source + if source.count(OLD_READY) != 1 or source.count(OLD_ATTACH) != 1: + raise RuntimeError( + "nRF52 USB power fix: unrecognized TinyUSB driver; review the " + "framework update before building (shared SDK was not modified)" + ) + return source.replace(OLD_READY, READY).replace(OLD_ATTACH, ATTACH) + + +def replace_driver(build_env, node): + # SCons passes a not-yet-created VariantDir node, not the SDK source path. + source = Path(node.srcnode().get_abspath()) + patched = patched_source(source.read_text(encoding="utf-8")) + destination = Path(build_env.subst("$BUILD_DIR")) / "patched-nrf52-usb" / source.name + destination.parent.mkdir(parents=True, exist_ok=True) + if not destination.exists() or destination.read_text(encoding="utf-8") != patched: + destination.write_text(patched, encoding="utf-8") + print("nRF52 USB: build-local bounded READY/HFCLK fix enabled") + return build_env.File(str(destination)) + + +def install(build_env): + build_env.AddBuildMiddleware( + replace_driver, + "*Adafruit_TinyUSB_Arduino*src*portable*nordic*nrf5x*dcd_nrf5x.c", + ) + + +if "Import" in globals(): + Import("env") + install(env) diff --git a/test/fixtures/nrf52_usb_power_original.c b/test/fixtures/nrf52_usb_power_original.c new file mode 100644 index 00000000..a5ac1d9a --- /dev/null +++ b/test/fixtures/nrf52_usb_power_original.c @@ -0,0 +1,188 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2019 Ha Thach (tinyusb.org) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * This file is part of the TinyUSB stack. + */ + +// Pinned framework d541301: real power handler before the build-local fix. +void tusb_hal_nrf_power_event(uint32_t event); +void tusb_hal_nrf_power_event(uint32_t event) { + // Value is chosen to be as same as NRFX_POWER_USB_EVT_* in nrfx_power.h + enum { + USB_EVT_DETECTED = 0, + USB_EVT_REMOVED = 1, + USB_EVT_READY = 2 + }; + +#if CFG_TUSB_DEBUG >= 3 + const char* const power_evt_str[] = {"Detected", "Removed", "Ready"}; + TU_LOG(3, "Power USB event: %s\r\n", power_evt_str[event]); +#endif + + switch (event) { + case USB_EVT_DETECTED: + if (!NRF_USBD->ENABLE) { + // Prepare for receiving READY event: disable interrupt since we will blocking wait + NRF_USBD->INTENCLR = USBD_INTEN_USBEVENT_Msk; + NRF_USBD->EVENTCAUSE = USBD_EVENTCAUSE_READY_Msk; + __ISB(); + __DSB(); // for sync + +#ifdef NRF52_SERIES // NRF53 does not need this errata + // ERRATA 171, 187, 166 + if (nrf52_errata_187()) { + // CRITICAL_REGION_ENTER(); + if (*((volatile uint32_t*) (0x4006EC00)) == 0x00000000) { + *((volatile uint32_t*) (0x4006EC00)) = 0x00009375; + *((volatile uint32_t*) (0x4006ED14)) = 0x00000003; + *((volatile uint32_t*) (0x4006EC00)) = 0x00009375; + } else { + *((volatile uint32_t*) (0x4006ED14)) = 0x00000003; + } + // CRITICAL_REGION_EXIT(); + } + + if (nrf52_errata_171()) { + // CRITICAL_REGION_ENTER(); + if (*((volatile uint32_t*) (0x4006EC00)) == 0x00000000) { + *((volatile uint32_t*) (0x4006EC00)) = 0x00009375; + *((volatile uint32_t*) (0x4006EC14)) = 0x000000C0; + *((volatile uint32_t*) (0x4006EC00)) = 0x00009375; + } else { + *((volatile uint32_t*) (0x4006EC14)) = 0x000000C0; + } + // CRITICAL_REGION_EXIT(); + } +#endif + + // Enable the peripheral (will cause Ready event) + NRF_USBD->ENABLE = 1; + __ISB(); + __DSB(); // for sync + + // Enable HFCLK + hfclk_enable(); + } + break; + + case USB_EVT_READY: + // Skip if pull-up is enabled and HCLK is already running. + // Application probably call this more than necessary. + if (NRF_USBD->USBPULLUP && hfclk_running()) break; + + // Waiting for USBD peripheral enabled + while (!(USBD_EVENTCAUSE_READY_Msk & NRF_USBD->EVENTCAUSE)) {} + + NRF_USBD->EVENTCAUSE = USBD_EVENTCAUSE_READY_Msk; + __ISB(); + __DSB(); // for sync + +#ifdef NRF52_SERIES + if (nrf52_errata_171()) { + // CRITICAL_REGION_ENTER(); + if (*((volatile uint32_t*) (0x4006EC00)) == 0x00000000) { + *((volatile uint32_t*) (0x4006EC00)) = 0x00009375; + *((volatile uint32_t*) (0x4006EC14)) = 0x00000000; + *((volatile uint32_t*) (0x4006EC00)) = 0x00009375; + } else { + *((volatile uint32_t*) (0x4006EC14)) = 0x00000000; + } + + // CRITICAL_REGION_EXIT(); + } + + if (nrf52_errata_187()) { + // CRITICAL_REGION_ENTER(); + if (*((volatile uint32_t*) (0x4006EC00)) == 0x00000000) { + *((volatile uint32_t*) (0x4006EC00)) = 0x00009375; + *((volatile uint32_t*) (0x4006ED14)) = 0x00000000; + *((volatile uint32_t*) (0x4006EC00)) = 0x00009375; + } else { + *((volatile uint32_t*) (0x4006ED14)) = 0x00000000; + } + // CRITICAL_REGION_EXIT(); + } + + if (nrf52_errata_166()) { + *((volatile uint32_t*) (NRF_USBD_BASE + 0x800)) = 0x7E3; + *((volatile uint32_t*) (NRF_USBD_BASE + 0x804)) = 0x40; + + __ISB(); + __DSB(); + } +#endif + + // ISO buffer Lower half for IN, upper half for OUT + NRF_USBD->ISOSPLIT = USBD_ISOSPLIT_SPLIT_HalfIN; + + // Enable bus-reset interrupt + NRF_USBD->INTENSET = USBD_INTEN_USBRESET_Msk; + + // Enable interrupt, priorities should be set by application + NVIC_ClearPendingIRQ(USBD_IRQn); + + // Don't enable USBD interrupt yet, if dcd_init() did not finish yet + // Interrupt will be enabled by tud_init(), when USB stack is ready + // to handle interrupts. + if (tud_inited()) { + NVIC_EnableIRQ(USBD_IRQn); + } + + // Wait for HFCLK + while (!hfclk_running()) {} + + // Enable pull up + NRF_USBD->USBPULLUP = 1; + __ISB(); + __DSB(); // for sync + break; + + case USB_EVT_REMOVED: + if (NRF_USBD->ENABLE) { + // Abort all transfers + + // Disable pull up + NRF_USBD->USBPULLUP = 0; + __ISB(); + __DSB(); // for sync + + // Disable Interrupt + NVIC_DisableIRQ(USBD_IRQn); + + // disable all interrupt + NRF_USBD->INTENCLR = NRF_USBD->INTEN; + + NRF_USBD->ENABLE = 0; + __ISB(); + __DSB(); // for sync + + hfclk_disable(); + + dcd_event_bus_signal(0, DCD_EVENT_UNPLUGGED, is_in_isr()); + } + break; + + default: + break; + } +} diff --git a/test/nrf5x_power_harness.py b/test/nrf5x_power_harness.py new file mode 100644 index 00000000..c72f4315 --- /dev/null +++ b/test/nrf5x_power_harness.py @@ -0,0 +1,217 @@ +#!/usr/bin/env python3 +"""Execute the real nRF5x power handler against write-one-to-clear registers. + +No board/SDK is required. The negative control restores the inherited READY +prefix and must reproduce the hang, not merely fail a source-text assertion. +""" + +import argparse +import os +from pathlib import Path +import re +import shlex +import subprocess +import tempfile + + +HARNESS = r''' +#include +#include +#include +#include +#include + +static std::function on_read; +static std::function on_barrier; +static bool auto_ready = true, clock_requested, clock_ok = true, irq_enabled; +static unsigned clock_reads, clock_delay, clock_requests, ready_reads, unplugged; +static std::function on_clock; +enum { ENABLE, PULLUP, CAUSE, PLAIN }; +static constexpr uint32_t USBD_EVENTCAUSE_READY_Msk = 1u << 11; +struct Register { + int kind; + uint32_t value = 0; + explicit Register(int k = PLAIN): kind(k) {} + operator uint32_t() const { + if (kind == CAUSE) ++ready_reads; + if (on_read) on_read(kind); + return value; + } + void operator=(uint32_t v); +}; +static struct { + Register ENABLE{::ENABLE}, USBPULLUP{PULLUP}, EVENTCAUSE{CAUSE}; + Register INTENCLR, INTENSET, INTEN, ISOSPLIT; +} usbd; +void Register::operator=(uint32_t v) { + if (kind == CAUSE) value &= ~v; // hardware event register is W1C + else value = v; + if (kind == ENABLE && v && auto_ready) + usbd.EVENTCAUSE.value |= USBD_EVENTCAUSE_READY_Msk; +} +#define NRF_USBD (&usbd) +#define CFG_TUSB_DEBUG 0 +static constexpr unsigned USBD_INTEN_USBEVENT_Msk = 1, USBD_INTEN_USBRESET_Msk = 2; +static constexpr unsigned USBD_ISOSPLIT_SPLIT_HalfIN = 0, USBD_IRQn = 39; +static constexpr unsigned DCD_EVENT_UNPLUGGED = 0; +static void hfclk_enable() { clock_requested = true; ++clock_requests; } +static void hfclk_disable() { clock_requested = false; } +static bool hfclk_running() { + ++clock_reads; + if (on_clock) on_clock(); + return clock_requested && clock_ok && clock_reads > clock_delay; +} +static void __ISB() { if (on_barrier) on_barrier(); } +static void __DSB() { if (on_barrier) on_barrier(); } +static void NVIC_ClearPendingIRQ(unsigned) {} +static void NVIC_EnableIRQ(unsigned) { irq_enabled = true; } +static void NVIC_DisableIRQ(unsigned) { irq_enabled = false; } +static bool tud_inited() { return true; } +static bool is_in_isr() { return false; } +static void dcd_event_bus_signal(unsigned, unsigned, bool) { ++unplugged; } + +/* DRIVER_FUNCTION */ + +static void reset() { + usbd.ENABLE.value = usbd.USBPULLUP.value = usbd.EVENTCAUSE.value = 0; + on_read = {}; on_barrier = {}; on_clock = {}; + auto_ready = clock_ok = true; + clock_requested = irq_enabled = false; + clock_reads = clock_delay = clock_requests = ready_reads = unplugged = 0; +} +static void detected() { tusb_hal_nrf_power_event(0); } +static void ready() { tusb_hal_nrf_power_event(2); } +static void removed() { tusb_hal_nrf_power_event(1); } +static void bounded() { assert(clock_reads + ready_reads <= 100010); } + +int main(int argc, char** argv) { + // Exact live failure state: attached, consumed READY, clock no longer running. + if (argc > 1 && !std::strcmp(argv[1], "consumed-ready")) { + reset(); usbd.ENABLE = 1; usbd.USBPULLUP = 1; + usbd.EVENTCAUSE.value = 0; + ready(); + assert(clock_requested && usbd.USBPULLUP.value == 1); bounded(); + return 0; + } + reset(); detected(); ready(); + assert(usbd.USBPULLUP.value == 1 && irq_enabled); + assert(usbd.EVENTCAUSE.value == 0 && clock_requested); + ready(); bounded(); + removed(); + assert(!usbd.ENABLE.value && !usbd.USBPULLUP.value && !irq_enabled); + assert(!clock_requested && unplugged == 1); + + // READY delivered after removal must not request clocks or reattach. + reset(); ready(); + assert(!clock_requests && !usbd.USBPULLUP.value); bounded(); + + // Missing peripheral READY and missing HFXO must return, never attach. + reset(); auto_ready = false; detected(); ready(); + assert(!usbd.USBPULLUP.value); bounded(); + usbd.EVENTCAUSE.value = USBD_EVENTCAUSE_READY_Msk; ready(); + assert(usbd.USBPULLUP.value == 1); // subsequent event can retry + reset(); clock_ok = false; detected(); ready(); + assert(!usbd.USBPULLUP.value); bounded(); + clock_ok = true; ready(); assert(usbd.USBPULLUP.value == 1); + + // Both clock and regulator may take a little time to settle. + reset(); auto_ready = false; clock_delay = 10; detected(); + on_read = [](int kind) { + if (kind == CAUSE && ready_reads == 10) + usbd.EVENTCAUSE.value = USBD_EVENTCAUSE_READY_Msk; + }; + ready(); assert(usbd.USBPULLUP.value == 1); bounded(); + + // An interrupt completes another READY before the outer READY read. + reset(); detected(); + bool injected = false; + on_read = [&](int kind) { + if (kind == CAUSE && !injected) { injected = true; ready(); } + }; + ready(); assert(injected && usbd.USBPULLUP.value == 1); bounded(); + + // A nested callback after W1C but before pull-up cannot block its caller. + reset(); detected(); injected = false; + on_barrier = [&]() { + if (!injected && !usbd.EVENTCAUSE.value) { injected = true; ready(); } + }; + ready(); assert(injected && usbd.USBPULLUP.value == 1); bounded(); + + // Removal during either wait and immediately before attachment. + reset(); clock_ok = false; detected(); injected = false; + on_clock = [&]() { if (!injected) { injected = true; removed(); } }; + ready(); assert(!usbd.ENABLE.value && !usbd.USBPULLUP.value); bounded(); + reset(); auto_ready = false; detected(); injected = false; + on_read = [&](int kind) { + if (kind == CAUSE && !injected) { injected = true; removed(); } + }; + ready(); assert(!usbd.ENABLE.value && !usbd.USBPULLUP.value); bounded(); + reset(); detected(); injected = false; + on_barrier = [&]() { + if (!injected && !usbd.EVENTCAUSE.value) { injected = true; removed(); } + }; + ready(); assert(!usbd.ENABLE.value && !usbd.USBPULLUP.value); bounded(); + + // A deliberately detached, enabled peripheral has no fresh READY event. + reset(); detected(); ready(); usbd.USBPULLUP = 0; ready(); + assert(!usbd.USBPULLUP.value); bounded(); + puts("PASS: actual USB power handler, 13 event/clock/removal scenarios"); +} +''' + + +def extract_handler(source): + start = source.index("void tusb_hal_nrf_power_event") + opening = source.index("{", start) + depth = 1 + cursor = opening + 1 + while depth: + if source[cursor] == "{": + depth += 1 + elif source[cursor] == "}": + depth -= 1 + cursor += 1 + return source[start:cursor] + + +def exercise(source): + handler = extract_handler(source) + start = handler.index(" case USB_EVT_READY:") + stop = handler.index(" NRF_USBD->EVENTCAUSE =", start) + inherited = """ case USB_EVT_READY: + { + if (NRF_USBD->USBPULLUP && hfclk_running()) break; + while (!(USBD_EVENTCAUSE_READY_Msk & NRF_USBD->EVENTCAUSE)) {} + +""" + # The fixed case has a scope; the remainder stays identical to the driver. + assert re.search(r"case USB_EVT_READY:\s*\{", handler) + broken = handler[:start] + inherited + handler[stop:] + with tempfile.TemporaryDirectory(prefix="nrf5x-power-test-") as directory: + root = Path(directory) + for label, function in (("fixed", handler), ("inherited", broken)): + cpp = root / (label + ".cpp") + binary = root / label + cpp.write_text(HARNESS.replace("/* DRIVER_FUNCTION */", function)) + subprocess.run([ + *shlex.split(os.environ.get("CXX", "c++")), "-std=c++11", "-O2", + "-Wall", "-Wextra", "-Werror", *shlex.split(os.environ.get("USB_TEST_CXXFLAGS", "")), + str(cpp), "-o", str(binary), + ], check=True) + if label == "fixed": + subprocess.run([str(binary)], check=True, timeout=5) + subprocess.run([str(binary), "consumed-ready"], check=True, timeout=5) + else: + try: + subprocess.run([str(binary), "consumed-ready"], check=True, timeout=0.5) + except subprocess.TimeoutExpired: + print("PASS: inherited READY loop reproduces hang (negative control)") + else: + raise AssertionError("negative control did not reproduce the hang") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("driver", nargs="?", type=Path, default=( + Path(__file__).resolve().parents[2] / "src/portable/nordic/nrf5x/dcd_nrf5x.c")) + exercise(parser.parse_args().driver.read_text()) diff --git a/test/test_nrf52_usb_power.py b/test/test_nrf52_usb_power.py new file mode 100644 index 00000000..86c141b7 --- /dev/null +++ b/test/test_nrf52_usb_power.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 +"""Exercise the actual patched framework handler and isolated build integration.""" + +import configparser +import importlib.util +from pathlib import Path +import tempfile +import unittest + +from nrf5x_power_harness import exercise + + +ROOT = Path(__file__).resolve().parents[1] +SPEC = importlib.util.spec_from_file_location("usb_fix", ROOT / "scripts/nrf52_usb_power_fix.py") +FIX = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(FIX) +ORIGINAL = (ROOT / "test/fixtures/nrf52_usb_power_original.c").read_text() + + +class UsbPowerTests(unittest.TestCase): + def test_actual_handler_and_inherited_hang(self): + exercise(FIX.patched_source(ORIGINAL)) + + def test_crlf_and_idempotence(self): + fixed = FIX.patched_source(ORIGINAL) + self.assertEqual(fixed, FIX.patched_source(ORIGINAL.replace("\n", "\r\n"))) + self.assertEqual(fixed, FIX.patched_source(fixed)) + + def test_unrecognized_or_partly_fixed_driver_fails_closed(self): + for source in ("", ORIGINAL.replace("hfclk_running()", "clock_running()"), + ORIGINAL + ORIGINAL, ORIGINAL.replace(FIX.OLD_READY, FIX.READY)): + with self.subTest(source=source[:40]), self.assertRaises(RuntimeError): + FIX.patched_source(source) + + def test_build_local_copy_does_not_modify_shared_package(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + original = root / "shared-sdk/dcd_nrf5x.c" + original.parent.mkdir() + original.write_text(ORIGINAL) + + class Node: + def get_abspath(self): + return str(original) + + class VariantNode: + def srcnode(self): + return Node() + + def get_abspath(self): + raise AssertionError("VariantDir file does not exist yet; use srcnode()") + + class Env: + def subst(self, value): + assert value == "$BUILD_DIR" + return str(root / "build") + + def File(self, value): + return Path(value) + + output = FIX.replace_driver(Env(), VariantNode()) + self.assertEqual(original.read_text(), ORIGINAL) + self.assertEqual(output, root / "build/patched-nrf52-usb/dcd_nrf5x.c") + self.assertEqual(output.read_text(), FIX.patched_source(ORIGINAL)) + modified = output.stat().st_mtime_ns + FIX.replace_driver(Env(), VariantNode()) + self.assertEqual(output.stat().st_mtime_ns, modified) + + def test_all_nrf52_environments_inherit_pre_hook(self): + config = configparser.ConfigParser(interpolation=None, strict=False) + config.read([str(ROOT / "platformio.ini"), *map(str, (ROOT / "variants").glob("*/platformio.ini"))]) + + def option(section, key, seen=()): + if section in seen: + raise AssertionError("configuration inheritance cycle") + if config.has_option(section, key): + value = config.get(section, key) + import re + return re.sub(r"\$\{([^}.]+)\.([^}]+)\}", + lambda m: option(m[1], m[2], seen + (section,)), value) + for parent in config.get(section, "extends", fallback="").split(","): + parent = parent.strip() + if parent and config.has_section(parent): + value = option(parent, key, seen + (section,)) + if value: + return value + return "" + + checked = 0 + for section in config.sections(): + if section.startswith("env:") and option(section, "platform") == "nordicnrf52": + with self.subTest(environment=section): + self.assertIn("pre:scripts/nrf52_usb_power_fix.py", option(section, "extra_scripts")) + checked += 1 + self.assertGreater(checked, 30) + print("nRF52 USB hook inherited by %d environments" % checked) + + +if __name__ == "__main__": + unittest.main()