Status: Draft for discussion. Companion to rx888-firmware's
docs/vendor-protocol-plan.md. Same recoverability invariant.
The existing wire protocol (13 commands + 2 planned read-only) works, and every shipped client depends on it. We keep all of it. But the shape of the surface — untyped argument bytes, GPIO bitmasks that conflate orthogonal concerns, a misnamed Si5351 command that ka9q-radio bypasses entirely — makes every new consumer reinvent the same chip-datasheet conversion tables. This plan defines an additive, typed, capability-discoverable command set so that ka9q-radio, Spectre, gr-rx888, and the next consumer that hasn't been written yet can all speak the same dialect.
Nothing here breaks deployed clients. The legacy opcodes stay wired, documented as such, with a one-line migration note pointing at the typed equivalent.
13 commands today (STARTFX3=0xAA–HANGMAIN=0xCF), plus
GETCAPABILITIES=0xBB / GETSTATE=0xBC in the firmware plan. Four
structural problems:
-
STARTADCis misnamed and bypassed in practice. It's Si5351 setup, not ADC activation. ka9q-radio doesn't use it: it runs Phil Karn's 450-line spur-aware solver on the host, then programs the Si5351 directly viaI2CWFX3. The firmware's ~75-line ik1xpv-lineage solver is unreachable to ka9q. Two solvers, one device, no contract. -
SETARGFX3is untyped.(DAT31_ATT, value)carries no unit, no range, no encoding. ka9q sendsatt*2(half-dB count, 0–63);AD8370_VGAneeds a 50-linegain2val()conversion because the byte format mixes a 7-bit code with a high-gain-range bit. Every host has to know the DAT-31 and AD8370 datasheets. -
GPIOFX3is a bitmask command for orthogonal concerns.DITH,RANDO,BIAS_HF,BIAS_VHF,VHF_EN,PGA_EN,LED_BLUE,SHDWN,ATT_SEL0/1all share one register write. Hosts maintain shadow state to flip one bit. Race-prone, requires every host to mirror device state correctly, and forecloses ever moving an LED or repurposing a GPIO line. -
Configuration is conflated with state transitions.
STARTFX3has undocumented preconditions discovered by reading host code. The firmware plan's three-bucket model — Configuration / Control / Query — fixes this. Make it real.
| Consumer | Wants from firmware | Won't move to firmware |
|---|---|---|
| ka9q-radio | Streaming, GPIO bits, attenuator + VGA, I2C_WRITE escape hatch for its preferred solver |
AGC loop, DC blocker, spur table, FFT — radiod's job |
| Spectre (SoapySDR seam) | GET_CAPABILITIES, typed setters, predictable units, STOP/RESET recoverability |
SoapySDR-shaped abstractions, multi-channel pretense |
| gr-rx888 | Same as Spectre minus the SoapySDR wrapper. Six typed setters → one streaming call → status messages | All DSP. The block is a digitizer source, not a receiver |
Common ground: typed setters with documented units, capability discovery, recoverable state machine, raw int16 stream, status counters. Divergence: ka9q wants the I²C/GPIO escape hatch; Spectre/gr-rx888 prefer never touching it.
Three buckets per the firmware plan, with concrete shape. Wire format:
vendor request byte + 16-bit wValue + 16-bit wIndex + optional
payload. All units explicit. All values clamped server-side to the
device's actual capability — the achieved value is observable via
GET_STATE.
| Cmd | Hex | Argument | Units / range |
|---|---|---|---|
SET_SAMPRATE |
0xD0 | uint64_t hz (payload) |
Hz, 1e6–130e6. Firmware runs the solver; achieved rate via GET_STATE. Single-rate-at-a-time — re-open the device to change rate domain. |
SET_REFERENCE |
0xD1 | uint64_t hz (payload) |
Hz, 10e6–100e6. Tells firmware what TCXO is installed. |
SET_RF_ATTEN |
0xD2 | int16_t cdB (wValue) |
centi-dB, 0–3150 in steps of 50. DAT-31 details internal. |
SET_VGA_GAIN |
0xD3 | int16_t cdB (wValue) |
centi-dB. Firmware picks AD8370 lo/hi range; reports actual via GET_STATE. |
SET_DITHER |
0xD4 | uint8_t on/off (wValue) |
|
SET_RAND |
0xD5 | uint8_t on/off (wValue) |
Also stamped into the stream header so hosts know whether to un-randomize. |
SET_BIAS |
0xD6 | port:uint8_t, on:uint8_t (wValue) |
port=0 HF, 1 VHF. One typed call instead of GPIO bit fiddling. |
SET_FRONT_END |
0xD7 | enum {HF=0, VHF=1, EXT=2} (wValue) |
Replaces VHF_EN+ATT_SEL bookkeeping. |
Idempotent: any of these may be issued in any order, in any state. Firmware applies them when it next enters the streaming state; hosts don't sequence.
Control (explicit state transitions, no hidden config)
| Cmd | Hex | Semantics |
|---|---|---|
START |
0xAA | Begin streaming. Preconditions: clock programmed (via SET_SAMPRATE or legacy STARTADC). Returns error on unmet precondition rather than silently misbehaving. |
STOP |
0xAB | Halt streaming. Always succeeds. |
RESET |
0xB1 | Soft reset to power-on-equivalent state. Subject to the recoverability invariant — firmware handles I²C-bus wedges, Si5351 lockup, and ADC misconfiguration internally without requiring a power cycle. |
Opcodes kept identical to today so deployed clients are unaffected.
| Cmd | Hex | Returns |
|---|---|---|
GET_CAPABILITIES |
0xBB | JSON, includes "schema_version": 1. Firmware version, board model, supported sample-rate set (from solver enumeration), attenuator range + step (cdB), VGA range + step (cdB), available GPIO functions, supported front-end ports. Issued once at open. |
GET_STATE |
0xBC | JSON, includes "schema_version": 1. Current samprate (achieved), reference, atten, vga (all cdB), dither, rand, bias states, front-end selection, Si5351 lock-detect bit, last config error. Polled or on demand. |
GET_STATS |
0xB3 | Counters: USB transfer errors, FIFO overruns, samples delivered. Polled at ~1 Hz for host status messages. |
GET_FIRMWARE_INFO |
0xAC | Version + build hash. (Today's TESTFX3, renamed in docs.) |
The schema-version field is the forward-compatibility hatch. Additive growth bumps minor; semantic changes bump major. Clients read it before parsing.
GET_CAPABILITIES is what kills "every host has its own DAT-31 /
AD8370 lookup table." Ask once, get the truth, including the
firmware's enumerated supported-rate set so hosts can snap to a
representable rate before calling SET_SAMPRATE.
| Cmd | Hex | Use case |
|---|---|---|
I2C_WRITE / I2C_READ |
0xAE / 0xAF | Direct chip access. ka9q-radio's spur-aware solver path. Documented as "the typed setters cover the common case; reach for these only when they don't." |
GPIO_RAW |
0xAD | Same disclaimer. Bypasses SET_BIAS / SET_FRONT_END bookkeeping at the host's risk. |
These stay forever. They're the safety valve and the reason the typed setters can stay narrow.
- Synonym sprawl in host configs.
att / atten / featten / rfattenis a config-file concern, not a wire concern. One canonicalSET_RF_ATTENin centi-dB; application layer maps synonyms. - Per-host conversion tables.
gain2val()/val2gain()in ka9q-radio, equivalent in any future SoapySDR plugin, and what we'd have to write in gr-rx888 v0.2 all go away. cdB in, cdB out. - Shadow-state bugs.
SET_BIAS/SET_FRONT_ENDmean no host needs to remember "which GPIO bit was which" or "what was the rest of the mask when I last wrote it." - The "which solver?" question. Firmware's solver is the contract,
advertised through
GET_CAPABILITIES. ka9q's spur-aware solver can stay as opt-in viaI2C_WRITEfor users who want it.
- All deployed clients. Every change is additive. Legacy opcodes
remain wired.
STARTADCstays as documented alias. - The fast path. Streaming endpoint and packet format are untouched. This is a control-plane refactor.
- Application-level features. AGC, DC blocking, spur notching, frequency planning — none belong in firmware. They stay where they are.
Aligns with the firmware plan's four-commit cadence:
- Commit A — Land
GET_CAPABILITIES/GET_STATEin their final shape (firmware plan's commit 3), includingschema_version. The keystone: every new typed setter validates against capability and reports through state. - Commit B — Typed configuration setters (
SET_SAMPRATEthroughSET_FRONT_END). Legacy opcodes remain wired in parallel. - Commit C — Update
docs/architecture.md(firmware plan's commit 4) to mark legacy opcodes as "kept, prefer the typed equivalent" with a migration table. - Commit D — librx888 + ka9q-radio + gr-rx888 adopt the typed setters; SoapySDR seam (Spectre) picks them up.
Hardware validation in the Spectre Docker harness (PR #124) and in gr-rx888's QA against real hardware once v0.2 lands.
VHF tuner driving, external trigger / sub-µs timestamping,
firmware-side filtering, multi-rate streaming. Punt list, revisit if
demand surfaces — matches the gr-rx888 PLAN.md stance.