124 lines
3.5 KiB
Go
124 lines
3.5 KiB
Go
// 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 <cmd> 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()
|
|
}
|