Files
trikkeSensors/docs/transport-layer-validation-2026-08-17.md
T

102 lines
5.3 KiB
Markdown

# 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`: 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 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. 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. 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
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;
- retry returned after acceptance fails closed without resubmission;
- invalid arguments and unknown backend states fail closed.
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
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.