Repository navigation
Performance/Settings split, capability-driven config, live-edit failures that actually surface, and an icon - #7
Merged
Conversation
…off" The firmware now tells the truth: a device does not report hardware it does not have (link-devices ESP-030). Absent hardware is ABSENT from /config.json, never reported false. This app has to decode an HONEST document — until now KsConfig required every field, so it would have THROWN on one, meaning the firmware told the truth and the app called it broken. KsConfig.metronome / .led / .followBeatEnabled are now OPTIONAL, and nil is a different statement from false: nil = there is no speaker fitted false = there IS a speaker and it is switched off Conflating them is how a UI ends up drawing a volume slider for a speaker that does not exist. Same class of error as the other four this codebase has produced today (T-009, T-010, T-016, T-018): two situations collapsed into one signal. Sections render only for hardware the device reports. No speaker, no metronome controls. No strip, no LED controls. saveFormFields only sends what exists, too — posting metronome=0 to a board with no speaker would mean this app INVENTED a value for absent hardware, which is exactly the fabricated-defaults clobbering that /config.json exists to prevent. Absent stays absent, in both directions. And capability belongs to the BUILD, not the product. Solder a strip onto the Touch, flip ONE firmware flag, and the LED section appears in this app with NO app change at all. That is the whole point of declaring capability instead of hardcoding a product identity. KsConfig.outputCount becomes maxOutputCount — a MAXIMUM, never an assumption. The real count is clock.count, which the firmware sets to what is actually fitted: one DIN jack on a Touch, four on a P4. Padding would draw three dead output cards. (T-015.) The test fixture is the REAL Touch's /config.json, captured verbatim from kstouch-dfd0: no metronome keys, no led keys, no follow_beat, three WiFi slots with the migrated credential in slot 0, and ONE clock output. 103 tests, 0 failures. Verified against the real device: it decodes, displays 120.0 BPM with 1 peer, and is controllable. No UNSUPPORTED FW badge.
The device screen is one scroll mixing transport with cable/rate/LED/wifi. On stage you want transport and nothing you can fat-finger; at soundcheck you want the knobs. Split criterion is not advanced-vs-basic, it's WHAT YOU REACH FOR MID-SET. Transport, obviously. Nudge — the firmware's own comment says it 'slides the RC-505 into the pocket', live via /nudge, no reboot: a playing control, not a setup one. Swing, same. Everything else is set once and left alone. PERFORMANCE (the screen you land on): tempo, meter bridge, master transport, and per output the big transport button + NUDGE + SWING. Nothing on it changes your SOUND — only your timing and transport. You cannot knock a cable assignment loose mid-set because it isn't there to knock. SETTINGS (gear sheet): everything LIVE — cable, rate, follow-link, click, LED, diagnostics. DEVICE SETUP (sheet from Settings): everything that REBOOTS — wifi, metronome enable, follow-beat enable. THE INVARIANT SURVIVES: no surface ever mixes live and reboot controls. One merged Settings sheet would have re-created the exact adjacency this app exists to fix — the device's own web page puts metronome-enable and metro-accent 40px apart as the identical control, one of which reboots the box mid-set. Better labelling is not the same as impossible. Firmware update stays in the toolbar, not inside Settings: Settings needs /config.json, which 404s on old firmware, and an update is how you ESCAPE that state (T-009).
The device screen was one scroll mixing transport with cable, rate, LED, click and diagnostics. On stage you want transport and nothing you can fat-finger; at soundcheck you want the knobs. The split criterion is not advanced-vs-basic. It's WHAT DO YOU REACH FOR MID-SET. PERFORMANCE (the screen you land on): tempo glass, meter bridge, master PLAY/STOP, and per output the big transport button + NUDGE + SWING. Nothing else. Nothing on this screen changes your SOUND — only your timing and your transport. You cannot knock a cable assignment or a clock division loose mid-song, because they are not there to knock. Nudge earns its place on the firmware's own account: nudge_mbeats is live via /nudge, no reboot, and the comment says it "slides the RC-505 into the pocket". That's a playing control, not a setup one. Swing is the same kind of thing. SETTINGS (gear sheet): everything LIVE — per-output enable/cable/rate/follow-link, click vol/voice/accent, LED, diagnostics. It opens with the reassuring counterpart of the reboot warning: "Everything on this screen applies instantly. Nothing here reboots the device." DEVICE SETUP (a DOOR inside Settings): everything that REBOOTS — WiFi, metronome enable, follow-beat enable. Consequence band, REBOOTS tags, WRITE & REBOOT key. THE INVARIANT SURVIVES, AND IT'S THE WHOLE POINT: no surface ever mixes live and reboot controls. Merging everything into one Settings sheet would have re-created the exact adjacency this app exists to fix — the device's own web page puts the metronome ENABLE toggle and the metro-accent toggle forty pixels apart, as the identical component, in the same box, and one of them restarts the box mid-set. Better labelling is not the same as impossible. LiveRebootPartitionTests still enforces it in CI. Capability still drives visibility: no speaker, no click section; no strip, no LED section (link-devices ESP-030). Firmware update stays in the toolbar, NOT inside Settings: Settings needs /config.json, which 404s on old firmware, and an update is how you ESCAPE that state. Gating the escape behind the broken thing would strand the user (T-009). 103 tests, 0 failures. Verified against the real Touch: Performance shows 120.0 BPM, CLOCK OUT 1 PLAYING, with NUDGE and SWING under it and nothing else.
THE BUG THE USER REPORTED: "nudge and swing buttons not working."
They weren't working because the Touch had no /live route and returned 404. But
that is not why it was UNDIAGNOSABLE. `apply()` was:
try? await client.postLive(edit)
The 404 was caught and dropped. The stepper moved, no error appeared, and the
device never changed. The device said "I have never heard of that endpoint" as
loudly as HTTP can, and the app said nothing at all.
- apply() now reports failure, and keeps the three causes APART:
unsupportedByFirmware (404 — no /live at all; the way out is an OTA update),
rejectedByDevice, unreachable. Collapsing situations into one signal is the
mistake this codebase keeps making (T-009, T-010, T-016, T-018) and it sends
the user to fix the wrong thing.
- A failed edit leaves the displayed value at the DEVICE'S truth. Never leave a
stepper showing a number the device does not have.
- A SUCCEEDED edit now updates the local config (KsConfig.apply, pure and
tested). Without it even a WORKING /live looked broken: the POST succeeded,
the device obeyed, and the number on screen never moved.
- The Performance screen draws the failure. A failure the view model records and
the view never shows is still invisible.
An icon, drawn rather than pasted: Tools/make_app_icon.swift renders it from the
same palette as the device's own web UI. It's a MIDI clock pulse train in lime on
charcoal with a taller accented downbeat — literally what the box emits, and a
square wave still reads at 40px where a wordmark does not. Re-runnable, so the
art is source, not a binary nobody can regenerate.
7 new tests, full suite green.
Claude-Session: https://claude.ai/code/session_01F7YsQgso3KNpeXbP5vNVj6
…th it
CLAUDE.md names this gap out loud: "Nothing has ever spoken to a real device. Every
network test runs against a stub." Every fixture in this suite was written by me from
a READING of the firmware, which makes them a test of my reading, not of the device.
These bytes are a verbatim capture from a Touch (a4:cb:8f:da:df:d0, fw 2.2.0) --
stopped and mid-song. Nothing prettified, reordered, or filled in.
They catch the reported bug immediately. `link_owns` was true while the device
cheerfully obeyed every play and stop; the app honours the ks_status.h contract
("when link_owns is true the manual buttons are ignored, so the UI greys them") and
so refused to even send the tap. Buttons dead, device fine.
test_the_app_can_start_a_stopped_touch FAILED against the device's real bytes. That
is the test that should have existed before I ever called the firmware "verified".
Also kept: the BROKEN bytes, as a fixture. A device that genuinely defers to Link (the
P4 does) SHOULD have its buttons greyed -- the app's contract-following is correct and
must not be "fixed" away. The fix belonged in whoever lied.
Claude-Session: https://claude.ai/code/session_01F7YsQgso3KNpeXbP5vNVj6
`-derivedDataPath build/dd` (used to build for a physical device) writes derived data into the repo, and .gitignore only covered DerivedData/. 2121 build artifacts were committed on the previous commit. Ignored under both names now. Claude-Session: https://claude.ai/code/session_01F7YsQgso3KNpeXbP5vNVj6
A fixture is a public artifact — it goes in the repo, the CI log, and the PR diff. The pre-push redaction hook caught the real SSID in two of them. Claude-Session: https://claude.ai/code/session_01F7YsQgso3KNpeXbP5vNVj6
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.
Everything here came out of using the app against a real KitchenSync Touch. Paired with link-devices#117, which fixes the firmware side of the same bugs.
Live-edit failures were invisible, and that is why the bugs were undiagnosable
The user reported "nudge and swing buttons not working". They weren't — the Touch had no
/liveroute and returned 404. But that is not why it was impossible to diagnose.apply()was:The 404 was caught and dropped. The stepper moved, no error appeared, and the device never changed. The device said "I have never heard of that endpoint" as loudly as HTTP can, and the app said nothing.
apply()now reports failure, and keeps the three causes apart:unsupportedByFirmware(404 — no/liveat all; the way out is an OTA update),rejectedByDevice,unreachable. Collapsing situations into one signal is the mistake this codebase keeps making (T-009, T-010, T-016, T-018) and it sends the user to fix the wrong thing.KsConfig.apply, pure and tested). Without it, even a working/livelooked broken: the POST succeeded, the device obeyed, and the number on screen never moved.The app has now actually spoken to a device
CLAUDE.mdnamed this gap out loud: "Nothing has ever spoken to a real device. Every network test runs against a stub." Every fixture in this repo was written from a reading of the firmware, which makes them a test of that reading, not of the device.RealTouchStatusTestsuses a verbatim/statuscapture from a Touch (fw 2.2.0), stopped and mid-song. Nothing prettified or filled in.It immediately caught a bug neither repo's tests could see. The device was sending
link_owns: truewhile cheerfully obeying every play and stop. The app honours theks_status.hcontract — "when link_owns is true the manual PLAY/STOP buttons are ignored, so the UI greys them" — and so refused to even send the tap. Play and stop were dead in the app.test_the_app_can_start_a_stopped_touchfailed against the device's real bytes. That is the test that should have existed before anyone called the firmware "verified". The lie is fixed in link-devices#117.The broken bytes are kept as a fixture: a device that genuinely defers to Link (the P4 does) should have its buttons greyed. The app's contract-following was never the bug, and must not be "fixed" away — the fix belonged in whoever lied.
Capability-driven config: nil means NOT FITTED, not "switched off"
metronome,ledandfollowBeatEnabledare optional. A device without a speaker doesn't report a metronome, and the app doesn't draw one.maxOutputCountis a maximum; the real count isclock.count— the Touch has one jack, the P4 has four, and nothing pads the array to invent three dead outputs.Capabilities are a property of the build, not the product: the Touch has no LED today, and all it would take is soldering one on.
Performance / Settings split
PerformanceOutputCardcarries transport, NUDGE and SWING — the things you reach for mid-set. Cable, rate, follow-Link and enable moved to Settings: knocking a cable assignment loose during a song is a way to ruin it, and you cannot knock what is not on the screen. Reboot-required settings stay behind a consequence band and a confirmation that names the device.Icon
Drawn, not pasted.
Tools/make_app_icon.swiftrenders it from the same palette as the device's own web UI: a MIDI clock pulse train in lime on charcoal with a taller accented downbeat — literally what the box emits, and a square wave still reads at 40px where a wordmark does not. Re-run the script to change the art; it is source, not a binary nobody can regenerate.Housekeeping
build/(Xcode derived data from-derivedDataPath) is now ignored under both names. 2121 artifacts nearly went into this PR.Full suite green. Verified on hardware: nudge/swing apply and persist, play arms on the bar line and runs, stop is immediate, and the UI follows the device.
https://claude.ai/code/session_01F7YsQgso3KNpeXbP5vNVj6