tighten transport failure semantics

This commit is contained in:
Jay
2026-08-17 17:33:44 -04:00
parent 3c95f3d7be
commit 73e5680fc3
11 changed files with 144 additions and 29 deletions
+10 -6
View File
@@ -89,11 +89,13 @@ new samples rather than overwriting older ones; sequence gaps and the cumulative
lost-sample counter expose that permanent loss.
The shared transport state machine distinguishes three nonfatal states. `RETRY`
means zero bytes were accepted and the complete frame may be submitted again.
`PENDING` means the backend owns an in-flight frame, so firmware may only poll
that transfer. `COMPLETE` permits the output task to reuse its packet buffer and
consume more samples. This prevents a timeout after partial progress from
causing an ambiguous whole-frame duplicate.
is valid only from initial submission: it means zero bytes were accepted and the
complete frame may be submitted again. `PENDING` means the backend owns an
in-flight frame, so firmware may only poll that transfer. A `RETRY` returned by
polling fails closed as `FATAL`, because generic code cannot prove whole-frame
resubmission is duplicate-safe. `COMPLETE` permits the output task to reuse its
packet buffer and consume more samples. This prevents a timeout after partial
progress from causing an ambiguous whole-frame duplicate.
The direct USB Serial/JTAG backend atomically copies a complete frame into its TX
ring, then polls a bounded transmit-drain wait. A timeout remains `PENDING`; it
@@ -104,7 +106,9 @@ USB drain is not end-to-end application delivery confirmation. A host process
may attach after earlier frames have already left the endpoint, or fail after
the endpoint accepts them. CRC and sequence checks make resulting loss visible,
but an application acknowledgement and replay window are still required to
guarantee receipt.
guarantee receipt. Accordingly, USB `COMPLETE` means endpoint drain, while the
planned reliable BLE backend will reserve `COMPLETE` for an application ACK of
the exact frame.
Receivers report bytes left in an incomplete trailing frame when capture ends.
Those bytes cannot pass CRC validation and are not silently admitted as samples.
+36 -9
View File
@@ -6,23 +6,38 @@ ESP-IDF's interrupt-driven USB Serial/JTAG driver.
## Transaction contract
- `RETRY`: the backend accepted zero bytes, so whole-frame resubmission is safe.
- `RETRY`: valid only from initial submission; the backend accepted zero bytes,
so whole-frame resubmission is safe.
- `PENDING`: the backend owns an in-flight frame. Only completion polling is
allowed; the caller must retain and not modify the packet buffer.
- `COMPLETE`: the backend's completion criterion is satisfied and the caller may
reuse the packet buffer.
- `COMPLETE`: the backend's documented completion criterion is satisfied and the
caller may reuse the packet buffer. USB means endpoint drain; reliable BLE
will mean application acknowledgement of the exact frame.
- `FATAL`: a programming or backend invariant failed. The output task stops
consuming the sample queue rather than silently discarding its in-flight data.
`RETRY` from a pending poll is promoted to `FATAL` because generic code cannot
prove resubmission is duplicate-safe.
For USB, submission uses a 512-byte TX ring and a bounded 50 ms write. ESP-IDF's
ring-buffer send is all-or-nothing for each `TRK1` frame. Once accepted, bounded
50 ms `usb_serial_jtag_wait_tx_done()` calls continue returning `PENDING` until
the host drains the endpoint. A timeout never resubmits the frame.
the host drains the endpoint. The first completion poll happens immediately
after acceptance; repeated pending polls are scheduler-paced. A timeout never
resubmits the frame.
The VFS is switched to driver mode after readable startup output so any
unexpected diagnostic output cannot race the driver's ISR by accessing the
hardware FIFO directly. Firmware logs are disabled before binary telemetry
tasks start, as before.
tasks start, as before. A terminal transport invariant re-enables error logging,
emits one final diagnostic, and suspends the output task. Because no later binary
frame can follow, that diagnostic cannot corrupt a recoverable stream.
Queue-allocation and task-creation failure paths switch the VFS back to its
non-driver mode and uninstall the USB driver before emitting their error, so the
only startup diagnostic is not stranded in a TX ring that is immediately freed.
The USB backend's zero-write-to-`RETRY` mapping depends explicitly on its checked
initialized/sole-owner lifecycle; a torn-down backend fails the guard as
`FATAL` instead of masquerading as backpressure.
## Verification
@@ -33,12 +48,24 @@ The host C transport fixture compiles the production state machine with
- accepted transfers become pending;
- repeated pending polls never call submission again;
- completion returns the sender to idle;
- backend-confirmed safe retry returns to idle;
- retry returned after acceptance fails closed without resubmission;
- invalid arguments and unknown backend states fail closed.
The assembled ESP32-C3 prototype produced a normal 1,728-sample direct-driver
capture with no packet gaps, sample gaps, resets, CRC failures, reported drops,
loop overruns, trailing partial bytes, or timestamp-saturation frames.
The assembled ESP32-C3 prototype first produced a normal 1,728-sample
pre-commit direct-driver capture with no packet gaps, sample gaps, resets, CRC
failures, reported drops, loop overruns, trailing partial bytes, or
timestamp-saturation frames.
After commit `3c95f3d` was built and flashed exactly, a second capture contained
1,680 contiguous samples, sequences 0 through 1,679, with the same zero-loss
integrity result. Its optional raw wire file contains all 563 startup bytes and
re-decodes to CSV byte-for-byte identical to the live-rendered CSV. Both sides
of that exact capture are tracked as fixtures:
- `tests/fixtures/direct_usb_3c95f3d.trk`, SHA-256
`f495486f094a758bb785e145026e3934d60b52dd5083aef7f5d896195d967869`
- `tests/fixtures/direct_usb_3c95f3d.wire`, SHA-256
`3bdaeadff7962c6eac48c4ebeda285c8eb359e439d5e1728104add2009122c03`
For the connected-stall case, the USB endpoint was left enumerated without a
serial reader long enough to overflow the 512-sample acquisition queue. When the