diff --git a/.gitignore b/.gitignore index e669a0e..e224e6a 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,10 @@ zmk-config/boards/arm/anaphase/widgets/temp .DS_Store hardware/kicad_5.99/*.lck -# west workspace (created by `west init -l zmk-config` for local builds) +# Local build artifacts. The west workspace lives in the `zmk-ws` Docker +# volume, not the repo; only output uf2s land here, in dist/. +dist/ +# stale guards in case a workspace is ever init'd in the repo again .west/ zmk/ zephyr/ diff --git a/build.yaml b/build.yaml index 3756737..45c5b8a 100644 --- a/build.yaml +++ b/build.yaml @@ -2,4 +2,5 @@ # Board IDs = the *_defconfig basenames under zmk-config/boards/arm/anaphase/. include: - board: anaphase_left + snippet: studio-rpc-usb-uart - board: anaphase_right diff --git a/zmk-config/boards/arm/anaphase/Kconfig.defconfig b/zmk-config/boards/arm/anaphase/Kconfig.defconfig index 6a169be..69e6e1f 100644 --- a/zmk-config/boards/arm/anaphase/Kconfig.defconfig +++ b/zmk-config/boards/arm/anaphase/Kconfig.defconfig @@ -1,4 +1,11 @@ # SPDX-License-Identifier: MIT +# +# Shared configuration for both halves lives here as Kconfig defaults. +# The per-half *_defconfig files hold only: SoC/board selection, the +# split-role and USB differences, widget selection, and *choice* symbols — +# choice-default overrides from this file are unreliable (learned the hard +# way with ZMK_LV_FONT_DEFAULT_SMALL), so choice members are set as explicit +# values in the defconfig files instead. if BOARD_ANAPHASE_LEFT @@ -33,6 +40,44 @@ config BOARD config ZMK_SPLIT default y +# +# Platform +# + +config ARM_MPU + default y + +config GPIO + default y + +# SPI pins are defined in anaphase-pinctrl.dtsi +config PINCTRL + default y + +# Place application at &code_partition, produce a .uf2 natively (family ID +# 0xada52840 and base 0x1000 both derive from the SoC + DT automatically) +config USE_DT_CODE_PARTITION + default y + +config BUILD_OUTPUT_UF2 + default y + +# NVS settings storage (BLE bonds, profiles) +config MPU_ALLOW_FLASH_WRITE + default y + +config NVS + default y + +config FLASH + default y + +config FLASH_PAGE_LAYOUT + default y + +config FLASH_MAP + default y + if USB config USB_NRFX @@ -53,6 +98,36 @@ config ZMK_BLE config ZMK_BATTERY_NRF_VDDH default y +# +# Power / input tuning +# + +config ZMK_SLEEP + default y + +# ZMK_IDLE_TIMEOUT / ZMK_IDLE_SLEEP_TIMEOUT / DISPLAY stack size live in the +# *_defconfig files: ZMK's app/Kconfig is the Kconfig root, so its defaults +# are seen before this file's and win the first-default race. Only symbols +# whose upstream defaults come from Zephyr or module Kconfigs (loaded after +# the board dir) can be defaulted here. + +# Debounce tuned against the actual switches for minimum first-press latency +# (2ms press) with chatter protection on release (10ms). Asymmetry is +# intentional — do not normalise to the 5/5 default. Last tuned 2022-03-05. +config ZMK_KSCAN_DEBOUNCE_PRESS_MS + default 2 + +config ZMK_KSCAN_DEBOUNCE_RELEASE_MS + default 10 + +# BT_MAX_CONN / BT_PERIPHERAL_PREF_* deliberately unset: ZMK v0.3.0's +# role-aware defaults give 6 conns central / 1 peripheral and 6/12/30/400 +# conn params (the old hand-tuned 7.5-15ms interval, plus slave latency 30). + +# +# Display +# + config ZMK_DISPLAY select LV_FONT_UNSCII_8 select LV_USE_LABEL @@ -64,10 +139,8 @@ endchoice if ZMK_DISPLAY -# UNSCII-8 everywhere: the 96x48 text layout only fits with this font. -# (ZMK's own default is Montserrat, which does not.) -# The theme "small" font counterpart is set in the *_defconfig files — -# a choice-default override here loses to ZMK's own default. +# UNSCII-8 everywhere: the 64x48 visible text layout only fits with this +# font. (The theme "small" font counterpart is in the *_defconfig files.) choice LV_FONT_DEFAULT default LV_FONT_DEFAULT_UNSCII_8 @@ -79,8 +152,8 @@ config SPI config SSD1306 default y -# Panel resolution now comes from the width/height DT properties on the -# ssd1306 node (the old LVGL_HOR_RES_MAX/VER_RES_MAX symbols are gone). +# Panel resolution comes from the width/height DT properties on the ssd1306 +# node (width is deliberately the full 128-column RAM — see anaphase.dtsi). config LV_Z_VDB_SIZE default 100 @@ -95,6 +168,20 @@ choice LV_COLOR_DEPTH endchoice +config LV_USE_THEME_MONO + default y + +# White-on-black like the 2022 build: LVGL 8's mono theme defaults to the +# light variant (LVGL 7 defaulted dark); this flips it back. +config ZMK_DISPLAY_INVERT + default y + +# LVGL pool sized explicitly (a custom status screen gets no upstream +# default; the 2022 build ran on LVGL 7's implicit pool and had a rare, +# unreproduced crash consistent with display-path memory pressure). +config LV_Z_MEM_POOL_SIZE + default 8192 + endif # ZMK_DISPLAY config CUSTOM_WIDGET_BATTERY_STATUS diff --git a/zmk-config/boards/arm/anaphase/anaphase.dtsi b/zmk-config/boards/arm/anaphase/anaphase.dtsi index 7c562d9..f98f0ab 100644 --- a/zmk-config/boards/arm/anaphase/anaphase.dtsi +++ b/zmk-config/boards/arm/anaphase/anaphase.dtsi @@ -7,6 +7,7 @@ /dts-v1/; #include #include +#include #include "anaphase-pinctrl.dtsi" / { @@ -22,7 +23,10 @@ // display subsystem fails to compile. zephyr,display = &oled; zmk,kscan = &kscan0; - zmk,matrix_transform = &transform; + // ZMK Studio: physical layout replaces the old zmk,matrix_transform + // chosen (physical layouts only activate when that chosen is absent); + // the transform is referenced from the layout node instead. + zmk,physical-layout = &physical_layout0; zmk,battery = &vbatt; }; #define K(n) RC(0,n) @@ -42,6 +46,61 @@ >; }; + // Schematic layout for ZMK Studio's UI: corne-style 6-column stagger, + // flat horizontal 3-key thumb clusters. Positions are representative, + // not measured from the case. Order = keymap position order (rows of + // 6L+6R, then left thumbs BSPC/E/FUNC, right thumbs SHIFT/SPACE/ENTER). + physical_layout0: physical_layout0 { + compatible = "zmk,physical-layout"; + display-name = "anaphase"; + transform = <&transform>; + + keys // w h x y rot rx ry + = <&key_physical_attrs 100 100 0 37 0 0 0> + , <&key_physical_attrs 100 100 100 37 0 0 0> + , <&key_physical_attrs 100 100 200 12 0 0 0> + , <&key_physical_attrs 100 100 300 0 0 0 0> + , <&key_physical_attrs 100 100 400 12 0 0 0> + , <&key_physical_attrs 100 100 500 24 0 0 0> + , <&key_physical_attrs 100 100 800 24 0 0 0> + , <&key_physical_attrs 100 100 900 12 0 0 0> + , <&key_physical_attrs 100 100 1000 0 0 0 0> + , <&key_physical_attrs 100 100 1100 12 0 0 0> + , <&key_physical_attrs 100 100 1200 37 0 0 0> + , <&key_physical_attrs 100 100 1300 37 0 0 0> + , <&key_physical_attrs 100 100 0 137 0 0 0> + , <&key_physical_attrs 100 100 100 137 0 0 0> + , <&key_physical_attrs 100 100 200 112 0 0 0> + , <&key_physical_attrs 100 100 300 100 0 0 0> + , <&key_physical_attrs 100 100 400 112 0 0 0> + , <&key_physical_attrs 100 100 500 124 0 0 0> + , <&key_physical_attrs 100 100 800 124 0 0 0> + , <&key_physical_attrs 100 100 900 112 0 0 0> + , <&key_physical_attrs 100 100 1000 100 0 0 0> + , <&key_physical_attrs 100 100 1100 112 0 0 0> + , <&key_physical_attrs 100 100 1200 137 0 0 0> + , <&key_physical_attrs 100 100 1300 137 0 0 0> + , <&key_physical_attrs 100 100 0 237 0 0 0> + , <&key_physical_attrs 100 100 100 237 0 0 0> + , <&key_physical_attrs 100 100 200 212 0 0 0> + , <&key_physical_attrs 100 100 300 200 0 0 0> + , <&key_physical_attrs 100 100 400 212 0 0 0> + , <&key_physical_attrs 100 100 500 224 0 0 0> + , <&key_physical_attrs 100 100 800 224 0 0 0> + , <&key_physical_attrs 100 100 900 212 0 0 0> + , <&key_physical_attrs 100 100 1000 200 0 0 0> + , <&key_physical_attrs 100 100 1100 212 0 0 0> + , <&key_physical_attrs 100 100 1200 237 0 0 0> + , <&key_physical_attrs 100 100 1300 237 0 0 0> + , <&key_physical_attrs 100 100 300 337 0 0 0> + , <&key_physical_attrs 100 100 400 337 0 0 0> + , <&key_physical_attrs 100 100 500 337 0 0 0> + , <&key_physical_attrs 100 100 800 337 0 0 0> + , <&key_physical_attrs 100 100 900 337 0 0 0> + , <&key_physical_attrs 100 100 1000 337 0 0 0> + ; + }; + vbatt: vbatt { compatible = "zmk,battery-nrf-vddh"; }; @@ -101,7 +160,9 @@ status = "okay"; }; -&usbd { +// zephyr_udc0 is the conventional label for the USB device controller; +// the ZMK Studio studio-rpc-usb-uart snippet attaches its CDC-ACM UART to it. +zephyr_udc0: &usbd { status = "okay"; }; diff --git a/zmk-config/boards/arm/anaphase/anaphase.keymap b/zmk-config/boards/arm/anaphase/anaphase.keymap index 982a5b0..5116aef 100644 --- a/zmk-config/boards/arm/anaphase/anaphase.keymap +++ b/zmk-config/boards/arm/anaphase/anaphase.keymap @@ -103,10 +103,10 @@ display-name = "SYS"; bindings = < //1 2 3 4 5 6 7 8 9 10 11 12 -&bt BT_CLR &bt BT_SEL 0 &bt BT_SEL 1 &bt BT_SEL 2 &bt BT_SEL 3 &bt BT_SEL 4 &none &none &none &none &none &none -&none &none &none &none &none &none &none &none &none &none &none &none -&none &none &none &none &none &none &none &none &none &none &none &none - &bootloader &none &none &none &none &none +&bt BT_CLR &bt BT_SEL 0 &bt BT_SEL 1 &bt BT_SEL 2 &bt BT_SEL 3 &bt BT_SEL 4 &none &none &none &none &none &none +&none &none &none &none &none &none &none &none &none &none &none &none +&none &none &none &none &none &studio_unlock &none &none &none &none &none &none + &bootloader &none &none &none &none &none >; }; diff --git a/zmk-config/boards/arm/anaphase/anaphase_left_defconfig b/zmk-config/boards/arm/anaphase/anaphase_left_defconfig index 06fa90a..8980e26 100644 --- a/zmk-config/boards/arm/anaphase/anaphase_left_defconfig +++ b/zmk-config/boards/arm/anaphase/anaphase_left_defconfig @@ -1,51 +1,25 @@ # SPDX-License-Identifier: MIT +# +# Left half (BLE central). Shared config lives in Kconfig.defconfig as +# defaults; this file holds SoC/board selection, choice symbols (which +# cannot reliably be defaulted from Kconfig.defconfig), and per-half deltas. CONFIG_SOC_SERIES_NRF52X=y CONFIG_SOC_NRF52840_QIAA=y CONFIG_BOARD_ANAPHASE_LEFT=y -# Sleep: 60s idle (display blanks), 2h to deep sleep. Deliberate 2021 tune. -CONFIG_ZMK_SLEEP=y +# ZMK-app defaults that must be overridden as user values (see +# Kconfig.defconfig header for the first-default-race explanation): +# sleep 60s idle / 2h deep (deliberate 2021 tune); display stack 4096 for +# LVGL 8 (upstream default 2048). CONFIG_ZMK_IDLE_TIMEOUT=60000 CONFIG_ZMK_IDLE_SLEEP_TIMEOUT=7200000 +CONFIG_ZMK_DISPLAY_DEDICATED_THREAD_STACK_SIZE=4096 -# Enable MPU -CONFIG_ARM_MPU=y - -# Enable GPIO + pinctrl (SPI pins are defined in anaphase-pinctrl.dtsi) -CONFIG_GPIO=y -CONFIG_PINCTRL=y - -# Place application at &code_partition address, produce a .uf2 natively -# (replaces the old uf2conv.py post-build hook; family ID 0xada52840 and -# base 0x1000 both derive from SOC/DT automatically) -CONFIG_USE_DT_CODE_PARTITION=y -CONFIG_BUILD_OUTPUT_UF2=y +# --- choice symbols --- -# Enable NVS for settings -CONFIG_MPU_ALLOW_FLASH_WRITE=y -CONFIG_NVS=y CONFIG_SETTINGS_NVS=y -CONFIG_FLASH=y -CONFIG_FLASH_PAGE_LAYOUT=y -CONFIG_FLASH_MAP=y - -# (USB_HID_POLL_INTERVAL_MS=1 is now ZMK's own default under ZMK_USB) - -# Display: dedicated work queue; stack raised 2048->4096 for LVGL 8, and the -# LVGL pool sized explicitly (a custom status screen gets no upstream default; -# the 2022 build ran on LVGL 7's implicit pool and had a rare, unreproduced -# crash consistent with display-path memory pressure). -CONFIG_ZMK_DISPLAY=y -CONFIG_ZMK_DISPLAY_WORK_QUEUE_DEDICATED=y -CONFIG_ZMK_DISPLAY_DEDICATED_THREAD_STACK_SIZE=4096 -CONFIG_LV_Z_MEM_POOL_SIZE=8192 -CONFIG_LV_USE_THEME_MONO=y -# White-on-black like the 2022 build: LVGL 8's mono theme defaults to the -# light variant (LVGL 7 defaulted dark); this flips it back. -CONFIG_ZMK_DISPLAY_INVERT=y -# Theme small font: UNSCII-8 like everything else (default is Montserrat 12) -CONFIG_ZMK_LV_FONT_DEFAULT_SMALL_UNSCII_8=y +CONFIG_ZMK_HID_CONSUMER_REPORT_USAGES_BASIC=y # 32kHz source is the internal RC on purpose. Y1/Y2 crystals are fitted but # their load caps were never populated; the crystal was unstable in testing @@ -54,24 +28,19 @@ CONFIG_ZMK_LV_FONT_DEFAULT_SMALL_UNSCII_8=y CONFIG_CLOCK_CONTROL_NRF_K32SRC_RC=y CONFIG_BT_CTLR_TX_PWR_PLUS_8=y -# BT_MAX_CONN / BT_PERIPHERAL_PREF_* deliberately unset: ZMK v0.3.0's -# role-aware defaults are 6 conns on the central and 6/12/30/400 conn params -# (same 7.5-15ms interval as the old hand-tune, plus slave latency 30 for -# ~30x fewer idle radio wakeups at zero added press latency). -# Debounce tuned against the actual switches for minimum first-press latency -# (2ms press) with chatter protection on release (10ms). Asymmetry is -# intentional — do not normalise to the 5/5 default. Last tuned 2022-03-05. -CONFIG_ZMK_KSCAN_DEBOUNCE_PRESS_MS=2 -CONFIG_ZMK_KSCAN_DEBOUNCE_RELEASE_MS=10 +CONFIG_ZMK_DISPLAY=y +CONFIG_ZMK_DISPLAY_WORK_QUEUE_DEDICATED=y +CONFIG_ZMK_DISPLAY_STATUS_SCREEN_CUSTOM=y +# Theme small font: UNSCII-8 like everything else (default is Montserrat 12) +CONFIG_ZMK_LV_FONT_DEFAULT_SMALL_UNSCII_8=y -CONFIG_ZMK_HID_CONSUMER_REPORT_USAGES_BASIC=y +# ZMK Studio (runtime keymap editing over USB; build with the +# studio-rpc-usb-uart snippet — see build.yaml) +CONFIG_ZMK_STUDIO=y +# --- per-half widget selection --- # Custom status screen: BLE profile nickname + pairing status + battery + layer -CONFIG_ZMK_DISPLAY_STATUS_SCREEN_CUSTOM=y CONFIG_CUSTOM_WIDGET_BATTERY_STATUS=y -CONFIG_ZMK_WIDGET_BATTERY_STATUS=n CONFIG_CUSTOM_WIDGET_OUTPUT_STATUS=y -CONFIG_ZMK_WIDGET_OUTPUT_STATUS=n CONFIG_CUSTOM_WIDGET_LAYER_STATUS=y -CONFIG_ZMK_WIDGET_LAYER_STATUS=n diff --git a/zmk-config/boards/arm/anaphase/anaphase_right_defconfig b/zmk-config/boards/arm/anaphase/anaphase_right_defconfig index 8f78d73..5d121af 100644 --- a/zmk-config/boards/arm/anaphase/anaphase_right_defconfig +++ b/zmk-config/boards/arm/anaphase/anaphase_right_defconfig @@ -1,51 +1,30 @@ # SPDX-License-Identifier: MIT +# +# Right half (peripheral: ZMK_SPLIT without ZMK_SPLIT_ROLE_CENTRAL). +# Shared config lives in Kconfig.defconfig as defaults; this file holds +# SoC/board selection, choice symbols, and per-half deltas. CONFIG_SOC_SERIES_NRF52X=y CONFIG_SOC_NRF52840_QIAA=y CONFIG_BOARD_ANAPHASE_RIGHT=y -# Peripheral role is implied by ZMK_SPLIT without ZMK_SPLIT_ROLE_CENTRAL. -# Sleep: 60s idle (display blanks), 2h to deep sleep. Deliberate 2021 tune. -CONFIG_ZMK_SLEEP=y +# ZMK-app defaults that must be overridden as user values (see +# Kconfig.defconfig header for the first-default-race explanation): +# sleep 60s idle / 2h deep (deliberate 2021 tune); display stack 4096 for +# LVGL 8 (upstream default 2048). CONFIG_ZMK_IDLE_TIMEOUT=60000 CONFIG_ZMK_IDLE_SLEEP_TIMEOUT=7200000 - -# Enable MPU -CONFIG_ARM_MPU=y - -# Enable GPIO + pinctrl (SPI pins are defined in anaphase-pinctrl.dtsi) -CONFIG_GPIO=y -CONFIG_PINCTRL=y - -# Place application at &code_partition address, produce a .uf2 natively -CONFIG_USE_DT_CODE_PARTITION=y -CONFIG_BUILD_OUTPUT_UF2=y - -# Enable NVS for settings -CONFIG_MPU_ALLOW_FLASH_WRITE=y -CONFIG_NVS=y -CONFIG_SETTINGS_NVS=y -CONFIG_FLASH=y -CONFIG_FLASH_PAGE_LAYOUT=y -CONFIG_FLASH_MAP=y +CONFIG_ZMK_DISPLAY_DEDICATED_THREAD_STACK_SIZE=4096 # USB HID output is central-only at v0.3.0, so no ZMK_USB here — but keep the # bare USB device stack so zmk_usb_is_powered() works and the display can show -# CHG vs BAT while charging (matches 2022 behaviour; the peripheral never sent -# HID over USB anyway). +# CHG vs BAT while charging (the peripheral never sent HID over USB anyway). CONFIG_USB_DEVICE_STACK=y -# Display (see left defconfig for stack/pool rationale) -CONFIG_ZMK_DISPLAY=y -CONFIG_ZMK_DISPLAY_WORK_QUEUE_DEDICATED=y -CONFIG_ZMK_DISPLAY_DEDICATED_THREAD_STACK_SIZE=4096 -CONFIG_LV_Z_MEM_POOL_SIZE=8192 -CONFIG_LV_USE_THEME_MONO=y -# White-on-black like the 2022 build: LVGL 8's mono theme defaults to the -# light variant (LVGL 7 defaulted dark); this flips it back. -CONFIG_ZMK_DISPLAY_INVERT=y -# Theme small font: UNSCII-8 like everything else (default is Montserrat 12) -CONFIG_ZMK_LV_FONT_DEFAULT_SMALL_UNSCII_8=y +# --- choice symbols --- + +CONFIG_SETTINGS_NVS=y +CONFIG_ZMK_HID_CONSUMER_REPORT_USAGES_BASIC=y # 32kHz source is the internal RC on purpose. Y1/Y2 crystals are fitted but # their load caps were never populated; the crystal was unstable in testing @@ -54,24 +33,15 @@ CONFIG_ZMK_LV_FONT_DEFAULT_SMALL_UNSCII_8=y CONFIG_CLOCK_CONTROL_NRF_K32SRC_RC=y CONFIG_BT_CTLR_TX_PWR_PLUS_8=y -# BT_MAX_CONN / BT_PERIPHERAL_PREF_* deliberately unset: the peripheral now -# gets BT_MAX_CONN=1 (was hardcoded 6 — 6x oversized) and 6/12/30/400 conn -# params from ZMK's role-aware defaults. - -# Debounce tuned against the actual switches for minimum first-press latency -# (2ms press) with chatter protection on release (10ms). Asymmetry is -# intentional — do not normalise to the 5/5 default. Last tuned 2022-03-05. -CONFIG_ZMK_KSCAN_DEBOUNCE_PRESS_MS=2 -CONFIG_ZMK_KSCAN_DEBOUNCE_RELEASE_MS=10 - -CONFIG_ZMK_HID_CONSUMER_REPORT_USAGES_BASIC=y -# Custom status screen: peripherals cannot see layer/endpoint state over the -# split GATT service, so the right half shows connection status + battery only. +CONFIG_ZMK_DISPLAY=y +CONFIG_ZMK_DISPLAY_WORK_QUEUE_DEDICATED=y CONFIG_ZMK_DISPLAY_STATUS_SCREEN_CUSTOM=y +# Theme small font: UNSCII-8 like everything else (default is Montserrat 12) +CONFIG_ZMK_LV_FONT_DEFAULT_SMALL_UNSCII_8=y + +# --- per-half widget selection --- +# Peripherals cannot see layer/endpoint state over the split GATT service, +# so the right half shows split-link status + battery only. CONFIG_CUSTOM_WIDGET_BATTERY_STATUS=y -CONFIG_ZMK_WIDGET_BATTERY_STATUS=n CONFIG_CUSTOM_WIDGET_OUTPUT_STATUS=y -CONFIG_ZMK_WIDGET_OUTPUT_STATUS=n -CONFIG_CUSTOM_WIDGET_LAYER_STATUS=n -CONFIG_ZMK_WIDGET_LAYER_STATUS=n diff --git a/zmk-next-steps.md b/zmk-next-steps.md new file mode 100644 index 0000000..dca9039 --- /dev/null +++ b/zmk-next-steps.md @@ -0,0 +1,172 @@ +# anaphase — post-migration options + +Menu of follow-up work after the v0.3.0 migration (2026-08-09). Everything here is +optional; items are independent unless noted. "Verified" = checked against the actual +ZMK v0.3.0 / Zephyr 3.5 trees during the migration sessions. + +--- + +## 1. Repo cleanup (§6.1 of the portability report) + +**Effort: ~1–2 h. Risk: low (Kconfig-only, CI catches mistakes). No hardware behaviour change.** + +- Move the ~85%-identical content of the two `*_defconfig` files into + `Kconfig.defconfig` defaults; leave per-half files holding only `BOARD_*`, + role, and widget selection (~15 lines each). + - Gotcha learned on hardware: choice-*default* overrides from board + Kconfig.defconfig are unreliable — set choice symbols directly in defconfig + (that's why `ZMK_LV_FONT_DEFAULT_SMALL_UNSCII_8` lives there). +- Cannot merge: `Kconfig` / `Kconfig.board` / `Kconfig.defconfig` (distinct roles in + board resolution) and the two one-line `.keymap` includes. +- Optional: split preference-style settings (sleep timeouts, TX power) into a + `zmk-config/anaphase.conf` so board defaults vs user prefs are distinguishable. + +## 2. ZMK Studio — ✅ DONE 2026-08-09, verified on hardware +(Runtime keymap editing works over **USB only** — the studio-rpc-usb-uart +snippet is a serial transport. Over BLE the client sees the keyboard but has +no RPC channel, so edits silently no-op; a BLE transport exists at v0.3.0 +(gatt_rpc_transport.c) and could be enabled later if cable-only annoys. +Remember: Studio edits live in NVS and override the compiled keymap until +"Restore Stock Settings".) + +**Effort: ~3–5 h. Risk: medium (touches kscan/chosen plumbing). RAM/flash headroom confirmed fine (25%/34%).** + +Available at v0.3.0. Verified present: `ZMK_STUDIO` Kconfig, `studio-rpc-usb-uart` +snippet, `&studio_unlock` behavior, `zmk,physical-layout` binding with `keys` property. + +Required changes (left/central only — Studio talks to the central): +1. A `zmk,physical-layout` node with per-key `keys = <&key_physical_attrs ...>` + positions. **Cannot be auto-extracted from the PCB** — switches hang off the JST + row connectors (R1–R4), not footprints on the main board. Sources: measure the + case/plate CAD, or draw it once in KLE. (Report open question: does a KLE/QMK + JSON of this layout exist anywhere?) +2. Point `chosen zmk,physical-layout` at it (replaces `zmk,matrix_transform` chosen; + the transform node is referenced *from* the layout instead — B9 coincidence + documented in anaphase.dtsi still applies). +3. `CONFIG_ZMK_STUDIO=y` + build left with the `studio-rpc-usb-uart` snippet + (`build.yaml` gains a `snippet:` line). +4. A `&studio_unlock` binding somewhere (SYS layer has free slots). + +Bonus once running: layer *renaming* at runtime (`ZMK_KEYMAP_SETTINGS_STORAGE`), +and it renders the physical layout — which is most of the work toward publishing +the board upstream if that ever appeals. + +## 3. Layer name on the right half — **not possible upstream, custom work** + +Answer to "was this possible now?": **no** — verified at v0.3.0 the split GATT service +still has no layer characteristic (six characteristics, none carry layer or endpoint +state), and `keymap.c` isn't even compiled on peripherals. Nothing in v0.4/main is +known to change this (unverified — worth a check when v0.4 lands). + +**Research done (2026-08-09):** no off-the-shelf module exists. Searched GitHub topic +lists and the awesome-zmk index: even `mctechnology17/zmk-nice-oled` (v0.3.0-tested, +the most complete OLED module) shows layers on the central only and gives peripherals +animations/battery — because the data isn't on that side. The closest building block +is `badjeff/zmk-split-peripheral-output-relay` (sideloads a custom GATT service into +the existing split connection, central→peripheral event relay) — but it's designed +for haptics/output events, has ~8 commits, unclear maintenance, no stated version +pin. Using it for layer state would be a contorted fit on top of an unmaintained dep. + +**DIY spec (preferred if ever done) — "layer relay" via the existing split behavior +channel, no new GATT:** + +ZMK's central can already invoke *behaviors* on peripherals over the stock +`RUN_BEHAVIOR` characteristic — that's how `&bootloader`/`&sys_reset` locality works. +Verified at v0.3.0: `zmk_split_central_invoke_behavior(source, binding, event, +pressed)` is exported in `zmk/split/central.h` and called from `behavior.c`. + +Design (all code lives in the board directory, like the widgets — no module needed): +1. **Peripheral side**: a tiny custom behavior `layer_ind` (DT node + driver, compiled + both halves, inert on central). Its `binding_pressed(param1 = layer_id)` raises a + local `layer_indicator_changed` event. +2. **Central side**: listener on `zmk_layer_state_changed` → build a + `zmk_behavior_binding {.behavior_dev = "layer_ind", .param1 = highest_layer}` → + `zmk_split_central_invoke_behavior()` to each peripheral. +3. **Peripheral widget**: subscribes to the local event; maps layer id → name using a + table generated at compile time from the shared keymap devicetree + (`DT_FOREACH_CHILD` over the `zmk,keymap` node reading `display-name` — the DT is + present on the peripheral even though `keymap.c` isn't compiled). + +Effort: ~0.5–1 day. Risks, honestly: +- **API stability**: `zmk_split_central_invoke_behavior` is exported but not a + documented public API; the v0.4 split-transport rework *will* touch it (there is + already a `split/transport/types.h` abstraction forming). Budget a re-port. +- **Staleness**: updates sent while the halves are disconnected are lost — display + shows the last-known layer until the next layer change (self-healing; could resend + on reconnect if it bothers you). +- **Shared channel**: rides the same GATT characteristic as reset behaviors — a bug + here can't break typing (keystrokes flow the other direction) but could interfere + with `&bootloader` locality. Failure domain is acceptable but not zero. +- **Daily-driver rule**: same as the migration — land it as its own commit, after a + stable baseline, so any BLE weirdness has one candidate cause. +- **c. Skip** remains legitimate: layer state is arguably central-half information. + +## 4. New OLED content available at v0.3.0 (wasn't possible in 2022) + +All verified in-tree; each is a small widget change (~1–2 h each given the LVGL 8 +widget skeleton now exists): + +- **Right-half battery on the LEFT screen** — `ZMK_SPLIT_BLE_CENTRAL_BATTERY_LEVEL_FETCHING=y` + + subscribe to `zmk_peripheral_battery_state_changed` (verified in + `battery_state_changed.h`). Probably the highest-value one: today the right half's + charge is only visible on the right half. Left screen has a free row (y=29, the old + WPM slot). +- **Caps-lock / HID indicators** — `ZMK_HID_INDICATORS=y` + `zmk_hid_indicators_changed` + event; `ZMK_SPLIT_PERIPHERAL_HID_INDICATORS=y` propagates to the right half too + (verified in split Kconfig). Small "CAPS" label when active. +- **Icons instead of text** — LVGL 8 mono `lv_img` works fine at 1bpp; + `corneish_zen/widgets/icons/` is a ready template (battery glyphs, BT status). + Cosmetic, moderate effort, flash is cheap (34% used). +- **Screen-estate note**: visible area is 64×48 = 6 rows of 8px text. Currently 4 + rows used (1/11/21/39). Rows at y≈29 free on left; right half uses 3. + +## 5. Keymap niceties (from the report, C3) + +**Effort: minutes each. Risk: behavioural — tune on the daily driver carefully.** + +- `require-prior-idle-ms` on the three `&mt` mod-taps (suppresses accidental holds + during fast typing; `&mt LSHFT ESC` is the classic misfire candidate). +- The deferred **Ctrl+Backspace → word delete** mod-morph from the original brief: + ```dts + ctrl_bspc: ctrl_bspc { + compatible = "zmk,behavior-mod-morph"; + #binding-cells = <0>; + bindings = <&kp BSPC>, <&kp LA(BSPC)>; + mods = <(MOD_LCTL|MOD_RCTL)>; + }; + ``` +- **Mouse keys** — upstream since v0.2.0 (`&mmv`, `&msc`); FUNC/FNSHFT layers have + free `&none` slots. + +## 6. Power / robustness (deferred items from the original brief) + +- **Soft-off** — `&soft_off` behavior + `zmk,soft-off-wakeup-sources` (verified in + tree, `pm.c`). Deeper than `ZMK_SLEEP` deep sleep; direct-wired GPIOs make wakeup + sources easy. Only worth it if months-scale storage matters; current 2-month + battery life says low priority. +- **Watchdog / crash instrumentation** — the brief deferred this until a working + baseline existed; one now does. Only revisit if the monthly crash *survives* the + migration (the LVGL pool/stack changes are plausible passive fixes — verdict needs + ~3 clean months). + +## 7. v0.4 / main (Zephyr 4.1) — parked + +Report appendix estimated 9–15 h: HWMv2 board restructure (board IDs gain SoC +qualifiers), LVGL 8→9.3 port, SSD1306→MIPI-DBI devicetree change. Nothing here is +urgent; v0.3.0 is a supported tagged release. Recheck when v0.4 tags. +**Note if attempted:** the width=128 / 64-visible-window arrangement and the theme +polarity are exactly the sort of thing the MIPI-DBI + LVGL 9 moves will disturb — +re-test the OLEDs first. + +--- + +## Suggested order (if doing several) + +1. **#1 cleanup** — while the migration context is fresh; makes everything after + easier to read. +2. **#4 right-battery-on-left** — best value/effort on the list. +3. **#2 Studio** — the physical layout is the only real work; unlocks #3's sibling + feature (layer *renaming*) for free. +4. **#5 keymap niceties** — one at a time, live with each for a week. +5. **#3 peripheral layer name** — only if (a) still wanted after Studio, and (b) a + maintained community module turns up.