diff --git a/hid.go b/hid.go index 63f6c7a..a188b32 100644 --- a/hid.go +++ b/hid.go @@ -1,70 +1,72 @@ -// HID package to access Human Interface Devices. -// The platform specific parts of this package are heavily based on -// Signal 11 - HIDAPI. (https://github.com/signal11/hidapi) -package hid - -import "strings" - -// DeviceInfo provides general information about a device -type DeviceInfo struct { - // Path contains a Platform-specific device path which is used to identify the device - Path string - // VendorId contains the USB Vendor ID of the device - VendorId uint16 - // ProductId contains the USB Product ID of the device - ProductId uint16 - // VersionNumber contains the Version / Release Number of the device - VersionNumber uint16 - // Manufacturer of the USB device - Manufacturer string - // Product contains the product name of the device - Product string - - InputReportLength uint16 - OutputReportLength uint16 - FeatureReportLength uint16 -} - -// Device interface for an opened HID USB device -type Device interface { - // Close closes the device and release all keept resources. - Close() - // Write to the device - // (technically a HID report with type 'output' is send to the device) - Write([]byte) error - // Write to the device - // (technically a HID report with type 'feature' is send to the device) - WriteFeature([]byte) error - // Preform an interrupt transfer to the device - WriteInterrupt(byte, []byte) (int, error) -} - -// FindDevices iterates through all devices with a given vendor and product id -func FindDevices(vendor uint16, product uint16) <-chan *DeviceInfo { - result := make(chan *DeviceInfo) - go func() { - for dev := range Devices() { - if dev.VendorId == vendor && dev.ProductId == product { - result <- dev - } - } - close(result) - }() - return result -} - -// FindDevicesByProduct iterates through all devices with a given vendor and product id -func FindDevicesByProduct(product string) <-chan *DeviceInfo { - result := make(chan *DeviceInfo) - - go func() { - for dev := range Devices() { - if strings.Contains(dev.Product, product) { - result <- dev - } - } - close(result) - }() - - return result -} +// HID package to access Human Interface Devices. +// The platform specific parts of this package are heavily based on +// Signal 11 - HIDAPI. (https://github.com/signal11/hidapi) +package hid + +import "strings" + +// DeviceInfo provides general information about a device +type DeviceInfo struct { + // Path contains a Platform-specific device path which is used to identify the device + Path string + // VendorId contains the USB Vendor ID of the device + VendorId uint16 + // ProductId contains the USB Product ID of the device + ProductId uint16 + // VersionNumber contains the Version / Release Number of the device + VersionNumber uint16 + // Manufacturer of the USB device + Manufacturer string + // Product contains the product name of the device + Product string + + InputReportLength uint16 + OutputReportLength uint16 + FeatureReportLength uint16 +} + +// Device interface for an opened HID USB device +type Device interface { + // Close closes the device and release all keept resources. + Close() + // Write to the device + // (technically a HID report with type 'output' is send to the device) + Write([]byte) error + // Write to the device + // (technically a HID report with type 'feature' is send to the device) + WriteFeature([]byte) error + // Perform an interrupt transfer to the device + WriteInterrupt(byte, []byte) (int, error) + // Read Input Report from the device + Read() ([]byte, error) +} + +// FindDevices iterates through all devices with a given vendor and product id +func FindDevices(vendor uint16, product uint16) <-chan *DeviceInfo { + result := make(chan *DeviceInfo) + go func() { + for dev := range Devices() { + if dev.VendorId == vendor && dev.ProductId == product { + result <- dev + } + } + close(result) + }() + return result +} + +// FindDevicesByProduct iterates through all devices with a given vendor and product id +func FindDevicesByProduct(product string) <-chan *DeviceInfo { + result := make(chan *DeviceInfo) + + go func() { + for dev := range Devices() { + if strings.Contains(dev.Product, product) { + result <- dev + } + } + close(result) + }() + + return result +} diff --git a/hid_darwin.go b/hid_darwin.go index e11f36d..2152d45 100644 --- a/hid_darwin.go +++ b/hid_darwin.go @@ -319,6 +319,10 @@ func (dev *osxDevice) WriteFeature(data []byte) error { return dev.setReport(C.kIOHIDReportTypeFeature, data) } +func (d *osxDevice) Read() ([]byte, error) { + return nil, errors.New("Read is not implemented") +} + func (dev *osxDevice) Write(data []byte) error { return dev.setReport(C.kIOHIDReportTypeOutput, data) } diff --git a/hid_linux.go b/hid_linux.go index eaa7dd5..98b11cd 100644 --- a/hid_linux.go +++ b/hid_linux.go @@ -151,6 +151,10 @@ func (dev *linuxDevice) WriteFeature(data []byte) error { return dev.writeReport(HID_REPORT_TYPE_FEATURE, data) } +func (d *linuxDevice) Read() ([]byte, error) { + return nil, errors.New("Read is not implemented") +} + func (dev *linuxDevice) Write(data []byte) error { return dev.writeReport(HID_REPORT_TYPE_OUTPUT, data) } diff --git a/hid_windows.go b/hid_windows.go index d086e15..c93e155 100644 --- a/hid_windows.go +++ b/hid_windows.go @@ -1,264 +1,290 @@ -package hid - -/* -#cgo LDFLAGS: -lSetupapi -lhid - -#ifdef __MINGW32__ -#include -#endif - -#include -#include -#include -*/ -import "C" - -import ( - "errors" - "syscall" - "unsafe" -) - -type winDevice struct { - handle syscall.Handle - info *DeviceInfo -} - -// returns the casted handle of the device -func (d *winDevice) h() C.HANDLE { - return (C.HANDLE)((unsafe.Pointer)(d.handle)) -} - -// checks if the handle of the device is valid -func (d *winDevice) isValid() bool { - return d.handle != syscall.InvalidHandle -} - -func (d *winDevice) Close() { - syscall.CloseHandle(d.handle) - d.handle = syscall.InvalidHandle -} - -func (d *winDevice) Write(data []byte) error { - // first make sure we send the correct amount of data to the device - outSize := int(d.info.OutputReportLength) - buffer := make([]byte, outSize, outSize) - copy(buffer, data) - - ol := new(syscall.Overlapped) - if err := syscall.WriteFile(d.handle, buffer, nil, ol); err != nil { - // IO Pending is ok we simply wait for it to finish a few lines below - // all other errors should be reported. - if err != syscall.ERROR_IO_PENDING { - return err - } - } - - // now wait for the overlapped device access to finish. - var written C.DWORD - if C.GetOverlappedResult(d.h(), (*C.OVERLAPPED)((unsafe.Pointer)(ol)), &written, C.TRUE) == 0 { - return syscall.GetLastError() - } - - if int(written) != outSize { - return errors.New("written bytes missmatch!") - } - return nil -} - -func (d *winDevice) WriteFeature(data []byte) error { - // ensure the correct amount of data - buffer := make([]byte, d.info.FeatureReportLength, d.info.FeatureReportLength) - copy(buffer, data) - - if C.HidD_SetFeature(d.h(), unsafe.Pointer(&buffer[0]), C.DWORD(len(buffer))) != 0 { - return nil - } else { - return syscall.GetLastError() - } -} - -func (d *winDevice) WriteInterrupt(endpoint byte, data []byte) (int, error) { - return 0, errors.New("WriteInterrupt is not implemented") -} - -type callCFn func(buf unsafe.Pointer, bufSize *C.DWORD) unsafe.Pointer - -// simple helper function for this windows -// "call a function twice to get the amount of space that needs to be allocated" stuff -func getCString(fnCall callCFn) string { - var requiredSize C.DWORD - fnCall(nil, &requiredSize) - if requiredSize <= 0 { - return "" - } - - buffer := C.malloc((C.size_t)(requiredSize)) - defer C.free(buffer) - - strPt := fnCall(buffer, &requiredSize) - - return C.GoString((*C.char)(strPt)) -} - -func openDevice(info *DeviceInfo, enumerate bool) (*winDevice, error) { - access := uint32(syscall.GENERIC_WRITE | syscall.GENERIC_READ) - shareMode := uint32(syscall.FILE_SHARE_READ) - if enumerate { - // if we just need a handle to get the device properties - // we should not claim exclusive access on the device - access = 0 - shareMode = uint32(syscall.FILE_SHARE_READ | syscall.FILE_SHARE_WRITE) - } - pPtr, err := syscall.UTF16PtrFromString(info.Path) - if err != nil { - return nil, err - } - - hFile, err := syscall.CreateFile(pPtr, access, shareMode, nil, syscall.OPEN_EXISTING, syscall.FILE_FLAG_OVERLAPPED, 0) - if err != nil { - return nil, err - } else { - return &winDevice{handle: hFile, info: info}, nil - } -} - -func getDeviceDetails(deviceInfoSet C.HDEVINFO, deviceInterfaceData *C.SP_DEVICE_INTERFACE_DATA) *DeviceInfo { - devicePath := getCString(func(buffer unsafe.Pointer, size *C.DWORD) unsafe.Pointer { - interfaceDetailData := (*C.SP_DEVICE_INTERFACE_DETAIL_DATA_A)(buffer) - if interfaceDetailData != nil { - interfaceDetailData.cbSize = C.DWORD(unsafe.Sizeof(interfaceDetailData)) - } - C.SetupDiGetDeviceInterfaceDetailA(deviceInfoSet, deviceInterfaceData, interfaceDetailData, *size, size, nil) - if interfaceDetailData != nil { - return (unsafe.Pointer)(&interfaceDetailData.DevicePath[0]) - } else { - return nil - } - }) - if devicePath == "" { - return nil - } - - // Make sure this device is of Setup Class "HIDClass" and has a driver bound to it. - var i C.DWORD - var devinfoData C.SP_DEVINFO_DATA - devinfoData.cbSize = C.DWORD(unsafe.Sizeof(devinfoData)) - isHID := false - for i = 0; ; i++ { - if res := C.SetupDiEnumDeviceInfo(deviceInfoSet, i, &devinfoData); res == 0 { - break - } - - classStr := getCString(func(buffer unsafe.Pointer, size *C.DWORD) unsafe.Pointer { - C.SetupDiGetDeviceRegistryPropertyA(deviceInfoSet, &devinfoData, C.SPDRP_CLASS, nil, (*C.BYTE)(buffer), *size, size) - return buffer - }) - - if classStr == "HIDClass" { - driverName := getCString(func(buffer unsafe.Pointer, size *C.DWORD) unsafe.Pointer { - C.SetupDiGetDeviceRegistryPropertyA(deviceInfoSet, &devinfoData, C.SPDRP_DRIVER, nil, (*C.BYTE)(buffer), *size, size) - return buffer - }) - isHID = driverName != "" - break - } - } - - if !isHID { - return nil - } - d, _ := ByPath(devicePath) - return d -} - -// ByPath gets the device which is bound to the given path. -func ByPath(devicePath string) (*DeviceInfo, error) { - devInfo := &DeviceInfo{Path: devicePath} - dev, err := openDevice(devInfo, true) - if err != nil { - return nil, err - } - defer dev.Close() - if !dev.isValid() { - return nil, errors.New("Failed to open device") - } - - var attrs C.HIDD_ATTRIBUTES - attrs.Size = C.DWORD(unsafe.Sizeof(attrs)) - C.HidD_GetAttributes(dev.h(), &attrs) - - devInfo.VendorId = uint16(attrs.VendorID) - devInfo.ProductId = uint16(attrs.ProductID) - devInfo.VersionNumber = uint16(attrs.VersionNumber) - - const bufLen = 256 - buff := make([]uint16, bufLen) - - C.HidD_GetManufacturerString(dev.h(), (C.PVOID)(&buff[0]), bufLen) - devInfo.Manufacturer = syscall.UTF16ToString(buff) - - C.HidD_GetProductString(dev.h(), (C.PVOID)(&buff[0]), bufLen) - devInfo.Product = syscall.UTF16ToString(buff) - - var preparsedData C.PHIDP_PREPARSED_DATA - if C.HidD_GetPreparsedData(dev.h(), &preparsedData) != 0 { - var caps C.HIDP_CAPS - - if C.HidP_GetCaps(preparsedData, &caps) == C.HIDP_STATUS_SUCCESS { - devInfo.InputReportLength = uint16(caps.InputReportByteLength) - devInfo.FeatureReportLength = uint16(caps.FeatureReportByteLength) - devInfo.OutputReportLength = uint16(caps.OutputReportByteLength) - } - - C.HidD_FreePreparsedData(preparsedData) - } - - return devInfo, nil -} - -// Devices returns all HID devices which are connected to the system. -func Devices() <-chan *DeviceInfo { - result := make(chan *DeviceInfo) - go func() { - var InterfaceClassGuid C.GUID - C.HidD_GetHidGuid(&InterfaceClassGuid) - deviceInfoSet := C.SetupDiGetClassDevsA(&InterfaceClassGuid, nil, nil, C.DIGCF_PRESENT|C.DIGCF_DEVICEINTERFACE) - defer C.SetupDiDestroyDeviceInfoList(deviceInfoSet) - - var deviceIdx C.DWORD = 0 - var deviceInterfaceData C.SP_DEVICE_INTERFACE_DATA - deviceInterfaceData.cbSize = C.DWORD(unsafe.Sizeof(deviceInterfaceData)) - - for ; ; deviceIdx++ { - res := C.SetupDiEnumDeviceInterfaces(deviceInfoSet, nil, &InterfaceClassGuid, deviceIdx, &deviceInterfaceData) - if res == 0 { - break - } - di := getDeviceDetails(deviceInfoSet, &deviceInterfaceData) - if di != nil { - result <- di - } - } - close(result) - }() - return result -} - -// Open openes the device for read / write access. -func (di *DeviceInfo) Open() (Device, error) { - d, err := openDevice(di, false) - if err != nil { - return nil, err - } - if d.isValid() { - return d, nil - } else { - d.Close() - err := syscall.GetLastError() - if err == nil { - err = errors.New("Unable to open device!") - } - return nil, err - } -} +package hid + +/* +#cgo LDFLAGS: -lSetupapi -lhid + +#ifdef __MINGW32__ +#include +#endif + +#include +#include +#include +*/ +import "C" + +import ( + "errors" + "syscall" + "unsafe" +) + +type winDevice struct { + handle syscall.Handle + info *DeviceInfo +} + +// returns the casted handle of the device +func (d *winDevice) h() C.HANDLE { + return (C.HANDLE)((unsafe.Pointer)(d.handle)) +} + +// checks if the handle of the device is valid +func (d *winDevice) isValid() bool { + return d.handle != syscall.InvalidHandle +} + +func (d *winDevice) Close() { + syscall.CloseHandle(d.handle) + d.handle = syscall.InvalidHandle +} + +func (d *winDevice) Read() ([]byte, error) { + inSize := int(d.info.InputReportLength) + buffer := make([]byte, inSize, inSize) + + ol := new(syscall.Overlapped) + if err := syscall.ReadFile(d.handle, buffer, nil, ol); err != nil { + // IO Pending is ok we simply wait for it to finish a few lines below + // all other errors should be reported. + if err != syscall.ERROR_IO_PENDING { + return nil, err + } + } + + // now wait for the overlapped device access to finish. + var read C.DWORD + if C.GetOverlappedResult(d.h(), (*C.OVERLAPPED)((unsafe.Pointer)(ol)), &read, C.TRUE) == 0 { + return nil, syscall.GetLastError() + } + + if int(read) != inSize { + return nil, errors.New("read bytes mismatch!") + } + + return buffer, nil +} + +func (d *winDevice) Write(data []byte) error { + // first make sure we send the correct amount of data to the device + outSize := int(d.info.OutputReportLength) + buffer := make([]byte, outSize, outSize) + copy(buffer, data) + + ol := new(syscall.Overlapped) + if err := syscall.WriteFile(d.handle, buffer, nil, ol); err != nil { + // IO Pending is ok we simply wait for it to finish a few lines below + // all other errors should be reported. + if err != syscall.ERROR_IO_PENDING { + return err + } + } + + // now wait for the overlapped device access to finish. + var written C.DWORD + if C.GetOverlappedResult(d.h(), (*C.OVERLAPPED)((unsafe.Pointer)(ol)), &written, C.TRUE) == 0 { + return syscall.GetLastError() + } + + if int(written) != outSize { + return errors.New("written bytes mismatch!") + } + return nil +} + +func (d *winDevice) WriteFeature(data []byte) error { + // ensure the correct amount of data + buffer := make([]byte, d.info.FeatureReportLength, d.info.FeatureReportLength) + copy(buffer, data) + + if C.HidD_SetFeature(d.h(), unsafe.Pointer(&buffer[0]), C.DWORD(len(buffer))) != 0 { + return nil + } else { + return syscall.GetLastError() + } +} + +func (d *winDevice) WriteInterrupt(endpoint byte, data []byte) (int, error) { + return 0, errors.New("WriteInterrupt is not implemented") +} + +type callCFn func(buf unsafe.Pointer, bufSize *C.DWORD) unsafe.Pointer + +// simple helper function for this windows +// "call a function twice to get the amount of space that needs to be allocated" stuff +func getCString(fnCall callCFn) string { + var requiredSize C.DWORD + fnCall(nil, &requiredSize) + if requiredSize <= 0 { + return "" + } + + buffer := C.malloc((C.size_t)(requiredSize)) + defer C.free(buffer) + + strPt := fnCall(buffer, &requiredSize) + + return C.GoString((*C.char)(strPt)) +} + +func openDevice(info *DeviceInfo, enumerate bool) (*winDevice, error) { + access := uint32(syscall.GENERIC_WRITE | syscall.GENERIC_READ) + shareMode := uint32(syscall.FILE_SHARE_READ) + if enumerate { + // if we just need a handle to get the device properties + // we should not claim exclusive access on the device + access = 0 + shareMode = uint32(syscall.FILE_SHARE_READ | syscall.FILE_SHARE_WRITE) + } + pPtr, err := syscall.UTF16PtrFromString(info.Path) + if err != nil { + return nil, err + } + + hFile, err := syscall.CreateFile(pPtr, access, shareMode, nil, syscall.OPEN_EXISTING, syscall.FILE_FLAG_OVERLAPPED, 0) + if err != nil { + return nil, err + } else { + return &winDevice{handle: hFile, info: info}, nil + } +} + +func getDeviceDetails(deviceInfoSet C.HDEVINFO, deviceInterfaceData *C.SP_DEVICE_INTERFACE_DATA) *DeviceInfo { + devicePath := getCString(func(buffer unsafe.Pointer, size *C.DWORD) unsafe.Pointer { + interfaceDetailData := (*C.SP_DEVICE_INTERFACE_DETAIL_DATA_A)(buffer) + if interfaceDetailData != nil { + interfaceDetailData.cbSize = C.DWORD(unsafe.Sizeof(interfaceDetailData)) + } + C.SetupDiGetDeviceInterfaceDetailA(deviceInfoSet, deviceInterfaceData, interfaceDetailData, *size, size, nil) + if interfaceDetailData != nil { + return (unsafe.Pointer)(&interfaceDetailData.DevicePath[0]) + } else { + return nil + } + }) + if devicePath == "" { + return nil + } + + // Make sure this device is of Setup Class "HIDClass" and has a driver bound to it. + var i C.DWORD + var devinfoData C.SP_DEVINFO_DATA + devinfoData.cbSize = C.DWORD(unsafe.Sizeof(devinfoData)) + isHID := false + for i = 0; ; i++ { + if res := C.SetupDiEnumDeviceInfo(deviceInfoSet, i, &devinfoData); res == 0 { + break + } + + classStr := getCString(func(buffer unsafe.Pointer, size *C.DWORD) unsafe.Pointer { + C.SetupDiGetDeviceRegistryPropertyA(deviceInfoSet, &devinfoData, C.SPDRP_CLASS, nil, (*C.BYTE)(buffer), *size, size) + return buffer + }) + + if classStr == "HIDClass" { + driverName := getCString(func(buffer unsafe.Pointer, size *C.DWORD) unsafe.Pointer { + C.SetupDiGetDeviceRegistryPropertyA(deviceInfoSet, &devinfoData, C.SPDRP_DRIVER, nil, (*C.BYTE)(buffer), *size, size) + return buffer + }) + isHID = driverName != "" + break + } + } + + if !isHID { + return nil + } + d, _ := ByPath(devicePath) + return d +} + +// ByPath gets the device which is bound to the given path. +func ByPath(devicePath string) (*DeviceInfo, error) { + devInfo := &DeviceInfo{Path: devicePath} + dev, err := openDevice(devInfo, true) + if err != nil { + return nil, err + } + defer dev.Close() + if !dev.isValid() { + return nil, errors.New("Failed to open device") + } + + var attrs C.HIDD_ATTRIBUTES + attrs.Size = C.DWORD(unsafe.Sizeof(attrs)) + C.HidD_GetAttributes(dev.h(), &attrs) + + devInfo.VendorId = uint16(attrs.VendorID) + devInfo.ProductId = uint16(attrs.ProductID) + devInfo.VersionNumber = uint16(attrs.VersionNumber) + + const bufLen = 256 + buff := make([]uint16, bufLen) + + C.HidD_GetManufacturerString(dev.h(), (C.PVOID)(&buff[0]), bufLen) + devInfo.Manufacturer = syscall.UTF16ToString(buff) + + C.HidD_GetProductString(dev.h(), (C.PVOID)(&buff[0]), bufLen) + devInfo.Product = syscall.UTF16ToString(buff) + + var preparsedData C.PHIDP_PREPARSED_DATA + if C.HidD_GetPreparsedData(dev.h(), &preparsedData) != 0 { + var caps C.HIDP_CAPS + + if C.HidP_GetCaps(preparsedData, &caps) == C.HIDP_STATUS_SUCCESS { + devInfo.InputReportLength = uint16(caps.InputReportByteLength) + devInfo.FeatureReportLength = uint16(caps.FeatureReportByteLength) + devInfo.OutputReportLength = uint16(caps.OutputReportByteLength) + } + + C.HidD_FreePreparsedData(preparsedData) + } + + return devInfo, nil +} + +// Devices returns all HID devices which are connected to the system. +func Devices() <-chan *DeviceInfo { + result := make(chan *DeviceInfo) + go func() { + var InterfaceClassGuid C.GUID + C.HidD_GetHidGuid(&InterfaceClassGuid) + deviceInfoSet := C.SetupDiGetClassDevsA(&InterfaceClassGuid, nil, nil, C.DIGCF_PRESENT|C.DIGCF_DEVICEINTERFACE) + defer C.SetupDiDestroyDeviceInfoList(deviceInfoSet) + + var deviceIdx C.DWORD = 0 + var deviceInterfaceData C.SP_DEVICE_INTERFACE_DATA + deviceInterfaceData.cbSize = C.DWORD(unsafe.Sizeof(deviceInterfaceData)) + + for ; ; deviceIdx++ { + res := C.SetupDiEnumDeviceInterfaces(deviceInfoSet, nil, &InterfaceClassGuid, deviceIdx, &deviceInterfaceData) + if res == 0 { + break + } + di := getDeviceDetails(deviceInfoSet, &deviceInterfaceData) + if di != nil { + result <- di + } + } + close(result) + }() + return result +} + +// Open openes the device for read / write access. +func (di *DeviceInfo) Open() (Device, error) { + d, err := openDevice(di, false) + if err != nil { + return nil, err + } + if d.isValid() { + return d, nil + } else { + d.Close() + err := syscall.GetLastError() + if err == nil { + err = errors.New("Unable to open device!") + } + return nil, err + } +}