- Python 83.1%
- Nix 16.9%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
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
|
||
| .github/workflows | ||
| config | ||
| keymap-drawer | ||
| tools | ||
| training | ||
| .gitignore | ||
| build.yaml | ||
| flake.lock | ||
| flake.nix | ||
| keymap_drawer.config.yaml | ||
| README.md | ||
TOTEM — ZMK config
TOTEM, 38 keys, split, run with a dongle as central. Both halves are peripherals; the keymap lives on the dongle.
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 isxiao_ble//zmk, the ZMK variant — the plain Zephyr board builds but fails upstream's compat check (see #1). prospector-zmk-moduleis pinned to a commit, not to itsfeat/new-status-screensbranch. That branch is the Zephyr 4.1 line (the module'smainis still 3.5) and it moves, which would break the flake's fixed-outputzephyrDepsHashat 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=25is upstream's documented escape hatch. - The halves carry
-DCONFIG_ZMK_STUDIO=yon purpose, even though they are peripherals and compile no Studio code.ZMK_STUDIOgates the RPC on!ZMK_SPLIT || ZMK_SPLIT_ROLE_CENTRALbut selectsPM_DEVICEunconditionally, 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.