feat: motion (IMU) panel, ES60 frame key, shared keyboard canvas - #8
Open
maltmannx wants to merge 17 commits into
Open
feat: motion (IMU) panel, ES60 frame key, shared keyboard canvas#8maltmannx wants to merge 17 commits into
maltmannx wants to merge 17 commits into
Conversation
Adds Studio support for the two new products' extra hardware, plus the capability gating needed so each feature only appears where it exists. Motion / IMU (PH60SCV2EVO, LIS2DH12) - New `zmk.motion` client contract in src/motion/motionRpc.ts, mirroring the proto proposed in docs/motion-proto.md field-for-field: case-tap actions and the walk-detect key lock. - Thresholds are exposed as the raw sensor register semantics (CLICK_THS, ACT_THS, ...) rather than an invented sensitivity scale, so firmware and UI can't drift apart over a mapping table. - New "Motion" section with a live meter: lock state, case orientation and a magnitude bar with threshold markers, because a raw count threshold can only honestly be picked by watching the signal while moving. - The generated ts-client fork doesn't carry the subsystem yet, and ts-proto encoders silently drop unknown fields, so real RPC cannot be used until it is regenerated. `clientSupportsMotion()` round-trips a probe request through the codec and switches from the in-memory demo backend to real RPC automatically once the client catches up; real hardware reports "unsupported" until then rather than showing invented state. ES60 frame key - The button is per-layer, so it belongs in the keymap: it needs no subsystem of its own, only two optional KeyPhysicalAttrs fields (`kind`, `label`). All reading of those lives in keyboard/keyMeta.ts, so different firmware field names are a one-file change. - Renders outlined with a corner label in the keymap canvas, and is excluded from the lighting canvas (dimmed, unselectable) since it has no LED behind it. Capability-derived navigation - The sidebar was a hardcoded list, so unsupported sections appeared and only revealed an empty state once clicked. Availability now comes from the RPC probes for lighting, tap-hold, combos and motion — never from the device name, keeping firmware the single authority. - A persisted default view can name a section the current device lacks, so it is resolved rather than trusted (previously a blank main pane). - Readiness waits on the motion probe too, so entries can't pop in after the shell is already usable. Shared keyboard canvas - New keyboard/KeyboardCanvas.tsx provides the centred scroll area that doubles as the auto-fit measuring box, plus the zoom control. - PhysicalLayout falls back to `element.parentElement` without an explicit fitContainerRef, which silently broke auto-fit in the lighting views where the layout is nested in a content-sized wrapper. Passing the ref down is the point of the component; the lighting canvas gains auto-fit and zoom it never had. - The lighting drawer moves from a flat 360px to the same responsive height ramp as the binding drawer, returning ~120px to the canvas. - The combo trigger picker deliberately keeps its own compact fixed-size selector: it is a form control, not a canvas. It already shares PhysicalLayout, which is the part worth having in common. UI consistency - Every on/off control now uses the shared Carbon `Toggle`: the lighting panel's On|Off segmented pair and the rounded-corners button pairs in both settings views. - rounded-mode set `border-radius: 50%` on the toggle, squashing its 40x22 track into an ellipse; the track now keeps its pill radius and only the knob is circular. Demo mode gains `sideKey` and `motion` toggles so both features are reviewable without hardware, and the in-memory motion firmware drives a self-running signal that plays the whole lock/unlock state machine — it doubles as the reference implementation for the firmware side. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Groundwork for making the studio unlock gesture a first-class, user-managed thing. Read-only for now: changing the gesture needs a guided verify-before-replace flow and two firmware additions, both specified in docs/unlock-combo.md. Why the unlock binding needs special handling - `core` has no unlock RPC by design, so unlocking is always physical and Studio can only ever change the gesture, never bootstrap it. - `resetSettings` requires being unlocked too, so it is not a recovery path: losing the last `&studio_unlock` binding means the keyboard can't be edited again without reflashing. Unlock paths - New src/unlock collects every way a keyboard can be unlocked — reserved combos plus any `&studio_unlock` still bound in the keymap — so a future edit path can refuse to take that count to zero. - The Device page lists them as key chips, marking slots the firmware protects via `editableBehavior: false`. That flag is the real invariant: our own UI guard can be bypassed by any other client, firmware's can't. - The reserved slot is hidden from the Combos page; it isn't a shortcut the user authored. Slot numbering still uses the firmware index, and an all-reserved keyboard now says so instead of claiming combos are unsupported. - Behaviors are identified by `displayName`, since the proto carries no stable id — a known weakness the doc asks firmware to close. Lock screens now name the gesture (Fn + \) instead of saying "press the studio-unlock combo". It can only be the factory default: nothing about the keyboard is readable while locked, which is also the most likely way for someone to get stranded after changing it. Both surfaces are unverified at runtime because the demo firmware always reports unlocked. Device vs Preferences - "Quick settings" → "Device" (what lives on the keyboard: identity, active layout, unlock shortcut, factory reset) and "Settings" → "Preferences" (what lives in this app). The rule: anything that issues an RPC belongs on the device side. - `core.resetSettings` moves accordingly — it was sitting with the app preferences. - Nav ids change with the views, so persisted ones are migrated on read to avoid silently relocating someone's default view. The quick-controls block on the Device page is still a deliberate duplicate, pending a call on whether the shortcut is worth the blurred boundary. Demo firmware - Reserves slot 0 for unlock (Fn + \, non-editable behavior) so the whole path is reviewable without hardware. - `&kp` parameters are now encoded as real HID usages (`page << 16 | id`) instead of bare ids, and media keys moved to the consumer page where they belong. The bare ids made every label resolve to "?" and stopped live keypress highlighting from ever matching. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Lets the user replace the studio unlock gesture without ever risking access to the keyboard, and finishes the Device/Preferences split. Verify before replacing - The reserved slot is rewritten only after the candidate gesture has been seen to fire, so a working gesture is never given up on the strength of a guess. `resetSettings` needs an unlocked keyboard, so there is no way back from a wrong guess short of reflashing. - Verification borrows a spare combo slot, binds the candidate positions to `&kp F13`, and watches for that keystroke arriving at the host — the same channel the key tester uses. The keyboard is never locked during the test, so a gesture the user turns out not to be able to perform strands nobody. The borrowed slot is released on success, on cancel, and on unmount. - Locking and retrying instead would be ambiguous: during such a test both the old and new gestures work, so pressing the old one would falsely confirm the new one. Firmware reporting which combo unlocked the keyboard would fix that properly; docs/unlock-combo.md asks for it. - Only `key_positions` is written. The behavior is echoed back exactly as reported, since a differing behavior is (rightly) rejected on a reserved slot. `require_prior_idle_ms` is raised to 200ms when the chord has no layer or modifier key, because an all-letters chord can otherwise fire mid-typing and unlock the keyboard behind the user's back — with a positions-only retry for firmware that refuses the field. - If the host never delivers F13, a 30s timeout offers to apply anyway, naming the consequence and whether another unlock path still exists. - The two slots are snapshotted when the flow opens: writing the probe makes the borrowed slot stop looking spare, which would otherwise tear the flow down halfway through its own test. Device page - Quick controls removed; they duplicated Preferences and blurred the line the rename was meant to draw. - Two columns where there's room, so the page uses the width instead of crowding a 560px strip. Demo firmware - `setCombo` no longer hands back `editableBehavior: true` on every write. That silently stripped the reserved slot's protection, which is the one guarantee that keeps a keyboard from losing its only way in. The flags now stay firmware-owned and writes that violate them are refused. Known demo gap, pre-existing: the mock transport can't push notifications, so `unsavedChangesStatusChanged` never fires and the footer doesn't flag combo or keymap writes as unsaved. Real firmware does emit it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…risky Addresses three points from testing the change flow on hardware. The test combo never fired - The probe borrowed the *spare* slot's `layerMask` instead of the unlock combo's. `0` means "all layers" here, so a spare slot carrying a restricted mask produces a combo that cannot fire on the layer the user is actually on — and the test just silently never completes. `slowRelease` was inherited from the wrong side too. Both now mirror the unlock combo, which is the condition the test is supposed to reproduce. - The demo couldn't have caught this: its unused slots happen to carry `layerMask: 0`, so the probe worked there by coincidence. Made the failure legible instead of silent - While waiting, the flow now shows which test key it's listening for and which keystrokes are actually reaching Studio. If ordinary keys arrive but the test key doesn't, the chord isn't firing on the keyboard; if nothing arrives, the keystrokes aren't reaching Studio at all. That's a firmware problem versus a focus problem, and it was previously indistinguishable. - The timeout message now says which of those two it is, and offers to retry with a different test key: no key is delivered on every platform (F13-F15 don't exist on many keyboards, Pause is absent on most Macs), so being stuck on one the OS eats was a dead end. Change button reads as risky - Warning outline rather than a plain secondary button. The flow refuses to give up a working gesture, so it isn't destructive, but it's still the keyboard's only key — it shouldn't look like changing the theme. Device page keeps the unlock panel below the device info in a single column, rather than moving it into a second column. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Testing showed keys arriving when pressed separately but nothing at all when pressed together. That is the combo *working*: ZMK swallows the keypresses it consumes and runs the bound behavior instead, so the only thing going missing was the F13 substitute keystroke. - The probe ladder now drops to ordinary letters (Z, Q) after F13. The F-row above F12 is the tidy choice because nothing is bound to it, but it's also the least reliable — those keys are absent from most keyboards and plenty of systems never surface them to the browser. Letters always arrive, and the probe listener swallows the keystroke so nothing is typed anywhere. - Candidates that are part of the chord being tested are filtered out, or a key that merely passed through would be mistaken for the combo firing. Reading a usage out of param1 is gated on the binding actually being keypress-shaped, since a layer key's param1 is a layer number. - The timeout now explains the swallowing, because "no keystrokes reaching Studio" while pressing the chord means the opposite of what it looks like. The retry button names the key it will switch to, and clears the timeout state so the next attempt gets a full window. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The combo fires and swallows its keys but emits neither F13 nor Z. Since a letter is delivered by every platform, the missing piece isn't key delivery — the trigger is landing and the bound behavior isn't running. The `&kp` parameter encoding was ruled out first: `(page << 16) | id` is what HidUsageGrid uses for every working binding the app writes, and 0x70068 / 0x7001D match it. So the remaining unknown is whether the behavior reaches the slot at all, which `setCombo`'s own "ok" can't answer and the local combo list can't either — `applyConfig` updates it optimistically, so it always agrees with what we sent. `useCombos` gains `readCombo`, a plain `getCombo`, and the waiting screen now prints what firmware actually holds: Slot #3 · keys [59,67] · behavior 1 · param1 0x70068 · layers 0x0 · 200ms If the behavior or params come back empty or different, the flow says so and names it a firmware issue rather than leaving the user to conclude the feature is broken. Positions are checked the same way. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Testing revealed the wrong assumption: shipping firmware reserves no unlock combo slot. Its factory gesture is a keymap binding (`&studio_unlock` on a layer), so after a settings reset there is no combo bound to unlock at all — and the flow simply hid its own entry point, which reads as a regression. It also explains the earlier confusion. The F+J combo that made the button appear was hand-made by the user and bound to studio unlock, so pressing F+J fired *that* combo: keys swallowed, no keystroke emitted, probe never involved. - The flow gains a `create` mode: a free slot is bound to `&studio_unlock` once the gesture is confirmed. One slot is enough, since the slot used for the probe becomes the unlock combo — no second write, nothing to release. This is purely additive (the keymap gesture stays), so it cannot strand anyone, and it gets a plain button while replacing an existing combo keeps the warning outline. - Overlapping combos are now called out. Exact duplicates were already blocked, but a combo merely *sharing* a key competes with the candidate and may win, which is precisely how this looked broken. The warning names the slot. - The demo defaults to no reserved unlock slot so it matches real firmware; the reserved-slot design is behind a new demo toggle, off by default. The previous default is why the demo couldn't reproduce any of this. - docs/unlock-combo.md no longer claims the reserved slot exists today, and documents both paths plus the read-back and probe-ladder steps. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
"Retry with a different test key" set `probeIndex` and re-armed in the same handler, so the write closed over the previous render's probe. The label moved but the keyboard didn't, one step apart forever: choosing Q emitted Z. Worse than a cosmetic lag — the listener was derived from the same stale value, watching the newly chosen key while the keyboard emitted the previous one. Every retry was guaranteed to fail, which is why the first attempt looked dead and no amount of switching helped. Split the two ideas that were conflated: `probeIndex` points at the candidate to arm *next*, `armedProbe` is what's actually on the keyboard. The listener, the on-screen label and the read-back comparison all derive from `armedProbe`, which is only set after the write succeeds — so Studio never watches for a key the keyboard isn't sending. `arm()` takes the candidate as an argument rather than reading state that hasn't updated yet. Verified: retry advertising "Z" now arms Z (read-back `param1 0x7001d`), a stale F13 no longer counts, and the armed key confirms. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
F13 never reached the browser in testing on Windows, so the default path was a 30-second dead end before the user could reach a key that works. Ordinary letters go first now — they're the only candidates every platform delivers, and the probe listener swallows the keystroke so nothing is typed anywhere. The F-row above F12 stays as a fallback since it's tidier when it does work. Also corrected the closing hint, which told the user to press "Save to keyboard". Nothing in the app marks the session dirty on a combo write — no subscriber for `combosChanged`, and `extraSaveEnabled` only covers lighting and motion — so that button may not be showing at all. Whether combos survive a power cycle on their own is a firmware question, so the hint now says to use the button if it's there and otherwise to power-cycle once and check, rather than asserting a control that may not exist. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
In change mode the existing unlock combo keeps its old trigger throughout the test, so if the candidate shares keys with it, ZMK has two competing combos and the unlock one tends to win: the chord swallows its keys and emits nothing, because studio unlock produces no keystroke. Indistinguishable from a dead test. The default case walked straight into it — the picker pre-selects the current gesture, so pressing "Test this shortcut" without changing anything tested a chord the unlock combo was guaranteed to take. Create mode was unaffected, which is why it worked there and not here. Root cause was conflating two questions that need different answers: - end state: would the finished combo duplicate another? The slot being rewritten is excluded, since reusing its own keys is fine. - the test: the probe sits in a borrowed slot while every other combo is still live, unlock combo included. Nothing is excluded but the probe's own slot. Overlap detection now uses the second rule, and two cases are blocked outright rather than warned about, since neither test can conclude anything: a chord identical to the gesture already in use (nothing to confirm), and one the live unlock combo would shadow. Each says which it is. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Persistence is confirmed: firmware marks the session unsaved on a combo write, so the header's Save button appears on its own, and `keymap.saveChanges` carries the combo through a power cycle. Studio doesn't need to track combo dirtiness itself — doing so would risk a false "unsaved" state. The closing hint says to save rather than hedging about whether the button is there. Also captured the four device-only findings, since each cost a debugging round and all of them present identically as "the test does nothing": - a firing combo swallows its keys, so keys arriving separately but not together means the chord works and only the substitute keystroke is missing — the opposite of how it reads; - `&studio_unlock` emits nothing, so any combo bound to it looks dead, and an overlapping one silently wins the chord under test; - `layer_mask == 0` means "all layers", and an unused slot may carry a restricted mask; - F13 never reached the browser on Windows. None of these are reproducible against the demo firmware, which is the reason they all surfaced late. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Motion's live meter now keeps a peak marker. Calibrating a tap threshold
means reading the transient, and a bar that tracks the signal live has
already fallen back before you can read it — the marker is pushed up
instantly, held ~0.9s, then bled away, never below the live value so it
reads as the bar carrying it rather than a second independent thing.
Toggle gains Carbon's two real sizes (48x24 and 32x16-with-checkmark)
instead of one in-between 40x22. "Require flat" takes the small one: it's
a row among sliders, not the switch that owns a section.
Preferences moves to a proper Carbon content switcher for theme and
language. The selected item is inverse, not the interactive blue — that's
what tells a switcher apart from a button group, and it means an accent
change leaves it alone. Default view outgrew Carbon's five-item ceiling
(it scales with the keyboard's capabilities), so that one is a Select.
Combos get "clear slot" at the bottom of the slot rail, where the layer
rail keeps its destructive actions, replacing the field-level button that
only cleared positions and needed a separate Save. It writes immediately;
a rail button that appears inert until you find Save on the far side of
the pane isn't worth the symmetry.
Two removals:
- Rounded-corners beta, along with the blanket `* { border-radius }`
rule, its six opt-outs, and Badge's `square` prop that existed only to
escape it.
- "Follow browser" accent. The only web API for it is the CSS
`AccentColor` keyword, which Chromium doesn't implement (measured:
CSS.supports is false on Chrome 148) — and since Studio needs Web
Serial/WebHID, every browser that can reach a keyboard is Chromium, so
the option could never have worked for a real user. `Highlight` is the
near miss: it parses, but Chromium answers with a fixed #0078D7 rather
than the OS accent (#0078D4 on the test machine), which would have
shipped a hard-coded blue dressed up as a preference. A stored "system"
accent migrates to Blue.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…bon's grid Three things, one of them serious. **The unlock combo vanished after a power cycle, and then there was no way back.** Two separate defects, both mine: A combo write is a `combos` call, but the header's unsaved indicator reads `keymap.checkUnsavedChanges`. Studio was relying on firmware to raise the keymap flag for another subsystem's write. Last round I recorded that as "confirmed on hardware" off a single session where the Save button appeared — that was one observation dressed up as a fact, and this round contradicted it: the combo lived in RAM, was never written, and went away with the power. Studio now marks itself dirty on every combo write, so the Save button cannot fail to appear, and the unlock flow saves inline and reads the slot back instead of pointing at a button on another screen. Then the recovery path was blocked. `findSpareComboSlot` refused any slot carrying `&studio_unlock`, while `collectUnlockPaths` only counts a slot as an unlock path if it has key positions. A slot with the behavior and no positions — exactly what a failed save leaves — was therefore neither, so the Device page offered nothing, and the "free a slot on the Combos page" hint pointed at a slot that was already free. Such a slot is now used last and never refused. Covered by four cases: orphaned slot recovers, an untouched slot is still preferred, firmware-reserved and occupied slots are still refused. What Studio still cannot do is prove persistence: `saveChanges` returning ok and a `get_combo` readback both only describe the live slot. So the copy says "written", not "permanent", and tells the user to power-cycle and check while their old unlock method still works. The doc note that claimed otherwise is retracted, and an unsaved-state signal for `combos` is written up as a firmware ask. **Motion settings rebuilt as a Carbon form.** The old shape put a fixed label column beside whatever control the setting needed, so a 22px toggle, a 35px segmented control, a native slider and a padded button each set their own row height — nothing lined up and there was no baseline to read down. Carbon stacks label over control and fixes the control height, which is where the rhythm comes from. Now: label → 40px control → optional helper text, in an auto-fit grid. Measured: every control 40px, controls in a row share a y to the pixel, 3 columns at 1440px and 2 at 1280px. Register names (CLICK_THS…) ride on the label line rather than becoming helper text, so they don't cost height. Section headings are 14px semibold with a rule, not 11px letterspaced caps, which isn't a Carbon pattern. The native slider also got Carbon's 4px track and 14px handle — its default height was part of why rows sat at different levels. The lock panel still scrolls 55px at exactly 720px tall (0 at 900px). Closing that last gap would mean dropping the two helper texts that explain what the settings do, which trades the wrong thing. **"Require flat" is back on the default toggle** — the small one was the wrong call. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… save gap **Motion layout.** The grid was the wrong answer — two columns of fields read as a form to fill in, not as settings to adjust. Now it's one column with the same shape as the Device page: label → helper → control, a rule between entries, capped at 680px because a slider stops being readable much past that. Values moved onto the label line, where Carbon's slider puts its number input anyway; that right-aligns every value in the list to one edge and keeps a slider entry a line shorter. Sliders gained min/max end labels. The live meter is full width now (bar 802px at 1280, 1122px at 1600, up from ~380) with a 32px track and a 22px readout. Those two pull against each other in a 600px pane, so the lock state card moved into the capability rail. It's a status readout that applies to both sections, the rail had 400px going spare, and every pixel it doesn't spend in the content column is one the settings list gets. Net: the meter block dropped 255px → 149px while getting bigger, tap overflow 194px → 101px, lock 451px → 368px. The lock panel does still scroll — 380px visible of 748px at 1280x720, 88px of overflow at 1600x1000. Seven settings and three section headings in one column is just tall; the Device page behaves the same way. If that's too much, the next lever is collapsing the meter to the bar alone once you scroll, which needs a real mechanism rather than more padding-shaving, so it's worth deciding deliberately. **Require flat** is back on the default toggle. **Combo persistence is a firmware gap, written up in docs/combo-persistence.md.** The `zmk.combos` subsystem is list/get/set plus a `combos_changed` notification — there is no save or commit request anywhere in it, and `keymap.save_changes` persists the keymap subtree only. So `set_combo` reaches the runtime array and nothing Studio can call reaches storage. The note covers what firmware needs: a settings handler modelled on ZMK's keymap.c; a stable `zmk_behavior_local_id_t` rather than the `behavior_dev` pointer or Studio's per-build `behavior_id`, both of which are meaningless after a reboot or a rebuild; resolving in `h_commit` because behaviors aren't registered during `settings_load()`; `settings_delete` on clear, or the old key positions come back at boot; an unsaved-state signal so Studio can drop its client-side guess; and checking `settings_save_one` for -ENOSPC so a combo that can never persist doesn't report success. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…in fades **Unlock table's doubled bottom edge.** Rules were on the bottom of every row, so the last one landed on top of the container's own border. Moved to the top of every row but the first. Most visible with a single row, which is the state before a combo has been set up. Now 1px top and bottom, measured. **Factory reset has a confirmation and a busy state.** Click once and the button becomes Carbon's solid danger red reading "click again to erase everything", with a Cancel and a line saying what goes; it disarms itself after 6s so a stale red button isn't left lying around. Confirming disables it and shows "Restoring…" plus "don't unplug the keyboard" for as long as the RPC takes, which is the part that stops the double-clicking. `resetSettings` returns a promise now so the button can await it. It also clears the motion and combo dirty flags, which it wasn't doing — the header claimed unsaved changes for subsystems that had just been wiped. **Drawers resize by dragging their top edge**, keymap and lighting both, via a shared ResizableDrawer. Height persists per panel and double-click restores the default. `maxHeight: 70%` rather than clamping the stored number, so a height saved on a tall window can't squash the canvas on a short one, and the user's number is never quietly rewritten. `panel-resizing` on <html> suppresses the canvas's 240ms scale easing during the drag — without it every frame restarts the curve and the keyboard trails the pointer. The fit recalc also moved to the leading edge of its per-frame throttle for the same reason. **No more load-in fade** on keycap legends when switching layer (it was keyed on the layer index, so all 100+ keycaps replayed at once), nor on the binding picker's behavior list, hint pane, or hold-tap mode row. **Content switchers default to Carbon's `sm`** (32px, was 40) with 12px inline padding. Font size untouched at 14px; checked that no label truncates, including the five-item language row. Verified in the demo: single-row unlock table symmetric at 1px; reset states idle → armed (solid #da1e28) → working (disabled, hint shown); both drawers carry a handle with independently persisted heights and a 70% cap; zero `animate-fade-in` elements anywhere; switcher items 32px at 14px with no ellipsis. One thing I could not verify here: the keyboard re-fitting continuously *during* a drag. The Browser pane isn't compositing, so requestAnimationFrame and ResizeObserver never deliver in it — proven by arming both and seeing zero callbacks. The fit math and a recalc firing are confirmed (scale matched the hand-computed height-bound fit), and it's the same path window resizing already uses, but the per-frame updates need a look on a real display. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
alt-0191
force-pushed
the
feat/motion-imu-and-frame-key
branch
from
August 24, 2026 11:04
e7e3352 to
cbabcc4
Compare
alt-0191
force-pushed
the
feat/motion-imu-and-frame-key
branch
from
August 24, 2026 11:10
cbabcc4 to
50ae195
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.
Adds Studio support for the two new products' extra hardware, plus the capability gating needed so each feature only appears where it exists.
Motion / IMU (PH60SCV2EVO, LIS2DH12)
zmk.motionclient contract in src/motion/motionRpc.ts, mirroring the proto proposed in docs/motion-proto.md field-for-field: case-tap actions and the walk-detect key lock.clientSupportsMotion()round-trips a probe request through the codec and switches from the in-memory demo backend to real RPC automatically once the client catches up; real hardware reports "unsupported" until then rather than showing invented state.ES60 frame key
kind,label). All reading of those lives in keyboard/keyMeta.ts, so different firmware field names are a one-file change.Capability-derived navigation
Shared keyboard canvas
element.parentElementwithout an explicit fitContainerRef, which silently broke auto-fit in the lighting views where the layout is nested in a content-sized wrapper. Passing the ref down is the point of the component; the lighting canvas gains auto-fit and zoom it never had.UI consistency
Toggle: the lighting panel's On|Off segmented pair and the rounded-corners button pairs in both settings views.border-radius: 50%on the toggle, squashing its 40x22 track into an ellipse; the track now keeps its pill radius and only the knob is circular.Demo mode gains
sideKeyandmotiontoggles so both features are reviewable without hardware, and the in-memory motion firmware drives a self-running signal that plays the whole lock/unlock state machine — it doubles as the reference implementation for the firmware side.