Totem dongle ZMK config (migrated from GitHub)
  • Python 83.1%
  • Nix 16.9%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
ma3s7r0 831881703a
All checks were successful
/ Fetch Build Keyboards (push) Successful in 4s
/ Build (totem_prospector, xiao_ble//zmk, -DCONFIG_ZMK_STUDIO=y, totem_dongle prospector_adapter, studio-rpc-usb-uart) (push) Successful in 3m32s
/ Build (xiao_ble//zmk, -DCONFIG_ZMK_STUDIO=y, totem_left) (push) Successful in 3m37s
/ Build (xiao_ble//zmk, -DCONFIG_ZMK_STUDIO=y, totem_dongle, studio-rpc-usb-uart) (push) Successful in 3m59s
/ Build (xiao_ble//zmk, settings_reset) (push) Successful in 3m24s
/ Build (xiao_ble//zmk, -DCONFIG_ZMK_STUDIO=y, totem_right) (push) Successful in 3m34s
/ build (push) Successful in 0s
training: document the keybr route alongside monkeytype
2026-08-29 23:48:10 +02:00
.github/workflows ci: call the vendored build workflow 2026-08-15 01:30:01 +02:00
config fix(keymap): swap RET and BSPC back, DEL follows BSPC 2026-08-29 15:52:11 +02:00
keymap-drawer fix(keymap): swap RET and BSPC back, DEL follows BSPC 2026-08-29 15:52:11 +02:00
tools tools: generate keybr layout and geometry from the dtsi 2026-08-29 22:19:35 +02:00
training training: document the keybr route alongside monkeytype 2026-08-29 23:48:10 +02:00
.gitignore chore: ignore .claude/ 2026-08-28 11:54:30 +02:00
build.yaml feat(dongle): free the dongle's pins and add a Prospector build 2026-08-27 21:06:52 +02:00
flake.lock build: port the Nix flake and keymap-drawer from the Sofle config 2026-08-27 21:06:27 +02:00
flake.nix docs: add a README, and explain why the halves get the Studio flag 2026-08-27 21:20:08 +02:00
keymap_drawer.config.yaml build: port the Nix flake and keymap-drawer from the Sofle config 2026-08-27 21:06:27 +02:00
README.md docs: switches and keycaps used on the physical build 2026-08-29 15:52:11 +02:00

TOTEM — ZMK config

TOTEM, 38 keys, split, run with a dongle as central. Both halves are peripherals; the keymap lives on the dongle.

keymap

Regenerate the drawing with nix run .#draw after any keymap change. It is the authoritative picture — the ASCII art in config/totem.keymap is hand-maintained and has drifted before.

Hardware

Four Seeed XIAO BLE boards, three roles:

Board Runs
left half totem_left
right half totem_right
bare XIAO in a printed case totem_dongle — travel
Prospector totem_prospector — desk

The two dongle images are interchangeable at the hardware level: same board, same shield, same keymap. prospector_adapter only adds the ST7789 display, the APDS9960 ambient-light sensor and the status screen. Either image boots on either XIAO — the Prospector one simply has nothing to draw on if the display is absent.

Switches are Keeb Supply Twilight low-profile throughout, except S and H: Twilight's actuation force turned out low enough that resting the pinkys on the homerow fired them on their own, so those two are Keeb Supply Bokeh low-profile instead, which need more force. Keycaps are Keeb Supply MBK Clacky, uniform Choc profile — see training/README.md for why the uniform profile matters when rearranging caps for PUQ30.

Flashing

Double-tap reset, then drop the .uf2 on the XIAO-SENSE drive.

&bootloader cannot put a dongle into bootloader mode. The behaviour is BEHAVIOR_LOCALITY_EVENT_SOURCE, so it runs on the board that sourced the key press — the half you pressed it on, never the central. A dongle has a mock kscan and no keys of its own, so nothing can source the event there. The dongle is the board that needs reflashing most often and is the one board no key can reach: it always takes the physical double-tap.

The same locality is why pressing it on a half looks like nothing happened. That half is in bootloader; on battery, with no USB, no drive enumerates. Press its reset once to boot back into firmware.

Order matters once, after flashing the dongle: pair the left half first, then the right. The Prospector's peripheral-battery widget arranges its sub-widgets in pairing order, so a swapped pair shows the halves the wrong way round.

totem_settings_reset.uf2 clears stored BLE pairings. It is side-independent — flash it to whichever board needs forgetting, then flash that board's real image back.

Clear every board in the group, not one of them. A bond has two sides. Reset one board and the others keep their half of it, and the mismatch does not announce itself: the link comes up, the halves show as connected, battery levels report normally — and no key positions arrive at all. Battery and key positions are separate characteristics, so a connection can carry one and not the other.

The tell is the dongle's own display: if pressing a modifier or a layer key moves nothing on it, positions are not reaching the central, and no amount of re-pairing that same stale state will fix it. Reset each board in turn, then pair left first, then right.

Beware of resetting the central after the halves have already bonded to it, which is the easy way to create exactly this. Do the halves last, or do all three before pairing anything.

The GER layer needs a specific host layout

config/totem.keymap's GER layer is an AltGr passthrough:

Key Sends Requires host to map
&kp RA(Q) ä AltGr+q
&kp RA(S) ß AltGr+s
&kp RA(Y) ü AltGr+y
&kp RA(P) ö AltGr+p
&kp RA(5) AltGr+5

That is exactly us + variant altgr-intl, verified against xkeyboard-config 2.47 (symbols/us: us(intl) supplies the level-3 symbols, altgr-intl adds include "level3(ralt_switch)" so RightAlt becomes the level-3 shift). No separate lv3:ralt_switch option is needed — the variant carries it.

Under any other layout the whole layer produces Alt-chords instead of umlauts. RALT must therefore also stay off the home-row mods.

Building

CI (build.yaml + a vendored copy of upstream's workflow, pinned to upload-artifact v3 for Forgejo) builds all five images on push.

Locally, without CI:

nix build .#firmware   # all five .uf2 under their build.yaml names
nix build .#keymap     # the drawing, sandboxed
nix run .#draw         # regenerate keymap-drawer/totem.{yaml,svg}
nix develop            # zmk-nix toolchain shell

flake.nix mirrors build.yaml; keep the two in sync. After changing config/west.yml, refresh zephyrDepsHash — build once and take the hash nix reports as got:.

Notes

  • ZMK is unpinned (revision: main), so this rides the Zephyr 4.1 line. The board is xiao_ble//zmk, the ZMK variant — the plain Zephyr board builds but fails upstream's compat check (see #1).
  • prospector-zmk-module is pinned to a commit, not to its feat/new-status-screens branch. That branch is the Zephyr 4.1 line (the module's main is still 3.5) and it moves, which would break the flake's fixed-output zephyrDepsHash at random.
  • The Prospector image is tight on RAM — 93.4% of 256 KB with Studio enabled. Check the memory report after adding anything to it; CONFIG_LV_Z_VDB_SIZE=25 is upstream's documented escape hatch.
  • The halves carry -DCONFIG_ZMK_STUDIO=y on purpose, even though they are peripherals and compile no Studio code. ZMK_STUDIO gates the RPC on !ZMK_SPLIT || ZMK_SPLIT_ROLE_CENTRAL but selects PM_DEVICE unconditionally, which physical-layout switching needs. It is not dead config.
  • The dongle uses a mock kscan. It has no keys, and inheriting the halves' matrix would claim every GPIO the Prospector needs.