Vendor Protocol Evolution Plan
Status as of v0.1.0: Commits 1 and 2 shipped (firmware Si5351 chip-query and the docker patch-03 retirement, both via PR #122). Commits 3 and 4 — GETCAPABILITIES / GETSTATE vendor commands and the recommended-sequence documentation — remain unwritten. The plan text below is preserved as the design rationale for the remaining work; the Commit 1 and Commit 2 subsections are marked DONE (v0.1.0) with shipping-commit pointers.
Trigger: ka9q-radio Docker test harness surfaced a hidden coupling between STARTADC and STARTFX3 (audit §8). Fixing only the bug missed the larger lesson; this plan addresses both.
For the executing Claude instance
You are picking up this plan from a previous session. Constraints:
CLAUDE.mdapplies in full. Plan first; ask before committing each commit; provide the standard change-doc block (detailed description / build instructions / validation test / regression test) before any commit; commit only with explicit user OK.- Execute commits in order: 1 → 2 → 3 → 4. Each is independently mergeable; do not bundle.
- Before editing each file,
Readthe path to confirm line numbers haven’t shifted — the repository may have evolved since this plan was written. Line numbers in this plan are accurate as of PR #91 merge. - Hardware validation is the user’s responsibility. You can build, lint, run static patch-apply checks, and write tests. End-to-end streaming validation requires the user’s RX888mk2.
- Soak/fuzz tests are mandatory for commits 1 and 3 (per Principle 2 below). If you cannot run them yourself, at minimum write the script, commit it, and document expected output.
- If a sub-decision in this plan looks wrong, STOP and ask the user. Do not silently improvise.
- Page 3 of this document (below “Approval gate”) contains the implementation specifics — file paths, line numbers, code skeletons, validation commands. Read it before opening any source file.
Context
The vendor command set was drafted ~2018 around the original BBRF/SDR-Console reference code. Reviewing it cold against three current consumers (ExtIO on Windows, rx888_stream, ka9q-radio), three structural issues stand out:
- State-changing and configuration commands are mixed.
STARTADCreads as a state transition (“start the ADC”) but is actually Si5351 configuration.STARTFX3is the real state transition, with hidden preconditions. - Implicit ordering is nowhere documented. The recommended sequence (
GPIOFX3→STARTADC→ gains →STARTFX3) is discovered by reading existing host code. When a host like ka9q-radio takes a different but technically valid path (program Si5351 directly viaI2CWFX3, skipSTARTADC), the firmware silently refuses. - No discovery or capability negotiation. Hosts cannot ask “what does this firmware support?” or “what is the current state?” — they probe by sending commands and watching for STALLs.
The wire protocol cannot break: ExtIO, rx888_stream, ka9q-radio, and ka9q-web are all deployed. But the API can be improved by additive changes plus a hardware-grounded fix.
Guiding principles
Principle 1 — Separation of concerns
Move toward an API where commands fall into three orthogonal categories:
| Category | Property | Today | Goal |
|---|---|---|---|
| Configuration | Idempotent; order-free; no effect on data flow | STARTADC, GPIOFX3, SETARGFX3 | unchanged on the wire; clearer in docs |
| Control | Explicit state transitions; may fail with a reason | STARTFX3, STOPFX3, RESETFX3 | preflights query the chip, not flags |
| Query | Read-only; returns state or capabilities | GETSTATS (partial) | add GETCAPABILITIES, GETSTATE |
Principle 2 — Robustness / Recoverability invariant (new)
No vendor command, regardless of arguments, current state, or sequence, may leave the RX888 in a state requiring a power cycle to recover. A misbehaving host should always be able to reach a clean baseline by issuing well-known commands or by host-side USB reset.
Concretely, the firmware must guarantee:
- Any vendor request (valid or not) returns to the EP0 idle state within a bounded time — no infinite loops, no busy-waits without timeout.
- Any sequence of valid commands in any order is recoverable: e.g.
STARTFX3beforeSTARTADC,STOPFX3when already stopped, repeatedSTARTFX3without interveningSTOPFX3, etc. Each is either a no-op or returns a clear failure (STALL or response code) — neither wedges the GPIF, the DMA, nor the I2C bus. - Malformed payloads (wrong length, out-of-range values) are rejected at the EP0 boundary; they do not corrupt internal state.
- The escape hatches
RESETFX3andSTOPFX3always work, including from a wedged GPIF state. - Host-side
libusb_reset_device()— and ultimately a USB unplug/replug — always brings the device back to bootloader (04b4:00f3) or to the loaded baseline (04b4:00f1with idle GPIF). - Unknown-opcode contract (feature-detection guarantee). Any vendor request byte not currently implemented must STALL cleanly without advancing any internal state. Clients (e.g.
librx888) may probe-then-issue to detect optional commands without risking a wedge. Validated by the soak/fuzz harness, which deliberately injects unallocated opcodes (0xFFtoday; reserved-range bytes once commit 3 lands).
This principle binds every firmware change in this plan. Each commit must include a soak/fuzz test (or pointer to one) demonstrating that the change does not create a new wedge mode.
Plan: four independent commits
Each commit is independently mergeable. Order matters only for documentation cross-references.
Commit 1 — fix(Si5351): query CLK0_PDN directly instead of trusting STARTADC flag ✅ DONE (v0.1.0)
Shipped in commit
13b2091via PR #122, released in v0.1.0. The companion docker change (Commit 2 below — retire patch 03) shipped in the same PR. Scope was slightly extended in flight:GETSTATSwas widened from 24 to 26 bytes to expose CLK0_CONTROL raw byte ([24]) and the boolean chip-query result ([25]) for diagnostics. Subsections preserved below as historical record.
Scope: SDDC_FX3/driver/Si5351.c only (~20 lines edited / removed).
Change:
- Replace
si5351_clk0_enabled()to read CLK0_CONTROL register (16) bit 7 via I2C. Bit 7 = 0 means powered up. Symmetric with the existingsi5351_pll_locked()which already reads register 0 bit 5. - Remove the
glAdcClockEnabledglobal flag and the two write sites insi5351aSetFrequencyA().
Why this is correct: the hardware register is authoritative. Any path that programs the Si5351 — STARTADC (firmware path) or I2CWFX3 (host path) — leaves register 16 in the same state, so GpifPreflightCheck() passes for both.
Recoverability check: if the I2C read fails (bus glitch), the function returns CyFalse — STARTFX3 refuses safely. Recovery: retry, or RESETFX3. No new wedge surface.
Compatibility: rx888_stream, ExtIO, and ka9q-radio (with no host-side patches) all work after this change. No wire-protocol change.
Validation: existing tests/fw_test.sh covers the STARTADC-then-STARTFX3 path; the Docker container without patch 03 covers the I2CWFX3-direct path. Both must pass.
Risk: low. I2cTransfer() is a proven path (used by si5351_pll_locked). Reading register 16 is non-destructive.
Commit 2 — refactor(docker): drop ka9q patch 03; firmware fix supersedes it ✅ DONE (v0.1.0)
Shipped in PR #122 (commit
60af0b7), released in v0.1.0. Implementation diverged slightly from the plan: rather than deleting the patch outright, it was renamed to03-startadc-before-startfx3.patch.disabledso the Dockerfile glob skips it but the historical record stays in-tree. The Dockerfile loop was also hardened with a POSIX empty-glob guard (commit0993747) after a Codex review flagged the originalshopt -s nullglobapproach as bash-only.
Scope: test-harness side only.
- Remove
docker/ka9q-radio/patches/03-startadc-before-startfx3.patch. - Update
docs/ka9q-compat-audit.md§8: reframe as “fixed firmware-side; no ka9q-radio patches required.” - Update
docker/ka9q-radio/patches/README.mdand the docker README to reflect “zero ka9q patches needed.” Patches/ directory is now empty. - The
git applyloop in the Dockerfile should still work (no-op when patches/ is empty).
Validation: rebuild the image with --no-cache; rerun the helper-script workflow; confirm streaming still works against firmware from commit 1.
Risk: zero — this is a downgrade-friction reduction, not a behavior change.
Commit 3 — feat(USBHandler): add GETCAPABILITIES and GETSTATE vendor commands
Scope: SDDC_FX3/protocol.h, SDDC_FX3/USBHandler.c, plus a documentation block in docs/architecture.md.
New commands at vendor request bytes 0xBB and 0xBC (confirmed unused):
-
GETCAPABILITIES(0xBB) — returns a JSON object describing firmware version, build SHA, supported-commands list, sample-rate min/max, GPIO bit map (so the LED ambiguity in audit §4 becomes queryable), ADC bit width. JSON keeps the schema flexible — fields can be added without breaking parsers. Example:{"schema_version":1,"version":"0.0.1","build":"f78cff9","commands":["STARTFX3","STOPFX3","STARTADC","GPIOFX3","I2CWFX3","I2CRFX3","SETARGFX3","RESETFX3","READINFODEBUG","GETSTATS","GETCAPABILITIES","GETSTATE"],"sample_rate_hz":{"min":2000000,"max":129600000},"gpio_bits":{"DITHER":0,"RANDOM":1,"VHF_EN":2,"LED_BLUE":11},"adc_bits":16}Returned over EP0 in chunks if it exceeds
CYFX_SDRAPP_MAX_EP0LEN; host issues repeat reads until done. Length capped (e.g. 1024 bytes) so this never wedges. -
GETSTATE(0xBC) — returns a smaller JSON object with current SM state, current Si5351 frequency (read from chip), current GPIO bits, last vendor-command error code:{"schema_version":1,"sm_state":"IDLE","si5351_clk0_hz":64800000,"gpio":3,"last_error":0,"streaming":false}
Why JSON over fixed binary: schema flexibility (new fields don’t break old hosts), introspection by hand (curl-style debugging via usbcontrol or similar), easier integration with web-based tooling (ka9q-web, future telemetry). Cost is parser size — small in C with a string-matching scanner, and the firmware only needs to emit, not parse.
Future compatibility (librx888 and other downstream clients). The following invariants are guarantees, not coincidences, and must be preserved as new commands and fields are added:
schema_versionis the negotiation hatch. Major-version bumps signal a breaking change (field removed, type changed, semantics altered). Minor-version bumps signal additive growth and must not break clients that ignore unknown fields. Today bothGETCAPABILITIESandGETSTATEship at"schema_version": 1.- JSON growth contract. Future firmware revisions may add fields to either response; clients must ignore unknown fields rather than rejecting the document. Removing a field, or changing its type or semantics, requires a major-version bump.
commandsis the feature-detection surface. The string array must enumerate every vendor command the firmware will accept. Any future opcode added to the firmware must be appended to this list in the same revision; existing entries are stable. Clients perform feature detection by string lookup rather than by hardcoded opcode tables.- Opcode reservations. To leave room for the typed-setter expansion that downstream clients (e.g.
librx888) will need without colliding with one-off additions, the following ranges are reserved:0xD0–0xDF— typed configuration setters. Reserved for future single-concern, typed-argument setters (e.g.SET_SAMPRATE,SET_RF_ATTEN). Do not allocate these bytes for ad-hoc additions; pull from0xB7–0xB9or0xBD–0xCDinstead.0xE0–0xEF— typed query expansion. Reserved for future structured query commands beyondGETCAPABILITIES/GETSTATE.- These reservations are advisory only; no firmware code today enforces them. A future commit may add an enum range guard.
Recoverability check: both commands are read-only. Truncated reads or aborted transfers leave the firmware in EP0 idle. Bounded length cap means no buffer-overflow surface.
Validation:
- Issue
GETCAPABILITIES, parse JSON, verify version field matches built-ingit describeoutput. - Issue each command listed in the response and confirm none STALL.
- Issue
GETSTATEbefore and afterSTARTFX3/STOPFX3and confirmsm_stateandstreamingchange. - Recoverability soak: issue these commands at high rate concurrently with
STARTFX3/STOPFX3cycles; confirm no resource leak, no GPIF wedge.
Risk: low-medium. Pure additions but introduces the firmware’s first JSON serialization — keep it simple (single static buffer + snprintf).
Commit 4 — docs(architecture): document recommended vendor command sequence and recoverability guarantees
Scope: docs/architecture.md only.
Content:
- Recommended initialization sequence for HF receive on a fresh device:
GPIOFX3(mode bits) → set Si5351 (viaSTARTADCor directI2CWFX3— both are valid) →SETARGFX3 DAT31_ATT→SETARGFX3 AD8340_VGA→STARTFX3→ bulk read →STOPFX3. - Hardware preconditions that
STARTFX3actually checks (CLK0 powered up via register-16 query, PLL A locked) — the truth, not the historical proxy. - Capability discovery flow for new hosts:
GETCAPABILITIESfirst, branch oncommandslist, then issue commands. - Recoverability contract (Principle 2 above) — guarantees the firmware makes to host authors. Includes the recovery escape sequence:
STOPFX3→RESETFX3→libusb_reset_device()→ unplug/replug → re-upload firmware. - Reference to deprecated commands (
TUNERINIT,TUNERTUNE,TUNERSTDBY) so a new developer who finds them inprotocol.hknows they STALL on this firmware.
Risk: zero (docs only).
Out of scope (deferred)
- VHF support / R82xx tuner. Audit §3 — non-blocking for HF.
- LED bit-mapping convergence with ka9q. Audit §4 — cosmetic; once
GETCAPABILITIESexposes the GPIO bit map, hosts can adapt programmatically. - ka9q-radio’s missing
libusb_clear_halt(). Audit-mentioned but is upstream’s call. - A general fuzz harness for vendor commands (worth doing as a separate effort, separate from this plan).
Open questions — resolved
- Vendor request bytes for
GETCAPABILITIES/GETSTATE. → Resolved: 0xBB and 0xBC. - Capability struct format. → Resolved: JSON text (schema flexibility outweighs parser cost).
- Patch 03 / audit cleanup placement. → Resolved: separate commit (commit 2), immediately after the firmware fix.
- Single PR vs. split. → Resolved: split across multiple commits as designed; merge order 1 → 2 → 3 → 4.
Approval gate
This plan is ready to execute pending OK on this document. Once approved, work proceeds commit-by-commit with the standard CLAUDE.md change-doc block before each commit.
Page 3 — Implementation specifics
Repository state at plan-approval time
- Current
main: includes PR #91 merge (f9077f7and predecessors). All work below targets a fresh branch offmain. - Suggested branch name:
claude/vendor-protocol-v1(or similar; the executing instance picks). - Build the firmware:
cd SDDC_FX3 && make clean && make all→ producesSDDC_FX3.img. - Build the test-harness image:
docker build -t ka9q-radio docker/ka9q-radio/. - End-to-end validation requires real RX888mk2 hardware. An instance running headlessly can build, syntax-check, run static patch-apply, and run unit-style tests, but cannot exercise the streaming path. When end-to-end testing is required, ask the user to run the helper script.
Files in scope per commit
Commit 1 — fix(Si5351): query CLK0_PDN directly
Primary file: SDDC_FX3/driver/Si5351.c
Locations as of PR #91 merge (re-verify with Read before editing):
- Line ~55:
static CyBool_t glAdcClockEnabled = CyFalse;— REMOVE - Lines ~140–163: existing
si5351_pll_locked()— model for the new function - Lines ~165–175: current
si5351_clk0_enabled()(returns the bare flag) — REPLACE - Line ~192:
glAdcClockEnabled = CyFalse;insidesi5351aSetFrequencyA(freq=0)— REMOVE - Line ~250:
glAdcClockEnabled = CyTrue;inside the success path ofsi5351aSetFrequencyA(freq>0)— REMOVE
Proposed replacement (drop in at the old si5351_clk0_enabled location):
CyBool_t si5351_clk0_enabled(void)
{
uint8_t reg16 = 0xFF; /* default = "powered down" if I2C fails */
CyU3PReturnStatus_t rc;
/* Read CLK0_CONTROL register (16). Bit 7 = CLK0_PDN:
* 0 = clock powered up and running
* 1 = powered down (chip default at power-on)
* Authoritative — reflects actual chip state regardless of
* which path programmed it (STARTADC vs direct I2CWFX3).
*/
rc = I2cTransfer(SI_CLK0_CONTROL, SI5351_ADDR, 1, ®16, CyTrue);
if (rc != CY_U3P_SUCCESS)
return CyFalse; /* I2C failed — refuse start */
return (reg16 & 0x80) == 0;
}
I2cTransfersignature is inSDDC_FX3/driver/I2cControl.h; the existingsi5351_pll_locked(already in this file) shows the calling pattern.SI_CLK0_CONTROLalready exists as a constant inSDDC_FX3/driver/Si5351.h(referenced atSi5351.c:193in thefreq=0path). Reuse it.
Validation:
- Build:
cd SDDC_FX3 && make clean && make all— must produceSDDC_FX3.imgwith no warnings. - Hardware (STARTADC path, host-driven):
./tests/fw_test.sh --firmware SDDC_FX3/SDDC_FX3.img— must pass. - Hardware (I2CWFX3-direct path, after commit 2):
./docker/ka9q-radio/ka9q.sh start && ./docker/ka9q-radio/ka9q.sh monitor— must produce audio.
Soak test (Commit 1 must ship this script): see template at the end of Page 3.
Commit 2 — refactor(docker): drop ka9q patch 03
Files:
docker/ka9q-radio/patches/03-startadc-before-startfx3.patch— DELETE (git rm).docker/ka9q-radio/patches/README.md— table loses its only row. Add note: “All previously-required ka9q-radio source patches are now resolved firmware-side; the patches/ directory is empty. Container patches are reserved for future incompatibilities that have no firmware-side or container-side fix.”docs/ka9q-compat-audit.md§8 — reframe. Replace the “Fix” subsection with: “Resolution: fixed firmware-side in<commit-1-sha>of this repo (driver/Si5351.c).si5351_clk0_enabled()now queries CLK0_CONTROL register 16 bit 7 directly. ka9q-radio against the current firmware requires no source modifications.”docker/ka9q-radio/README.md:- “What this tests” bullet 2 — revert to: “Si5351 clock programming via
I2CWFX3(host-side direct programming, validated by SDDC firmware’s chip-level preflight check).” - “Known compatibility notes” — remove the “Missing STARTADC” bullet entirely.
- “What this tests” bullet 2 — revert to: “Si5351 clock programming via
Validation: rebuild image with docker build --no-cache -t ka9q-radio docker/ka9q-radio/; re-run the three-terminal flow; confirm rx888 running and audible output via ./ka9q.sh monitor.
Commit 3 — feat(USBHandler): add GETCAPABILITIES and GETSTATE
Files:
SDDC_FX3/protocol.h— add to the command enum:GETCAPABILITIES = 0xBB, /* Return JSON describing firmware capabilities */ GETSTATE = 0xBC, /* Return JSON describing current runtime state */SDDC_FX3/USBHandler.c— add two newcaseblocks in the EP0 vendor-request switch. Model on the existingGETSTATShandler at line ~246–266; sameglEp0Buffer+CyU3PUsbSendEP0Data+isHandled = CyTruepattern.docs/architecture.md— document the wire format (bmRequestType=0xC0,bRequest=0xBB/0xBC, response inglEp0Buffer, JSON UTF-8) and schema (full sample JSON of each).
JSON emission pattern (place adjacent to the new case statements in USBHandler.c):
static int build_capabilities_json(char *buf, int max)
{
return snprintf(buf, max,
"{"
"\"schema_version\":1,"
"\"version\":\"%s\","
"\"build\":\"%s\","
"\"commands\":[\"STARTFX3\",\"STOPFX3\",\"STARTADC\",\"GPIOFX3\","
"\"I2CWFX3\",\"I2CRFX3\",\"SETARGFX3\",\"RESETFX3\","
"\"READINFODEBUG\",\"GETSTATS\","
"\"GETCAPABILITIES\",\"GETSTATE\"],"
"\"sample_rate_hz\":{\"min\":2000000,\"max\":129600000},"
"\"gpio_bits\":{\"DITHER\":0,\"RANDOM\":1,\"VHF_EN\":2,\"LED_BLUE\":11},"
"\"adc_bits\":16"
"}",
SDDC_FW_VERSION, SDDC_BUILD_HASH);
}
SDDC_FW_VERSION and SDDC_BUILD_HASH should be passed via -D flags from make. If the Makefile does not already inject these (check first), add:
GIT_VERSION := $(shell git describe --tags --always --dirty 2>/dev/null || echo "unknown")
GIT_HASH := $(shell git rev-parse --short HEAD 2>/dev/null || echo "unknown")
CFLAGS += -DSDDC_FW_VERSION=\"$(GIT_VERSION)\" -DSDDC_BUILD_HASH=\"$(GIT_HASH)\"
Sub-decision resolutions:
- GPIO bit map: SDDC-canonical, sourced from
docs/architecture.md§”GPIOFX3 bitmask protocol”. NOT ka9q’s mapping (ka9q’s differs in bits 11–12; see audit §4). - Sample-rate min/max:
min= 2 000 000 Hz (lowest reliable Si5351 output for direct sampling),max= 129 600 000 Hz (per existingrx888-test.confand standard ka9q config). Re-verify by readingSi5351.cfor any hardcoded clamps before finalizing. - ADC bit width: 16 (LTC2208 in RX888mk2 hardware). Constant.
EP0 chunking: if the JSON exceeds CYFX_SDRAPP_MAX_EP0LEN, chunk via repeated CyU3PUsbSendEP0Data calls — the existing READINFODEBUG handler is the model. In practice the capabilities JSON above is ~250 bytes, well under the buffer size, so chunking is unlikely to trigger.
Validation script (commit alongside, at tests/test_capabilities.sh):
#!/bin/bash
# Issue GETCAPABILITIES and GETSTATE; validate JSON.
set -euo pipefail
JQ_PATH="${JQ_PATH:-jq}"
"$JQ_PATH" --version >/dev/null # require jq
./tests/fx3_cmd --vendor 0xBB --read 1024 > /tmp/caps.json
"$JQ_PATH" -e '.version, .commands, .sample_rate_hz.min, .adc_bits' /tmp/caps.json
./tests/fx3_cmd --vendor 0xBC --read 1024 > /tmp/state.json
"$JQ_PATH" -e '.sm_state, .si5351_clk0_hz, .streaming' /tmp/state.json
echo "OK"
(tests/fx3_cmd is the existing helper from tests/Makefile. If it does not currently support --read N, extend it as part of this commit.)
Commit 4 — docs(architecture): recommended sequence + recoverability contract
File: docs/architecture.md only.
Add new top-level sections (place them after the existing “Vendor command table” section, in this order):
- “Recommended initialization sequence” — paste the sequence from Principle 1 of this plan, with rationale: “Any sequence that leaves CLK0 enabled and PLL A locked at the time
STARTFX3is sent will succeed; the recommended sequence is the easy path, not the only path.” - “Hardware preconditions for STARTFX3” — quote the actual checks (post-commit-1): CLK0 power-up via register-16 query, PLL A lock via register-0 LOL_A bit.
- “Capability discovery” — pointer to
GETCAPABILITIES/GETSTATE(commit 3); recommended first request from new hosts. - “Recoverability contract” — Principle 2’s four invariants verbatim plus the recovery escape ladder:
STOPFX3→RESETFX3→ hostlibusb_reset_device()→ unplug/replug → re-upload firmware viaezusb_load_ramorfxload. - “Deprecated commands” —
TUNERINIT(0xB4),TUNERTUNE(0xB5),TUNERSTDBY(0xB8) STALL on this firmware (R82xx removed, seedocs/LICENSE_ANALYSIS.md).
Soak/fuzz test template
Each firmware-touching commit (1 and 3) must ship a script in tests/ that proves no command sequence can wedge the device. Template:
#!/bin/bash
# tests/soak_<topic>.sh
#
# Goal: prove that no sequence of valid + malformed vendor commands
# wedges the device. Recovery is checked by sending GETSTATE
# (commit 3+) or by RESETFX3 then a known-good probe.
#
# Usage: ITERATIONS=1000 DEVICE=/dev/bus/usb/<bus>/<dev> ./tests/soak_<topic>.sh
#
# Exits 0 on success; nonzero with diagnostic on first wedge.
set -euo pipefail
ITERATIONS="${ITERATIONS:-1000}"
DEVICE="${DEVICE:-}"
[ -n "$DEVICE" ] || { echo "Set DEVICE=/dev/bus/usb/..." >&2; exit 2; }
cmds=(0xAA 0xAB 0xB2 0xAD 0xAE 0xAF 0xB6 0xB1 0xBA 0xB3 0xBB 0xBC 0xFF)
# 0xFF is "unknown" — must STALL cleanly, never wedge.
for i in $(seq 1 "$ITERATIONS"); do
cmd="${cmds[$RANDOM % ${#cmds[@]}]}"
arg=$RANDOM
# Don't propagate STALLs; that's the test target.
./tests/fx3_cmd --device "$DEVICE" --vendor "$cmd" --arg "$arg" || true
# Recovery probe after EVERY iteration:
./tests/fx3_cmd --device "$DEVICE" --vendor 0xB1 || true # RESETFX3
./tests/fx3_cmd --device "$DEVICE" --vendor 0xBC --read 1024 > /dev/null \
|| { echo "WEDGED at iter $i after vendor=$cmd arg=$arg" >&2; exit 1; }
done
echo "$ITERATIONS iterations: no wedges."
- For commit 1 (
tests/soak_si5351.sh), focus the random selection onSTARTADC/STARTFX3/STOPFX3/I2CWFX3to exercise the chip-query path. - For commit 3 (
tests/soak_capabilities.sh), includeGETCAPABILITIES/GETSTATEand0xFF(unknown) heavily. - Expected runtime: ~2 minutes for 1000 iterations on a typical host.
- The recovery probe in commit 1 (where
GETSTATEdoesn’t exist yet) should use a cheap known-good command instead — recommendREADINFODEBUG(0xBA).