- Nix 55.6%
- Shell 44.4%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
All checks were successful
Build ZMK firmware / Fetch Build Keyboards (push) Successful in 4s
Build ZMK firmware / Build (eyelash_sofle_right, nice_nano_v2, eyelash_sofle_right nice_view_adapter nice_view_custom) (push) Successful in 2m39s
Build ZMK firmware / Build (eyelash_sofle_left, nice_nano_v2, eyelash_sofle_left nice_view_adapter nice_epaper) (push) Successful in 2m43s
Build ZMK firmware / Build (eyelash_sofle_settings_reset, nice_nano_v2, settings_reset) (push) Successful in 2m8s
Build ZMK firmware / Build (eyelash_sofle_studio_left, nice_nano_v2, -DCONFIG_ZMK_STUDIO=y -DCONFIG_ZMK_STUDIO_LOCKING=n, eyelash_sofle_left nice_view_adapter nice_epaper, studio-rpc-usb-uart) (push) Successful in 2m30s
Build ZMK firmware / build (push) Successful in 0s
Also fixes the CI patch step, which is why this branch had been failing since 19 Aug. It shelled out to patch(1), and the zmk-build-arm container does not ship patch -- verified by running the image directly. Both the original unconditional 'patch -p1' and a first attempt at guarding it failed identically, for that reason rather than the one I assumed. Now git apply, which is present in the container, handles plain unified diffs, and works from a directory that is not itself a repository -- base_dir is the west topdir, holding the module repos as subdirectories. Kept idempotent via a reverse --check, since the job restores a west module cache that may already be patched, and a patch that neither applies nor is already applied fails the job rather than silently producing an unpatched build. Only the reverse check is silenced; the forward check keeps its stderr, because swallowing it is what hid 'patch: command not found' in the first place. |
||
| .github/workflows | ||
| boards/shields/eyelash_sofle | ||
| config | ||
| keymap-drawer | ||
| patches | ||
| stock-firmware | ||
| zephyr | ||
| .gitignore | ||
| build.sh | ||
| build.yaml | ||
| flake.lock | ||
| flake.nix | ||
| glyphs.sh | ||
| keymap_drawer.config.yaml | ||
| LICENSE | ||
| README.md | ||
3C Sofle "Eyelash" Firmware (ZMK)
ZMK firmware for a Sofle-style split keyboard sold on AliExpress under the "3C"
brand. Fork of Arawasu/zmk-sofle-eyelash-config,
itself a fork of the vendor's a741725193/zmk-sofle.
🛠️ Hardware Setup
- Controllers: Nice!Nano v2 clones, one per half. Nordic nRF52840,
Board-ID: nRF52840-nicenano, Adafruit UF2 bootloader 0.6.0, SoftDevice S140 6.1.1 — read off the bootloader, not assumed. - Displays: Nice!View on both halves
- Left half: EC11 rotary encoder
- Right half: 5-way hat switch, wired as matrix column 7 across rows 0–4
- Lighting: WS2812 per-key RGB, 29 LEDs per half driven as 7 addressable
zones (hence
chain-length = <7>), plus a single-channel PWM backlight
🪰 Firmware Details
- Base: ZMK, pinned to
v0.3.0— see Building - Layers (4):
qwerty,nav&special,sym,config - Encoder: scroll on
qwertyandconfig, volume onnav&specialandsym - 5-way hat: arrow keys on
qwerty, mouse movement on the other layers - Pointing: mouse movement and scrolling via
CONFIG_ZMK_POINTING, withzip_xy_scaler/zip_scroll_scalerinput processors - ZMK Studio: enabled on the left half over USB and BLE, unlocked
- Displays: battery, Bluetooth profile and layer name on the left
(zmk-nice-oled); Hammerbeam
slideshow on the right (hammerbeam-slideshow),
20s per image. Neither side animates — see
patches/for the local fixups that turn off Luna and slow the slideshow down from its 10-minute default. - The keymap is plain ZMK — no hold-taps, combos or macros, and the only
custom behavior is
scroll_encoder
📺 Display Configuration
Both halves carry the same Nice!View panel; the two shields differ only in what they draw.
- Left (
nice_epaper): battery, Bluetooth profile number + status, layer name - Right (
nice_view_custom): battery, Bluetooth status, Hammerbeam slideshow (20s/image, no animation)
The backlight is on at boot and has no keybinding — the displays were observed to depend on it, so it is left enabled.
🔨 Building
Everything builds locally with Nix — no Docker required. .github/workflows/build.yml
also runs as a Forgejo Actions pipeline on every push, using the same
patches/ fixups as the local builds (see Display Configuration).
draw.yml is disabled there — it calls a cross-repo reusable workflow that
Forgejo cannot expand — so local builds remain the supported path for the
keymap SVG.
nix build .#firmware # all four UF2 images -> ./result/
nix build .#keymap # the keymap SVG -> ./result
nix run .#draw # regenerate the committed keymap-drawer/*.{yaml,svg}
nix run .#flash # build and flash the Studio image
nix develop # dev shell with the ZMK toolchain
nix build .#firmware produces, matching the artifact names in build.yaml:
| image | use |
|---|---|
eyelash_sofle_studio_left.uf2 |
left half — ZMK Studio |
eyelash_sofle_right.uf2 |
right half |
eyelash_sofle_left.uf2 |
left half without Studio |
eyelash_sofle_settings_reset.uf2 |
wipe bonds/settings (either half) |
Individual targets are also exposed: nix build .#studioLeft, .#left,
.#right, .#settingsReset.
Flashing
Double-tap the reset button on a half; it mounts as a NICENANO volume.
Copy the image on and it reboots itself. Flash the right half first.
cp result/eyelash_sofle_right.uf2 /run/media/$USER/NICENANO/
cp result/eyelash_sofle_studio_left.uf2 /run/media/$USER/NICENANO/
/dev/sda is reused between halves, so check which one is mounted before each
copy rather than trusting the device node.
If the halves stop pairing after a firmware change, flash
eyelash_sofle_settings_reset.uf2 to both halves, let each boot once, then
flash the real firmware.
To go back to the firmware the keyboard shipped with, see
stock-firmware/. The two halves differ — check the md5.
ZMK Studio
Connect the left half over USB and open https://zmk.studio in Chrome or
Chromium (WebSerial; Firefox will not work). Two CDC-ACM ports appear — the
Studio RPC endpoint is normally /dev/ttyACM1.
Your user must be able to open the port. On NixOS either add yourself to
dialout and re-login, or scope it:
services.udev.extraRules = ''
SUBSYSTEM=="tty", ATTRS{idVendor}=="1d50", ATTRS{idProduct}=="615e", TAG+="uaccess"
'';
Studio locking is off, so no &studio_unlock binding is needed.
Maintenance
- After editing
config/west.yml, refresh the pinned dependency hash withnix run .#update, or take thegot:value from the failing build and put it inzephyrDepsHashinflake.nix. - After adding a legend that uses a new
$$mdi:...$$icon, run./glyphs.sh. Icons are vendored intokeymap_drawer.config.yamlbecause the Nix sandbox has no network access; the script is idempotent. build.yamland the target list inflake.nixare separate — keep them in step.
Without Nix
./build.sh does the same builds inside ZMK's container image, writing to
firmware/. It keeps its west workspace in ~/.cache/zmk-workspace. Note
that Docker here is rootless, so it deliberately runs as container root.
Note on the ZMK version
config/west.yml still pins ZMK to v0.3.0. The keyboard is now a shield
(boards/shields/eyelash_sofle/) on the stock nice_nano_v2 board, so the
hardware-model-v2 blocker is gone — shields need no HWMv2 changes. Two things
still hold the pin in place:
v0.3.0is still the newest ZMK release. There is no tag after it, so moving off the pin means tracking unreleasedmain.- The two display modules are LVGL 8. ZMK
mainis on Zephyr 4.1, which ships LVGL 9;lv_canvas_draw_*and theLV_IMG_CF_*image descriptors are both gone. Neitherzmk-nice-olednorhammerbeam-slideshowhas been ported upstream, and between them that is ~19k lines with ~70 canvas draw calls and ~300 image descriptors to convert.
So the bump waits on either a ZMK release, an upstream port of the display
modules, or dropping them for ZMK's stock nice_view shield.
📌 Acknowledgements
- Vendor firmware and hardware definition from
a741725193 - Display modules and this config's parent fork from
Arawasu - Luna animation originally from mctechnology17
- Slideshow originally from GPeye
- zmk-nix for the Nix build
- ZMK community for documentation and modules
🖼️ Layout
Regenerate with nix run .#draw after changing the keymap.