├── .gitignore ├── LICENSE ├── README.md ├── hid.go ├── hid_darwin.go ├── hid_darwin_go110.go ├── hid_darwin_go19.go ├── hid_linux.go ├── hid_windows.go └── ioctl_linux.go /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.iml 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Florian Sundermann 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HID 2 | 3 | A Go package to access Human Interface Devices. The platform specific parts of 4 | this package are heavily based on [Signal 11's HIDAPI](https://github.com/signal11/hidapi). 5 | 6 | ## Supported operating systems 7 | 8 | The following operating systems are supported targets 9 | (as used by [*$GOOS* environment variable](https://golang.org/doc/install/source#environment)) 10 | 11 | * darwin (uses native IOKit framework) 12 | * linux (uses hidraw) 13 | * windows (uses native Windows HID library) 14 | 15 | ## Known quirks for building on Windows 64-bit 16 | 17 | For building this HID package, you need to have a gcc.exe in your *%PATH%* environment variable. 18 | There are two tested GCC toolchains: [tdm-gcc](http://tdm-gcc.tdragon.net/) 19 | and [mingw-w64](http://mingw-w64.yaxm.org/). At the moment (March 2015), both toolchains 20 | are missing some declarations in header files, which will result in the following error message, 21 | when running the ```go build```: 22 | 23 | ``` 24 | D:\projects.go\src\github.com\boombuler\hid> go build -v -work 25 | WORK=C:\Users\xxx\AppData\Local\Temp\go-build011586055 26 | github.com/boombuler/hid 27 | # github.com/boombuler/hid 28 | could not determine kind of name for C.HidD_FreePreparsedData 29 | could not determine kind of name for C.HidD_GetPreparsedData 30 | ``` 31 | 32 | The solutions is simple: just add these four lines to your gcc toolchain header file ```hidsdi.h``` 33 | ````C 34 | /* http://msdn.microsoft.com/en-us/library/windows/hardware/ff538893(v=vs.85).aspx */ 35 | HIDAPI BOOLEAN NTAPI HidD_FreePreparsedData(PHIDP_PREPARSED_DATA PreparsedData); 36 | 37 | /* http://msdn.microsoft.com/en-us/library/windows/hardware/ff539679(v=vs.85).aspx */ 38 | HIDAPI BOOLEAN NTAPI HidD_GetPreparsedData(HANDLE HidDeviceObject, PHIDP_PREPARSED_DATA *PreparsedData); 39 | ```` 40 | Depending on your gcc toolchain installation folder, the files are located in 41 | 42 | ``` C:\TDM-GCC-64\x86_64-w64-mingw32\include\hidsdi.h ``` 43 | 44 | or 45 | 46 | ``` c:\mingw-w64\x86_64-4.9.2-win32-seh-rt_v3-rev1\mingw64\x86_64-w64-mingw32\include\hidsdi.h ``` 47 | 48 | After patching the header file, this package will compile. 49 | Future releases of the gcc toolchains will surely fix this issue. 50 | 51 | ## License 52 | 53 | [![License: MIT](https://img.shields.io/:license-MIT-blue.svg)](http://opensource.org/licenses/MIT) 54 | -------------------------------------------------------------------------------- /hid.go: -------------------------------------------------------------------------------- 1 | // Package hid provides access to Human Interface Devices. 2 | package hid 3 | 4 | // DeviceInfo provides general information about a device. 5 | type DeviceInfo struct { 6 | // Path contains a platform-specific device path which is used to identify the device. 7 | Path string 8 | 9 | VendorID uint16 10 | ProductID uint16 11 | VersionNumber uint16 12 | Manufacturer string 13 | Product string 14 | 15 | UsagePage uint16 16 | Usage uint16 17 | 18 | InputReportLength uint16 19 | OutputReportLength uint16 20 | } 21 | 22 | // A Device provides access to a HID device. 23 | type Device interface { 24 | // Close closes the device and associated resources. 25 | Close() 26 | 27 | // Write writes an output report to device. The first byte must be the 28 | // report number to write, zero if the device does not use numbered reports. 29 | Write([]byte) error 30 | 31 | // ReadCh returns a channel that will be sent input reports from the device. 32 | // If the device uses numbered reports, the first byte will be the report 33 | // number. 34 | ReadCh() <-chan []byte 35 | 36 | // ReadError returns the read error, if any after the channel returned from 37 | // ReadCh has been closed. 38 | ReadError() error 39 | } 40 | -------------------------------------------------------------------------------- /hid_darwin.go: -------------------------------------------------------------------------------- 1 | package hid 2 | 3 | /* 4 | #cgo LDFLAGS: -L . -L/usr/local/lib -framework CoreFoundation -framework IOKit 5 | 6 | #include 7 | #include 8 | 9 | 10 | static inline CFIndex cfstring_utf8_length(CFStringRef str, CFIndex *need) { 11 | CFIndex n, usedBufLen; 12 | CFRange rng = CFRangeMake(0, CFStringGetLength(str)); 13 | 14 | return CFStringGetBytes(str, rng, kCFStringEncodingUTF8, 0, 0, NULL, 0, need); 15 | } 16 | 17 | void deviceUnplugged(IOHIDDeviceRef osd, IOReturn ret, void *dev); 18 | 19 | void reportCallback(void *context, IOReturn result, void *sender, IOHIDReportType report_type, uint32_t report_id, uint8_t *report, CFIndex report_length); 20 | 21 | */ 22 | import "C" 23 | 24 | import ( 25 | "errors" 26 | "fmt" 27 | "reflect" 28 | "runtime" 29 | "sync" 30 | "unsafe" 31 | ) 32 | 33 | func ioReturnToErr(ret C.IOReturn) error { 34 | switch ret { 35 | case C.kIOReturnSuccess: 36 | return nil 37 | case C.kIOReturnError: 38 | return errors.New("hid: general error") 39 | case C.kIOReturnNoMemory: 40 | return errors.New("hid: can't allocate memory") 41 | case C.kIOReturnNoResources: 42 | return errors.New("hid: resource shortage") 43 | case C.kIOReturnIPCError: 44 | return errors.New("hid: error during IPC") 45 | case C.kIOReturnNoDevice: 46 | return errors.New("hid: no such device") 47 | case C.kIOReturnNotPrivileged: 48 | return errors.New("hid: privilege violation") 49 | case C.kIOReturnBadArgument: 50 | return errors.New("hid: invalid argument") 51 | case C.kIOReturnLockedRead: 52 | return errors.New("hid: device read locked") 53 | case C.kIOReturnLockedWrite: 54 | return errors.New("hid: device write locked") 55 | case C.kIOReturnExclusiveAccess: 56 | return errors.New("hid: exclusive access and device already open") 57 | case C.kIOReturnBadMessageID: 58 | return errors.New("hid: sent/received messages had different msg_id") 59 | case C.kIOReturnUnsupported: 60 | return errors.New("hid: unsupported function") 61 | case C.kIOReturnVMError: 62 | return errors.New("hid: misc. VM failure") 63 | case C.kIOReturnInternalError: 64 | return errors.New("hid: internal error") 65 | case C.kIOReturnIOError: 66 | return errors.New("hid: general I/O error") 67 | case C.kIOReturnCannotLock: 68 | return errors.New("hid: can't acquire lock") 69 | case C.kIOReturnNotOpen: 70 | return errors.New("hid: device not open") 71 | case C.kIOReturnNotReadable: 72 | return errors.New("hid: read not supported") 73 | case C.kIOReturnNotWritable: 74 | return errors.New("hid: write not supported") 75 | case C.kIOReturnNotAligned: 76 | return errors.New("hid: alignment error") 77 | case C.kIOReturnBadMedia: 78 | return errors.New("hid: media Error") 79 | case C.kIOReturnStillOpen: 80 | return errors.New("hid: device(s) still open") 81 | case C.kIOReturnRLDError: 82 | return errors.New("hid: rld failure") 83 | case C.kIOReturnDMAError: 84 | return errors.New("hid: DMA failure") 85 | case C.kIOReturnBusy: 86 | return errors.New("hid: device Busy") 87 | case C.kIOReturnTimeout: 88 | return errors.New("hid: i/o timeout") 89 | case C.kIOReturnOffline: 90 | return errors.New("hid: device offline") 91 | case C.kIOReturnNotReady: 92 | return errors.New("hid: not ready") 93 | case C.kIOReturnNotAttached: 94 | return errors.New("hid: device not attached") 95 | case C.kIOReturnNoChannels: 96 | return errors.New("hid: no DMA channels left") 97 | case C.kIOReturnNoSpace: 98 | return errors.New("hid: no space for data") 99 | case C.kIOReturnPortExists: 100 | return errors.New("hid: port already exists") 101 | case C.kIOReturnCannotWire: 102 | return errors.New("hid: can't wire down physical memory") 103 | case C.kIOReturnNoInterrupt: 104 | return errors.New("hid: no interrupt attached") 105 | case C.kIOReturnNoFrames: 106 | return errors.New("hid: no DMA frames enqueued") 107 | case C.kIOReturnMessageTooLarge: 108 | return errors.New("hid: oversized msg received on interrupt port") 109 | case C.kIOReturnNotPermitted: 110 | return errors.New("hid: not permitted") 111 | case C.kIOReturnNoPower: 112 | return errors.New("hid: no power to device") 113 | case C.kIOReturnNoMedia: 114 | return errors.New("hid: media not present") 115 | case C.kIOReturnUnformattedMedia: 116 | return errors.New("hid: media not formatted") 117 | case C.kIOReturnUnsupportedMode: 118 | return errors.New("hid: no such mode") 119 | case C.kIOReturnUnderrun: 120 | return errors.New("hid: data underrun") 121 | case C.kIOReturnOverrun: 122 | return errors.New("hid: data overrun") 123 | case C.kIOReturnDeviceError: 124 | return errors.New("hid: the device is not working properly!") 125 | case C.kIOReturnNoCompletion: 126 | return errors.New("hid: a completion routine is required") 127 | case C.kIOReturnAborted: 128 | return errors.New("hid: operation aborted") 129 | case C.kIOReturnNoBandwidth: 130 | return errors.New("hid: bus bandwidth would be exceeded") 131 | case C.kIOReturnNotResponding: 132 | return errors.New("hid: device not responding") 133 | case C.kIOReturnIsoTooOld: 134 | return errors.New("hid: isochronous I/O request for distant past!") 135 | case C.kIOReturnIsoTooNew: 136 | return errors.New("hid: isochronous I/O request for distant future") 137 | case C.kIOReturnNotFound: 138 | return errors.New("hid: data was not found") 139 | default: 140 | return errors.New("hid: unknown error") 141 | } 142 | } 143 | 144 | var deviceCtxMtx sync.Mutex 145 | var deviceCtx = make(map[C.IOHIDDeviceRef]*osxDevice) 146 | 147 | type cleanupDeviceManagerFn func() 148 | type osxDevice struct { 149 | mtx sync.Mutex 150 | osDevice C.IOHIDDeviceRef 151 | disconnected bool 152 | closeDM cleanupDeviceManagerFn 153 | 154 | readSetup sync.Once 155 | readCh chan []byte 156 | readErr error 157 | readBuf []byte 158 | runLoop C.CFRunLoopRef 159 | } 160 | 161 | func cfstring(s string) C.CFStringRef { 162 | n := C.CFIndex(len(s)) 163 | return C.CFStringCreateWithBytes(C.kCFAllocatorDefault, *(**C.UInt8)(unsafe.Pointer(&s)), n, C.kCFStringEncodingUTF8, 0) 164 | } 165 | 166 | func gostring(cfs C.CFStringRef) string { 167 | if cfs == nilCfStringRef { 168 | return "" 169 | } 170 | 171 | var usedBufLen C.CFIndex 172 | n := C.cfstring_utf8_length(cfs, &usedBufLen) 173 | if n <= 0 { 174 | return "" 175 | } 176 | rng := C.CFRange{location: C.CFIndex(0), length: n} 177 | buf := make([]byte, int(usedBufLen)) 178 | 179 | bufp := unsafe.Pointer(&buf[0]) 180 | C.CFStringGetBytes(cfs, rng, C.kCFStringEncodingUTF8, 0, 0, (*C.UInt8)(bufp), C.CFIndex(len(buf)), &usedBufLen) 181 | 182 | sh := &reflect.StringHeader{ 183 | Data: uintptr(bufp), 184 | Len: int(usedBufLen), 185 | } 186 | return *(*string)(unsafe.Pointer(sh)) 187 | } 188 | 189 | func getIntProp(device C.IOHIDDeviceRef, key C.CFStringRef) int32 { 190 | var value int32 191 | ref := C.IOHIDDeviceGetProperty(device, key) 192 | if ref == nilCfTypeRef { 193 | return 0 194 | } 195 | if C.CFGetTypeID(ref) != C.CFNumberGetTypeID() { 196 | return 0 197 | } 198 | C.CFNumberGetValue(C.CFNumberRef(ref), C.kCFNumberSInt32Type, unsafe.Pointer(&value)) 199 | return value 200 | } 201 | 202 | func getStringProp(device C.IOHIDDeviceRef, key C.CFStringRef) string { 203 | s := C.IOHIDDeviceGetProperty(device, key) 204 | return gostring(C.CFStringRef(s)) 205 | } 206 | 207 | func getPath(osDev C.IOHIDDeviceRef) string { 208 | return fmt.Sprintf("%s_%04x_%04x_%08x", 209 | getStringProp(osDev, cfstring(C.kIOHIDTransportKey)), 210 | uint16(getIntProp(osDev, cfstring(C.kIOHIDVendorIDKey))), 211 | uint16(getIntProp(osDev, cfstring(C.kIOHIDProductIDKey))), 212 | uint32(getIntProp(osDev, cfstring(C.kIOHIDLocationIDKey)))) 213 | } 214 | 215 | func iterateDevices(action func(device C.IOHIDDeviceRef) bool) cleanupDeviceManagerFn { 216 | var mgr C.IOHIDManagerRef 217 | mgr = C.IOHIDManagerCreate(C.kCFAllocatorDefault, C.kIOHIDOptionsTypeNone) 218 | C.IOHIDManagerSetDeviceMatching(mgr, nilCfDictionaryRef) 219 | C.IOHIDManagerOpen(mgr, C.kIOHIDOptionsTypeNone) 220 | 221 | var allDevicesSet C.CFSetRef 222 | allDevicesSet = C.IOHIDManagerCopyDevices(mgr) 223 | if allDevicesSet == nilCfSetRef { 224 | return func() {} 225 | } 226 | defer C.CFRelease((C.CFTypeRef)(allDevicesSet)) 227 | devCnt := C.CFSetGetCount(allDevicesSet) 228 | allDevices := make([]unsafe.Pointer, uint64(devCnt)) 229 | C.CFSetGetValues(allDevicesSet, &allDevices[0]) 230 | 231 | for _, pDev := range allDevices { 232 | if !action(C.IOHIDDeviceRef(pDev)) { 233 | break 234 | } 235 | } 236 | return func() { 237 | C.IOHIDManagerClose(mgr, C.kIOHIDOptionsTypeNone) 238 | C.CFRelease(C.CFTypeRef(mgr)) 239 | } 240 | } 241 | 242 | func Devices() ([]*DeviceInfo, error) { 243 | var result []*DeviceInfo 244 | iterateDevices(func(device C.IOHIDDeviceRef) bool { 245 | result = append(result, &DeviceInfo{ 246 | VendorID: uint16(getIntProp(device, cfstring(C.kIOHIDVendorIDKey))), 247 | ProductID: uint16(getIntProp(device, cfstring(C.kIOHIDProductIDKey))), 248 | VersionNumber: uint16(getIntProp(device, cfstring(C.kIOHIDVersionNumberKey))), 249 | Manufacturer: getStringProp(device, cfstring(C.kIOHIDManufacturerKey)), 250 | Product: getStringProp(device, cfstring(C.kIOHIDProductKey)), 251 | UsagePage: uint16(getIntProp(device, cfstring(C.kIOHIDPrimaryUsagePageKey))), 252 | Usage: uint16(getIntProp(device, cfstring(C.kIOHIDPrimaryUsageKey))), 253 | InputReportLength: uint16(getIntProp(device, cfstring(C.kIOHIDMaxInputReportSizeKey))), 254 | OutputReportLength: uint16(getIntProp(device, cfstring(C.kIOHIDMaxOutputReportSizeKey))), 255 | Path: getPath(device), 256 | }) 257 | return true 258 | })() 259 | return result, nil 260 | } 261 | 262 | func ByPath(path string) (*DeviceInfo, error) { 263 | devices, err := Devices() 264 | if err != nil { 265 | return nil, err 266 | } 267 | for _, d := range devices { 268 | if d.Path == path { 269 | return d, nil 270 | } 271 | } 272 | return nil, errors.New("hid: device not found") 273 | } 274 | 275 | func (di *DeviceInfo) Open() (Device, error) { 276 | err := errors.New("hid: device not found") 277 | var dev *osxDevice 278 | closeDM := iterateDevices(func(device C.IOHIDDeviceRef) bool { 279 | if getPath(device) == di.Path { 280 | res := C.IOHIDDeviceOpen(device, C.kIOHIDOptionsTypeSeizeDevice) 281 | if res == C.kIOReturnSuccess { 282 | C.CFRetain(C.CFTypeRef(device)) 283 | dev = &osxDevice{osDevice: device} 284 | err = nil 285 | deviceCtxMtx.Lock() 286 | deviceCtx[device] = dev 287 | deviceCtxMtx.Unlock() 288 | C.IOHIDDeviceRegisterRemovalCallback(device, (C.IOHIDCallback)(unsafe.Pointer(C.deviceUnplugged)), unsafe.Pointer(device)) 289 | } else { 290 | err = ioReturnToErr(res) 291 | } 292 | return false 293 | } 294 | return true 295 | }) 296 | if dev != nil { 297 | dev.closeDM = closeDM 298 | dev.readBuf = make([]byte, int(di.InputReportLength)) 299 | } 300 | 301 | return dev, err 302 | } 303 | 304 | //export deviceUnplugged 305 | func deviceUnplugged(osdev C.IOHIDDeviceRef, result C.IOReturn, dev unsafe.Pointer) { 306 | deviceCtxMtx.Lock() 307 | od := deviceCtx[C.IOHIDDeviceRef(dev)] 308 | deviceCtxMtx.Unlock() 309 | od.readErr = errors.New("hid: device unplugged") 310 | od.close(true) 311 | } 312 | 313 | func (dev *osxDevice) Close() { 314 | dev.readErr = errors.New("hid: device closed") 315 | dev.close(false) 316 | } 317 | 318 | func (dev *osxDevice) close(disconnected bool) { 319 | dev.mtx.Lock() 320 | defer dev.mtx.Unlock() 321 | 322 | if dev.disconnected { 323 | return 324 | } 325 | 326 | if dev.readCh != nil { 327 | if !disconnected { 328 | C.IOHIDDeviceRegisterInputReportCallback(dev.osDevice, (*C.uint8_t)(&dev.readBuf[0]), C.CFIndex(len(dev.readBuf)), nil, unsafe.Pointer(dev.osDevice)) 329 | C.IOHIDDeviceUnscheduleFromRunLoop(dev.osDevice, dev.runLoop, C.kCFRunLoopDefaultMode) 330 | } 331 | C.CFRunLoopStop(dev.runLoop) 332 | } 333 | if !disconnected { 334 | C.IOHIDDeviceRegisterRemovalCallback(dev.osDevice, nil, nil) 335 | C.IOHIDDeviceClose(dev.osDevice, C.kIOHIDOptionsTypeSeizeDevice) 336 | } 337 | 338 | deviceCtxMtx.Lock() 339 | delete(deviceCtx, dev.osDevice) 340 | deviceCtxMtx.Unlock() 341 | C.CFRelease(C.CFTypeRef(dev.osDevice)) 342 | dev.osDevice = nilIOHIDDeviceRef 343 | dev.closeDM() 344 | dev.disconnected = true 345 | } 346 | 347 | func (dev *osxDevice) setReport(typ C.IOHIDReportType, data []byte) error { 348 | dev.mtx.Lock() 349 | defer dev.mtx.Unlock() 350 | 351 | if dev.disconnected { 352 | return errors.New("hid: device disconnected") 353 | } 354 | 355 | reportNo := int32(data[0]) 356 | if reportNo == 0 { 357 | data = data[1:] 358 | } 359 | 360 | res := C.IOHIDDeviceSetReport(dev.osDevice, typ, C.CFIndex(reportNo), (*C.uint8_t)(&data[0]), C.CFIndex(len(data))) 361 | if res != C.kIOReturnSuccess { 362 | return ioReturnToErr(res) 363 | } 364 | return nil 365 | } 366 | 367 | func (dev *osxDevice) Write(data []byte) error { 368 | return dev.setReport(C.kIOHIDReportTypeOutput, data) 369 | } 370 | 371 | func (dev *osxDevice) ReadCh() <-chan []byte { 372 | dev.readSetup.Do(dev.startReadThread) 373 | return dev.readCh 374 | } 375 | 376 | func (dev *osxDevice) startReadThread() { 377 | dev.mtx.Lock() 378 | dev.readCh = make(chan []byte, 30) 379 | dev.mtx.Unlock() 380 | 381 | go func() { 382 | runtime.LockOSThread() 383 | dev.mtx.Lock() 384 | dev.runLoop = C.CFRunLoopGetCurrent() 385 | C.IOHIDDeviceScheduleWithRunLoop(dev.osDevice, dev.runLoop, C.kCFRunLoopDefaultMode) 386 | C.IOHIDDeviceRegisterInputReportCallback(dev.osDevice, (*C.uint8_t)(&dev.readBuf[0]), C.CFIndex(len(dev.readBuf)), (C.IOHIDReportCallback)(unsafe.Pointer(C.reportCallback)), unsafe.Pointer(dev.osDevice)) 387 | dev.mtx.Unlock() 388 | C.CFRunLoopRun() 389 | close(dev.readCh) 390 | }() 391 | } 392 | 393 | func (dev *osxDevice) ReadError() error { 394 | return dev.readErr 395 | } 396 | 397 | //export reportCallback 398 | func reportCallback(context unsafe.Pointer, result C.IOReturn, sender unsafe.Pointer, reportType C.IOHIDReportType, reportID uint32, report *C.uint8_t, reportLength C.CFIndex) { 399 | deviceCtxMtx.Lock() 400 | dev, ok := deviceCtx[(C.IOHIDDeviceRef)(context)] 401 | deviceCtxMtx.Unlock() 402 | if !ok { 403 | return 404 | } 405 | data := C.GoBytes(unsafe.Pointer(report), C.int(reportLength)) 406 | 407 | // readCh is buffered, drop the data if we can't send to avoid blocking the 408 | // run loop 409 | select { 410 | case dev.readCh <- data: 411 | default: 412 | } 413 | } 414 | -------------------------------------------------------------------------------- /hid_darwin_go110.go: -------------------------------------------------------------------------------- 1 | // +build go1.10,darwin 2 | 3 | package hid 4 | 5 | /* 6 | #include 7 | #include 8 | */ 9 | import "C" 10 | 11 | var nilCfStringRef C.CFStringRef= 0 12 | var nilCfTypeRef C.CFTypeRef = 0 13 | var nilCfSetRef C.CFSetRef = 0 14 | var nilIOHIDDeviceRef C.IOHIDDeviceRef = 0 15 | var nilCfDictionaryRef C.CFDictionaryRef = 0 16 | -------------------------------------------------------------------------------- /hid_darwin_go19.go: -------------------------------------------------------------------------------- 1 | // +build go1.9,!go1.10,darwin 2 | 3 | package hid 4 | 5 | /* 6 | #include 7 | #include 8 | */ 9 | import "C" 10 | 11 | var nilCfStringRef C.CFStringRef= nil 12 | var nilCfTypeRef C.CFTypeRef = nil 13 | var nilCfSetRef C.CFSetRef = nil 14 | var nilIOHIDDeviceRef C.IOHIDDeviceRef = nil 15 | var nilCfDictionaryRef C.CFDictionaryRef = nil 16 | -------------------------------------------------------------------------------- /hid_linux.go: -------------------------------------------------------------------------------- 1 | package hid 2 | 3 | // #include 4 | import "C" 5 | 6 | import ( 7 | "bytes" 8 | "io/ioutil" 9 | "os" 10 | "path/filepath" 11 | "sync" 12 | "unsafe" 13 | ) 14 | 15 | var ( 16 | ioctlHIDIOCGRDESCSIZE = ioR('H', 0x01, C.sizeof_int) 17 | ioctlHIDIOCGRDESC = ioR('H', 0x02, C.sizeof_struct_hidraw_report_descriptor) 18 | ioctlHIDIOCGRAWINFO = ioR('H', 0x03, C.sizeof_struct_hidraw_devinfo) 19 | ) 20 | 21 | func ioctlHIDIOCGRAWNAME(size int) uintptr { 22 | return ioR('H', 0x04, uintptr(size)) 23 | } 24 | 25 | func ioctlHIDIOCGRAWPHYS(size int) uintptr { 26 | return ioR('H', 0x05, uintptr(size)) 27 | } 28 | 29 | func ioctlHIDIOCSFEATURE(size int) uintptr { 30 | return ioRW('H', 0x06, uintptr(size)) 31 | } 32 | 33 | func ioctlHIDIOCGFEATURE(size int) uintptr { 34 | return ioRW('H', 0x07, uintptr(size)) 35 | } 36 | 37 | type linuxDevice struct { 38 | f *os.File 39 | info *DeviceInfo 40 | 41 | readSetup sync.Once 42 | readErr error 43 | readCh chan []byte 44 | } 45 | 46 | func Devices() ([]*DeviceInfo, error) { 47 | sys, err := os.Open("/sys/class/hidraw") 48 | if err != nil { 49 | return nil, err 50 | } 51 | names, err := sys.Readdirnames(0) 52 | sys.Close() 53 | if err != nil { 54 | return nil, err 55 | } 56 | 57 | var res []*DeviceInfo 58 | for _, dir := range names { 59 | path := filepath.Join("/dev", filepath.Base(dir)) 60 | info, err := getDeviceInfo(path) 61 | if os.IsPermission(err) { 62 | continue 63 | } else if err != nil { 64 | return nil, err 65 | } 66 | res = append(res, info) 67 | } 68 | 69 | return res, nil 70 | } 71 | 72 | func getDeviceInfo(path string) (*DeviceInfo, error) { 73 | d := &DeviceInfo{ 74 | Path: path, 75 | } 76 | 77 | dev, err := os.OpenFile(path, os.O_RDWR, 0) 78 | if err != nil { 79 | return nil, err 80 | } 81 | defer dev.Close() 82 | fd := uintptr(dev.Fd()) 83 | 84 | var descSize C.int 85 | if err := ioctl(fd, ioctlHIDIOCGRDESCSIZE, uintptr(unsafe.Pointer(&descSize))); err != nil { 86 | return nil, err 87 | } 88 | 89 | rawDescriptor := C.struct_hidraw_report_descriptor{ 90 | size: C.__u32(descSize), 91 | } 92 | if err := ioctl(fd, ioctlHIDIOCGRDESC, uintptr(unsafe.Pointer(&rawDescriptor))); err != nil { 93 | return nil, err 94 | } 95 | d.parseReport(C.GoBytes(unsafe.Pointer(&rawDescriptor.value), descSize)) 96 | 97 | var rawInfo C.struct_hidraw_devinfo 98 | if err := ioctl(fd, ioctlHIDIOCGRAWINFO, uintptr(unsafe.Pointer(&rawInfo))); err != nil { 99 | return nil, err 100 | } 101 | d.VendorID = uint16(rawInfo.vendor) 102 | d.ProductID = uint16(rawInfo.product) 103 | 104 | rawName := make([]byte, 256) 105 | if err := ioctl(fd, ioctlHIDIOCGRAWNAME(len(rawName)), uintptr(unsafe.Pointer(&rawName[0]))); err != nil { 106 | return nil, err 107 | } 108 | d.Product = string(rawName[:bytes.IndexByte(rawName, 0)]) 109 | 110 | if p, err := filepath.EvalSymlinks(filepath.Join("/sys/class/hidraw", filepath.Base(path), "device")); err == nil { 111 | if rawManufacturer, err := ioutil.ReadFile(filepath.Join(p, "/../../manufacturer")); err == nil { 112 | d.Manufacturer = string(bytes.TrimRight(rawManufacturer, "\n")) 113 | } 114 | } 115 | 116 | return d, nil 117 | } 118 | 119 | // very basic report parser that will pull out the usage page, usage, and the 120 | // sizes of the first input and output reports 121 | func (d *DeviceInfo) parseReport(b []byte) { 122 | var reportSize uint16 123 | 124 | for len(b) > 0 { 125 | // read item size, type, and tag 126 | size := int(b[0] & 0x03) 127 | if size == 3 { 128 | size = 4 129 | } 130 | typ := (b[0] >> 2) & 0x03 131 | tag := (b[0] >> 4) & 0x0f 132 | b = b[1:] 133 | 134 | if len(b) < size { 135 | return 136 | } 137 | 138 | // read item value 139 | var v uint64 140 | for i := 0; i < size; i++ { 141 | v += uint64(b[i]) << (8 * uint(i)) 142 | } 143 | b = b[size:] 144 | 145 | switch { 146 | case typ == 0 && tag == 8 && d.InputReportLength == 0 && reportSize > 0: // input report type 147 | d.InputReportLength = reportSize 148 | reportSize = 0 149 | case typ == 0 && tag == 9 && d.OutputReportLength == 0 && reportSize > 0: // output report type 150 | d.OutputReportLength = reportSize 151 | reportSize = 0 152 | case typ == 1 && tag == 0: // usage page 153 | d.UsagePage = uint16(v) 154 | case typ == 1 && tag == 9: // report count 155 | reportSize = uint16(v) 156 | case typ == 2 && tag == 0 && d.Usage == 0: // usage 157 | d.Usage = uint16(v) 158 | } 159 | 160 | if d.UsagePage > 0 && d.Usage > 0 && d.InputReportLength > 0 && d.OutputReportLength > 0 { 161 | return 162 | } 163 | } 164 | } 165 | 166 | func ByPath(path string) (*DeviceInfo, error) { 167 | return getDeviceInfo(path) 168 | } 169 | 170 | func (d *DeviceInfo) Open() (Device, error) { 171 | f, err := os.OpenFile(d.Path, os.O_RDWR, 0) 172 | if err != nil { 173 | return nil, err 174 | } 175 | 176 | return &linuxDevice{f: f, info: d}, nil 177 | } 178 | 179 | func (d *linuxDevice) Close() { 180 | d.f.Close() 181 | } 182 | 183 | func (d *linuxDevice) Write(data []byte) error { 184 | _, err := d.f.Write(data) 185 | return err 186 | } 187 | 188 | func (d *linuxDevice) ReadCh() <-chan []byte { 189 | d.readSetup.Do(func() { 190 | d.readCh = make(chan []byte, 30) 191 | go d.readThread() 192 | }) 193 | return d.readCh 194 | } 195 | 196 | func (d *linuxDevice) ReadError() error { 197 | return d.readErr 198 | } 199 | 200 | func (d *linuxDevice) readThread() { 201 | defer close(d.readCh) 202 | for { 203 | buf := make([]byte, d.info.InputReportLength) 204 | n, err := d.f.Read(buf) 205 | if err != nil { 206 | d.readErr = err 207 | return 208 | } 209 | select { 210 | case d.readCh <- buf[:n]: 211 | default: 212 | } 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /hid_windows.go: -------------------------------------------------------------------------------- 1 | package hid 2 | 3 | /* 4 | #cgo LDFLAGS: -lsetupapi -lhid 5 | 6 | #ifdef __MINGW32__ 7 | #include 8 | #endif 9 | 10 | #include 11 | #include 12 | #include 13 | */ 14 | import "C" 15 | 16 | import ( 17 | "errors" 18 | "fmt" 19 | "sync" 20 | "syscall" 21 | "unsafe" 22 | ) 23 | 24 | type winDevice struct { 25 | handle syscall.Handle 26 | info *DeviceInfo 27 | 28 | readSetup sync.Once 29 | readCh chan []byte 30 | readErr error 31 | readOl *syscall.Overlapped 32 | } 33 | 34 | // returns the casted handle of the device 35 | func (d *winDevice) h() C.HANDLE { 36 | return (C.HANDLE)((unsafe.Pointer)(d.handle)) 37 | } 38 | 39 | // checks if the handle of the device is valid 40 | func (d *winDevice) isValid() bool { 41 | return d.handle != syscall.InvalidHandle 42 | } 43 | 44 | func (d *winDevice) Close() { 45 | // cancel any pending reads and unblock read loop 46 | d.readErr = errors.New("hid: device closed") 47 | C.CancelIo(d.h()) 48 | C.SetEvent(C.HANDLE(unsafe.Pointer(d.readOl.HEvent))) 49 | syscall.CloseHandle(d.readOl.HEvent) 50 | 51 | syscall.CloseHandle(d.handle) 52 | d.handle = syscall.InvalidHandle 53 | } 54 | 55 | func (d *winDevice) Write(data []byte) error { 56 | // first make sure we send the correct amount of data to the device 57 | outSize := int(d.info.OutputReportLength + 1) 58 | if len(data) != outSize { 59 | buf := make([]byte, outSize) 60 | copy(buf, data) 61 | data = buf 62 | } 63 | 64 | ol := new(syscall.Overlapped) 65 | if err := syscall.WriteFile(d.handle, data, nil, ol); err != nil { 66 | // IO Pending is ok we simply wait for it to finish a few lines below 67 | // all other errors should be reported. 68 | if err != syscall.ERROR_IO_PENDING { 69 | return err 70 | } 71 | } 72 | 73 | // now wait for the overlapped device access to finish. 74 | var written C.DWORD 75 | if C.GetOverlappedResult(d.h(), (*C.OVERLAPPED)((unsafe.Pointer)(ol)), &written, C.TRUE) == 0 { 76 | return syscall.GetLastError() 77 | } 78 | 79 | if int(written) != outSize { 80 | return errors.New("written bytes missmatch!") 81 | } 82 | return nil 83 | } 84 | 85 | type callCFn func(buf unsafe.Pointer, bufSize *C.DWORD) unsafe.Pointer 86 | 87 | // simple helper function for this windows 88 | // "call a function twice to get the amount of space that needs to be allocated" stuff 89 | func getCString(fnCall callCFn) string { 90 | var requiredSize C.DWORD 91 | fnCall(nil, &requiredSize) 92 | if requiredSize <= 0 { 93 | return "" 94 | } 95 | 96 | buffer := C.malloc((C.size_t)(requiredSize)) 97 | defer C.free(buffer) 98 | 99 | strPt := fnCall(buffer, &requiredSize) 100 | 101 | return C.GoString((*C.char)(strPt)) 102 | } 103 | 104 | func openDevice(info *DeviceInfo, enumerate bool) (*winDevice, error) { 105 | access := uint32(syscall.GENERIC_WRITE | syscall.GENERIC_READ) 106 | shareMode := uint32(syscall.FILE_SHARE_READ) 107 | if enumerate { 108 | // if we just need a handle to get the device properties 109 | // we should not claim exclusive access on the device 110 | access = 0 111 | shareMode = uint32(syscall.FILE_SHARE_READ | syscall.FILE_SHARE_WRITE) 112 | } 113 | pPtr, err := syscall.UTF16PtrFromString(info.Path) 114 | if err != nil { 115 | return nil, err 116 | } 117 | 118 | hFile, err := syscall.CreateFile(pPtr, access, shareMode, nil, syscall.OPEN_EXISTING, syscall.FILE_FLAG_OVERLAPPED, 0) 119 | if err != nil { 120 | return nil, err 121 | } else { 122 | return &winDevice{ 123 | handle: hFile, 124 | info: info, 125 | readOl: &syscall.Overlapped{ 126 | HEvent: syscall.Handle(C.CreateEvent(nil, C.FALSE, C.FALSE, nil)), 127 | }, 128 | }, nil 129 | } 130 | } 131 | 132 | func getDeviceDetails(deviceInfoSet C.HDEVINFO, deviceInterfaceData *C.SP_DEVICE_INTERFACE_DATA) *DeviceInfo { 133 | devicePath := getCString(func(buffer unsafe.Pointer, size *C.DWORD) unsafe.Pointer { 134 | interfaceDetailData := (*C.SP_DEVICE_INTERFACE_DETAIL_DATA_A)(buffer) 135 | if interfaceDetailData != nil { 136 | interfaceDetailData.cbSize = C.DWORD(unsafe.Sizeof(interfaceDetailData)) 137 | } 138 | C.SetupDiGetDeviceInterfaceDetailA(deviceInfoSet, deviceInterfaceData, interfaceDetailData, *size, size, nil) 139 | if interfaceDetailData != nil { 140 | return (unsafe.Pointer)(&interfaceDetailData.DevicePath[0]) 141 | } else { 142 | return nil 143 | } 144 | }) 145 | if devicePath == "" { 146 | return nil 147 | } 148 | 149 | // Make sure this device is of Setup Class "HIDClass" and has a driver bound to it. 150 | var i C.DWORD 151 | var devinfoData C.SP_DEVINFO_DATA 152 | devinfoData.cbSize = C.DWORD(unsafe.Sizeof(devinfoData)) 153 | isHID := false 154 | for i = 0; ; i++ { 155 | if res := C.SetupDiEnumDeviceInfo(deviceInfoSet, i, &devinfoData); res == 0 { 156 | break 157 | } 158 | 159 | classStr := getCString(func(buffer unsafe.Pointer, size *C.DWORD) unsafe.Pointer { 160 | C.SetupDiGetDeviceRegistryPropertyA(deviceInfoSet, &devinfoData, C.SPDRP_CLASS, nil, (*C.BYTE)(buffer), *size, size) 161 | return buffer 162 | }) 163 | 164 | if classStr == "HIDClass" { 165 | driverName := getCString(func(buffer unsafe.Pointer, size *C.DWORD) unsafe.Pointer { 166 | C.SetupDiGetDeviceRegistryPropertyA(deviceInfoSet, &devinfoData, C.SPDRP_DRIVER, nil, (*C.BYTE)(buffer), *size, size) 167 | return buffer 168 | }) 169 | isHID = driverName != "" 170 | break 171 | } 172 | } 173 | 174 | if !isHID { 175 | return nil 176 | } 177 | d, _ := ByPath(devicePath) 178 | return d 179 | } 180 | 181 | // ByPath gets the device which is bound to the given path. 182 | func ByPath(devicePath string) (*DeviceInfo, error) { 183 | devInfo := &DeviceInfo{Path: devicePath} 184 | dev, err := openDevice(devInfo, true) 185 | if err != nil { 186 | return nil, err 187 | } 188 | defer dev.Close() 189 | if !dev.isValid() { 190 | return nil, errors.New("Failed to open device") 191 | } 192 | 193 | var attrs C.HIDD_ATTRIBUTES 194 | attrs.Size = C.DWORD(unsafe.Sizeof(attrs)) 195 | C.HidD_GetAttributes(dev.h(), &attrs) 196 | 197 | devInfo.VendorID = uint16(attrs.VendorID) 198 | devInfo.ProductID = uint16(attrs.ProductID) 199 | devInfo.VersionNumber = uint16(attrs.VersionNumber) 200 | 201 | const bufLen = 256 202 | buff := make([]uint16, bufLen) 203 | 204 | C.HidD_GetManufacturerString(dev.h(), (C.PVOID)(&buff[0]), bufLen) 205 | devInfo.Manufacturer = syscall.UTF16ToString(buff) 206 | 207 | C.HidD_GetProductString(dev.h(), (C.PVOID)(&buff[0]), bufLen) 208 | devInfo.Product = syscall.UTF16ToString(buff) 209 | 210 | var preparsedData C.PHIDP_PREPARSED_DATA 211 | if C.HidD_GetPreparsedData(dev.h(), &preparsedData) != 0 { 212 | var caps C.HIDP_CAPS 213 | 214 | if C.HidP_GetCaps(preparsedData, &caps) == C.HIDP_STATUS_SUCCESS { 215 | devInfo.UsagePage = uint16(caps.UsagePage) 216 | devInfo.Usage = uint16(caps.Usage) 217 | devInfo.InputReportLength = uint16(caps.InputReportByteLength - 1) 218 | devInfo.OutputReportLength = uint16(caps.OutputReportByteLength - 1) 219 | } 220 | 221 | C.HidD_FreePreparsedData(preparsedData) 222 | } 223 | 224 | return devInfo, nil 225 | } 226 | 227 | // Devices returns all HID devices which are connected to the system. 228 | func Devices() ([]*DeviceInfo, error) { 229 | var result []*DeviceInfo 230 | var InterfaceClassGuid C.GUID 231 | C.HidD_GetHidGuid(&InterfaceClassGuid) 232 | deviceInfoSet := C.SetupDiGetClassDevsA(&InterfaceClassGuid, nil, nil, C.DIGCF_PRESENT|C.DIGCF_DEVICEINTERFACE) 233 | defer C.SetupDiDestroyDeviceInfoList(deviceInfoSet) 234 | 235 | var deviceIdx C.DWORD = 0 236 | var deviceInterfaceData C.SP_DEVICE_INTERFACE_DATA 237 | deviceInterfaceData.cbSize = C.DWORD(unsafe.Sizeof(deviceInterfaceData)) 238 | 239 | for ; ; deviceIdx++ { 240 | res := C.SetupDiEnumDeviceInterfaces(deviceInfoSet, nil, &InterfaceClassGuid, deviceIdx, &deviceInterfaceData) 241 | if res == 0 { 242 | break 243 | } 244 | di := getDeviceDetails(deviceInfoSet, &deviceInterfaceData) 245 | if di != nil { 246 | result = append(result, di) 247 | } 248 | } 249 | return result, nil 250 | } 251 | 252 | // Open openes the device for read / write access. 253 | func (di *DeviceInfo) Open() (Device, error) { 254 | d, err := openDevice(di, false) 255 | if err != nil { 256 | return nil, err 257 | } 258 | if d.isValid() { 259 | return d, nil 260 | } else { 261 | d.Close() 262 | err := syscall.GetLastError() 263 | if err == nil { 264 | err = errors.New("Unable to open device!") 265 | } 266 | return nil, err 267 | } 268 | } 269 | 270 | func (d *winDevice) ReadCh() <-chan []byte { 271 | d.readSetup.Do(func() { 272 | d.readCh = make(chan []byte, 30) 273 | go d.readThread() 274 | }) 275 | return d.readCh 276 | } 277 | 278 | func (d *winDevice) ReadError() error { 279 | return d.readErr 280 | } 281 | 282 | func (d *winDevice) readThread() { 283 | defer close(d.readCh) 284 | 285 | for { 286 | buf := make([]byte, d.info.InputReportLength+1) 287 | C.ResetEvent(C.HANDLE(unsafe.Pointer(d.readOl.HEvent))) 288 | 289 | if err := syscall.ReadFile(d.handle, buf, nil, d.readOl); err != nil { 290 | if err != syscall.ERROR_IO_PENDING { 291 | if d.readErr == nil { 292 | d.readErr = err 293 | } 294 | return 295 | } 296 | } 297 | 298 | // Wait for the read to finish 299 | res := C.WaitForSingleObject(C.HANDLE(unsafe.Pointer(d.readOl.HEvent)), C.INFINITE) 300 | if res != C.WAIT_OBJECT_0 { 301 | if d.readErr == nil { 302 | d.readErr = fmt.Errorf("hid: unexpected read wait state %d", res) 303 | } 304 | return 305 | } 306 | 307 | var n C.DWORD 308 | if r := C.GetOverlappedResult(d.h(), (*C.OVERLAPPED)((unsafe.Pointer)(d.readOl)), &n, C.TRUE); r == 0 { 309 | if d.readErr == nil { 310 | d.readErr = fmt.Errorf("hid: unexpected read result state %d", r) 311 | } 312 | return 313 | } 314 | if n == 0 { 315 | if d.readErr == nil { 316 | d.readErr = errors.New("hid: zero byte read") 317 | } 318 | return 319 | } 320 | 321 | if buf[0] == 0 { 322 | // Report numbers are not being used, so remove zero to match other platforms 323 | buf = buf[1:] 324 | } 325 | 326 | select { 327 | case d.readCh <- buf[:int(n-1)]: 328 | default: 329 | } 330 | } 331 | 332 | } 333 | -------------------------------------------------------------------------------- /ioctl_linux.go: -------------------------------------------------------------------------------- 1 | package hid 2 | 3 | import "syscall" 4 | 5 | // This file is https://github.com/wolfeidau/gioctl/blob/0a268ca608219d1d45cfcc50ca4dbfe232baaf0d/ioctl.go 6 | // 7 | // Copyright (c) 2014 Mark Wolfe and licenced under the MIT licence. All rights 8 | // not explicitly granted in the MIT license are reserved. 9 | // 10 | // Permission is hereby granted, free of charge, to any person obtaining a copy 11 | // of this software and associated documentation files (the "Software"), to deal 12 | // in the Software without restriction, including without limitation the rights 13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | // copies of the Software, and to permit persons to whom the Software is 15 | // furnished to do so, subject to the following conditions: 16 | // 17 | // The above copyright notice and this permission notice shall be included in 18 | // all copies or substantial portions of the Software. 19 | // 20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | // SOFTWARE. 27 | 28 | const ( 29 | typeBits = 8 30 | numberBits = 8 31 | sizeBits = 14 32 | directionBits = 2 33 | 34 | typeMask = (1 << typeBits) - 1 35 | numberMask = (1 << numberBits) - 1 36 | sizeMask = (1 << sizeBits) - 1 37 | directionMask = (1 << directionBits) - 1 38 | 39 | directionNone = 0 40 | directionWrite = 1 41 | directionRead = 2 42 | 43 | numberShift = 0 44 | typeShift = numberShift + numberBits 45 | sizeShift = typeShift + typeBits 46 | directionShift = sizeShift + sizeBits 47 | ) 48 | 49 | func ioc(dir, t, nr, size uintptr) uintptr { 50 | return (dir << directionShift) | (t << typeShift) | (nr << numberShift) | (size << sizeShift) 51 | } 52 | 53 | // io used for a simple ioctl that sends nothing but the type and number, and receives back nothing but an (integer) retval. 54 | func io(t, nr uintptr) uintptr { 55 | return ioc(directionNone, t, nr, 0) 56 | } 57 | 58 | // ioR used for an ioctl that reads data from the device driver. The driver will be allowed to return sizeof(data_type) bytes to the user. 59 | func ioR(t, nr, size uintptr) uintptr { 60 | return ioc(directionRead, t, nr, size) 61 | } 62 | 63 | // ioW used for an ioctl that writes data to the device driver. 64 | func ioW(t, nr, size uintptr) uintptr { 65 | return ioc(directionWrite, t, nr, size) 66 | } 67 | 68 | // ioRW a combination of IoR and IoW. That is, data is both written to the driver and then read back from the driver by the client. 69 | func ioRW(t, nr, size uintptr) uintptr { 70 | return ioc(directionRead|directionWrite, t, nr, size) 71 | } 72 | 73 | // ioctl simplified ioct call 74 | func ioctl(fd, op, arg uintptr) error { 75 | _, _, ep := syscall.Syscall(syscall.SYS_IOCTL, fd, op, arg) 76 | if ep != 0 { 77 | return syscall.Errno(ep) 78 | } 79 | return nil 80 | } 81 | --------------------------------------------------------------------------------