hid.go 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227
  1. package hid
  2. import (
  3. "bytes"
  4. "fmt"
  5. "os"
  6. "strings"
  7. "syscall"
  8. "time"
  9. "unsafe"
  10. )
  11. // HID device access via /dev/hidraw* using pure Go syscalls.
  12. // No cgo, no libudev.
  13. const (
  14. ioctlRDGET = 0x80000000 + 1
  15. ioctlWRSET = 0x40000000 + 2
  16. ioctlGET = 0x30000000 + 3
  17. ioctlSET = 0x40000000 + 4
  18. )
  19. // Device represents a connected HID device.
  20. type Device struct {
  21. fd int
  22. path string
  23. vendorID uint16
  24. productID uint16
  25. }
  26. // Devices lists all connected HID devices with VID/PID.
  27. type DevicesInfo struct {
  28. Devices []DeviceInfo
  29. }
  30. // DeviceInfo holds device metadata without open handle.
  31. type DeviceInfo struct {
  32. Path string
  33. VendorID uint16
  34. ProductID uint16
  35. RawHID bool
  36. }
  37. // OpenPath opens a HID device by path (e.g. "/dev/hidraw0").
  38. func OpenPath(path string) (*Device, error) {
  39. fd, err := syscall.Open(path, syscall.O_RDWR, 0)
  40. if err != nil {
  41. return nil, fmt.Errorf("open %s: %w", path, err)
  42. }
  43. return &Device{fd: fd, path: path}, nil
  44. }
  45. // VID returns the vendor ID.
  46. func (d *Device) VID() uint16 { return d.vendorID }
  47. // PID returns the product ID.
  48. func (d *Device) PID() uint16 { return d.productID }
  49. // Path returns the device file path.
  50. func (d *Device) Path() string { return d.path }
  51. // SendReport sends an HID report.
  52. // report[0] must be the report ID (0 for single-report devices).
  53. func (d *Device) SendReport(reportID byte, report []byte) (int, error) {
  54. buf := make([]byte, 1+len(report))
  55. buf[0] = reportID
  56. copy(buf[1:], report)
  57. n, _, errno := syscall.Syscall(syscall.SYS_WRITE, uintptr(d.fd), uintptr(unsafe.Pointer(&buf[0])), uintptr(len(buf)))
  58. if errno != 0 {
  59. return 0, fmt.Errorf("write: %w", errno)
  60. }
  61. return int(n), nil
  62. }
  63. // Read reads a response report.
  64. var hidReadTimeout = 500 * time.Millisecond
  65. func (d *Device) Read(buf []byte) (int, error) {
  66. if len(buf) == 0 {
  67. return 0, nil
  68. }
  69. var readSet syscall.FdSet
  70. readSet.Bits[d.fd/64] |= 1 << uint(d.fd%64)
  71. timeout := syscall.Timeval{
  72. Sec: int64(hidReadTimeout / time.Second),
  73. Usec: int64((hidReadTimeout % time.Second) / time.Microsecond),
  74. }
  75. ready, err := syscall.Select(d.fd+1, &readSet, nil, nil, &timeout)
  76. if err != nil {
  77. return 0, fmt.Errorf("select: %w", err)
  78. }
  79. if ready == 0 {
  80. return 0, fmt.Errorf("read timeout")
  81. }
  82. n, _, errno := syscall.Syscall(syscall.SYS_READ, uintptr(d.fd), uintptr(unsafe.Pointer(&buf[0])), uintptr(len(buf)))
  83. if errno != 0 {
  84. return 0, fmt.Errorf("read: %w", errno)
  85. }
  86. return int(n), nil
  87. }
  88. // Close closes the device.
  89. func (d *Device) Close() error {
  90. return syscall.Close(d.fd)
  91. }
  92. // DiscoverAll returns a list of HID devices with their VID/PID.
  93. // It iterates /dev/hidraw* and reads VID/PID from /sys/class/hidraw/*/device/uevent.
  94. func DiscoverAll() ([]DeviceInfo, error) {
  95. entries, err := os.ReadDir("/dev")
  96. if err != nil {
  97. return nil, fmt.Errorf("read /dev: %w", err)
  98. }
  99. var devices []DeviceInfo
  100. for _, entry := range entries {
  101. if !strings.HasPrefix(entry.Name(), "hidraw") {
  102. continue
  103. }
  104. hidrawPath := "/dev/" + entry.Name()
  105. sysEventPath := "/sys/class/hidraw/" + entry.Name() + "/device/uevent"
  106. data, err := os.ReadFile(sysEventPath)
  107. if err != nil {
  108. fmt.Fprintf(os.Stderr, "DEBUG: %s: uevent read err: %v\n", entry.Name(), err)
  109. continue
  110. }
  111. vid, pid := parseUEVENT(string(data))
  112. if vid == 0 || pid == 0 {
  113. fmt.Fprintf(os.Stderr, "DEBUG: %s: vid=0x%04X pid=0x%04X (zero!)\n", entry.Name(), vid, pid)
  114. continue
  115. }
  116. descriptor, err := os.ReadFile("/sys/class/hidraw/" + entry.Name() + "/device/report_descriptor")
  117. if err != nil {
  118. fmt.Fprintf(os.Stderr, "DEBUG: %s: report descriptor read err: %v\n", entry.Name(), err)
  119. continue
  120. }
  121. devices = append(devices, DeviceInfo{
  122. Path: hidrawPath,
  123. VendorID: vid,
  124. ProductID: pid,
  125. RawHID: isRawHIDDescriptor(descriptor),
  126. })
  127. }
  128. fmt.Fprintf(os.Stderr, "DEBUG: DiscoverAll done, %d devices\n", len(devices))
  129. return devices, nil
  130. }
  131. func isRawHIDDescriptor(data []byte) bool {
  132. inputReport := []byte{
  133. 0x09, 0x62, 0x15, 0x00, 0x26, 0xff, 0x00, 0x95, 0x20, 0x75, 0x08, 0x81, 0x02,
  134. }
  135. outputReport := []byte{
  136. 0x09, 0x63, 0x15, 0x00, 0x26, 0xff, 0x00, 0x95, 0x20, 0x75, 0x08, 0x91, 0x02,
  137. }
  138. usage := []byte{0x06, 0x60, 0xff, 0x09, 0x61, 0xa1, 0x01}
  139. return bytes.Contains(data, usage) && bytes.Contains(data, inputReport) && bytes.Contains(data, outputReport)
  140. }
  141. func parseUEVENT(s string) (vid, pid uint16) {
  142. for _, line := range splitLines(s) {
  143. if len(line) >= 25 && line[:12] == "HID_ID=0003:" {
  144. data := line[12:]
  145. colon := -1
  146. for i, c := range data {
  147. if c == ':' {
  148. colon = i
  149. break
  150. }
  151. }
  152. if colon > 0 && colon+1 < len(data) {
  153. if v, e := parseHex(data[:colon]); e == nil {
  154. vid = v & 0xFFFF
  155. }
  156. if p, e := parseHex(data[colon+1:]); e == nil {
  157. pid = p & 0xFFFF
  158. }
  159. }
  160. }
  161. }
  162. return vid, pid
  163. }
  164. func splitLines(s string) []string {
  165. var lines []string
  166. var current string
  167. for _, c := range s {
  168. if c == '\n' {
  169. lines = append(lines, current)
  170. current = ""
  171. } else {
  172. current += string(c)
  173. }
  174. }
  175. if current != "" {
  176. lines = append(lines, current)
  177. }
  178. return lines
  179. }
  180. func parseHex(s string) (uint16, error) {
  181. var val uint16
  182. for _, c := range s {
  183. val <<= 4
  184. switch {
  185. case c >= '0' && c <= '9':
  186. val |= uint16(c - '0')
  187. case c >= 'a' && c <= 'f':
  188. val |= uint16(c - 'a' + 10)
  189. case c >= 'A' && c <= 'F':
  190. val |= uint16(c - 'A' + 10)
  191. default:
  192. return 0, fmt.Errorf("invalid hex char: %c", c)
  193. }
  194. }
  195. return val, nil
  196. }