| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197 |
- 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
- // 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
- }
- // 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,
- })
- 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,
- })
- })
- if err != nil {
- return err
- }
- return nil
- }
- func isRawHID(info *hid.DeviceInfo) bool {
- if info.UsagePage != 0xff60 {
- return false
- }
- if info.Usage != 0x61 {
- return false
- }
- return true
- }
- // 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()
- }
|