hub: assemble scope and spectrum WebSocket frames - #2
Merged
Conversation
While the meter is on the waveform or spectrum LCD page, the firmware
populates a 320-byte display buffer that we used to ignore. The HID
owner now interleaves a 6-tick cycle (cmd '0','1','2','3','4','5')
during those modes, assembles the 5 sample segments into one
SampleBufferSize-byte buffer, and emits it on a new channel that the
hub broadcasts as `{"type":"scope"}` or `{"type":"spectrum"}` over
the existing /ws endpoint. In power_swr / setup modes the legacy
poll cycle (cmd '0' every tick, cmd '6' every 10th) is preserved —
no overhead is paid on the modes that don't use it.
The wire format was reverse-engineered with a new `probe -samples`
subcommand that drives the meter through known channel/range/top_mode
states, dumps the 64-byte IN response to each sample cmd, and dedups
identical consecutive frames so the output stays readable. Two
firmware quirks fell out of this:
- F2 (channel) and F3 (range) are no-ops when the meter is on the
waveform or spectrum LCD page; the probe now mode_steps to
power_swr before driving channel/range. (Already documented in
CLAUDE.md for F4 in auto-channel; this extends the same gating
to F2/F3 in waveform/spectrum.)
- In sample modes the firmware repurposes the WHOLE 64-byte IN
frame as sample data — not just the bytes-40..63 "secondary
slot" abstraction that holds for telemetry/status frames.
Samples are 8-bit unsigned, normalized for LCD display height (the
firmware auto-scales each trace, mirroring the Teensy reference's
`map(plot, 0, PLOT_CENTER * f, 0, PLOT_CENTER - 50)` logic). They
describe the *shape* of the envelope / FFT spectrum, not absolute
watts — for power readings clients use the matching telemetry frame.
JSON encoding uses a custom SampleBytes type with MarshalJSON so the
320-element field marshals as a decimal int array (e.g.
`[151,151,0,8,...]`) rather than Go's default base64 string for
[]byte. Clients deserialize this as `[Int]` directly.
# Frame routing is by SHAPE, not by OUT-write order
The first iteration of this work used a FIFO of (write-cmd → expected-
response) to attribute each IN frame to the OUT cmd it answered.
That approach desyncs on any single missed event (stale kernel-
buffered frame at HID open, an unsolicited firmware frame on mode
change, etc.) and the misalignment then cascades — sample bytes leak
into the telemetry decoder, producing garbage WS frames with
nonsensical SWR / channel / status-message values that compound into
top_mode flapping. Field reports of impossible values
(`swr: 20.52`, `peak < avg`, channel/peak_mode/top_mode flapping
without commands, ASCII-leaked status garbage like
`?BFILORUY|_bfilorvy|`) confirmed this in production.
The new routing classifies each frame on its own merits, with no
per-write state that can desync:
1. ECHO — byte[0] in '0'..'?' AND every other byte zero.
Firmware refused the OUT (wrong LCD page or no-op
in current state). Dropped.
2. TELEMETRY — passes a tight byte-range structural check
(isLikelyTelemetry in owner.go): top_mode ≤ 3,
channel ≤ 4, channel-auto ≤ 4, range ≤ 11,
alarm-disabled flag ≤ 1, peak-mode ≤ 2, plus two
power-coherence invariants the firmware guarantees
(peak_power ≥ avg_power; SWR raw ≤ 1000 = SWR 10).
Random sample data passes all eight with
probability < 10⁻¹². Decoded as a Snapshot and
broadcast.
3. SAMPLE — everything else. In waveform/spectrum mode this is
the next segment of the 5×64-byte buffer; assemble
in arrival order (the 1:1 firmware response keeps
order aligned with our 6-tick cycle). A sample
counter resets on every telemetry frame and on
every top-mode change, so transient desync self-
corrects within one cycle.
Plus three smaller fixes the LP-700-App-side post-mortem called out:
- Scope/spectrum emit is now gated on `channel ∈ {1..4}` AND
`!auto_channel`. Auto-channel × waveform/spectrum is a hardware-
invalid combination on the LP-700; the sample buffers in that
state are indeterminate.
- extractStatusMessage now requires BOTH an ASCII space AND a
3-letter ASCII run. The old ≥75 % printable threshold passed
sample-byte leakage like `?BFILORUY|_bfilorvy|` (FFT bin values
that happen to be in printable-ASCII range). Real LP-700
status messages are English phrases.
- probe -samples mode_steps to power_swr first when -channel /
-range flags are set, so F2/F3 control writes take effect
instead of being no-op'd by the firmware's per-page gating.
Verified on the live LP-700:
- probe -samples confirmed cmds '1'..'5' return contiguous 64-byte
segments (cmd '1' tail blends into cmd '2' head when there's RF)
- server emits scope frames at the design rate (median gap 240 ms
between consecutive scope frames)
- 20-second WS verify run: 71 telemetry frames + scope/spectrum
frames; zero impossible-value telemetry (swr > 5: 0, peak < avg:
0, peak_mode flap: 0; the remaining top_mode / channel flap
matched the operator's F1 / channel_step presses)
- control verbs and existing telemetry still work
What's NOT in this change (deferred):
- Sample rate / time-base of the scope buffer is unknown; needs a
CW key-up edge for timing correlation. The values render fine
as a shape without it.
- Mac-client renderers live in VU3ESV/LP-700-App; this change is
server-side only.
- The simulator backend doesn't synthesize scope/spectrum yet —
clients see no frames of those types under -backend simulator.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Surfaces the LP-500 / LP-700's on-LCD scope (waveform) and spectrum (FFT)
buffers over the existing
/wsendpoint as two new frame types:While the meter is on the waveform or spectrum LCD page, the HID owner
interleaves a 6-tick cycle (
cmd '0', then sample cmds'1'..'5')instead of the legacy
cmd '0'/cmd '6'cycle, assembles the5 × 64-byte segments into one 320-byte buffer, and emits ~4 Hz. In
power_swr/setupmodes the legacy cycle is preserved — nooverhead is paid where it isn't useful.
Why
The firmware exposes the scope and spectrum buffers via
cmds '1'..'5',but until now we ignored them — the Mac and web clients couldn't
mirror the LCD's Waveform / Spectrum pages. The wire format wasn't
publicly documented; a new
probe -samplessubcommand was added toreverse-engineer it against the bench LP-700, then this change
productionises it.
The interesting design call: frame routing is by SHAPE, not write order
An earlier iteration matched each IN frame to the OUT cmd that
produced it via a FIFO. That approach desyncs on any single missed
event (stale kernel-buffered frame at HID open, an unsolicited
firmware frame on mode change) and the misalignment then cascades —
sample bytes leak into the telemetry decoder and clients see
nonsensical telemetry (
swr: 20.52,peak < avg, channel /peak_mode / top_mode flapping without commands, ASCII-leaked status
garbage). All of those were observed in production with the FIFO.
This PR uses shape-based classification instead. Three classes:
byte[0]in'0'..'?'AND rest zero. Firmware refusedthe OUT. Dropped.
isLikelyTelemetryinowner.go: byte-3≤ 3 (top_mode), byte-4 ≤ 4 (channel), byte-5 ≤ 4, byte-6 ≤ 11
(range), byte-7 ≤ 1 (alarm flag), byte-8 ≤ 2 (peak_mode), plus
two power-coherence invariants (peak ≥ avg; SWR raw ≤ 1000).
Random sample data passes all eight with probability < 10⁻¹².
Decoded as a
Snapshotand broadcast.the next segment of the 5 × 64-byte buffer; assemble in arrival
order. A sample counter resets on every telemetry frame and on
every top-mode change, so transient desync self-corrects within
one cycle.
No per-write state that can desync.
Secondary fixes
scope/spectrumemit is now gated onchannel ∈ {1..4}AND!auto_channel. Auto-channel × waveform/spectrum is ahardware-invalid combination on this firmware; sample buffers in
that state are indeterminate.
extractStatusMessagenow requires both an ASCII space and a3-letter run, rejecting sample-byte leakage like
?BFILORUY|_bfilorvy|that the prior ≥75 %-printable thresholdlet through.
probe -samplesmode_steps topower_swrbefore driving-channel/
-range, since F2/F3 are no-ops on the waveform/spectrum LCDpages.
What's NOT in this change (deferred)
CW key-up edge for timing correlation. The values render fine as
a shape without it.
server-side only.
clients see no frames of those types under
-backend simulator.Test plan
go vet ./...cleango test -race ./...clean (new tests coverSampleReport,isCommandEcho,isLikelyTelemetrywith the productionleak-path frames, and
extractStatusMessagerejectingsample-byte leakage)
probe -samples -cycle-modesagainst the bench LP-700confirmed the 320-byte buffer is contiguous across cmds and
that segments straddle cmd boundaries (spectrum cmd '1' tail
0b 0bblends into cmd '2' head0b 0b 0b 0b 0b 0b 0b)linux/arm64, deployed to a live LP-700 overhidraw, ran a 20-second WS verify:
- 71 telemetry frames + scope/spectrum frames
- swr > 5: 0, peak < avg: 0, peak_mode flap: 0
-
samples/binsJSON-deserialize aslistof length 320- Control verbs and the heartbeat still work as before
🤖 Generated with Claude Code