| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227 |
- package hid
- import (
- "bytes"
- "fmt"
- "os"
- "strings"
- "syscall"
- "time"
- "unsafe"
- )
- // HID device access via /dev/hidraw* using pure Go syscalls.
- // No cgo, no libudev.
- const (
- ioctlRDGET = 0x80000000 + 1
- ioctlWRSET = 0x40000000 + 2
- ioctlGET = 0x30000000 + 3
- ioctlSET = 0x40000000 + 4
- )
- // Device represents a connected HID device.
- type Device struct {
- fd int
- path string
- vendorID uint16
- productID uint16
- }
- // Devices lists all connected HID devices with VID/PID.
- type DevicesInfo struct {
- Devices []DeviceInfo
- }
- // DeviceInfo holds device metadata without open handle.
- type DeviceInfo struct {
- Path string
- VendorID uint16
- ProductID uint16
- RawHID bool
- }
- // OpenPath opens a HID device by path (e.g. "/dev/hidraw0").
- func OpenPath(path string) (*Device, error) {
- fd, err := syscall.Open(path, syscall.O_RDWR, 0)
- if err != nil {
- return nil, fmt.Errorf("open %s: %w", path, err)
- }
- return &Device{fd: fd, path: path}, nil
- }
- // VID returns the vendor ID.
- func (d *Device) VID() uint16 { return d.vendorID }
- // PID returns the product ID.
- func (d *Device) PID() uint16 { return d.productID }
- // Path returns the device file path.
- func (d *Device) Path() string { return d.path }
- // SendReport sends an HID report.
- // report[0] must be the report ID (0 for single-report devices).
- func (d *Device) SendReport(reportID byte, report []byte) (int, error) {
- buf := make([]byte, 1+len(report))
- buf[0] = reportID
- copy(buf[1:], report)
- n, _, errno := syscall.Syscall(syscall.SYS_WRITE, uintptr(d.fd), uintptr(unsafe.Pointer(&buf[0])), uintptr(len(buf)))
- if errno != 0 {
- return 0, fmt.Errorf("write: %w", errno)
- }
- return int(n), nil
- }
- // Read reads a response report.
- var hidReadTimeout = 500 * time.Millisecond
- func (d *Device) Read(buf []byte) (int, error) {
- if len(buf) == 0 {
- return 0, nil
- }
- var readSet syscall.FdSet
- readSet.Bits[d.fd/64] |= 1 << uint(d.fd%64)
- timeout := syscall.Timeval{
- Sec: int64(hidReadTimeout / time.Second),
- Usec: int64((hidReadTimeout % time.Second) / time.Microsecond),
- }
- ready, err := syscall.Select(d.fd+1, &readSet, nil, nil, &timeout)
- if err != nil {
- return 0, fmt.Errorf("select: %w", err)
- }
- if ready == 0 {
- return 0, fmt.Errorf("read timeout")
- }
- n, _, errno := syscall.Syscall(syscall.SYS_READ, uintptr(d.fd), uintptr(unsafe.Pointer(&buf[0])), uintptr(len(buf)))
- if errno != 0 {
- return 0, fmt.Errorf("read: %w", errno)
- }
- return int(n), nil
- }
- // Close closes the device.
- func (d *Device) Close() error {
- return syscall.Close(d.fd)
- }
- // DiscoverAll returns a list of HID devices with their VID/PID.
- // It iterates /dev/hidraw* and reads VID/PID from /sys/class/hidraw/*/device/uevent.
- func DiscoverAll() ([]DeviceInfo, error) {
- entries, err := os.ReadDir("/dev")
- if err != nil {
- return nil, fmt.Errorf("read /dev: %w", err)
- }
- var devices []DeviceInfo
- for _, entry := range entries {
- if !strings.HasPrefix(entry.Name(), "hidraw") {
- continue
- }
- hidrawPath := "/dev/" + entry.Name()
- sysEventPath := "/sys/class/hidraw/" + entry.Name() + "/device/uevent"
- data, err := os.ReadFile(sysEventPath)
- if err != nil {
- fmt.Fprintf(os.Stderr, "DEBUG: %s: uevent read err: %v\n", entry.Name(), err)
- continue
- }
- vid, pid := parseUEVENT(string(data))
- if vid == 0 || pid == 0 {
- fmt.Fprintf(os.Stderr, "DEBUG: %s: vid=0x%04X pid=0x%04X (zero!)\n", entry.Name(), vid, pid)
- continue
- }
- descriptor, err := os.ReadFile("/sys/class/hidraw/" + entry.Name() + "/device/report_descriptor")
- if err != nil {
- fmt.Fprintf(os.Stderr, "DEBUG: %s: report descriptor read err: %v\n", entry.Name(), err)
- continue
- }
- devices = append(devices, DeviceInfo{
- Path: hidrawPath,
- VendorID: vid,
- ProductID: pid,
- RawHID: isRawHIDDescriptor(descriptor),
- })
- }
- fmt.Fprintf(os.Stderr, "DEBUG: DiscoverAll done, %d devices\n", len(devices))
- return devices, nil
- }
- func isRawHIDDescriptor(data []byte) bool {
- inputReport := []byte{
- 0x09, 0x62, 0x15, 0x00, 0x26, 0xff, 0x00, 0x95, 0x20, 0x75, 0x08, 0x81, 0x02,
- }
- outputReport := []byte{
- 0x09, 0x63, 0x15, 0x00, 0x26, 0xff, 0x00, 0x95, 0x20, 0x75, 0x08, 0x91, 0x02,
- }
- usage := []byte{0x06, 0x60, 0xff, 0x09, 0x61, 0xa1, 0x01}
- return bytes.Contains(data, usage) && bytes.Contains(data, inputReport) && bytes.Contains(data, outputReport)
- }
- func parseUEVENT(s string) (vid, pid uint16) {
- for _, line := range splitLines(s) {
- if len(line) >= 25 && line[:12] == "HID_ID=0003:" {
- data := line[12:]
- colon := -1
- for i, c := range data {
- if c == ':' {
- colon = i
- break
- }
- }
- if colon > 0 && colon+1 < len(data) {
- if v, e := parseHex(data[:colon]); e == nil {
- vid = v & 0xFFFF
- }
- if p, e := parseHex(data[colon+1:]); e == nil {
- pid = p & 0xFFFF
- }
- }
- }
- }
- return vid, pid
- }
- func splitLines(s string) []string {
- var lines []string
- var current string
- for _, c := range s {
- if c == '\n' {
- lines = append(lines, current)
- current = ""
- } else {
- current += string(c)
- }
- }
- if current != "" {
- lines = append(lines, current)
- }
- return lines
- }
- func parseHex(s string) (uint16, error) {
- var val uint16
- for _, c := range s {
- val <<= 4
- switch {
- case c >= '0' && c <= '9':
- val |= uint16(c - '0')
- case c >= 'a' && c <= 'f':
- val |= uint16(c - 'a' + 10)
- case c >= 'A' && c <= 'F':
- val |= uint16(c - 'A' + 10)
- default:
- return 0, fmt.Errorf("invalid hex char: %c", c)
- }
- }
- return val, nil
- }
|