Low-profile sofle (eyelash) ZMK config (migrated from GitHub)
  • Nix 55.6%
  • Shell 44.4%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
ma3s7r0 07b8c0ed02
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
displays: drop Luna animation, show BT profile number, slow slideshow to 20s
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.
2026-08-27 23:44:20 +02:00
.github/workflows displays: drop Luna animation, show BT profile number, slow slideshow to 20s 2026-08-27 23:44:20 +02:00
boards/shields/eyelash_sofle displays: drop Luna animation, show BT profile number, slow slideshow to 20s 2026-08-27 23:44:20 +02:00
config fix(keymap): the euro binding never sent anything 2026-08-27 23:13:07 +02:00
keymap-drawer flake: build firmware and draw the keymap with Nix 2026-08-18 19:43:56 +02:00
patches displays: drop Luna animation, show BT profile number, slow slideshow to 20s 2026-08-27 23:44:20 +02:00
stock-firmware stock-firmware: commit the vendor firmware backups 2026-08-19 19:34:31 +02:00
zephyr Initial commit 2025-04-17 13:46:03 +02:00
.gitignore Fix build: pin ZMK to v0.3, de-duplicate modules, add local build 2026-08-18 17:56:27 +02:00
build.sh displays: drop Luna animation, show BT profile number, slow slideshow to 20s 2026-08-27 23:44:20 +02:00
build.yaml board: convert eyelash_sofle from HWMv1 board to a nice_nano_v2 shield 2026-08-19 11:05:21 +02:00
flake.lock flake: build firmware and draw the keymap with Nix 2026-08-18 19:43:56 +02:00
flake.nix displays: drop Luna animation, show BT profile number, slow slideshow to 20s 2026-08-27 23:44:20 +02:00
glyphs.sh flake: build firmware and draw the keymap with Nix 2026-08-18 19:43:56 +02:00
keymap_drawer.config.yaml Remove dead vendored files 2026-08-18 19:49:03 +02:00
LICENSE Initial commit 2025-04-17 13:46:03 +02:00
README.md displays: drop Luna animation, show BT profile number, slow slideshow to 20s 2026-08-27 23:44:20 +02:00

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 04
  • 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 qwerty and config, volume on nav&special and sym
  • 5-way hat: arrow keys on qwerty, mouse movement on the other layers
  • Pointing: mouse movement and scrolling via CONFIG_ZMK_POINTING, with zip_xy_scaler / zip_scroll_scaler input 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 with nix run .#update, or take the got: value from the failing build and put it in zephyrDepsHash in flake.nix.
  • After adding a legend that uses a new $$mdi:...$$ icon, run ./glyphs.sh. Icons are vendored into keymap_drawer.config.yaml because the Nix sandbox has no network access; the script is idempotent.
  • build.yaml and the target list in flake.nix are 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.0 is still the newest ZMK release. There is no tag after it, so moving off the pin means tracking unreleased main.
  • The two display modules are LVGL 8. ZMK main is on Zephyr 4.1, which ships LVGL 9; lv_canvas_draw_* and the LV_IMG_CF_* image descriptors are both gone. Neither zmk-nice-oled nor hammerbeam-slideshow has 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

Keymap Layout

Regenerate with nix run .#draw after changing the keymap.