Skip to content

[Gen 3] Fix USB CDC write triggering an assertion failure; migrates wiring/usbserial to be run under test runner, more usb tests - #2939

Open
avtolstoy wants to merge 6 commits into
developfrom
fix/gen3-usb-cdc-migrate-usbserial-tests
Open

[Gen 3] Fix USB CDC write triggering an assertion failure; migrates wiring/usbserial to be run under test runner, more usb tests#2939
avtolstoy wants to merge 6 commits into
developfrom
fix/gen3-usb-cdc-migrate-usbserial-tests

Conversation

@avtolstoy

@avtolstoy avtolstoy commented Aug 6, 2026

Copy link
Copy Markdown
Member

Problem

  1. On Gen 3 a non-blocking Serial.write() racing a full TX buffer could SOS the device. hal_usb_cdc_pvt_send_data() SPARK_ASSERTed that app_fifo_write() succeeds, but a full FIFO returns NRF_ERROR_NO_MEM - so writing into a full buffer was an assertion failure instead of a short write. SerialUSBStream had the same problem one layer up: errors from the HAL propagated out of read()/peek()/write() instead of being reported as "0 bytes done". This is a 6.5.0 regression introduced in USB Serial/UART serial buffer read/write/peek + wait event #2935

  2. wiring/usbserial was a manual test, so it never ran on HIL.

  3. USB coverage beyond CDC data was basically nonexistent: control-request transport (chunking, concurrency, cancellation), descriptors/WCID, device modes and bus-reset robustness had no automated tests. user/tests/app/usb_ctrl_request predates the test runner and never runs.

