Skip to content

feat(app): set-tempo control — tap, numeric, ± (ESP-037) - #10

Merged
Skeyelab merged 6 commits into
mainfrom
feat/esp-037-app-tempo
Jul 15, 2026
Merged

Skeyelab merged 6 commits into
mainfrom
feat/esp-037-app-tempo

Conversation

@Skeyelab

Copy link
Copy Markdown
Member

The app can now set a clock box's free-run tempo. Pairs with link-devices #123.

  • KsLiveEdit.setTempo(Double) → the bpm /live field (formatted en_US_POSIX so a European decimal comma can't truncate it). Tap, numeric, and ± all resolve to this one number.
  • KsConfig.tempo (Double?), decoded from bpm; nil = listener-only (X32Link omits it). This is the set tempo, distinct from KsStatus.bpm (the effective, Link-driven one).
  • TempoControl — shown only when the device has a settable tempo:
    • tap — computed locally from tap intervals (never sent as tap events over WiFi),
    • numeric — tap the readout and type it (commits via a keyboard "Set" button / tap-away — the decimalPad has no return key),
    • ± — nudge by 1, optimistic so it feels instant.
  • Follows the effective tempo: the control shows what the box is actually clocking, so it tracks Link while a session drives and there's no jump when Link leaves. While Link drives, the controls stand down ("LINK IS DRIVING").

Full app suite green; installed and exercised on a real phone against the bench Touch.

https://claude.ai/code/session_01F7YsQgso3KNpeXbP5vNVj6

"I see kstouch. not the headless." The X32Link was powered, on the network, and
following the Link session, and the app showed nothing.

Adding `x32link` to the hostname prefix list is the small half of this. The real cause
was in the firmware: that unit had NO mDNS WHATSOEVER -- it never advertised, so no
matcher on this side could have found it, and `dns-sd` could not see it either. Fixed
in link-devices (ESP-035), where it now announces `x32link-<mac>` like the rest of the
fleet and serves the shared /status and /config.json.

The prefix list stays a documented HEURISTIC, not a guarantee -- a TXT record is still
the fix that would make identity a fact. Tests pin both halves: the headless is one of
ours, and a stranger's box at a venue still is not.

Verified against the real unit: both devices now appear in the list, live BPM.

Claude-Session: https://claude.ai/code/session_01F7YsQgso3KNpeXbP5vNVj6
The app can now set a clock box's free-run tempo, the last piece of ESP-037.

- KsLiveEdit.setTempo(Double) -> the `bpm` /live field. Formatted en_US_POSIX so a
  European phone's decimal comma can't turn 128.5 into a "128" the firmware's atof stops
  at. Tap, numeric and ± all resolve to this one number.

- KsConfig.tempo (Double?), decoded from `bpm`. nil = listener-only (X32Link omits it),
  the same nil-means-not-fitted rule as metronome/led. This is the SET tempo, distinct
  from KsStatus.bpm (the EFFECTIVE tempo Link drives) -- so the control doesn't jump when
  a session takes over. apply(.setTempo) only reflects it when tempo != nil.

- TempoControl: the set surface, shown only when config.tempo != nil.
    tap     -- computed HERE from local tap intervals, never sent as tap events over
               WiFi (jitter would wreck the timing); resolves to a BPM and sends it.
    numeric -- tap the readout, type it.
    ±       -- nudge by 1.
  Clamped to 20..300 (the firmware's band). When a Link session is driving it says "LINK
  IS DRIVING" -- a set tempo the device is currently ignoring is exactly the silent lie
  this app keeps stamping out.

Verified against the bench Touch protocol side: /live bpm applies on the wire and
/config.json reads the stored tempo back; the app model decodes it and posts the same
field. Full app suite green; installed on the phone.

Claude-Session: https://claude.ai/code/session_01F7YsQgso3KNpeXbP5vNVj6
Reported: set 150 manually, turn on Link (Ableton 120), and the control kept showing
150 while the box clocked 120. It should track what's actually playing.

- The control now shows the EFFECTIVE tempo (status.bpm, polled) via
  DeviceDetailViewModel.tempoDisplay, not the stored set value. So it follows Link
  while a session drives, and when Link leaves there's no jump: the box keeps Link's
  tempo (the firmware arbiter's free-run seed) and the control was already showing it.

- While Link drives, the ± / tap / numeric stand down (dimmed, disabled) -- Link owns
  the tempo; the control just reports it. Editable again when solo.

- Edits are OPTIMISTIC: a ± tap shows immediately instead of waiting ~500 ms for the
  next /status to catch up, then reconciles when the device's reported tempo converges.

Pairs with a firmware change (the box records Link's tempo as its own and persists it,
so a power-cycle comes back at the last tempo it played). Full app suite green;
installed on the phone.

Claude-Session: https://claude.ai/code/session_01F7YsQgso3KNpeXbP5vNVj6
…turn key

The numeric entry was a dead end: .onSubmit/.submitLabel(.done) do nothing on a
decimalPad keyboard, which has no return key at all. You could type a BPM and never
set it.

Focus drives it now: a "Set" button in the keyboard toolbar, and commit-on-dismiss
when you tap away from the field. commitDraft guards against the double (Set button
also drops focus). ± and TAP were never affected.

Claude-Session: https://claude.ai/code/session_01F7YsQgso3KNpeXbP5vNVj6
@Skeyelab
Skeyelab merged commit 16a4520 into main Jul 15, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants