Files
hkkb-layout-web/doc/protocol.md
2026-05-27 18:50:35 +09:00

3.1 KiB
Raw Blame History

HHKB Professional vendor HID protocol

Reverse-engineered notes for the configuration channel used by PFU's Keymap Tool, as implemented in internal/hhkb. Verified against the HHKB Professional Classic (04fe:0020); the Hybrid (0021/0022) is the same.

Transport

The keyboard exposes three USB-HID interfaces. Interface 2 is vendor-defined and carries the config protocol. Its report descriptor begins:

06 00 ff   Usage Page (Vendor-Defined 0xFF00)
09 01      Usage 1
a1 01      Collection (Application)
09 02 ... 75 08 95 40 81 02   Input  report, 64 bytes
09 03 ... 75 08 95 40 91 02   Output report, 64 bytes
c0

We find it by scanning /sys/class/hidraw/*: the node whose device/uevent contains HID id …04FE:00000020 and whose device/report_descriptor starts with 06 00 ff.

Reports are 64 bytes with no report id. A write(2) to the hidraw node takes a leading 0x00 report-number byte (65 bytes total); the kernel strips it.

Framing

Request (64 bytes):

byte 0   0xAA
byte 1   0xAA
byte 2   command
byte 3+  arguments

Reply (64 bytes):

byte 0   0x55       acknowledgement
byte 1   0x55
byte 2+  status / payload   (data fields start at byte 6)

Commands

id name request args reply payload (from byte 6)
1 notify app 00 01 <closed>
2 get info type[20] rev[4] serial[16] fw…
3 factory reset header 55 55 03 00 on success
4 confirm keymap
5 get DIP 6 bytes, one per switch
6 get mode 1 byte: 0 HHK, 1 Mac, 2 Lite, 3 Secret
7 reset DIP 00 01
134 write keymap see below
135 get keymap 00 02 <mode> <fn> streamed, see below

Firmware commands (208, 224231) exist but are deliberately unimplemented here.

Keymap layout

A layer is 128 bytes: layer[keyNumber] = scancode. Key numbers and the US (ANSI) geometry live in keymap.go (ANSI). <fn> selects base (0) or Fn (1).

Read (command 135)

One request, then three input reports tiling the layer (data at byte 6 of each):

report 1  -> layer[0:58]
report 2  -> layer[58:116]
report 3  -> layer[116:128]

Write (command 134)

Three requests. The pair after the command id marks offset/length; the first pass also carries mode and fn before the key bytes:

AA AA 86  41 3B  <mode> <fn>  layer[0:57]
AA AA 86  82 3B               layer[57:116]
AA AA 86  C3 0C               layer[116:128]

A full remap is: notify-app(open) → get mode → read layer → set the byte → write (3 passes) → confirm (4) → reset DIP (7) → notify-app(closed). Reversible via factory reset (3).