mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-10-23 01:51:23 +00:00
316 lines
13 KiB
Python
316 lines
13 KiB
Python
#!/usr/bin/env python3
|
|
"""Run actual nRF52 USB lifecycle code with independent FIFO/endpoint state."""
|
|
from pathlib import Path
|
|
import shutil
|
|
import subprocess
|
|
import tempfile
|
|
import unittest
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
USB = (ROOT / "src/helpers/UsbLogging.cpp").read_text()
|
|
|
|
|
|
def function(signature):
|
|
start = USB.index(signature)
|
|
end = USB.index("{", start) + 1
|
|
depth = 1
|
|
while depth:
|
|
depth += (USB[end] == "{") - (USB[end] == "}")
|
|
end += 1
|
|
return USB[start:end]
|
|
|
|
|
|
class Nrf52UsbLifecycleTest(unittest.TestCase):
|
|
def run_native(self, source):
|
|
compiler = shutil.which("g++")
|
|
if compiler is None:
|
|
self.skipTest("C++ compiler unavailable")
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
cpp = Path(directory) / "lifecycle.cpp"
|
|
binary = Path(directory) / "lifecycle"
|
|
cpp.write_text(source)
|
|
compiled = subprocess.run([compiler, "-std=c++17", "-Wall", "-Wextra",
|
|
str(cpp), "-o", str(binary)],
|
|
capture_output=True, text=True)
|
|
self.assertEqual(compiled.returncode, 0, compiled.stdout + compiled.stderr)
|
|
checked = subprocess.run([str(binary)], capture_output=True, text=True)
|
|
self.assertEqual(checked.returncode, 0, checked.stdout + checked.stderr)
|
|
|
|
def test_framework_detach_queues_local_unmount_without_vbus_edge(self):
|
|
pinned_driver = (ROOT / "test/fixtures/nrf52_usb_power_original.c").read_text()
|
|
start = pinned_driver.index("void dcd_disconnect(")
|
|
disconnect = pinned_driver[start:pinned_driver.index("\n}\n", start) + 3]
|
|
self.run_native(r'''
|
|
#include <cassert>
|
|
#include <cstdint>
|
|
static bool mounted = true, vbus = true;
|
|
static unsigned owner_events = 0, reset_count = 0;
|
|
static constexpr unsigned DCD_EVENT_UNPLUGGED = 0;
|
|
struct Registers { unsigned USBPULLUP = 1; } registers;
|
|
#define NRF_USBD (®isters)
|
|
static void dcd_event_bus_signal(unsigned port, unsigned event, bool isr) {
|
|
assert(port == 0 && event == DCD_EVENT_UNPLUGGED && !isr);
|
|
++owner_events; // enqueue, do not automatically fake a VBUS/unmount edge
|
|
}
|
|
@DISCONNECT@
|
|
// Actual usbd.c wrapper and Adafruit_USBD_Device.h inline entry point.
|
|
static bool tud_disconnect() { dcd_disconnect(0); return true; }
|
|
struct Device { bool detach() { return tud_disconnect(); } } TinyUSBDevice;
|
|
static void usbd_reset(unsigned port) {
|
|
assert(port == 0); mounted = false; ++reset_count;
|
|
}
|
|
static void tud_umount_cb() {}
|
|
static void owner_task() {
|
|
assert(owner_events == 1); --owner_events;
|
|
// Exact DCD_EVENT_UNPLUGGED owner branch from the pinned usbd.c.
|
|
usbd_reset(0);
|
|
tud_umount_cb();
|
|
}
|
|
int main() {
|
|
assert(TinyUSBDevice.detach());
|
|
assert(vbus && registers.USBPULLUP == 0 && mounted);
|
|
assert(owner_events == 1); // without this, recovery would wait forever
|
|
owner_task();
|
|
assert(vbus && !mounted && reset_count == 1);
|
|
}
|
|
'''.replace("@DISCONNECT@", disconnect))
|
|
|
|
def test_descriptor_mutation_is_detached_even_before_mount(self):
|
|
source = r'''
|
|
#include <atomic>
|
|
#include <cassert>
|
|
#include <cstdint>
|
|
#define MESH_DUAL_CDC_LOGGING 1
|
|
static bool attached = true, mounted = false, descriptor_fetched = false;
|
|
static unsigned detach_count = 0, attach_count = 0, descriptor_count = 1;
|
|
static unsigned delay_ms = 0;
|
|
struct Device {
|
|
bool mounted() { return ::mounted; }
|
|
void detach() { attached = false; ++detach_count; }
|
|
void attach() {
|
|
assert(descriptor_count == 2 && delay_ms >= 10);
|
|
attached = true; ++attach_count;
|
|
// Fresh enumeration replaces a host's old cached one-port descriptor.
|
|
descriptor_fetched = false;
|
|
}
|
|
} TinyUSBDevice;
|
|
static void delay(unsigned value) { assert(!attached); delay_ms += value; }
|
|
namespace mesh {
|
|
static std::atomic<bool> usb_logging_preference_known{false};
|
|
static bool enabled = false, dedicated_usb_logging_port_started = false;
|
|
static bool dedicated_usb_logging_port_configured = false;
|
|
static bool isUsbLoggingEnabled() { return enabled; }
|
|
static bool isUsbLoggingPacketStream() { return false; }
|
|
static bool isUsbDebugLoggingEnabled() { return false; }
|
|
static void setPlatformDebugOutputEnabled(bool) {}
|
|
struct Port {
|
|
void begin(unsigned baud) {
|
|
assert(baud == 115200 && !attached && delay_ms >= 10);
|
|
++descriptor_count;
|
|
}
|
|
} dedicated_usb_logging_port;
|
|
@BEGIN@
|
|
}
|
|
int main() {
|
|
// No descriptor or reconnect when the preference is unknown/disabled.
|
|
mesh::beginUsbLoggingPort();
|
|
mesh::usb_logging_preference_known = true;
|
|
mesh::beginUsbLoggingPort();
|
|
assert(detach_count == 0);
|
|
// Host fetched the old descriptor but has not SET_CONFIGURATION yet.
|
|
descriptor_fetched = true;
|
|
assert(!mounted);
|
|
mesh::enabled = true;
|
|
mesh::beginUsbLoggingPort();
|
|
assert(detach_count == 1 && attach_count == 1 && attached);
|
|
assert(!descriptor_fetched && descriptor_count == 2);
|
|
assert(mesh::dedicated_usb_logging_port_configured);
|
|
mesh::beginUsbLoggingPort();
|
|
assert(detach_count == 1 && attach_count == 1); // idempotent
|
|
}
|
|
'''.replace("@BEGIN@", function("void beginUsbLoggingPort()"))
|
|
self.run_native(source)
|
|
|
|
def test_full_owner_event_queue_defers_stack_detach_but_quarantines_packet(self):
|
|
start = USB.index("static std::atomic<uint32_t> primary_usb_reset_generation")
|
|
gates = USB[start:USB.index("static void endPrimaryUsbHostSession(", start)]
|
|
pinned_driver = (ROOT / "test/fixtures/nrf52_usb_power_original.c").read_text()
|
|
start = pinned_driver.index("void dcd_disconnect(")
|
|
disconnect = pinned_driver[start:pinned_driver.index("\n}\n", start) + 3]
|
|
source = r'''
|
|
#include <atomic>
|
|
#include <cassert>
|
|
#include <cstdint>
|
|
#define ENABLE_USB_INTERFACE 1
|
|
static bool mounted = true, armed[2] = {false, true};
|
|
static uint32_t now = 100, fifo[2] = {0, 32};
|
|
static bool owner_callback = false;
|
|
static constexpr unsigned DCD_EVENT_UNPLUGGED = 1, queue_capacity = 4;
|
|
static unsigned events[queue_capacity] = {0, 0, 0, 0};
|
|
static unsigned queue_head = 0, queue_tail = 0, queue_count = queue_capacity;
|
|
static unsigned event_sends = 0, detach_count = 0, attach_count = 0;
|
|
static unsigned owner_resets = 0;
|
|
struct Registers { unsigned USBPULLUP = 1; } registers;
|
|
#define NRF_USBD (®isters)
|
|
static void __ISB() {}
|
|
static void __DSB() {}
|
|
static uint32_t millis() { return now; }
|
|
static bool tud_mounted() { return mounted; }
|
|
static uint32_t tud_cdc_n_available(uint8_t) { return 0; }
|
|
static void tud_cdc_n_read_flush(uint8_t) {}
|
|
static bool tud_cdc_n_write_clear(uint8_t n) { fifo[n] = 0; return true; }
|
|
static bool mesh_tud_cdc_n_tx_pending(uint8_t n) { return armed[n]; }
|
|
static void owner_dispatch_one() {
|
|
assert(!owner_callback && queue_count != 0);
|
|
owner_callback = true;
|
|
const unsigned event = events[queue_tail];
|
|
queue_tail = (queue_tail + 1) % queue_capacity;
|
|
--queue_count;
|
|
if (event == DCD_EVENT_UNPLUGGED) {
|
|
mounted = false;
|
|
armed[0] = armed[1] = false;
|
|
++owner_resets;
|
|
}
|
|
owner_callback = false;
|
|
}
|
|
static void dcd_event_bus_signal(unsigned port, unsigned event, bool isr) {
|
|
assert(port == 0 && event == DCD_EVENT_UNPLUGGED && !isr);
|
|
// Real osal_queue_send uses an infinite FreeRTOS timeout here. If called
|
|
// by its sole consumer while full, it could never return. Fail immediately
|
|
// rather than hanging the test on precisely that regression.
|
|
assert(!owner_callback);
|
|
++event_sends;
|
|
if (queue_count == queue_capacity) {
|
|
// A blocked application sender yields to the independent USB owner,
|
|
// which drains an existing event and releases a slot for UNPLUGGED.
|
|
owner_dispatch_one();
|
|
}
|
|
assert(queue_count < queue_capacity);
|
|
events[queue_head] = event;
|
|
queue_head = (queue_head + 1) % queue_capacity;
|
|
++queue_count;
|
|
}
|
|
@DISCONNECT@
|
|
struct Device {
|
|
void detach() { ++detach_count; dcd_disconnect(0); }
|
|
void attach() { ++attach_count; registers.USBPULLUP = 1; }
|
|
} TinyUSBDevice;
|
|
namespace mesh {
|
|
@GATES@
|
|
}
|
|
int main() {
|
|
// The owner is servicing a close/SOF callback while IRQ events fill every
|
|
// available queue slot. The old data is armed in the controller, not FIFO.
|
|
owner_callback = true;
|
|
mesh::clearNrf52UsbTxForSession(1);
|
|
assert(queue_count == queue_capacity && event_sends == 0);
|
|
assert(registers.USBPULLUP == 0 && armed[1] && fifo[1] == 0);
|
|
assert(!mesh::canAccessPrimaryUsbSession(nullptr));
|
|
mesh::requestNrf52UsbSessionReenumeration(); // repeated callback is harmless
|
|
assert(detach_count == 0 && event_sends == 0);
|
|
owner_callback = false;
|
|
|
|
// A busy application loop can service much later. Its first pass only
|
|
// issues the stack detach; it must not reuse an expired callback deadline.
|
|
now += 400;
|
|
mesh::serviceNrf52UsbSessionReenumeration();
|
|
assert(detach_count == 1 && event_sends == 1 && attach_count == 0);
|
|
mesh::serviceNrf52UsbSessionReenumeration();
|
|
assert(detach_count == 1 && attach_count == 0);
|
|
now += 20;
|
|
mesh::serviceNrf52UsbSessionReenumeration();
|
|
assert(attach_count == 0); // owner has not consumed UNPLUGGED yet
|
|
while (queue_count != 0) owner_dispatch_one();
|
|
assert(owner_resets == 1 && !mounted && !armed[1]);
|
|
mesh::serviceNrf52UsbSessionReenumeration();
|
|
assert(attach_count == 1 && registers.USBPULLUP == 1);
|
|
mesh::serviceNrf52UsbSessionReenumeration();
|
|
assert(detach_count == 1 && attach_count == 1); // recovery is one-shot
|
|
}
|
|
'''
|
|
self.run_native(source.replace("@DISCONNECT@", disconnect).replace("@GATES@", gates))
|
|
|
|
def test_armed_logging_packet_and_racing_primary_packet_force_clean_bus(self):
|
|
start = USB.index("static std::atomic<uint32_t> primary_usb_reset_generation")
|
|
end_start = USB.index("static void endPrimaryUsbHostSession(", start)
|
|
gates = USB[start:end_start] + function("static void endPrimaryUsbHostSession(")
|
|
source = r'''
|
|
#include <atomic>
|
|
#include <cassert>
|
|
#include <cstdint>
|
|
#define ENABLE_USB_INTERFACE 1
|
|
static bool mounted = true, attached = true, armed[2] = {false, true};
|
|
static uint32_t fifo[2] = {0, 32}, now = UINT32_MAX - 10;
|
|
static unsigned detach_count = 0, attach_count = 0, reset_count = 0;
|
|
struct Registers { unsigned USBPULLUP = 1; } registers;
|
|
#define NRF_USBD (®isters)
|
|
static void __ISB() {}
|
|
static void __DSB() {}
|
|
static uint32_t millis() { return now; }
|
|
static bool tud_mounted() { return mounted; }
|
|
static uint32_t tud_cdc_n_available(uint8_t) { return 0; }
|
|
static void tud_cdc_n_read_flush(uint8_t) {}
|
|
static bool tud_cdc_n_write_clear(uint8_t n) { fifo[n] = 0; return true; }
|
|
static bool mesh_tud_cdc_n_tx_pending(uint8_t n) { return armed[n]; }
|
|
struct Device {
|
|
void detach() { ++detach_count; attached = false; registers.USBPULLUP = 0; }
|
|
void attach() { ++attach_count; attached = true; registers.USBPULLUP = 1; }
|
|
} TinyUSBDevice;
|
|
namespace mesh {
|
|
static void clearUsbLoggingClientActivity() {}
|
|
static bool isUsbLoggingPacketStream() { return false; }
|
|
@DEDICATED@
|
|
@GATES@
|
|
static std::atomic<bool> dedicated_usb_logging_port_connected{true};
|
|
static std::atomic<uint32_t> dedicated_usb_logging_reset_generation{0};
|
|
static uint8_t dedicated_usb_logging_quiet_sofs = 0;
|
|
static constexpr uint8_t dedicated_usb_logging_host_settle_sofs = 50;
|
|
struct StatsParser { void reset() {} } dedicated_usb_logging_stats_parser;
|
|
static uint32_t primary_usb_terminal_taken_reset_generation = 0;
|
|
static void resetDedicatedUsbLoggingUsbTaskState() { ++reset_count; }
|
|
@RESTART@
|
|
@COMPLETE@
|
|
}
|
|
int main() {
|
|
// CDC1 old data is in endpoint RAM, not the software FIFO. No 50-SOF
|
|
// waiting/clearing policy can retract it without re-enumeration.
|
|
mesh::restartDedicatedUsbLoggingHostSession();
|
|
assert(fifo[1] == 0 && armed[1] && registers.USBPULLUP == 0);
|
|
assert(attached && detach_count == 0); // no owner-task event send
|
|
assert(!mesh::dedicated_usb_logging_port_connected && reset_count == 1);
|
|
assert(!mesh::canAccessPrimaryUsbSession(nullptr)); // both ports gated
|
|
mesh::serviceNrf52UsbSessionReenumeration();
|
|
assert(!attached && detach_count == 1);
|
|
mesh::restartDedicatedUsbLoggingHostSession();
|
|
assert(detach_count == 1); // repeated control requests do not reset timer
|
|
now += 19; mounted = false; armed[1] = false;
|
|
mesh::serviceNrf52UsbSessionReenumeration();
|
|
assert(attach_count == 0); // wrap-safe bounded detach interval
|
|
++now;
|
|
mesh::serviceNrf52UsbSessionReenumeration();
|
|
assert(attach_count == 1 && attached);
|
|
mesh::serviceNrf52UsbSessionReenumeration();
|
|
assert(attach_count == 1);
|
|
// The final exclusive purge also catches an old producer that submitted
|
|
// a primary IN packet after the owner callback's first FIFO clear.
|
|
mounted = true; armed[0] = true; fifo[0] = 16;
|
|
mesh::primary_usb_terminal_taken_reset_generation =
|
|
mesh::primaryUsbSessionGeneration();
|
|
mesh::completePrimaryUsbSessionReset(nullptr);
|
|
assert(fifo[0] == 0 && armed[0] && registers.USBPULLUP == 0);
|
|
assert(attached && detach_count == 1);
|
|
assert(!mesh::canAccessPrimaryUsbSession(nullptr));
|
|
mesh::serviceNrf52UsbSessionReenumeration();
|
|
assert(!attached && detach_count == 2);
|
|
}
|
|
'''
|
|
source = source.replace("@GATES@", gates)
|
|
source = source.replace("@DEDICATED@", function("bool hasDedicatedUsbLoggingPort("))
|
|
source = source.replace("@RESTART@", function("static void restartDedicatedUsbLoggingHostSession()"))
|
|
source = source.replace("@COMPLETE@", function("static void completePrimaryUsbSessionReset("))
|
|
self.run_native(source)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|