// Package hhkb talks to the vendor HID interface of an HHKB Professional // keyboard (PFU, USB 04fe:0020-0022) over /dev/hidraw, the same channel the // official Keymap Tool uses. It needs no cgo: just file I/O on the device node. package hhkb import ( "fmt" "os" "path/filepath" "strings" ) const reportLen = 64 // Device is an open handle to the keyboard's vendor HID interface. type Device struct { f *os.File product uint16 } // Open finds the HHKB vendor interface and opens it for reading and writing. // // It identifies the interface from sysfs alone: the right hidraw node belongs // to USB 04fe:0020 and carries a report descriptor that begins with // 06 00 ff (Usage Page = Vendor-Defined 0xFF00). The keyboard exposes three // hidraw nodes; only this one accepts keymap commands. func Open() (*Device, error) { nodes, err := filepath.Glob("/sys/class/hidraw/hidraw*") if err != nil { return nil, err } for _, node := range nodes { name := filepath.Base(node) uevent, err := os.ReadFile(filepath.Join(node, "device/uevent")) if err != nil { continue } product, ok := parseProduct(string(uevent)) if !ok { continue } desc, err := os.ReadFile(filepath.Join(node, "device/report_descriptor")) if err != nil || len(desc) < 3 || desc[0] != 0x06 || desc[1] != 0x00 || desc[2] != 0xff { continue } f, err := os.OpenFile("/dev/"+name, os.O_RDWR, 0) if err != nil { return nil, fmt.Errorf("found %s but cannot open it: %w", name, err) } return &Device{f: f, product: product}, nil } return nil, fmt.Errorf("HHKB vendor interface (04fe:0020-0022) not found") } // Close releases the device. func (d *Device) Close() error { return d.f.Close() } // Product is the USB product id of the open device: 0x0020 Classic, // 0x0021 Hybrid, 0x0022 Hybrid Type-S. func (d *Device) Product() uint16 { return d.product } // parseProduct reads the USB product id from a hidraw uevent and reports // whether the node is a supported HHKB (vendor 04fe, product 0020-0022). func parseProduct(uevent string) (uint16, bool) { for _, line := range strings.Split(uevent, "\n") { v, ok := strings.CutPrefix(line, "HID_ID=") if !ok { continue } parts := strings.Split(v, ":") // bus:vendor:product if len(parts) != 3 || !strings.EqualFold(parts[1], "000004FE") { return 0, false } var p uint32 if _, err := fmt.Sscanf(parts[2], "%x", &p); err != nil { return 0, false } if p < 0x0020 || p > 0x0022 { return 0, false } return uint16(p), true } return 0, false } // request builds a 64-byte report: AA AA followed by args, zero-padded. func request(cmd byte, args ...byte) []byte { b := make([]byte, reportLen) b[0], b[1], b[2] = 0xAA, 0xAA, cmd copy(b[3:], args) return b } // send writes one report. hidraw takes a leading report-number byte, which the // kernel discards because this device uses unnumbered reports. func (d *Device) send(report []byte) error { _, err := d.f.Write(append([]byte{0x00}, report...)) return err } // recv reads one 64-byte report. In a reply, the payload starts at byte 6; // bytes 0..1 are an acknowledgement header (55 55). func (d *Device) recv() ([]byte, error) { b := make([]byte, reportLen) n, err := d.f.Read(b) if err != nil { return nil, err } if n != reportLen { return nil, fmt.Errorf("short read: %d/%d bytes", n, reportLen) } return b, nil } // do sends a command and returns its single reply. func (d *Device) do(cmd byte, args ...byte) ([]byte, error) { if err := d.send(request(cmd, args...)); err != nil { return nil, err } return d.recv() }