FN+middle was scrolling same as plain middle because the FN-alt path always converted TrackPoint to wheel/pan. Now FN+middle jumps to normal mouse passthrough (REL_X/REL_Y + BTN_MIDDLE), giving proper middle-drag behavior. FN without middle still does back/forward + scroll. Verified on real hardware via evtest on higgs (Pi 5). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ku1255cfw
Custom open firmware for the Lenovo KU-1255 compact USB keyboard
Overview
The Lenovo ThinkPad Compact USB Keyboard with TrackPoint (KU-1255) uses a Sonix SN8F2288FG 8-bit MCU with 12K words (24KB) flash and 512 bytes RAM. The TrackPoint is connected via bit-banged I2C (P2.4 SCL, P2.5 SDA, address 0x2A, Synaptics proprietary protocol).
This firmware is a from-scratch rewrite based on the original by ranma, adding standalone middle-button scroll and other missing features so the keyboard works fully without an external converter.
Features added over base firmware
Middle-button scroll
3-state state machine (IDLE / UNDECIDED / SCROLLING) with 150ms timeout:
- Short press (<150ms): sends a normal middle click (deferred on release)
- Hold + TrackPoint movement: converts XY deltas to scroll wheel events
- FN + middle button: passes middle button through directly (no scroll logic)
CapsLock LED feedback
Host LED output reports (SET_REPORT) are parsed and CapsLock state (bit 1) drives the power LED on P5.3/PWM0 (active-low). The flasher magic byte sequence detection is preserved.
FN+F7 through FN+F12
| Key | Normal | With FN held |
|---|---|---|
| F7 | F7 | LGUI+P (display settings) |
| F8 | F8 | F8 (passthrough) |
| F9 | F9 | LGUI+I (settings) |
| F10 | F10 | LGUI (search) |
| F11 | F11 | LCTRL+LALT+TAB (task switch) |
| F12 | F12 | F12 (passthrough) |
USB compliance fixes
- HID GET_REPORT: returns proper per-interface empty reports (8B keyboard / 5B mouse) instead of stale buffer contents. Some OSes query this on resume from suspend.
- SET/CLEAR FEATURE: tracks DEVICE_REMOTE_WAKEUP state instead of just ACKing.
Flash space
| Firmware | Words used | Free | Utilization |
|---|---|---|---|
| OEM | 10,226 / 10,239 | 13 | 99.9% |
| This | 10,238 / 10,239 | 1 | 100.0% |
Key debouncing is not implemented (and not needed) — the 8ms scan cycle naturally debounces scissor switches (<1ms bounce time). The OEM firmware also does not debounce.
Flashing
Requires vpelletier/dissn8 tools.
- Build:
asn8 main.s -o ku1255cfw.bin - Enter bootloader: hold Return while plugging in the keyboard
- Flash:
flashsn8 ku1255cfw.bin
Simulator testing
test_scroll.py runs 23 automated tests against the
dissn8 simulator via the ku1255_sim.py
harness. Tests cover all scroll state transitions, deferred click timing, FN modifier
interaction, and edge cases (rapid clicks, timeout behavior).
Note: The simulator requires a fix for PnUR register read handlers (see below).
Simulator fixes (for vpelletier/dissn8)
Running this firmware in the dissn8 simulator exposed three bugs:
-
PnUR read handlers crash —
_volatile_dictmaps P0UR-P5UR read handlers toNone. B0BSET/B0BCLR (read-modify-write) on these registers causesTypeError: 'NoneType' object is not callable. Fixed by addingreadPullUp()returning the latch value. Branch:pnur_readpullup(PR pending upstream). -
Hardcoded HID descriptor sizes —
ku1255_sim.pyassumed 0x51/0xD3 byte HID report descriptors (OEM sizes). This firmware uses 91/61 bytes. Fixed by parsing sizes dynamically from the config descriptor. -
Wrong HID descriptor recipient — HID report descriptor requests must use interface recipient (0x81), not device (0x80), per USB spec. The OEM firmware happened to accept both.
Dev setup
- OpenViszla USB protocol analyzer
- 5V-tolerant PL2303 UART interface (e.g. https://www.adafruit.com/product/954)
- S15 pad (SN8F2288 UTX) connected to header for UART debug interface
