hid.go 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263
  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. // UsagePage and Usage are the HID collection this entry describes. A
  20. // keyboard reports itself as several collections over one path, one per
  21. // top-level collection in its descriptor: a keyboard, a consumer control, a
  22. // vendor page. macOS reports every one of them separately, Linux one
  23. // interface at a time, and the QMK Raw HID collection is the one with this
  24. // pair. They travel with the entry because an entry that was not taken is
  25. // only explainable by the pair it was rejected for.
  26. UsagePage uint16
  27. Usage uint16
  28. // ProductString is the USB product string, which is where a keyboard states
  29. // its own model name. Firmware chooses it, so it can be empty.
  30. ProductString string
  31. }
  32. // rawHIDUsagePage and rawHIDUsage are the QMK Raw HID collection: the signature
  33. // qmk/qmk_udev matches on, and the only one this tool drives. A board that
  34. // declares a different pair is a board this tool cannot address, and saying so
  35. // is the point of reporting the pair a rejected device came with.
  36. const (
  37. rawHIDUsagePage uint16 = 0xff60
  38. rawHIDUsage uint16 = 0x61
  39. )
  40. // DevicesInfo lists all connected HID devices.
  41. type DevicesInfo struct {
  42. Devices []DeviceInfo
  43. }
  44. var initOnce sync.Once
  45. func ensureInit() error {
  46. var err error
  47. initOnce.Do(func() {
  48. err = hid.Init()
  49. })
  50. return err
  51. }
  52. // DiscoverAll returns a list of all connected HID devices with VID/PID.
  53. // On platforms that report multiple interfaces per device (e.g. macOS),
  54. // only one entry per VID/PID is returned.
  55. func DiscoverAll() ([]DeviceInfo, error) {
  56. if err := ensureInit(); err != nil {
  57. return nil, fmt.Errorf("init hid: %w", err)
  58. }
  59. seen := make(map[string]bool)
  60. var devices []DeviceInfo
  61. err := hid.Enumerate(hid.VendorIDAny, hid.ProductIDAny, func(info *hid.DeviceInfo) error {
  62. if !isRawHID(info) {
  63. return nil
  64. }
  65. if seen[info.Path] {
  66. return nil
  67. }
  68. seen[info.Path] = true
  69. devices = append(devices, DeviceInfo{
  70. Path: info.Path,
  71. VendorID: info.VendorID,
  72. ProductID: info.ProductID,
  73. ProductString: info.ProductStr,
  74. RawHID: true,
  75. UsagePage: info.UsagePage,
  76. Usage: info.Usage,
  77. })
  78. return nil
  79. })
  80. if err != nil {
  81. return nil, fmt.Errorf("enumerate: %w", err)
  82. }
  83. return devices, nil
  84. }
  85. // DiscoverEvery returns every HID collection the system reports, the one this
  86. // tool drives included. It exists so that a device which was not taken can be
  87. // explained: no keyboard found and a keyboard whose collection is not the QMK
  88. // Raw HID one look identical from outside, and only the second is worth telling
  89. // a user about.
  90. //
  91. // A collection is the unit here, not a device. One path appears once per usage
  92. // pair, so a keyboard with a keyboard collection and a vendor collection is two
  93. // entries, and each carries the pair it was rejected for. Order is the
  94. // enumeration order, which nothing may sort: these entries carry no number a user
  95. // could pass to a command, so they have no order to promise.
  96. func DiscoverEvery() ([]DeviceInfo, error) {
  97. if err := ensureInit(); err != nil {
  98. return nil, fmt.Errorf("init hid: %w", err)
  99. }
  100. seen := make(map[string]bool)
  101. var devices []DeviceInfo
  102. err := hid.Enumerate(hid.VendorIDAny, hid.ProductIDAny, func(info *hid.DeviceInfo) error {
  103. // The pair belongs in the key as well as the path: a keyboard reports
  104. // several collections on one path, and the same collection listed twice
  105. // is one entry.
  106. key := fmt.Sprintf("%s/%04X/%04X", info.Path, info.UsagePage, info.Usage)
  107. if seen[key] {
  108. return nil
  109. }
  110. seen[key] = true
  111. devices = append(devices, DeviceInfo{
  112. Path: info.Path,
  113. VendorID: info.VendorID,
  114. ProductID: info.ProductID,
  115. ProductString: info.ProductStr,
  116. RawHID: isRawHID(info),
  117. UsagePage: info.UsagePage,
  118. Usage: info.Usage,
  119. })
  120. return nil
  121. })
  122. if err != nil {
  123. return nil, fmt.Errorf("enumerate: %w", err)
  124. }
  125. return devices, nil
  126. }
  127. // Enumerate visits each HID device with matching VID/PID.
  128. // On platforms that report multiple interfaces per device,
  129. // only the first match per VID/PID is passed to fn.
  130. func Enumerate(vendorID, productID uint16, fn func(info *DeviceInfo) error) error {
  131. seen := make(map[string]bool)
  132. err := hid.Enumerate(vendorID, productID, func(info *hid.DeviceInfo) error {
  133. if !isRawHID(info) {
  134. return nil
  135. }
  136. if seen[info.Path] {
  137. return nil
  138. }
  139. seen[info.Path] = true
  140. return fn(&DeviceInfo{
  141. Path: info.Path,
  142. VendorID: info.VendorID,
  143. ProductID: info.ProductID,
  144. ProductString: info.ProductStr,
  145. RawHID: true,
  146. UsagePage: info.UsagePage,
  147. Usage: info.Usage,
  148. })
  149. })
  150. if err != nil {
  151. return err
  152. }
  153. return nil
  154. }
  155. // isRawHID reports whether a HID collection is the QMK Raw HID one, the signature
  156. // qmk/qmk_udev matches on. It is an exact pair on purpose: a vendor page that
  157. // happens to be near it is not a raw HID interface, and a board that puts its
  158. // lighting on another page is a board this tool cannot address rather than one it
  159. // may guess at.
  160. func isRawHID(info *hid.DeviceInfo) bool {
  161. return info.UsagePage == rawHIDUsagePage && info.Usage == rawHIDUsage
  162. }
  163. // Open opens a HID device by VID/PID.
  164. func Open(vendorID, productID uint16) (*Device, error) {
  165. if err := ensureInit(); err != nil {
  166. return nil, fmt.Errorf("init hid: %w", err)
  167. }
  168. dev, err := hid.Open(vendorID, productID, "")
  169. if err != nil {
  170. return nil, fmt.Errorf("open device VID=0x%04x PID=0x%04x: %w", vendorID, productID, err)
  171. }
  172. info, err := dev.GetDeviceInfo()
  173. if err != nil {
  174. dev.Close()
  175. return nil, fmt.Errorf("get device info: %w", err)
  176. }
  177. return &Device{dev: dev, path: info.Path}, nil
  178. }
  179. // OpenPath opens a HID device by path.
  180. func OpenPath(path string) (*Device, error) {
  181. if err := ensureInit(); err != nil {
  182. return nil, fmt.Errorf("init hid: %w", err)
  183. }
  184. dev, err := hid.OpenPath(path)
  185. if err != nil {
  186. return nil, fmt.Errorf("open path %s: %w", path, err)
  187. }
  188. return &Device{dev: dev, path: path}, nil
  189. }
  190. // VID returns the vendor ID.
  191. func (d *Device) VID() uint16 {
  192. info, err := d.dev.GetDeviceInfo()
  193. if err != nil {
  194. return 0
  195. }
  196. return info.VendorID
  197. }
  198. // PID returns the product ID.
  199. func (d *Device) PID() uint16 {
  200. info, err := d.dev.GetDeviceInfo()
  201. if err != nil {
  202. return 0
  203. }
  204. return info.ProductID
  205. }
  206. // Path returns the device path.
  207. func (d *Device) Path() string { return d.path }
  208. // SendReport sends a HID report.
  209. // report[0] must be the report ID (0 for single-report devices).
  210. // Uses Write for output reports (report ID == 0).
  211. func (d *Device) SendReport(reportID byte, report []byte) (int, error) {
  212. n, err := d.dev.Write(report)
  213. if err != nil {
  214. return n, fmt.Errorf("write report: %w", err)
  215. }
  216. return n, nil
  217. }
  218. // Read reads a response report with a timeout.
  219. var hidReadTimeout = 500 * time.Millisecond
  220. func (d *Device) Read(buf []byte) (int, error) {
  221. n, err := d.dev.ReadWithTimeout(buf, hidReadTimeout)
  222. if err == hid.ErrTimeout {
  223. return 0, fmt.Errorf("read timeout")
  224. }
  225. if err != nil {
  226. return n, fmt.Errorf("read: %w", err)
  227. }
  228. return n, nil
  229. }
  230. // Close closes the device.
  231. func (d *Device) Close() error {
  232. return d.dev.Close()
  233. }