harden USB telemetry transport

This commit is contained in:
Jay
2026-08-17 14:51:12 -04:00
parent 1cf0a9ac77
commit 3c95f3d7be
17 changed files with 709 additions and 114 deletions
+17 -9
View File
@@ -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.