Solution

  1. hal_usb_cdc_pvt_send_data() returns SYSTEM_ERROR_NO_MEMORY on a full FIFO instead of asserting; SerialUSBStream treats it as a zero-length transfer.

  2. Migrated wiring/usbserial under the test runner (20 tests). Host side drives the CDC tty via a new UsbCdcPort helper (user/tests/integration/test/usb_cdc.js): resolves the tty by USB serial number through sysfs, toggles DTR via open/close, creates TX backpressure by pausing the reader. Covers blocking/non-blocking TX stress, RX stress, isConnected() transitions, RX buffer fill, end()/begin() while the port is closed.

  3. New wiring/usb suite (18 tests, gen3 + gen4) - everything USB that isn't CDC data:

    • system control requests (CTRL_REQUEST_ECHO) across the allocation/chunking boundaries (1–10000 bytes: pooled vs async alloc at 64, host-side chunked SEND/RECV above 4096), 4 concurrent requests
    • app custom request path end-to-end, including deferred completion so requests stay PENDING across multiple host CHECK polls and complete out of order
    • raw vendor requests (bRequest 0x50: DEVICE_ID, SYSTEM_VERSION), raw INIT->RESET->CHECK cancellation, DIAGNOSTIC_INFO
    • descriptor assertions: VID/PID vs @particle/device-constants, iSerialNumber == device ID, per-platform interface layout, MS OS string + WCID compat ID / extended properties
    • listening mode enter/leave observed from both host and device; control requests keep working in listening mode
    • host-initiated bus reset x5 with recovery and Serial.end()/begin() re-enumeration stress

    Supersedes user/tests/app/usb_ctrl_request (GET_MODULE_INFO is already covered by the ota/* suites).

  4. Test runner application: the request handler gained an e echo command with an optional delay - delayed requests are completed from testAppLoop(), same pattern as the old usb_ctrl_request app. Removed the dead RequestHandler::instance() singleton: the live handler is TestSuite::reqHandler_, and the stale singleton silently absorbed the loop hook (deferred requests queued on one object, drained on the other). Added test/platform_util.js - wiring specs can't require() node modules directly (module resolution uses the spec's realpath, which is outside integration/), so helpers with npm deps have to live in integration/test/.

Steps to Test

device-os-test --device-os-dir=. run <platform> wiring/usbserial wiring/usb

@avtolstoy avtolstoy added this to the 6.5.1 milestone Aug 6, 2026
@avtolstoy avtolstoy added the bug label Aug 6, 2026
@avtolstoy avtolstoy changed the title [Gen 3] Fix USB CDC write triggering an assertion failure; migrates wiring/usbserial to be run under test runner [Gen 3] Fix USB CDC write triggering an assertion failure; migrates wiring/usbserial to be run under test runner, more usb tests Aug 25, 2026
@avtolstoy
avtolstoy marked this pull request as ready for review August 25, 2026 08:26

@scott-brust scott-brust left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Works for me on msom

➜  device-os git:(fix/gen3-usb-cdc-migrate-usbserial-tests) ✗ device-os-test --device-os-dir=. run msom wiring/usbserial wiring/usb


  USB
    msom
      systemThread=enabled
        ✓ USB_00_SystemEchoShort (4.8s)
        ✓ USB_01_SystemEchoLarge (1s)
        ✓ USB_02_SystemEchoConcurrent
        ✓ USB_03_AppCustomEcho
        ✓ USB_04_AppCustomEchoDeferred (3.3s)
        ✓ USB_05_DiagnosticInfo
        ✓ USB_06_RawVendorRequests (1.1s)
        ✓ USB_07_ServiceRequestCancellation
        ✓ USB_08_DeviceDescriptor
        ✓ USB_09_InterfaceLayout
        ✓ USB_10_MsftOsStringDescriptor
        ✓ USB_11_WcidCompatIdDescriptor
        ✓ USB_12_EnterListeningMode (1.4s)
        ✓ USB_13_DeviceObservesListeningMode (1.3s)
        ✓ USB_14_DeviceObservesNormalMode
        ✓ USB_15_HostBusResetRecovery (16.9s)
          [2a] Hub-cycle disabled, device not found by portPath
          [2a] Hub-cycle disabled, device not found by portPath
          [2a] Hub-cycle disabled, device not found by portPath
          [2a] Hub-cycle disabled, device not found by portPath
Opened USB CDC port /dev/cu.usbmodem1102 for 0a10aced202194944a04b000
        ✓ USB_16_DeviceEndBeginStress (34.6s)
        ✓ USB_17_FinalCdcSanity

  USBSerial
    msom
      systemThread=enabled
Opened USB CDC port /dev/cu.usbmodem1102 for 0a10aced202194944a04b000
        ✓ USBSERIAL_00_RingBufferHelperIsSane (4.9s)
        ✓ USBSERIAL_01_SerialDoesNotDeadlockWhenInterruptsAreMasked
        ✓ USBSERIAL_02_ReadWrite
        ✓ USBSERIAL_03_ReadWriteVerifiesHostData
        ✓ USBSERIAL_04_isConnectedInitially
        ✓ USBSERIAL_05_ClosedPortWritesFailWithoutBlocking
        ✓ USBSERIAL_06_isConnectedDetectsReopenedPort
        ✓ USBSERIAL_07_EndBeginWhilePortIsClosed (4.3s)
        ✓ USBSERIAL_08_RxBufferSetup (1.1s)
        ✓ USBSERIAL_09_RxBufferFillsCompletelyFirst
        ✓ USBSERIAL_10_RxBufferFillsCompletelySecond
        ✓ USBSERIAL_11_RxBufferFillsCompletelyThird
        ✓ USBSERIAL_12_NonBlockingWriteStressSetup
        ✓ USBSERIAL_13_NonBlockingWriteStressHandlesBackpressure
        ✓ USBSERIAL_14_BlockingWriteStressWritesEveryBuffer (2.3s)
        ✓ USBSERIAL_15_NonBlockingCharacterWriteStressSetup
        ✓ USBSERIAL_16_NonBlockingCharacterWriteStressHandlesBackpressure (1.8s)
        ✓ USBSERIAL_17_BlockingCharacterWriteStressWritesEveryByte (16.3s)
        ✓ USBSERIAL_18_DeviceReceiveStressSetup
        ✓ USBSERIAL_19_DeviceReceiveStress (2.8s)


  38 passing (2m)

user/tests/app/usb_ctrl_request predates the test runner and never runs
Do we want to delete this app since its effectively been replaced?

}

int SerialUSBStream::skip(size_t size) {
return read(nullptr, size);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there is an issue with this.
On gen3 read(nullptr, size) -> hal_usb_cdc_pvt_recv_data() -> app_fifo_read() with a null buffer will be interpreted as querying the length of the fifo and return NRF_SUCCESS with the size skipped set to the entire length of the fifo:

uint32_t app_fifo_read(app_fifo_t * p_fifo, uint8_t * p_byte_array, uint32_t * p_size)
{
...
    const uint32_t byte_count    = fifo_length(p_fifo);
...
    (*p_size) = byte_count;

   // Check if application has requested only the size.
    if (p_byte_array == NULL)
    {
        return NRF_SUCCESS;
    }
...

I think we need to have a dummy buffer to actually consume the bytes

skip() doesnt appear to be used right now but if someone does use it, I dont think it will work

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, looks like this is broken. Will fix and add tests, as it wasn't caught. Good catch.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There aren't any tests for SerialUSBStream right now, right? Its only used for USB tethering, which have their own tests on top of it I suppose.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, but HAL_USB_USART_Peek_Buffer() and others are testable.

Comment on lines +102 to +103
if (r == SYSTEM_ERROR_NO_MEMORY) {
return 0;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For gen4, SYSTEM_ERROR_NO_MEMORY means there is no data to peek?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kinda weird, I admit. I'll take another look at peek/read in particular

Comment on lines +791 to +795
uint32_t result = app_fifo_write(&m_usb_instance.tx_fifo, data, &bytesToWrite);
if (result == NRF_ERROR_NO_MEM) {
return SYSTEM_ERROR_NO_MEMORY;
}
SPARK_ASSERT(result == NRF_SUCCESS);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants