Repository navigation
Read writes back, and convert HPF Hz to the board's position (#31, #29) - #35
Merged
Merged
Conversation
`set` and the channel/mix/send verbs printed `✓ set <path> = <value>` as soon as
the frame went out. The board silently clamps or rejects a value it will not
accept, so that line described the request, not the result — a whole board's
worth of high-pass filters ended up wide open with every write reporting
success.
Every write now re-reads its paths on a fresh connection and reports what the
board holds. A write that did not take names both values and exits 1, the code
verify and apply already use for a board that does not match what was asked of
it:
✗ set line/ch3/filter/hpf: wrote 90, board holds 1
The read-back needs a fresh ZB, so the writing connection closes first — the
in-session read-back quirk means it cannot confirm its own writes. It costs one
extra connection per command, not per write; `--no-verify` skips it.
apply is unchanged: it never goes through this path and already runs
verifyAfterApply on a fresh connection.
The fake board now clamps PV writes to 0..1 for known controls, as a real 32R
does. Without that it stored 90.0 for a 0..1 control and the read-back agreed
with itself — the missing fidelity that let this class of bug reach a live desk.
Confirmed against a real 32R: `channel 1 hpf 90Hz` writes 90, the board holds 1,
and the CLI now says so.
Tolerances move to internal/schema so config drift and the post-write read-back
cannot disagree on what "the same setting" means.
Refs #31, #29
Claude-Session: https://claude.ai/code/session_01Pgcg59QJrz9BCmeEyb4B5U
`line/ch{n}/filter/hpf` is a 0..1 position, not a frequency. Both the config
schema and `channel hpf` took Hz and neither converted, so any value at or above
1 pinned the filter wide open. Applying a config left every vocal and instrument
channel with its high-pass at maximum, with no error to point at it.
taper.HPF is now the log sweep the board follows:
pos = ln(Hz / 24) / ln(1000 / 24)
Calibrated against a 32R holding a configured show: 0.35438603 is 90 Hz,
0.13696200 is 40 Hz, 0.38263556 is 100 Hz. Three positions landing on
frequencies a human would dial. Confirmed end to end on the same desk —
`channel 1 hpf 90Hz` now writes 0.35438639 and reads back clean.
The three spellings of one state are reconciled. Position 0 is both the bottom
of the sweep and how the board stores a filter that is off, so `hpf: off`,
`hpf: 0` and `hpf: raw:0.0` all compile to 0; `hpf: 0` used to be rejected as
"0 Hz out of range". Validation moves from a generic 20-20000 audio band to the
board's real 24-1000 sweep, so a frequency the board cannot reach is named at
load time instead of silently pinning the filter.
`dump --as-config` emits Hz instead of a raw position, rounded to a tenth (a
position read back as a 32-bit float inverts to 89.99984, and what belongs in a
config is 90), so a dumped config round-trips through apply and verify clean.
The read-back from #31 compares against what the control can hold, not the raw
number asked for: writing 0 Hz to a filter that sweeps from 24 Hz settles at
24 Hz, and that is the control working, not a failed write.
Closes #29
Claude-Session: https://claude.ai/code/session_01Pgcg59QJrz9BCmeEyb4B5U
CI was red on linux/amd64 and green on darwin/arm64: settledValue for 90 Hz comes back 90.00000000000001 on one and 90 on the other. A Hz -> position -> Hz round trip is a log and an exp, so the last bits are the platform's libm. The test asserted exact float equality, which can only ever be arch-dependent. Fixed at the representation, not in the assertion. Humanized reads now round to one decimal on the way out (schema.HumanPlaces). `ucmix get line/ch3/filter/hpf` printed 90.00000306137774 and `line/ch1/volume` printed -6.000000847568462 — the fader case predates this branch. A read should look like the value that was set, which is the whole point of #29: a dumped config has to round-trip and read like something a human dialled. `--raw` still shows the exact wire position, and Client.Get still returns the exact inversion; only presentation rounds. The rule already existed as boardconfig's round1, with the same rationale. It moves to internal/schema next to Tolerance so the config writer, `get`, `dump`, the drift table, and the post-write read-back cannot disagree. The settledValue test now compares within the control's tolerance — the same band the read-back comparison uses — instead of exactly, and says why in the comment so it is not re-tightened later. Also documents that --no-verify goes before the positionals: every write command takes its values verbatim so a fader like -6dB is not read as a flag, and a trailing --no-verify lands as another positional with an error that names the argument count rather than the flag. Claude-Session: https://claude.ai/code/session_01Pgcg59QJrz9BCmeEyb4B5U
steveclarke
force-pushed
the
29-hpf-hz-and-write-verify
branch
from
July 25, 2026 20:46
40e6b3e to
216502b
Compare
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.
Two bugs on the same path. #31 is why #29 went unnoticed, so it lands first and
proves #29. Rebased on master after #36.
#31 — writes reported success on send
setand the channel/mix/send verbs printed✓ set <path> = <value>as soon asthe frame went out. The board silently clamps or rejects a value it will not
accept, so that line described the request, not the result.
Every write now re-reads its paths on a fresh connection and reports what the
board holds. A write that did not take names both values and exits 1:
The read-back needs a fresh ZB, so the writing connection closes first — the
in-session read-back quirk means it cannot confirm its own writes. It costs one
extra connection per command, not per write, and a genuine mismatch retries up to
5 times before it reports. On a board with other UCNET clients on it that is real
contention, which is what
--no-verifyis for.The fake board now clamps PV writes to 0..1 for known controls, as a real 32R
does. Without that it stored 90.0 for a 0..1 control and the read-back agreed
with itself — the missing fidelity that let this class of bug reach a live desk.
#29 — HPF sent Hz to a control that wants 0..1
taper.HPFis now the log sweep the board follows:Calibrated against a 32R holding a configured show:
0.35438603is 90 Hz,0.13696200is 40 Hz,0.38263556is 100 Hz. Three positions landing onfrequencies a human would dial.
The three spellings of one state are reconciled: position 0 is both the bottom of
the sweep and how the board stores a filter that is off, so
hpf: off,hpf: 0and
hpf: raw:0.0all compile to 0 (hpf: 0used to be rejected as "0 Hz out ofrange"). Validation moves from a generic 20–20000 band to the board's real
24–1000 sweep.
dump --as-configemits Hz instead of a raw position, so a dumpedconfig round-trips through apply and verify clean.
Humanized reads round to one decimal
The first CI run was green on darwin/arm64 and red on linux/amd64: a
Hz → position → Hz round trip is a log and an exp, so the last bits are the
platform's libm and an exact float compare could only ever be arch-dependent.
Fixed at the representation rather than in the assertion, the same choice #36
made for color. Inverting a 32-bit wire position leaves noise —
getprinted90.00000306137774for the filter and-6.000000847568462for a fader, thelatter predating this branch — so humanized reads now round to one decimal on the
way out.
--rawstill shows the exact position andClient.Getstill returns theexact inversion; only presentation rounds. The rule already existed as
boardconfig's
round1; it moves tointernal/schemanext toToleranceso theconfig writer,
get,dump, the drift table, and the read-back cannot disagree.The
settledValuetest now compares within the control's tolerance — the bandthe read-back comparison already uses — and says why, so it is not re-tightened.
Confirmed on hardware
Run against the 32R at 192.168.77.82, targeted writes to
line/ch1/filter/hpfonly, read back and restored each time:
set line/ch1/filter/hpf 90(before the fix)1— the bug, reproducedset line/ch1/filter/hpf 0.354386490.35438639(float32 of the same position)channel 1 hpf 90Hz(before the fix)✗ set line/ch1/filter/hpf: wrote 90, board holds 1, exit 1channel 1 hpf 90Hz(after the fix)✓ set line/ch1/filter/hpf = 90, wire0.35438639, exit 0channel 1 hpf 2000Hztaper: value above control range: 2000 Hz > 1000 HzThe HPF positions already on the desk (
0.35438603on ch3/ch4,0.13696200onch7) invert to exactly 90 Hz and 40 Hz — the strongest confirmation of the curve.
ch1 was the only path written and is back at
0, its original value. Confirmedindependently: the live board still verifies clean against the show config.
Not touched
runs
verifyAfterApplyon a fresh connection. A per-write read-back there wouldduplicate it.
Tests
490 unit (was 373) and 86 e2e (was 72), plus lint, vet, race, govulncheck, fmt
and tidy.
Closes #29
Closes #31
https://claude.ai/code/session_01Pgcg59QJrz9BCmeEyb4B5U