hid.go 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
  1. package hid
  2. import (
  3. "fmt"
  4. "sync"
  5. "time"
  6. "github.com/sstallion/go-hid"
  7. )
  8. // Device represents a connected HID device.
  9. type Device struct {
  10. dev *hid.Device
  11. path string
  12. }
  13. // DeviceInfo holds device metadata without open handle.
  14. type DeviceInfo struct {
  15. Path string
  16. VendorID uint16
  17. ProductID uint16
  18. RawHID bool
  19. }
  20. // DevicesInfo lists all connected HID devices.
  21. type DevicesInfo struct {
  22. Devices []DeviceInfo
  23. }
  24. var initOnce sync.Once
  25. func ensureInit() error {
  26. var err error
  27. initOnce.Do(func() {
  28. err = hid.Init()
  29. })
  30. return err
  31. }
  32. // DiscoverAll returns a list of all connected HID devices with VID/PID.
  33. // On platforms that report multiple interfaces per device (e.g. macOS),
  34. // only one entry per VID/PID is returned.
  35. func DiscoverAll() ([]DeviceInfo, error) {
  36. if err := ensureInit(); err != nil {
  37. return nil, fmt.Errorf("init hid: %w", err)
  38. }
  39. seen := make(map[string]bool)
  40. var devices []DeviceInfo
  41. err := hid.Enumerate(hid.VendorIDAny, hid.ProductIDAny, func(info *hid.DeviceInfo) error {
  42. if !isRawHID(info) {
  43. return nil
  44. }
  45. if seen[info.Path] {
  46. return nil
  47. }
  48. seen[info.Path] = true
  49. devices = append(devices, DeviceInfo{
  50. Path: info.Path,
  51. VendorID: info.VendorID,
  52. ProductID: info.ProductID,
  53. RawHID: true,
  54. })
  55. return nil
  56. })
  57. if err != nil {
  58. return nil, fmt.Errorf("enumerate: %w", err)
  59. }
  60. return devices, nil
  61. }
  62. // Enumerate visits each HID device with matching VID/PID.
  63. // On platforms that report multiple interfaces per device,
  64. // only the first match per VID/PID is passed to fn.
  65. func Enumerate(vendorID, productID uint16, fn func(info *DeviceInfo) error) error {
  66. seen := make(map[string]bool)
  67. err := hid.Enumerate(vendorID, productID, func(info *hid.DeviceInfo) error {
  68. if !isRawHID(info) {
  69. return nil
  70. }
  71. if seen[info.Path] {
  72. return nil
  73. }
  74. seen[info.Path] = true
  75. return fn(&DeviceInfo{
  76. Path: info.Path,
  77. VendorID: info.VendorID,
  78. ProductID: info.ProductID,
  79. RawHID: true,
  80. })
  81. })
  82. if err != nil {
  83. return err
  84. }
  85. return nil
  86. }
  87. func isRawHID(info *hid.DeviceInfo) bool {
  88. if info.UsagePage != 0xff60 {
  89. return false
  90. }
  91. if info.Usage != 0x61 {
  92. return false
  93. }
  94. return true
  95. }
  96. // Open opens a HID device by VID/PID.
  97. func Open(vendorID, productID uint16) (*Device, error) {
  98. if err := ensureInit(); err != nil {
  99. return nil, fmt.Errorf("init hid: %w", err)
  100. }
  101. dev, err := hid.Open(vendorID, productID, "")
  102. if err != nil {
  103. return nil, fmt.Errorf("open device VID=0x%04x PID=0x%04x: %w", vendorID, productID, err)
  104. }
  105. info, err := dev.GetDeviceInfo()
  106. if err != nil {
  107. dev.Close()
  108. return nil, fmt.Errorf("get device info: %w", err)
  109. }
  110. return &Device{dev: dev, path: info.Path}, nil
  111. }
  112. // OpenPath opens a HID device by path.
  113. func OpenPath(path string) (*Device, error) {
  114. if err := ensureInit(); err != nil {
  115. return nil, fmt.Errorf("init hid: %w", err)
  116. }
  117. dev, err := hid.OpenPath(path)
  118. if err != nil {
  119. return nil, fmt.Errorf("open path %s: %w", path, err)
  120. }
  121. return &Device{dev: dev, path: path}, nil
  122. }
  123. // VID returns the vendor ID.
  124. func (d *Device) VID() uint16 {
  125. info, err := d.dev.GetDeviceInfo()
  126. if err != nil {
  127. return 0
  128. }
  129. return info.VendorID
  130. }
  131. // PID returns the product ID.
  132. func (d *Device) PID() uint16 {
  133. info, err := d.dev.GetDeviceInfo()
  134. if err != nil {
  135. return 0
  136. }
  137. return info.ProductID
  138. }
  139. // Path returns the device path.
  140. func (d *Device) Path() string { return d.path }
  141. // SendReport sends a HID report.
  142. // report[0] must be the report ID (0 for single-report devices).
  143. // Uses Write for output reports (report ID == 0).
  144. func (d *Device) SendReport(reportID byte, report []byte) (int, error) {
  145. n, err := d.dev.Write(report)
  146. if err != nil {
  147. return n, fmt.Errorf("write report: %w", err)
  148. }
  149. return n, nil
  150. }
  151. // Read reads a response report with a timeout.
  152. var hidReadTimeout = 500 * time.Millisecond
  153. func (d *Device) Read(buf []byte) (int, error) {
  154. n, err := d.dev.ReadWithTimeout(buf, hidReadTimeout)
  155. if err == hid.ErrTimeout {
  156. return 0, fmt.Errorf("read timeout")
  157. }
  158. if err != nil {
  159. return n, fmt.Errorf("read: %w", err)
  160. }
  161. return n, nil
  162. }
  163. // Close closes the device.
  164. func (d *Device) Close() error {
  165. return d.dev.Close()
  166. }