first commit
This commit is contained in:
123
internal/hhkb/device.go
Normal file
123
internal/hhkb/device.go
Normal file
@@ -0,0 +1,123 @@
|
||||
// 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()
|
||||
}
|
||||
Reference in New Issue
Block a user