harden USB telemetry transport
This commit is contained in:
@@ -88,15 +88,23 @@ the oldest retained data is sent first. If the queue fills, acquisition drops
|
||||
new samples rather than overwriting older ones; sequence gaps and the cumulative
|
||||
lost-sample counter expose that permanent loss.
|
||||
|
||||
That retry guarantee requires the transport's success result to mean that the
|
||||
complete frame was accepted for eventual delivery. The current USB Serial/JTAG
|
||||
VFS/stdio path does not fully satisfy that contract: if the host remains
|
||||
connected but stops draining, its internal timeout can discard bytes while the
|
||||
stdio write appears successful. CRC and sequence checks make that loss visible
|
||||
to a receiver, but it does not increment the device's drop counter. A direct
|
||||
driver path with bounded transmit-drain waits can report this condition; an
|
||||
application acknowledgement and replay window is required for end-to-end
|
||||
delivery confirmation.
|
||||
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.
|
||||
|
||||
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
|
||||
does not trigger resubmission. This closes the VFS/stdio path's silent-discard
|
||||
case for a connected host that stops draining.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
@@ -39,11 +39,11 @@ bytes even though stdio reports a successful write. Firmware therefore cannot
|
||||
retain that particular frame or increment its drop counter. The receiver still
|
||||
detects the loss through CRC resynchronization and packet/sample sequence gaps.
|
||||
|
||||
ESP-IDF's direct USB Serial/JTAG driver provides bounded writes and an explicit
|
||||
transmit-drain wait, allowing a connected stall to become observable to the
|
||||
transport policy. That is a useful improvement for the common transport layer.
|
||||
It is not proof of receiver delivery; application acknowledgements and replay
|
||||
are needed for that stronger guarantee and are planned with BLE integration.
|
||||
This limitation was subsequently closed at the device/endpoint boundary by the
|
||||
shared transport state machine and direct USB Serial/JTAG driver described in
|
||||
`transport-layer-validation-2026-08-17.md`. It is not proof of receiver delivery;
|
||||
application acknowledgements and replay are still needed for that stronger
|
||||
guarantee and are planned with BLE integration.
|
||||
|
||||
## Final hardware capture
|
||||
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
# Direct USB Transport Validation — 2026-08-17
|
||||
|
||||
This milestone moved framed telemetry off stdio and the USB Serial/JTAG VFS data
|
||||
path. The output task now targets a transport-neutral state machine backed by
|
||||
ESP-IDF's interrupt-driven USB Serial/JTAG driver.
|
||||
|
||||
## Transaction contract
|
||||
|
||||
- `RETRY`: 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.
|
||||
- `FATAL`: a programming or backend invariant failed. The output task stops
|
||||
consuming the sample queue rather than silently discarding its in-flight data.
|
||||
|
||||
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 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.
|
||||
|
||||
## Verification
|
||||
|
||||
The host C transport fixture compiles the production state machine with
|
||||
`-Wall -Wextra -Werror` and verifies:
|
||||
|
||||
- zero-accept submissions remain retryable;
|
||||
- accepted transfers become pending;
|
||||
- repeated pending polls never call submission again;
|
||||
- completion returns the sender to idle;
|
||||
- backend-confirmed safe retry returns to idle;
|
||||
- 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.
|
||||
|
||||
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
|
||||
reader opened, delivery preserved the oldest block through sequence 511 and
|
||||
resumed at sequence 1,706. The one observed gap and the cumulative device drop
|
||||
count both equal 1,194 samples. The timestamp difference across that gap is
|
||||
11,950,000 us, exactly 1,195 sample intervals. Packet gaps, CRC failures, loop
|
||||
overruns, trailing partial bytes, and timestamp-saturation flags are zero.
|
||||
|
||||
That exact validated stream is tracked as
|
||||
`tests/fixtures/direct_usb_stall.trk`, SHA-256
|
||||
`40f874b7eaa7f705524ecdd75f832e8a724252366633116ac015fc75dfd16558`, and
|
||||
its signature is asserted by the regression suite.
|
||||
|
||||
## Remaining delivery boundary
|
||||
|
||||
The stall capture begins at sample sequence 8. The flashing process still had
|
||||
the serial endpoint open long enough to drain sequences 0 through 7 before the
|
||||
capture application attached. This is not silent device-side loss; it precisely
|
||||
demonstrates the boundary of USB drain confirmation. Only an application-level
|
||||
ACK can prove that the intended receiver received and persisted a frame.
|
||||
|
||||
The planned BLE backend will use the same `PENDING` ownership rule while waiting
|
||||
for acknowledgements, retain unacknowledged frames for replay, and expose
|
||||
per-cause transport/queue counters separately.
|
||||
|
||||
## Host evidence handling
|
||||
|
||||
Live capture and offline decode now share one integrity tracker for packet and
|
||||
sample gaps/resets, timestamp anomalies, sensor status, saturation flags, drops,
|
||||
and acquisition overruns. Sequence classification is wrap-aware. The capture
|
||||
tool also accepts `--wire PATH` to preserve every serial byte before parsing,
|
||||
including startup text, corrupt frames, and trailing fragments.
|
||||
Reference in New Issue
Block a user