hid.go 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  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. key := fmt.Sprintf("%04x:%04x", info.VendorID, info.ProductID)
  46. if seen[key] {
  47. return nil
  48. }
  49. seen[key] = true
  50. devices = append(devices, DeviceInfo{
  51. Path: info.Path,
  52. VendorID: info.VendorID,
  53. ProductID: info.ProductID,
  54. RawHID: true,
  55. })
  56. return nil
  57. })
  58. if err != nil {
  59. return nil, fmt.Errorf("enumerate: %w", err)
  60. }
  61. return devices, nil
  62. }
  63. // Enumerate visits each HID device with matching VID/PID.
  64. // On platforms that report multiple interfaces per device,
  65. // only the first match per VID/PID is passed to fn.
  66. func Enumerate(vendorID, productID uint16, fn func(info *DeviceInfo) error) error {
  67. seen := make(map[string]bool)
  68. err := hid.Enumerate(vendorID, productID, func(info *hid.DeviceInfo) error {
  69. if !isRawHID(info) {
  70. return nil
  71. }
  72. key := fmt.Sprintf("%04x:%04x", info.VendorID, info.ProductID)
  73. if seen[key] {
  74. return nil
  75. }
  76. seen[key] = true
  77. return fn(&DeviceInfo{
  78. Path: info.Path,
  79. VendorID: info.VendorID,
  80. ProductID: info.ProductID,
  81. RawHID: true,
  82. })
  83. })
  84. if err != nil {
  85. return err
  86. }
  87. return nil
  88. }
  89. func isRawHID(info *hid.DeviceInfo) bool {
  90. if info.UsagePage != 0xff60 {
  91. return false
  92. }
  93. if info.Usage != 0x61 {
  94. return false
  95. }
  96. return true
  97. }
  98. // Open opens a HID device by VID/PID.
  99. func Open(vendorID, productID uint16) (*Device, error) {
  100. if err := ensureInit(); err != nil {
  101. return nil, fmt.Errorf("init hid: %w", err)
  102. }
  103. dev, err := hid.Open(vendorID, productID, "")
  104. if err != nil {
  105. return nil, fmt.Errorf("open device VID=0x%04x PID=0x%04x: %w", vendorID, productID, err)
  106. }
  107. info, err := dev.GetDeviceInfo()
  108. if err != nil {
  109. dev.Close()
  110. return nil, fmt.Errorf("get device info: %w", err)
  111. }
  112. return &Device{dev: dev, path: info.Path}, nil
  113. }
  114. // OpenPath opens a HID device by path.
  115. func OpenPath(path string) (*Device, error) {
  116. if err := ensureInit(); err != nil {
  117. return nil, fmt.Errorf("init hid: %w", err)
  118. }
  119. dev, err := hid.OpenPath(path)
  120. if err != nil {
  121. return nil, fmt.Errorf("open path %s: %w", path, err)
  122. }
  123. return &Device{dev: dev, path: path}, nil
  124. }
  125. // VID returns the vendor ID.
  126. func (d *Device) VID() uint16 {
  127. info, err := d.dev.GetDeviceInfo()
  128. if err != nil {
  129. return 0
  130. }
  131. return info.VendorID
  132. }
  133. // PID returns the product ID.
  134. func (d *Device) PID() uint16 {
  135. info, err := d.dev.GetDeviceInfo()
  136. if err != nil {
  137. return 0
  138. }
  139. return info.ProductID
  140. }
  141. // Path returns the device path.
  142. func (d *Device) Path() string { return d.path }
  143. // SendReport sends a HID report.
  144. // report[0] must be the report ID (0 for single-report devices).
  145. // Uses Write for output reports (report ID == 0).
  146. func (d *Device) SendReport(reportID byte, report []byte) (int, error) {
  147. n, err := d.dev.Write(report)
  148. if err != nil {
  149. return n, fmt.Errorf("write report: %w", err)
  150. }
  151. return n, nil
  152. }
  153. // Read reads a response report with a timeout.
  154. var hidReadTimeout = 500 * time.Millisecond
  155. func (d *Device) Read(buf []byte) (int, error) {
  156. n, err := d.dev.ReadWithTimeout(buf, hidReadTimeout)
  157. if err == hid.ErrTimeout {
  158. return 0, fmt.Errorf("read timeout")
  159. }
  160. if err != nil {
  161. return n, fmt.Errorf("read: %w", err)
  162. }
  163. return n, nil
  164. }
  165. // Close closes the device.
  166. func (d *Device) Close() error {
  167. return d.dev.Close()
  168. }