hid.go 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  1. package hid
  2. import (
  3. "fmt"
  4. "os"
  5. "strings"
  6. "syscall"
  7. "unsafe"
  8. )
  9. // HID device access via /dev/hidraw* using pure Go syscalls.
  10. // No cgo, no libudev.
  11. const (
  12. ioctlRDGET = 0x80000000 + 1
  13. ioctlWRSET = 0x40000000 + 2
  14. ioctlGET = 0x30000000 + 3
  15. ioctlSET = 0x40000000 + 4
  16. )
  17. // Device represents a connected HID device.
  18. type Device struct {
  19. fd int
  20. path string
  21. vendorID uint16
  22. productID uint16
  23. }
  24. // Devices lists all connected HID devices with VID/PID.
  25. type DevicesInfo struct {
  26. Devices []DeviceInfo
  27. }
  28. // DeviceInfo holds device metadata without open handle.
  29. type DeviceInfo struct {
  30. Path string
  31. VendorID uint16
  32. ProductID uint16
  33. }
  34. // OpenPath opens a HID device by path (e.g. "/dev/hidraw0").
  35. func OpenPath(path string) (*Device, error) {
  36. fd, err := syscall.Open(path, syscall.O_RDWR, 0)
  37. if err != nil {
  38. return nil, fmt.Errorf("open %s: %w", path, err)
  39. }
  40. return &Device{fd: fd, path: path}, nil
  41. }
  42. // VID returns the vendor ID.
  43. func (d *Device) VID() uint16 { return d.vendorID }
  44. // PID returns the product ID.
  45. func (d *Device) PID() uint16 { return d.productID }
  46. // Path returns the device file path.
  47. func (d *Device) Path() string { return d.path }
  48. // SendReport sends an HID report.
  49. // report[0] must be the report ID (0 for single-report devices).
  50. func (d *Device) SendReport(reportID byte, report []byte) (int, error) {
  51. buf := make([]byte, 1+len(report))
  52. buf[0] = reportID
  53. copy(buf[1:], report)
  54. n, _, errno := syscall.Syscall(syscall.SYS_WRITE, uintptr(d.fd), uintptr(unsafe.Pointer(&buf[0])), uintptr(len(buf)))
  55. if errno != 0 {
  56. return 0, fmt.Errorf("write: %w", errno)
  57. }
  58. return int(n), nil
  59. }
  60. // Read reads a response report.
  61. func (d *Device) Read(buf []byte) (int, error) {
  62. n, _, errno := syscall.Syscall(syscall.SYS_READ, uintptr(d.fd), uintptr(unsafe.Pointer(&buf[0])), uintptr(len(buf)))
  63. if errno != 0 {
  64. return 0, fmt.Errorf("read: %w", errno)
  65. }
  66. return int(n), nil
  67. }
  68. // Close closes the device.
  69. func (d *Device) Close() error {
  70. return syscall.Close(d.fd)
  71. }
  72. // DiscoverAll returns a list of HID devices with their VID/PID.
  73. // It iterates /dev/hidraw* and reads VID/PID from /sys/class/hidraw/*/device/uevent.
  74. func DiscoverAll() ([]DeviceInfo, error) {
  75. entries, err := os.ReadDir("/dev")
  76. if err != nil {
  77. return nil, fmt.Errorf("read /dev: %w", err)
  78. }
  79. var devices []DeviceInfo
  80. for _, entry := range entries {
  81. if !strings.HasPrefix(entry.Name(), "hidraw") {
  82. continue
  83. }
  84. hidrawPath := "/dev/" + entry.Name()
  85. sysEventPath := "/sys/class/hidraw/" + entry.Name() + "/device/uevent"
  86. data, err := os.ReadFile(sysEventPath)
  87. if err != nil {
  88. continue
  89. }
  90. vid, pid := parseUEVENT(string(data))
  91. if vid == 0 || pid == 0 {
  92. continue
  93. }
  94. devices = append(devices, DeviceInfo{
  95. Path: hidrawPath,
  96. VendorID: vid,
  97. ProductID: pid,
  98. })
  99. }
  100. return devices, nil
  101. }
  102. func parseUEVENT(s string) (vid, pid uint16) {
  103. for _, line := range splitLines(s) {
  104. if len(line) >= 25 && line[:12] == "HID_ID=0003:" {
  105. data := line[12:]
  106. colon := -1
  107. for i, c := range data {
  108. if c == ':' {
  109. colon = i
  110. break
  111. }
  112. }
  113. if colon > 0 && colon+1 < len(data) {
  114. if v, e := parseHex(data[:colon]); e == nil {
  115. vid = v
  116. }
  117. if p, e := parseHex(data[colon+1:]); e == nil {
  118. pid = p
  119. }
  120. }
  121. }
  122. }
  123. return vid, pid
  124. }
  125. func splitLines(s string) []string {
  126. var lines []string
  127. var current string
  128. for _, c := range s {
  129. if c == '\n' {
  130. lines = append(lines, current)
  131. current = ""
  132. } else {
  133. current += string(c)
  134. }
  135. }
  136. if current != "" {
  137. lines = append(lines, current)
  138. }
  139. return lines
  140. }
  141. func parseHex(s string) (uint16, error) {
  142. var val uint16
  143. for _, c := range s {
  144. val <<= 4
  145. switch {
  146. case c >= '0' && c <= '9':
  147. val |= uint16(c - '0')
  148. case c >= 'a' && c <= 'f':
  149. val |= uint16(c - 'a' + 10)
  150. case c >= 'A' && c <= 'F':
  151. val |= uint16(c - 'A' + 10)
  152. default:
  153. return 0, fmt.Errorf("invalid hex char: %c", c)
  154. }
  155. }
  156. return val, nil
  157. }