Lemon Wired ZMK firmware (USB split via Pico-PIO-USB)#90
Open
Olson3R wants to merge 17 commits into
Open
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
b6bfc2a to
c546775
Compare
c546775 to
b77a885
Compare
Cosmos previously baked QWERTY into letterForKeycap() and FLIPPED_KEY, so every keycap legend and ZMK/QMK keycode came out QWERTY. This adds a layout dimension to the cosmos config (proto field 33, persisted in the URL) and wires it through the alpha-letter generation path so swapping the layout updates both the on-keycap legends and the firmware export. - src/lib/layouts/ exposes 5 layouts as a const-keyed registry with per-layout right-row letters and split-half flip maps. DEFAULT_LAYOUT preserves QWERTY back-compat for older shared configs. - letterForKeycap, cosmosFingers, keycapInfo, and flippedKey accept an optional layout (default: QWERTY); mirrorCluster threads the keyboard's layout in via its callers (config.cosmos, toCode, HandFitView, VisualEditor2). - Editor adds a Layout field after Keycaps. Switching layout calls applyLayoutToKeys, which uses alphaColumns() to update only the alpha block; number/F/punctuation rows stay layout-independent. - Firmware exporters need no changes: ZMK/QMK already key off keycap.letter, which is now layout-aware end-to-end. Tests cover all five layouts plus flip behavior. CI svelte-check (bun src/scripts/check.ts) clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Lock in the contract that the layout dimension flows from cosmos config through key letters to firmware keycodes: - Round-trip every LAYOUT_IDS value through encode → serialize → decode and verify the layout survives. - Verify a legacy URL (encoded before this feature) still decodes as QWERTY for back-compat. - Build a default Cosmos config and confirm applyLayoutToKeys updates alpha-block letters per layout while leaving the number row untouched. - Confirm ZMK and QMK keycode() emit per-letter codes, and that the rightmost-of-center home-row position emits a different keycode under each layout (h/h/m/d/y across QWERTY/Colemak/Colemak-DH/Dvorak/Workman). The keycode() helpers in zmk.ts and qmk.ts are now exported so the end-to-end tests can verify the firmware contract directly without needing a full FullGeometry stack. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
tsconfig-paths' matchPath returns the absolute path verbatim, so a specifier like \`\$lib/layouts\` (a folder with index.ts) resolved to \`src/lib/layouts.js\` and 404'd in Vercel's Node-only build path. Bun handles this natively, so it only surfaces under the Node loader. Stat the resolved path; if it's a directory, append \`/index\` before the extension so ts-node finds index.ts (or index.js). Verified by importing \`\$lib/layouts\` through register_loader.js (previously: Cannot find module 'src/lib/layouts.js'; now: resolves and exports DEFAULT_LAYOUT, LAYOUT, etc.). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per PR review, the keycap icon was a placeholder; the Letter icon (mdiAlphaA) is more semantically aligned with a layout selector. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
PR-review bug: switching from QWERTY to Colemak (or any layout swap)
duplicated the bottom-row outer-punctuation keys (e.g. [, ]) into the
row above them.
Cause: applyLayoutToKeys gated the swap on k.profile.row, but
keycapInfo() collapses row 5 → 4 for MT3 keycap-profile reasons. So a
row-5 key with letter `[` arrives in the swap loop as profile.row === 4
and its letter is replaced with the target layout's row-4 letter at the
same alpha-column index.
Fix: gate by whether the original letter is one a layout actually
manages — add isAlphaLetter() that recognizes any legend appearing in
any registered layout's row 2/3/4 (right side or, after flipping, left
side). Row-5 outer punctuation (`{`, `}`, `[`, `]`, `\`) isn't in any
alpha row, so it stays untouched across all layouts.
Tests in layouts.test.ts cover the helper directly. The end-to-end test
file was already broken pre-PR due to bun-test not resolving SvelteKit
$assets aliases (separate infra issue).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three of the PR-review items, sharing a layout-matcher and a CUSTOM enum entry: #5 (detect layout on expert→basic): toFullCosmosConfig used to leave state.options.layout at QWERTY regardless of what the user actually typed in expert mode. Run the new detectLayout(kbd) right after toFullCosmosConfig in App.setMode so the dropdown reflects the keymap the user has — including switching to "Custom" when the keymap doesn't match any registered layout. #4 (Custom layout + auto-detect): add LAYOUT.CUSTOM to the registry as a sentinel with no letter mapping. The dropdown lists it explicitly (LAYOUT_NAMES is the new label source). Picking Custom from a named layout opens a small helper dialog explaining how to edit individual key legends. A reactive `$:` block in VisualEditor2 watches protoConfig and auto-flips to Custom whenever detectLayout no longer matches the stored named layout — covers per-key letter edits, alpha-column deletes, etc. rianadon#6 (block named-layout switch from Custom when keys are missing): when the user is on Custom and picks a named layout, missingKeysFor() checks that the keyboard has the canonical 5-col alpha block. If not, the dropdown reverts and a dialog lists the letters they'd need to add back (e.g. "p ; / '"). detectLayout / missingKeysFor live in visualEditorHelpers.ts (alongside applyLayoutToKeys — they need CosmosKeyboard awareness). The matcher allows extra alpha columns past the canonical block (per the maintainer: "adding a key outside the layout's range" shouldn't auto-flip), and treats anything below 5 alpha cols as Custom. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…eview
Per PR review, replace the basic <Select> with the SelectThingy used
elsewhere (keycaps, switches, microcontroller). On hover each option
shows:
- The layout's name and a one-sentence description (added to the
KeyboardLayout schema).
- A small split-ortholinear render of its alpha block — left and
right halves side-by-side, home row highlighted. Custom shows '?'
placeholders.
The dropdown change also flips updateLayout from a DOM Event to a
SelectThingy CustomEvent. The Custom-out-of-Custom revert no longer
needs to mutate target.value — leaving $protoConfig.layout alone is
enough because SelectThingy is bound to the store value and re-syncs
on the next reactive tick.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Most cosmos configs store only the right finger cluster and synthesize the left at render time via mirrorCluster + flipLetter (now layout- aware after PR rianadon#87 added the layouts feature). The Letter editor in Viewer3D was bound to the raw stored letter, so clicking a visually- left key in 3D — say Colemak's "p" — showed the right-side source-of-truth letter "l" in the input. Same key on screen, two different letters in two places. Track which visual half the user clicked in a new clickedVisualSide store (set by KeyboardKey + KeyboardKeyInstance, both of which already know their cluster's side). When a left-side click resolves to a right-cluster key (i.e. mirror form is active), flip the displayed value through flipLetter on read AND on write. Edits still go to the right cluster's storage so the mirror behavior — edit one half, both update — is preserved. Type fix in the same area: ALPHA_ROWS in SelectLayoutInner is now typed as `(2 | 3 | 4)[]` so the rightRows record can be indexed without TS noise about a generic number index. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
b77a885 to
0b4a4dc
Compare
Without the protoConfig.set(next) the layout dropdown and 3D view keep showing whatever was loaded before the expert-mode edits, even though state.options is updated to the post-detect cosmos config. The dropdown binds to \$protoConfig.layout, so the store has to be the channel. Also recompute config = fromCosmosConfig(next) so the 3D view re-materializes from the new cosmos state. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
0b4a4dc to
8bc92d8
Compare
Per maintainer review: layout no longer lives as a stored field on CosmosKeyboard. It's a pure function of the alpha-key labels — like clusterAngle/clusterSeparation already are. The dropdown calls detectLayout(\$protoConfig) at render time and applyLayoutToKeys when the user picks a new option. Custom is the absence of a named match. This subsumes the maintainer's other code-review concern (the suspicious reactive `$:` block in VisualEditor2 that broke the browser back button) — there's nothing to auto-flip because nothing holds stale state. The dropdown re-derives on every reactivity trigger. Schema changes: - src/proto/cosmos.proto: \`reserved 33;\` for the old layout field number. Old URLs with \`layout=N\` are silently dropped on decode; the layout is re-derived from the keys' letters. - CosmosKeyboard interface drops the \`layout\` field; KEYBOARD_DEFAULTS drops \`layout\`; encode/decode drops \`encodeLayout\`/\`decodeLayout\`. Helper relocation: detectLayout, missingKeysFor, alphaColumns, STANDARD_ALPHA_COLS move from visualEditorHelpers.ts (editor module) into config.cosmos.ts (the data-model module) so mirrorCluster can call detectLayout internally without circular imports. mirrorCluster signature: takes \`kbd: CosmosKeyboard | undefined\` instead of \`layout: LayoutId\`. Internal callers pass the kbd in scope; the module-level cluster cache passes \`undefined\` and gets QWERTY semantics (labels are erased anyway). All 6 internal sites + 3 external sites (HandFitView, toCode, visualEditorHelpers) updated. Read sites converted to detectLayout(kbd): - setClusterSize: detect BEFORE the resize so the seed letters reflect the user's pre-resize layout. CUSTOM falls back to QWERTY for the seed (CUSTOM has no canonical letters). - Viewer3D Letter input flip-on-read/write: detectLayout(\$protoConfig) is the layout for the flipMap. Write sites removed: the dropdown handler, App.setMode, and the endToEnd test fixture all stop writing \`kbd.layout\`. The first two just call applyLayoutToKeys; the test asserts via detectLayout instead. Tests rewritten to round-trip via the keys (the layout survives because applyLayoutToKeys writes Colemak/etc. letters into \`profile.letter\`; serialize/deserialize preserves those; detectLayout re-derives the named layout from them). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
A small popover-alert system that the layout dropdown can use for "missing keys" warnings and "switched to Custom" hints without blocking the page with a modal Dialog. Maintainer asked for this in PR rianadon#87 review ("now's a good time"). Public API in src/lib/store.ts: pushAlert({ message, anchor, variant?, durationMs? }): symbol dismissAlert(id) alerts: Writable<AlertItem[]> \`Alert.svelte\` mounts once in App.svelte and iterates the alerts store. Each alert renders via \`AlertPopover.svelte\`, which positions itself just below + left-aligned with its anchor element using \`getBoundingClientRect\` (no extra dep needed; the existing melt-ui/svelte-easy-popover libs are heavier than necessary for this). A 10s setTimeout drives auto-dismiss; a CSS keyframe animates a matching progress bar at the bottom. Both pause on pointer-enter and resume on pointer-leave so users can read longer messages. A MutationObserver watches the anchor — if it leaves the DOM the alert self-dismisses. Variants: info (pink, default), warn (amber), error (red). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Custom-info dialog → an info alert anchored to the Layout field. - "Missing keys for X" dialog → a warn alert with the missing letters. Both rely on the new pushAlert/Alert framework and dismiss the user out of a modal context (the dropdown stays usable). - Drop the now-unused base path import and the dialog templates. - Lighten the home-row pink in the dropdown's alpha-block preview per maintainer feedback (less contrast between rows; still visibly distinguished as the home row). - Drop the '?' grid preview for Custom — text-only is enough. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
8 tasks
Adds microcontroller, wiredVersion, splitTransport, linkPort fields so the ZMK generator can emit per-MCU configs. Excludes microcontroller from the persistent storable in PeaConfig.svelte since it's derived from the geometry config, not a user preference. No behavior change yet — generator code paths still hard-coded to Lemon Wireless. Subsequent commits add boardName dispatch, BOARD_OVERLAY split, and per-MCU DTSI generation. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Make the ZMK generator MCU-aware: boardName() picks cosmos_lemon_wired (or _v5) for the wired path, BOARD_OVERLAY is split into NRF52 and RP2040 variants, and generateDTSI is driven off a per-MCU profile (rows/cols/ encoder pins, optional SPI-595 shifter, ext-power-hog polarity, VIK SPI register prefix). The wired BOARD_OVERLAY sets up &uart0 on GP0/GP1 for the wired-split transport, &spi1 + &i2c1 for VIK, and the VIK gpio-map for the wired pinout. generateConf adds CONFIG_PINCTRL/GPIO/ZMK_SPLIT/ZMK_SPLIT_WIRED for the wired path and forces RGB underglow off (Phase 1 has no PIO ws2812 yet). generateWestYaml pins the wired path to Olson3R/rainadon-zmk on the lemon-wired-zmk-phase1 branch. UI: PeaConfig.svelte gains a Firmware select (QMK / ZMK) on the wired branch; ZMK mode shows centralSide / splitTransport (UART only for now) / Studio / USB logging plus the Miryoku picker, and exposes a Download ZMK code button. fullOptions now passes microcontroller through and forces underGlowAtStart=false for wired ZMK. Cross-link from the Lemon Wired doc to the new ZMK option. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The new cosmos_lemon_wired board (added in the rainadon-zmk fork on the lemon-wired-zmk-phase1 branch) already enables uart0/spi1/i2c1 with their pinctrl groups, so the per-shield overlay no longer needs to redefine them. Keep only the wired_split node + VIK connector pin map + bus aliases here. Drop the cosmos_lemon_wired_v5 board name — the only Phase-1 hardware diff between v0.4 and v0.5 (LED-relay polarity) already lives in the per-shield ext_power_hog. One Zephyr board is enough; if v0.5 ever needs its own DTS we can split then. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Wires the generator end-to-end with the Phase-2 USB split work that landed
in Olson3R/rainadon-zmk + Olson3R/zephyr-module-pio-usb (v0.1.0):
- generateConf: per-side conf — USB device stack + ZMK_USB on the
central, USB device stack only on the peripheral (its CDC ACM bulk
pair is what the central's PIO-USB UHC drives).
- generateDefconfig: ZMK_SPLIT_USB=y on both halves under usb-split.
- generateOverlay: per-side DT — central disables uart0, brings up
raspberrypi,pio-usb-host on GP0, exposes zmk,usb-split with that as
its UHC, and optionally wires a CDC ACM console on native USB-C.
Peripheral declares zsu_cdc_acm under zephyr_udc0.
- BOARD_OVERLAY_RP2040: drops the unconditional zmk,wired-split node;
the legacy UART transport now lives in the per-side overlay only
when splitTransport === 'uart'.
- generateWestYaml: bumps revision from lemon-wired-zmk-phase1 to
main on Olson3R/rainadon-zmk (the fork's app/west.yml pulls in
zephyr-module-pio-usb @ v0.1.0 transitively).
Also lifts splitTransport / microcontroller string-literal types into
exported `as const` enum-like objects (SplitTransport, Microcontroller)
and updates PeaConfig.svelte to match — Pico-PIO-USB is now the default,
UART is the legacy alternative.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Post-review pass on commit 73bac5a. Three real bugs + two QoL gaps: - generateConf treated unibody as central and emitted USB-split configs for non-existent splits. Mirror generateOverlay's split/isCentral semantics so unibody+wired stays out of the split branches entirely (and ZMK_SPLIT/ZMK_SPLIT_WIRED no longer leak into unibody output). - Emit CONFIG_FLASH=n / NVS=n / SETTINGS=n for wired — Zephyr 3.5's RP2040 flash driver is broken in the rainadon-zmk fork; without the override, settings init hangs. The board defconfig sets these to y for when the driver lands; per-shield .conf overrides until then. - Emit CONFIG_USB_DEVICE_PRODUCT/VID/PID per side for the pio-usb path (peripheral product gets a "(peripheral)" suffix). - Emit a CONSOLE/LOG block on the central when enableConsole=true so the CDC ACM port actually surfaces logs (was silent before). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
8bc92d8 to
8e26f75
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.
Summary
Generator support for ZMK firmware on the Cosmos Lemon Wired RP2040 board, with Pico-PIO-USB as the split transport (a USB-C cable between the two halves' Link ports). Companion to the rianadon/zmk PR #1.
Depends on:
This PR's diff currently includes those two stacks. Once #87 and #88 land, the diff will narrow to just the lemon-wired commits below.
What's in this stack (lemon-wired only)
lemon-wiredmicrocontroller path throughgenerateConf/generateOverlay/generateDefconfig/generateWestYaml/boardOverlayinsrc/routes/beta/lib/firmware/zmk.ts.CONFIG_USB_DEVICE_STACK+CONFIG_ZMK_USB+ an optional CDC ACM console; peripheral gets the device stack + azsu_cdc_acmnode underzephyr_udc0(the central's PIO-USB UHC enumerates and drives that bulk pair). Both halves disableCONFIG_FLASH/NVS/SETTINGSuntil the rainadon-zmk fork ships a working RP2040 flash driver.&uart0, brings upraspberrypi,pio-usb-hoston GP0, exposeszmk,usb-splitover it, and addscdc_acm_uart0for the console when enabled. Peripheral declareszsu_cdc_acmunder&zephyr_udc0.west.ymlpoints atOlson3R/rainadon-zmk@main(the fork carrying the cosmos_lemon_wired board + USB split transport); the Pico-PIO-USB module +Olson3R/zephyr-module-pio-usb@v0.1.0wrapper come in transitively from the fork'sapp/west.yml.'lemon-wired' | 'lemon-wireless'and'uart' | 'pio-usb'intoas constenum-likes (Microcontroller,SplitTransport).Test plan
npm run checkpassesmake dev— open/beta, design a 36-key Lemon Wired layout, choose ZMK firmwareconfig/west.ymlreferencesOlson3R/rainadon-zmk@main.confhasCONFIG_USB_DEVICE_STACK=y+CONFIG_ZMK_USB=y+CONFIG_ZMK_SPLIT_USB=y+CONFIG_FLASH=n+CONFIG_USB_DEVICE_PRODUCT="<your name>"+ aCONSOLE/LOGblock when console is enabled.confhas the device stack but noCONFIG_ZMK_USB; product name has " (peripheral)" suffix&uart0and addspio_usb_host+usb_split+cdc_acm_uart0nodeszsu_cdc_acmunder&zephyr_udc0west init -l config/ && west update && west buildsucceeds for both halveszmk,wired-splitper-side (regression check)🤖 Generated with Claude Code