package device import ( "fmt" "sort" "netdome.biz/paul/qmk-rgb/internal/hid" ) // Device is a connected QMK Raw HID keyboard. // Index is 1-based and is what --device accepts. type Device struct { Index int `json:"index"` Path string `json:"path"` VendorID uint16 `json:"vendorId"` ProductID uint16 `json:"productId"` Name string `json:"name,omitempty"` } // DiscoverAll returns every connected QMK Raw HID keyboard, ordered // deterministically and numbered from 1. A keyboard that does not report a USB // product string comes back with no Name; that costs the name and nothing else. func DiscoverAll() ([]Device, error) { infos, err := hid.DiscoverAll() if err != nil { return nil, fmt.Errorf("discover qmk devices: %w", err) } devices := make([]Device, 0, len(infos)) for _, info := range infos { devices = append(devices, Device{ Path: info.Path, VendorID: info.VendorID, ProductID: info.ProductID, Name: info.ProductString, }) } return indexDevices(devices), nil } // UsagePair is one HID collection of a device: its usage page and its usage. // Both are needed to describe a collection, because a keyboard reports several // on one device and the page alone does not tell them apart. type UsagePair struct { UsagePage uint16 `json:"usagePage"` Usage uint16 `json:"usage"` } // HIDDevice is a connected device whose HID collections are not the QMK Raw HID // one, which is what a discovery failure has to name: a board that is connected // but not reachable is a different fact from a board that is not connected, and // the usage pages are the only thing that tells the two apart. // // It is per device rather than per collection, because a device that is not // reachable usually has several collections and one line per collection buries // the one line that says something. A keyboard over USB typically has five: a // keyboard page, two consumer-control pages, mouse and a vendor page. What is // missing is the vendor page at 0xFF60. // // There is deliberately no Index and no Path. A number would look like something // --device accepts, and nothing accepts these. type HIDDevice struct { VendorID uint16 `json:"vendorId"` ProductID uint16 `json:"productId"` Name string `json:"name,omitempty"` UsagePairs []UsagePair `json:"usagePages"` } // DiscoverOther returns every connected HID device that is not a QMK Raw HID // keyboard, each with the usage pages it does expose. Devices this tool drives // are left out: they are what DiscoverAll numbers, and a list repeating them // would suggest a second way to select a keyboard. // // Order is the order the HID layer reported the devices in, and the pairs within // a device are in the order they were reported. Neither is sorted, because // neither carries a number a user could act on and a keyboard's collections mean // nothing in an order. func DiscoverOther() ([]HIDDevice, error) { infos, err := hid.DiscoverEvery() if err != nil { return nil, fmt.Errorf("discover hid collections: %w", err) } return groupOther(infos), nil } // groupOther turns HID collections into the per-device list the commands report. // It is separate from DiscoverOther because the grouping is the whole of the // decision and nothing about it needs a keyboard plugged in to be checked. func groupOther(infos []hid.DeviceInfo) []HIDDevice { var devices []HIDDevice index := make(map[string]int) for _, info := range infos { if info.RawHID { continue } key := fmt.Sprintf("%04X/%04X/%s", info.VendorID, info.ProductID, info.ProductString) at, seen := index[key] if !seen { devices = append(devices, HIDDevice{ VendorID: info.VendorID, ProductID: info.ProductID, Name: info.ProductString, }) at = len(devices) - 1 index[key] = at } pair := UsagePair{UsagePage: info.UsagePage, Usage: info.Usage} if !containsUsagePair(devices[at].UsagePairs, pair) { devices[at].UsagePairs = append(devices[at].UsagePairs, pair) } } return devices } // containsUsagePair reports whether a device already lists a collection. The HID // layer reports some devices once per interface, so the same pair arrives twice // for one device and a line that says it twice is a line about nothing. func containsUsagePair(pairs []UsagePair, want UsagePair) bool { for _, p := range pairs { if p == want { return true } } return false } // indexDevices sorts devices deterministically and assigns 1-based indexes. // The input slice is left untouched. func indexDevices(devices []Device) []Device { sorted := make([]Device, len(devices)) copy(sorted, devices) sort.SliceStable(sorted, func(i, j int) bool { a, b := sorted[i], sorted[j] switch { case a.VendorID != b.VendorID: return a.VendorID < b.VendorID case a.ProductID != b.ProductID: return a.ProductID < b.ProductID default: return a.Path < b.Path } }) for i := range sorted { sorted[i].Index = i + 1 } return sorted }