| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263 |
- package hid
- import (
- "fmt"
- "sync"
- "time"
- "github.com/sstallion/go-hid"
- )
- // Device represents a connected HID device.
- type Device struct {
- dev *hid.Device
- path string
- }
- // DeviceInfo holds device metadata without open handle.
- type DeviceInfo struct {
- Path string
- VendorID uint16
- ProductID uint16
- RawHID bool
- // UsagePage and Usage are the HID collection this entry describes. A
- // keyboard reports itself as several collections over one path, one per
- // top-level collection in its descriptor: a keyboard, a consumer control, a
- // vendor page. macOS reports every one of them separately, Linux one
- // interface at a time, and the QMK Raw HID collection is the one with this
- // pair. They travel with the entry because an entry that was not taken is
- // only explainable by the pair it was rejected for.
- UsagePage uint16
- Usage uint16
- // ProductString is the USB product string, which is where a keyboard states
- // its own model name. Firmware chooses it, so it can be empty.
- ProductString string
- }
- // rawHIDUsagePage and rawHIDUsage are the QMK Raw HID collection: the signature
- // qmk/qmk_udev matches on, and the only one this tool drives. A board that
- // declares a different pair is a board this tool cannot address, and saying so
- // is the point of reporting the pair a rejected device came with.
- const (
- rawHIDUsagePage uint16 = 0xff60
- rawHIDUsage uint16 = 0x61
- )
- // DevicesInfo lists all connected HID devices.
- type DevicesInfo struct {
- Devices []DeviceInfo
- }
- var initOnce sync.Once
- func ensureInit() error {
- var err error
- initOnce.Do(func() {
- err = hid.Init()
- })
- return err
- }
- // DiscoverAll returns a list of all connected HID devices with VID/PID.
- // On platforms that report multiple interfaces per device (e.g. macOS),
- // only one entry per VID/PID is returned.
- func DiscoverAll() ([]DeviceInfo, error) {
- if err := ensureInit(); err != nil {
- return nil, fmt.Errorf("init hid: %w", err)
- }
- seen := make(map[string]bool)
- var devices []DeviceInfo
- err := hid.Enumerate(hid.VendorIDAny, hid.ProductIDAny, func(info *hid.DeviceInfo) error {
- if !isRawHID(info) {
- return nil
- }
- if seen[info.Path] {
- return nil
- }
- seen[info.Path] = true
- devices = append(devices, DeviceInfo{
- Path: info.Path,
- VendorID: info.VendorID,
- ProductID: info.ProductID,
- ProductString: info.ProductStr,
- RawHID: true,
- UsagePage: info.UsagePage,
- Usage: info.Usage,
- })
- return nil
- })
- if err != nil {
- return nil, fmt.Errorf("enumerate: %w", err)
- }
- return devices, nil
- }
- // DiscoverEvery returns every HID collection the system reports, the one this
- // tool drives included. It exists so that a device which was not taken can be
- // explained: no keyboard found and a keyboard whose collection is not the QMK
- // Raw HID one look identical from outside, and only the second is worth telling
- // a user about.
- //
- // A collection is the unit here, not a device. One path appears once per usage
- // pair, so a keyboard with a keyboard collection and a vendor collection is two
- // entries, and each carries the pair it was rejected for. Order is the
- // enumeration order, which nothing may sort: these entries carry no number a user
- // could pass to a command, so they have no order to promise.
- func DiscoverEvery() ([]DeviceInfo, error) {
- if err := ensureInit(); err != nil {
- return nil, fmt.Errorf("init hid: %w", err)
- }
- seen := make(map[string]bool)
- var devices []DeviceInfo
- err := hid.Enumerate(hid.VendorIDAny, hid.ProductIDAny, func(info *hid.DeviceInfo) error {
- // The pair belongs in the key as well as the path: a keyboard reports
- // several collections on one path, and the same collection listed twice
- // is one entry.
- key := fmt.Sprintf("%s/%04X/%04X", info.Path, info.UsagePage, info.Usage)
- if seen[key] {
- return nil
- }
- seen[key] = true
- devices = append(devices, DeviceInfo{
- Path: info.Path,
- VendorID: info.VendorID,
- ProductID: info.ProductID,
- ProductString: info.ProductStr,
- RawHID: isRawHID(info),
- UsagePage: info.UsagePage,
- Usage: info.Usage,
- })
- return nil
- })
- if err != nil {
- return nil, fmt.Errorf("enumerate: %w", err)
- }
- return devices, nil
- }
- // Enumerate visits each HID device with matching VID/PID.
- // On platforms that report multiple interfaces per device,
- // only the first match per VID/PID is passed to fn.
- func Enumerate(vendorID, productID uint16, fn func(info *DeviceInfo) error) error {
- seen := make(map[string]bool)
- err := hid.Enumerate(vendorID, productID, func(info *hid.DeviceInfo) error {
- if !isRawHID(info) {
- return nil
- }
- if seen[info.Path] {
- return nil
- }
- seen[info.Path] = true
- return fn(&DeviceInfo{
- Path: info.Path,
- VendorID: info.VendorID,
- ProductID: info.ProductID,
- ProductString: info.ProductStr,
- RawHID: true,
- UsagePage: info.UsagePage,
- Usage: info.Usage,
- })
- })
- if err != nil {
- return err
- }
- return nil
- }
- // isRawHID reports whether a HID collection is the QMK Raw HID one, the signature
- // qmk/qmk_udev matches on. It is an exact pair on purpose: a vendor page that
- // happens to be near it is not a raw HID interface, and a board that puts its
- // lighting on another page is a board this tool cannot address rather than one it
- // may guess at.
- func isRawHID(info *hid.DeviceInfo) bool {
- return info.UsagePage == rawHIDUsagePage && info.Usage == rawHIDUsage
- }
- // Open opens a HID device by VID/PID.
- func Open(vendorID, productID uint16) (*Device, error) {
- if err := ensureInit(); err != nil {
- return nil, fmt.Errorf("init hid: %w", err)
- }
- dev, err := hid.Open(vendorID, productID, "")
- if err != nil {
- return nil, fmt.Errorf("open device VID=0x%04x PID=0x%04x: %w", vendorID, productID, err)
- }
- info, err := dev.GetDeviceInfo()
- if err != nil {
- dev.Close()
- return nil, fmt.Errorf("get device info: %w", err)
- }
- return &Device{dev: dev, path: info.Path}, nil
- }
- // OpenPath opens a HID device by path.
- func OpenPath(path string) (*Device, error) {
- if err := ensureInit(); err != nil {
- return nil, fmt.Errorf("init hid: %w", err)
- }
- dev, err := hid.OpenPath(path)
- if err != nil {
- return nil, fmt.Errorf("open path %s: %w", path, err)
- }
- return &Device{dev: dev, path: path}, nil
- }
- // VID returns the vendor ID.
- func (d *Device) VID() uint16 {
- info, err := d.dev.GetDeviceInfo()
- if err != nil {
- return 0
- }
- return info.VendorID
- }
- // PID returns the product ID.
- func (d *Device) PID() uint16 {
- info, err := d.dev.GetDeviceInfo()
- if err != nil {
- return 0
- }
- return info.ProductID
- }
- // Path returns the device path.
- func (d *Device) Path() string { return d.path }
- // SendReport sends a HID report.
- // report[0] must be the report ID (0 for single-report devices).
- // Uses Write for output reports (report ID == 0).
- func (d *Device) SendReport(reportID byte, report []byte) (int, error) {
- n, err := d.dev.Write(report)
- if err != nil {
- return n, fmt.Errorf("write report: %w", err)
- }
- return n, nil
- }
- // Read reads a response report with a timeout.
- var hidReadTimeout = 500 * time.Millisecond
- func (d *Device) Read(buf []byte) (int, error) {
- n, err := d.dev.ReadWithTimeout(buf, hidReadTimeout)
- if err == hid.ErrTimeout {
- return 0, fmt.Errorf("read timeout")
- }
- if err != nil {
- return n, fmt.Errorf("read: %w", err)
- }
- return n, nil
- }
- // Close closes the device.
- func (d *Device) Close() error {
- return d.dev.Close()
- }
|