device.go 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
  1. package device
  2. import (
  3. "fmt"
  4. "sort"
  5. "netdome.biz/paul/qmk-rgb/internal/hid"
  6. )
  7. // Device is a connected QMK Raw HID keyboard.
  8. // Index is 1-based and is what --device accepts.
  9. type Device struct {
  10. Index int `json:"index"`
  11. Path string `json:"path"`
  12. VendorID uint16 `json:"vendorId"`
  13. ProductID uint16 `json:"productId"`
  14. Name string `json:"name,omitempty"`
  15. }
  16. // DiscoverAll returns every connected QMK Raw HID keyboard, ordered
  17. // deterministically and numbered from 1. A keyboard that does not report a USB
  18. // product string comes back with no Name; that costs the name and nothing else.
  19. func DiscoverAll() ([]Device, error) {
  20. infos, err := hid.DiscoverAll()
  21. if err != nil {
  22. return nil, fmt.Errorf("discover qmk devices: %w", err)
  23. }
  24. devices := make([]Device, 0, len(infos))
  25. for _, info := range infos {
  26. devices = append(devices, Device{
  27. Path: info.Path,
  28. VendorID: info.VendorID,
  29. ProductID: info.ProductID,
  30. Name: info.ProductString,
  31. })
  32. }
  33. return indexDevices(devices), nil
  34. }
  35. // UsagePair is one HID collection of a device: its usage page and its usage.
  36. // Both are needed to describe a collection, because a keyboard reports several
  37. // on one device and the page alone does not tell them apart.
  38. type UsagePair struct {
  39. UsagePage uint16 `json:"usagePage"`
  40. Usage uint16 `json:"usage"`
  41. }
  42. // HIDDevice is a connected device whose HID collections are not the QMK Raw HID
  43. // one, which is what a discovery failure has to name: a board that is connected
  44. // but not reachable is a different fact from a board that is not connected, and
  45. // the usage pages are the only thing that tells the two apart.
  46. //
  47. // It is per device rather than per collection, because a device that is not
  48. // reachable usually has several collections and one line per collection buries
  49. // the one line that says something. A keyboard over USB typically has five: a
  50. // keyboard page, two consumer-control pages, mouse and a vendor page. What is
  51. // missing is the vendor page at 0xFF60.
  52. //
  53. // There is deliberately no Index and no Path. A number would look like something
  54. // --device accepts, and nothing accepts these.
  55. type HIDDevice struct {
  56. VendorID uint16 `json:"vendorId"`
  57. ProductID uint16 `json:"productId"`
  58. Name string `json:"name,omitempty"`
  59. UsagePairs []UsagePair `json:"usagePages"`
  60. }
  61. // DiscoverOther returns every connected HID device that is not a QMK Raw HID
  62. // keyboard, each with the usage pages it does expose. Devices this tool drives
  63. // are left out: they are what DiscoverAll numbers, and a list repeating them
  64. // would suggest a second way to select a keyboard.
  65. //
  66. // Order is the order the HID layer reported the devices in, and the pairs within
  67. // a device are in the order they were reported. Neither is sorted, because
  68. // neither carries a number a user could act on and a keyboard's collections mean
  69. // nothing in an order.
  70. func DiscoverOther() ([]HIDDevice, error) {
  71. infos, err := hid.DiscoverEvery()
  72. if err != nil {
  73. return nil, fmt.Errorf("discover hid collections: %w", err)
  74. }
  75. return groupOther(infos), nil
  76. }
  77. // groupOther turns HID collections into the per-device list the commands report.
  78. // It is separate from DiscoverOther because the grouping is the whole of the
  79. // decision and nothing about it needs a keyboard plugged in to be checked.
  80. func groupOther(infos []hid.DeviceInfo) []HIDDevice {
  81. var devices []HIDDevice
  82. index := make(map[string]int)
  83. for _, info := range infos {
  84. if info.RawHID {
  85. continue
  86. }
  87. key := fmt.Sprintf("%04X/%04X/%s", info.VendorID, info.ProductID, info.ProductString)
  88. at, seen := index[key]
  89. if !seen {
  90. devices = append(devices, HIDDevice{
  91. VendorID: info.VendorID,
  92. ProductID: info.ProductID,
  93. Name: info.ProductString,
  94. })
  95. at = len(devices) - 1
  96. index[key] = at
  97. }
  98. pair := UsagePair{UsagePage: info.UsagePage, Usage: info.Usage}
  99. if !containsUsagePair(devices[at].UsagePairs, pair) {
  100. devices[at].UsagePairs = append(devices[at].UsagePairs, pair)
  101. }
  102. }
  103. return devices
  104. }
  105. // containsUsagePair reports whether a device already lists a collection. The HID
  106. // layer reports some devices once per interface, so the same pair arrives twice
  107. // for one device and a line that says it twice is a line about nothing.
  108. func containsUsagePair(pairs []UsagePair, want UsagePair) bool {
  109. for _, p := range pairs {
  110. if p == want {
  111. return true
  112. }
  113. }
  114. return false
  115. }
  116. // indexDevices sorts devices deterministically and assigns 1-based indexes.
  117. // The input slice is left untouched.
  118. func indexDevices(devices []Device) []Device {
  119. sorted := make([]Device, len(devices))
  120. copy(sorted, devices)
  121. sort.SliceStable(sorted, func(i, j int) bool {
  122. a, b := sorted[i], sorted[j]
  123. switch {
  124. case a.VendorID != b.VendorID:
  125. return a.VendorID < b.VendorID
  126. case a.ProductID != b.ProductID:
  127. return a.ProductID < b.ProductID
  128. default:
  129. return a.Path < b.Path
  130. }
  131. })
  132. for i := range sorted {
  133. sorted[i].Index = i + 1
  134. }
  135. return sorted
  136. }