├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── easyhid ├── __init__.py ├── easyhid.py └── version.py ├── setup.cfg └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | easyhid.egg-info 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | 0.0.6 3 | ================== 4 | 5 | * Add support for HID `get_feature_report()` and `set_feature_report()` to 6 | HID devices. 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright © 2018 jem@seethis.link 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the "Software"), 5 | to deal in the Software without restriction, including without limitation 6 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 7 | and/or sell copies of the Software, and to permit persons to whom the 8 | Software is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included 11 | in all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 14 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 15 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 16 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 17 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 18 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE 19 | OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # easyhid 2 | 3 | A simple python interface to the HIDAPI library. 4 | 5 | ```python 6 | # Examples 7 | from easyhid import Enumeration 8 | 9 | # Stores an enumeration of all the connected USB HID devices 10 | en = Enumeration() 11 | 12 | # return a list of devices based on the search parameters 13 | devices = en.find(manufacturer="Company", product="Widget", interface=3) 14 | 15 | # print a description of the devices found 16 | for dev in devices: 17 | print(dev.description()) 18 | 19 | # open a device 20 | dev.open() 21 | 22 | # write some bytes to the device 23 | dev.write(bytearray([0, 1, 2, 3])) 24 | 25 | # read some bytes 26 | print(dev.read()) 27 | 28 | # close a device 29 | dev.close() 30 | ``` 31 | -------------------------------------------------------------------------------- /easyhid/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # Copyright 2017 jem@seethis.link 4 | # Licensed under the MIT license (http://opensource.org/licenses/MIT) 5 | 6 | from __future__ import absolute_import, division, print_function, unicode_literals 7 | 8 | from easyhid.easyhid import * 9 | from easyhid.version import __version__ 10 | -------------------------------------------------------------------------------- /easyhid/easyhid.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # Copyright 2017 jem@seethis.link 4 | # Licensed under the MIT license (http://opensource.org/licenses/MIT) 5 | 6 | from __future__ import absolute_import, division, print_function, unicode_literals 7 | 8 | import cffi 9 | import ctypes.util 10 | import platform 11 | import sys 12 | 13 | ffi = cffi.FFI() 14 | ffi.cdef(""" 15 | struct hid_device_info { 16 | char *path; 17 | unsigned short vendor_id; 18 | unsigned short product_id; 19 | wchar_t *serial_number; 20 | unsigned short release_number; 21 | wchar_t *manufacturer_string; 22 | wchar_t *product_string; 23 | unsigned short usage_page; 24 | unsigned short usage; 25 | int interface_number; 26 | struct hid_device_info *next; 27 | }; 28 | 29 | typedef struct hid_device_ hid_device; 30 | 31 | int hid_init(void); 32 | int hid_exit(void); 33 | struct hid_device_info* hid_enumerate(unsigned short, unsigned short); 34 | void hid_free_enumeration (struct hid_device_info *devs); 35 | hid_device* hid_open (unsigned short vendor_id, unsigned short product_id, const wchar_t *serial_number); 36 | hid_device* hid_open_path (const char *path); 37 | int hid_write (hid_device *device, const unsigned char *data, size_t length); 38 | int hid_read_timeout (hid_device *dev, unsigned char *data, size_t length, int milliseconds); 39 | int hid_read (hid_device *device, unsigned char *data, size_t length); 40 | int hid_set_nonblocking (hid_device *device, int nonblock); 41 | int hid_send_feature_report (hid_device *device, const unsigned char *data, size_t length); 42 | int hid_get_feature_report (hid_device *device, unsigned char *data, size_t length); 43 | void hid_close (hid_device *device); 44 | int hid_get_manufacturer_string (hid_device *device, wchar_t *string, size_t maxlen); 45 | int hid_get_product_string (hid_device *device, wchar_t *string, size_t maxlen); 46 | int hid_get_serial_number_string (hid_device *device, wchar_t *string, size_t maxlen); 47 | int hid_get_indexed_string (hid_device *device, int string_index, wchar_t *string, size_t maxlen); 48 | const wchar_t* hid_error (hid_device *device); 49 | """) 50 | 51 | if "Windows" in platform.platform(): 52 | try: 53 | hidapi = ffi.dlopen('hidapi.dll') 54 | except: 55 | hidapi = ffi.dlopen(ctypes.util.find_library('hidapi.dll')) 56 | elif "Darwin" in platform.platform(): 57 | try: 58 | hidapi = ffi.dlopen('hidapi') 59 | except: 60 | hidapi = ffi.dlopen(ctypes.util.find_library('hidapi')) 61 | else: 62 | try: 63 | hidapi = ffi.dlopen('hidapi-hidraw') 64 | except: 65 | libname = ctypes.util.find_library('hidapi-hidraw') 66 | 67 | if sys.version_info < (3, 6) and libname == None: 68 | # Couldn't find lib, use hardcode value so AppImage works. 69 | # Not need in >= 3.6 since ctypes.util.find_library will also 70 | # check LD_LIBRARY_PATH in newer versions of python. 71 | libname = 'libhidapi-hidraw.so.0' 72 | hidapi = ffi.dlopen(libname) 73 | 74 | def _c_to_py_str(val): 75 | if val == ffi.NULL: 76 | return None 77 | 78 | new_val = ffi.string(val) 79 | if type(new_val) == bytes or type(new_val) == bytearray: 80 | return new_val.decode("utf-8") 81 | else: 82 | return new_val 83 | 84 | class HIDException(Exception): 85 | pass 86 | 87 | class HIDDevice(object): 88 | """ 89 | A HID device for communication with a HID interface. 90 | 91 | Should normally be created through an Enumeration object. 92 | """ 93 | 94 | def __init__(self, cdata): 95 | if cdata == ffi.NULL: 96 | raise TypeError 97 | self.path = _c_to_py_str(cdata.path) 98 | self.vendor_id = cdata.vendor_id 99 | self.product_id = cdata.product_id 100 | self.release_number = cdata.release_number 101 | self.manufacturer_string = _c_to_py_str(cdata.manufacturer_string) 102 | self.product_string = _c_to_py_str(cdata.product_string) 103 | self.serial_number = _c_to_py_str(cdata.serial_number) 104 | self.usage_page = cdata.usage_page 105 | self.usage = cdata.usage 106 | self.interface_number = cdata.interface_number 107 | 108 | self._device = None 109 | self._is_open = False 110 | 111 | def __del__(self): 112 | self.close() 113 | 114 | def __enter__(self): 115 | self.open() 116 | 117 | def __exit__(self, err_type, err_value, traceback): 118 | self.close() 119 | 120 | def open(self): 121 | """ 122 | Open the HID device for reading and writing. 123 | """ 124 | if self._is_open: 125 | raise HIDException("Failed to open device: HIDDevice already open") 126 | 127 | path = self.path.encode('utf-8') 128 | dev = hidapi.hid_open_path(path) 129 | 130 | if dev: 131 | self._is_open = True 132 | self._device = dev 133 | else: 134 | raise HIDException("Failed to open device") 135 | 136 | 137 | def close(self): 138 | """ 139 | Closes the hid device 140 | """ 141 | if self._is_open: 142 | self._is_open = False 143 | hidapi.hid_close(self._device) 144 | 145 | def write(self, data, report_id=0): 146 | """ 147 | Writes data to the HID device on its endpoint. 148 | 149 | Parameters: 150 | data: data to send on the HID endpoint 151 | report_id: the report ID to use. 152 | 153 | Returns: 154 | The number of bytes written including the report ID. 155 | """ 156 | 157 | if not self._is_open: 158 | raise HIDException("HIDDevice not open") 159 | 160 | write_data = bytearray([report_id]) + bytearray(data) 161 | cdata = ffi.new("const unsigned char[]", bytes(write_data)) 162 | num_written = hidapi.hid_write(self._device, cdata, len(write_data)) 163 | if num_written < 0: 164 | raise HIDException("Failed to write to HID device: " + str(num_written)) 165 | else: 166 | return num_written 167 | 168 | def read(self, size=64, timeout=None): 169 | """ 170 | Read from the hid device on its endpoint. 171 | 172 | 173 | Parameters: 174 | size: number of bytes to read 175 | timeout: length to wait in milliseconds 176 | 177 | Returns: 178 | The HID report read from the device. The first byte in the result 179 | will be the report ID if used. 180 | """ 181 | 182 | if not self._is_open: 183 | raise HIDException("HIDDevice not open") 184 | 185 | data = [0] * size 186 | cdata = ffi.new("unsigned char[]", data) 187 | bytes_read = None 188 | 189 | if timeout == None: 190 | bytes_read = hidapi.hid_read(self._device, cdata, len(cdata)) 191 | else: 192 | bytes_read = hidapi.hid_read_timeout(self._device, cdata, len(cdata), timeout) 193 | 194 | 195 | if bytes_read < 0: 196 | raise HIDException("Failed to read from HID device: " + str(bytes_read)) 197 | elif bytes_read == 0: 198 | return bytearray([]) 199 | else: 200 | return bytearray(cdata) 201 | 202 | def set_nonblocking(self, enable_nonblocking): 203 | if not self._is_open: 204 | raise HIDException("HIDDevice not open") 205 | 206 | if type(enable_nonblocking) != bool: 207 | raise TypeError 208 | hidapi.hid_set_nonblocking(self._device, enable_nonblocking) 209 | 210 | def is_open(self): 211 | """Check if the HID device is open""" 212 | return self._is_open 213 | 214 | def is_connected(self): 215 | """ 216 | Checks if the USB device is still connected 217 | """ 218 | if self._is_open: 219 | err = hidapi.hid_read_timeout(self._device, ffi.NULL, 0, 0) 220 | if err == -1: 221 | return False 222 | else: 223 | return True 224 | else: 225 | en = Enumeration(vid=self.vendor_id, pid=self.product_id).find(path=self.path) 226 | if len(en) == 0: 227 | return False 228 | else: 229 | return True 230 | 231 | def send_feature_report(self, data, report_id=0x00): 232 | """ 233 | Send a Feature report to a HID device. 234 | 235 | Feature reports are sent over the Control endpoint as a Set_Report 236 | transfer. 237 | 238 | Parameters: 239 | data The data to send 240 | 241 | Returns: 242 | This function returns the actual number of bytes written 243 | """ 244 | if not self._is_open: 245 | raise HIDException("HIDDevice not open") 246 | 247 | report = bytearray([report_id]) + bytearray(data) 248 | cdata = ffi.new("const unsigned char[]", bytes(report)) 249 | bytes_written = hidapi.hid_send_feature_report(self._device, cdata, len(report)) 250 | 251 | if bytes_written == -1: 252 | raise HIDException("Failed to send feature report to HID device") 253 | 254 | return bytes_written 255 | 256 | def get_feature_report(self, size, report_id=0x00): 257 | """ 258 | Get a feature report from a HID device. 259 | 260 | Feature reports are sent over the Control endpoint as a Get_Report 261 | transfer. 262 | 263 | Parameters: 264 | size The number of bytes to read. 265 | report_id The report id to read 266 | 267 | Returns: 268 | They bytes read from the HID report 269 | """ 270 | data = [0] * (size+1) 271 | cdata = ffi.new("unsigned char[]", bytes(data)) 272 | cdata[0] = report_id 273 | 274 | bytes_read = hidapi.hid_get_feature_report(self._device, cdata, len(cdata)) 275 | 276 | if bytes_read == -1: 277 | raise HIDException("Failed to get feature report from HID device") 278 | 279 | return bytearray(cdata[1:size+1]) 280 | 281 | def get_error(self): 282 | """ 283 | Get an error string from the device 284 | """ 285 | err_str = hidapi.hid_error(self._device) 286 | if err_str == ffi.NULL: 287 | return None 288 | else: 289 | return ffi.string(err_str) 290 | 291 | def _get_prod_string_common(self, hid_fn): 292 | max_len = 128 293 | str_buf = ffi.new("wchar_t[]", bytearray(max_len).decode('utf-8')) 294 | ret = hid_fn(self._device, str_buf, max_len) 295 | if ret < 0: 296 | raise HIDException(self._device.get_error()) 297 | else: 298 | assert(ret == 0) 299 | return ffi.string(str_buf) 300 | 301 | # Probably don't need these excpet for get_indexed_string, since they won't 302 | # change from the values found in the enumeration 303 | def get_manufacture_string(self): 304 | """ 305 | Get the manufacturer string of the device from its device descriptor 306 | """ 307 | return self._get_prod_string_common(hidapi.hid_get_manufacturer_string) 308 | 309 | def get_product_string(self): 310 | """ 311 | Get the product string of the device from its device descriptor 312 | """ 313 | return self._get_prod_string_common(hidapi.hid_get_product_string) 314 | 315 | def get_serial_number(self): 316 | """ 317 | Get the serial number string of the device from its device descriptor 318 | """ 319 | return self._get_prod_string_common(hidapi.hid_get_serial_number_string) 320 | 321 | def get_indexed_string(self, index): 322 | """ 323 | Get the string with the given index from the device 324 | """ 325 | max_len = 128 326 | str_buf = ffi.new("wchar_t[]", str(bytearray(max_len))) 327 | ret = hidapi.hid_get_indexed_string(self._device, index, str_buf, max_len) 328 | 329 | if ret < 0: 330 | raise HIDException(self._device.get_error()) 331 | elif ret == 0: 332 | return None 333 | else: 334 | return ffi.string(str_buf).encode('utf-8') 335 | 336 | 337 | def description(self): 338 | """ 339 | Get a string describing the HID descriptor. 340 | """ 341 | return \ 342 | """HIDDevice: 343 | {} | {:x}:{:x} | {} | {} | {} 344 | release_number: {} 345 | usage_page: {} 346 | usage: {} 347 | interface_number: {}\ 348 | """.format(self.path, 349 | self.vendor_id, 350 | self.product_id, 351 | self.manufacturer_string, 352 | self.product_string, 353 | self.serial_number, 354 | self.release_number, 355 | self.usage_page, 356 | self.usage, 357 | self.interface_number 358 | ) 359 | 360 | class Enumeration(object): 361 | def __init__(self, vid=0, pid=0): 362 | """ 363 | Create a USB HID enumeration. The enumeration is a list of all the HID 364 | interfaces connected at the time the object was created. 365 | """ 366 | self.device_list = _hid_enumerate(vid, pid) 367 | 368 | def show(self): 369 | """ 370 | Print the device description of each device in the Enumeration 371 | """ 372 | for dev in self.device_list: 373 | print(dev.description()) 374 | 375 | def find(self, vid=None, pid=None, serial=None, interface=None, \ 376 | path=None, release_number=None, manufacturer=None, 377 | product=None, usage=None, usage_page=None): 378 | """ 379 | Attempts to open a device in this `Enumeration` object. Optional 380 | arguments can be provided to filter the resulting list based on various 381 | parameters of the HID devices. 382 | 383 | Args: 384 | vid: filters by USB Vendor ID 385 | pid: filters by USB Product ID 386 | serial: filters by USB serial string (.iSerialNumber) 387 | interface: filters by interface number (bInterfaceNumber) 388 | release_number: filters by the USB release number (.bcdDevice) 389 | manufacturer: filters by USB manufacturer string (.iManufacturer) 390 | product: filters by USB product string (.iProduct) 391 | usage: filters by HID usage 392 | usage_page: filters by HID usage_page 393 | path: filters by HID API path. 394 | """ 395 | result = [] 396 | 397 | for dev in self.device_list: 398 | if vid not in [0, None] and dev.vendor_id != vid: 399 | continue 400 | if pid not in [0, None] and dev.product_id != pid: 401 | continue 402 | if serial and dev.serial_number != serial: 403 | continue 404 | if path and dev.path != path: 405 | continue 406 | if manufacturer and dev.manufacturer_string != manufacturer: 407 | continue 408 | if product and dev.product_string != product: 409 | continue 410 | if release_number != None and dev.release_number != release_number: 411 | continue 412 | if interface != None and dev.interface_number != interface: 413 | continue 414 | if usage != None and dev.usage != usage: 415 | continue 416 | if usage_page != None and dev.usage_page != usage_page: 417 | continue 418 | result.append(dev) 419 | return result 420 | 421 | 422 | def _hid_enumerate(vendor_id=0, product_id=0): 423 | """ 424 | Enumerates all the hid devices for VID:PID. Returns a list of `HIDDevice` 425 | objects. If vid is 0, then match any vendor id. Similarly, if pid is 0, 426 | match any product id. If both are zero, enumerate all HID devices. 427 | """ 428 | start = hidapi.hid_enumerate(vendor_id, product_id) 429 | result = [] 430 | cur = ffi.new("struct hid_device_info*"); 431 | cur = start 432 | 433 | # Copy everything into python list 434 | while cur != ffi.NULL: 435 | result.append(HIDDevice(cur)) 436 | cur = cur.next 437 | 438 | # Free the C memory 439 | hidapi.hid_free_enumeration(start) 440 | 441 | return result 442 | 443 | 444 | if __name__ == "__main__": 445 | # Examples 446 | from easyhid import Enumeration 447 | 448 | # Stores an enumeration of all the connected USB HID devices 449 | en = Enumeration() 450 | 451 | # return a list of devices based on the search parameters 452 | devices = en.find(manufacturer="Company", product="Widget", interface=3) 453 | 454 | # print a description of the devices found 455 | for dev in devices: 456 | print(dev.description()) 457 | 458 | # open a device 459 | dev.open() 460 | 461 | # write some bytes to the device 462 | dev.write(bytearray([0, 1, 2, 3])) 463 | 464 | # read some bytes 465 | print(dev.read()) 466 | 467 | # close a device 468 | dev.close() 469 | -------------------------------------------------------------------------------- /easyhid/version.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.0.10' 2 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # Copyright 2018 jem@seethis.link 4 | # Licensed under the MIT license (http://opensource.org/licenses/MIT) 5 | 6 | from setuptools import setup 7 | import os 8 | 9 | # Load the version number 10 | try: # python3 11 | fields = {} 12 | with open(os.path.join("easyhid", "version.py")) as f: 13 | exec(f.read(), fields) 14 | __version__ = fields['__version__'] 15 | except: # python2 16 | execfile(os.path.join("easyhid", "version.py")) 17 | 18 | setup( 19 | name = 'easyhid', 20 | version = __version__, 21 | description = "A simple interface to the HIDAPI library.", 22 | url = "http://github.com/ahtn/python-easyhid", 23 | author = "jem", 24 | author_email = "jem@seethis.link", 25 | license = 'MIT', 26 | packages = ['easyhid'], 27 | install_requires = ['cffi'], 28 | keywords = ['hidapi', 'usb', 'hid'], 29 | zip_safe = False 30 | ) 31 | --------------------------------------------------------------------------------