A tiny, dependency-free CLI to reconfigure PCsensor ElfKey devices on Linux
— developed against the MK424BT 4-key macropad (USB 3553:c140).
PCsensor's newer devices (VID 3553) speak the ElfKey protocol, which the
popular rgerganov/footswitch tool
does not support (it covers 3553:b001 and the older 0c45/413d
pedals). elfctl implements the ElfKey protocol directly over raw
/dev/hidraw — no hidapi, no libusb, just a single C file.
makeelfctl list # identify the device (model, firmware, enabled layers)
elfctl get # show current bindings (all layers)
elfctl get 2 # show just layer 2
elfctl set 1 f13 # set key 1 to F13 (layer 1)
elfctl set 2 ctrl-c # modifiers: ctrl- shift- alt- gui- (r* = right)
elfctl set 2:1 g # set layer 2, key 1 to 'g' (L:K syntax)
elfctl layers # show how many layers are enabled
elfctl layers 3 # enable all 3 layers (so the S button cycles them)
elfctl switch 2 # switch the active layer (software S-button press)
elfctl save my.conf # dump config incl. layers to a file (or stdout)
elfctl load my.conf # apply a config file
elfctl keys # list every supported key/modifier name
elfctl --version # print the elfctl versionThe MK424BT stores 3 layers of key bindings. The small round S button cycles the active layer among those that are enabled, and the backlight shows which is active: red = layer 1, green = layer 2, blue = layer 3.
At the factory only layer 1 is enabled, so pressing S appears to do nothing (it blinks the LED but has nowhere to switch). Enable the others first:
elfctl layers 3 # enable layers 1+2+3
elfctl set 2:1 f13 # give layer 2 its own bindings
elfctl set 3:1 gui-lelfctl layers 1 returns to the factory single-layer behaviour (bindings are
preserved in the device; they just stop being reachable via S). Layer/key
addressing is L:K everywhere a key is taken; a bare K means layer 1.
A single key with optional modifier prefixes joined by - or +:
- Keys:
a–z,0–9,f1–f24,enteresctabspacebackspacearrows (up/down/left/right),home/end/pageup/…, and common punctuation names (minus,equal,slash, …). - Modifiers:
ctrlshiftaltgui(=super/win), each with anr-prefixed right-hand variant (rctrl,ralt/altgr, …).
Examples: f13, ctrl-c, shift-tab, gui-l, ctrl-shift-esc.
Run elfctl keys for the full, authoritative list (it's generated from the
parser's own tables, so it always matches what set accepts).
# blanks and #-comments ignored
layers = 3 # how many layers to enable (1..3); optional
key1 = f13 # bare keyN targets layer 1 (backward compatible)
key2 = ctrl-c
key3 = gui-l
key4 = f14
layer2.key1 = g # higher layers use layerL.keyN
layer2.key2 = h
layer3.key1 = m
elfctl save emits exactly this format (a layers = N line plus one block per
layer), so save/load round-trips the full multi-layer config.
The config interface's hidraw node is root-only by default. Install the
bundled rule to grant the local user access (matched on 3553:c140):
sudo make install-udev # copies udev/60-elfctl.rules, reloads, re-triggersIt uses uaccess (logind seat ownership) with a wheel group fallback.
- Config happens on the OUT-endpoint HID interface (
bInterfaceNumber == 1);elfctlfinds it via sysfs, robust againsthidrawrenumbering. - The interface declares an unnumbered 8-byte report, so each
write()is prefixed with a0x00report number (kernel-stripped); the protocol's own leading0x01is the first payload byte. - Key read response is framed
[len=0x04, count, modifier, keycode]. After a set, the device emits an ACK report (byte[0]=0x81) whichelfctlskips so it always verifies against a real read-back. - Layers are addressed as device index
(layer-1)*16 + key(layer 1 → indices 1–4, layer 2 → 17–20, layer 3 → 33–36) using the same set/read-key opcodes. Which layers are enabled is a separate bitmask set by0xD2(0x01/0x03/0x07) and read by0xD3; the active layer is read by0xD1and switched by0xD4. Seedocs/PROTOCOL.mdfor the full byte-level writeup. elfctlonly ever emits read/set-key (0x82/0x83/0x81) and the layer opcodes (0xD1–0xD4). It never sends the firmware-flash (0x20) or set-model (0x60) opcodes that also live in this protocol family.
Single-key and shortcut (modifier+key) bindings work across all 3 layers and are verified by read-back; enabling/switching layers works (verified on hardware — the S button cycles enabled layers, LED red/green/blue). Macros / typed strings (the ElfKey multi-report macro family) and LED-color / Bluetooth-name / sleep-timeout settings are not implemented yet.
The git tag is the source of truth for the version (v-prefixed, e.g.
v0.1.0); the bare number is mirrored in ELFCTL_VERSION in elfctl.c and
reported by elfctl --version. Releases are cut with the bundled /release
Claude Code skill (.claude/skills/release/): /release patch|minor|major
bumps the latest tag, drags the macro along, pushes master, and creates the
GitHub release with an oldest-first changelog.
elfctl.c— the tool.docs/PROTOCOL.md— byte-level protocol writeup (opcodes, layer layout, the S-button notification, the layer-enable command and its provenance).docs/*.c— diagnostic / reverse-engineering harnesses, kept as reference for how the protocol was worked out. Build any of them withmake <name>(or all withmake diag); binaries land in the repo root. They are:probe(read-only model+key probe),readsweep(read-only index sweep that revealed the layer layout),layerprobe(read-only layer-addressing probe),listen(passive dual-interface listener),experiment/layerwrite(recoverable key-write harnesses), andlayerctl(drives the0xD1–0xD4layer opcodes).udev/60-elfctl.rules— device-access rule..claude/skills/release/— the/releaseskill.
