hid.go 4.4 KB

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