• Wooji Juice
  • Free
  • Blog
  • Support
  • About

PyHID Documentation

Overview

This is a library for accessing USB Human Interface Devices (HIDs) on Mac OS X, from within C++ or Python. Its philosophy is somewhat inspired by the SWT widget libraries for Java, in that it provides only very simple, low-level native APIs in C, getting as quickly as possible into a high-level language to provide clean, friendly APIs there.

At the lowest level, a C++ wrapper provides a simpler way to work with HIDs than the HIDManager Carbon API. This is contained in HID.cpp/HID.h. It’s useful in its own right if you’re writing C++ code, but not really the focus of the exercise.

Next, a Python C Extension Module, PyHID.lowlevel, exposes a plain procedural interface to HID.cpp/HID.h, while PyHID.consts exposes the #defines from IOHIDUsageTables.h to Python.

The high level interfaces are in PyHID.devices, PyHID.usage and, optionally, PyHID.gaming. These provide happy object-oriented interfaces to the HID drivers.

As a bonus, the specific subpackage contains modules that provide interfaces to specific bits of hardware that I’ve used. These generally don’t do anything you can’t do with the rest of the PyHID API, they’re just helpful. Right now, the only device in there is the k8055 with which you can read inputs and control outputs on the Velleman K8055 USB experiment board.

For convenience, the PyHID package imports some key functions from the submodules.

We’ll start at the top, since this is what most people using PyHID will be interested in, and then work down into the messy depths for those who need to access the drivers at a lower level.

PyHID

Call PyHID.initialise() before doing anything else with the library.

Call PyHID.finalise() when you’re completely done with the library.

PyHID.scan_devices() will rescan the system for connected USB HIDs and return a list containing a PyHID.devices.Device instance for each device found.

PyHID.devices.Device

The Device class has the following member variables of interest:

  • name – the device’s name. Typically the product name supplied by the manufacturer, but occasionally may have been set by the user (for instance, Apple Bluetooth Keyboards are usually named by their owner during the setup process).

  • manufacturer – the name of the device’s manufacturer, eg Apple.

  • elements – a list of all the elements (buttons, axes, LEDs, force-feedback controls, and other stuff present on the device and individually addressable by the HID drivers)

  • raw_elements – the same list. However, some subclasses of Device modify the elements list, while the raw_elements will be left intact.

  • classname – the USB class name of the device. A technical detail you probably don’t care about, but it’s there if you need it. You can, for instance, use it to tell USB and Bluetooth devices apart.

There are also three functions, which we will come back to later:

  • enable_monitoring() – creates an event queue for the device.

  • disable_monitoring() – destroys the event queue.

  • poll() – pulls the earliest event (or None if empty) from the queue.

PyHID.devices.Element

HID Elements are the meat of the module. These allow you to read or write the value of the elements, obviously, but also let you check the range of values they can return, and their intended usage, which can be invaluable for figuring out sensible default control mappings.

HID Element class members of interest:

  • usage and usagepage – These contain the USB Usage and Usage Pages. Together, these indicate the kind of element and its anticipated usage. They are set to integer constants from the PyHID.consts module, but a more convenient interface is found in PyHID.usage, which contains dictionaries allowing you to look up the constants to get a human-readable string.

  • min and max – These indicate the minimum and maximum values that the device element will normally return. Note that you can’t rely on elements only ever returning values in this range; for instance, a “hat switch” (a digital directional switch returning one of eight compass directions) on a device I own returns the range 0-7 (corresponding to the compass points) but also returns 15 when centred. Digital buttons generally have the range 0-1. The range of the axes depends on the resolution and anticipated use of the device.

  • device – A reference back to the device the element belongs to.

Methods:

  • read() – Returns the raw integer value of the element at that moment with no transformations applied.

  • __call__() – Calling the element like a function returns the value of the element at that moment, as a float which has been transformed to a known range – by default, from 0.0 to 1.0. Transformations are documented in more detail below.

  • set_transform() – Sets the range that the raw element value is transformed into. Takes up to three parameters: min, max and transform_func. See below for more.

  • enable_monitoring() and disable_monitoring() – These enable and disable event monitoring for this element. Described below.

  • get_usage_string() – Translates self.usage into a human-readable string

  • get_usage_page_string() – Translates self.usagepage into a human-readable string

Event queues

When event monitoring is enabled on an element, then every time the element’s value changes, the new value is stored in a queue, along with a timestamp (a float, measured in seconds, relative to when PyHID.initialise() was called). This can get very “spammy” for analogue joystick axes and the like, as “jitter” in the device’s hardware causes the values to continuously change by small amounts. However, for digital buttons, event queues can be invaluable as they allow you to register button-presses successfully even when your application is running slowly and would otherwise have missed them.

Events are stored in a single queue per device, from which you can pull them using device.poll(). The result is None if no events are pending, or a tuple (element, raw_value, timestamp).

Note that the queue is of limited size and old events will be thrown away when the queue fills up. You can set the size of the queue when you call device.enable_monitoring(). The default is 8 events.

Transformations

When you read an element by calling its read() method, the value you get back is always the raw value as returned by the HID Manager directly. However, the values returned by different brands or products vary wildly. One might return values from 0 to 255, another might range from -32767 to 32768. This gets old pretty quickly. It’s more useful to translate all values into a particular range; typically -1.0 to 1.0 for steering/movement axes, or 0.0 to 1.0 for magnitude axes such as audio volume or acceleration.

PyHID can take care of this for you. Calling an element as a function returns a normalised, floating-point value, from 0.0 to 1.0 by default. This range can be changed with set_transform(), eg steering_wheel.set_transform(-1.0, 1.0)

You can also supply a callable which is automatically applied to the values before they are returned. This might be used for creating a “dead zone”, or making axes non-linear, and so on. When writing such a transformation, it’s helpful to work in certain ranges – for example, most functions are easier to write when the range is centred around zero. Take this crude dead-zone example (I don’t recommend using this, by the way – use the smooth_deadzone provided in the gaming module):

Python

cheap-ass deadzone function:
def deadzone(v): if abs(v)<0.1: return 0 return v

However, both the device’s raw values, and the range requested by set_transform() are completely arbitrary. What to do? Again, PyHID helps, by automatically transforming device values into a suitable range for the transformation function, and then transforming the output from the function into the requested range.

If the callable passed to set_transform() has a member range, which is a tuple of two floats (min, max), this is assumed to be the domain of the function, and the raw values are transformed into this range before passing to the callable. Its output is assumed to be in the same range, and this is then transformed to the range specified in set_transform() before being returned to the application. Thus you simply need to add the following after the previous example:

python

example (ctd)
deadzone.range = -1.0, 1.0

If you don’t supply a range, the function is assumed to operate in the range 0.0 to 1.0.

PyHID.gaming

This is a convenience module for game developers (and anyone else interested in using similar controllers for non-gaming purposes). PyHID.gaming provides subclasses of Device: GameDevice and GamePad. Pass a normal Device instance to their constructors.

GameDevice is identical to Device except that it filters down the list of elements to just those which it deems relevant to traditional game input devices (you can always subclass it yourself if you need more esoteric support). A HID device/elements list is often very large as it may include LEDs, force-feedback controllers, telephone handsets and ringers – the USB HID spec even defines element usages for fire alarms, climate control, voltage regulation, battery charging, and all sorts of other unlikely stuff, so starting off with a filtered list can be useful.

GamePad subclasses GameDevice, and looks at each element that GameDevice accepted as relevant, and puts a reference to it into one of three lists: buttons, axes and pads. These store digital buttons, analogue axes and direction pads/hat switches respectively.

There are also some helpful global functions in the module:

  • is_game_element(element) – returns True if the element is believed to be relevant to gaming

  • is_probably_game_device(device) – returns True if any of the device’s elements cause is_game_element() to return True

  • guess_type(element) – attempts to guess if the element is a button, axis or directional pad. Returns one of three constants accordingly: Button, Pad or Axis.

  • smooth_deadzone() – Pass to set_transform() to get smooth dead-zone functionality.

C++ Interface

The core of the C++ interface is the HIDManager class. Constructing and destructing this corresponds to the initialise()/finalise() calls in the Python interface. Although HIDManager is not designed as a Singleton (I don’t really believe in them), you should not create more than one instance.

HIDManager members:

  • ScanDevices() – a scan is done automatically when constructed, but you can call this to rescan, to find removed or added devices. Note that any pointers to IHIDevices or IHIDElements will be invalidated, regardless of whether the device list has changed or not!

  • GetDeviceCount() – Retrieve the number of devices found

  • GetDevice(int i) – Retrieves the i’th device, 0 <= i < GetDeviceCount()

GetDevice() returns a pointer to an IHIDevice. This interface has these members:

  • GetDeviceName(), GetManufacturerName(), GetDeviceClassName() – Retrieve the appropriate info, as an STL std::string

  • StartQueue(int size) – Create and start monitoring an event queue for the device, with capacity for size events.

  • StopQueue() – Cease monitoring and destroy the event queue for the device.

  • bool PollQueue(IHIDElement& element, int& value, TimeUnits& timestamp) – If the queue is empty, returns false and leaves the reference parameters undefined. Otherwise returns true, and pulls the earliest event off the queue, setting the parameters to the appropriate members of the event. Note that value is the raw untransformed value of the element, and TimeUnits are currently typedef’d to floats and are measured in seconds since the HIDManager was instantiated.

  • GetElementCount() – Returns the number of elements the device has.

  • GetElement(int i) – Return the i’th element, 0 <= i < GetElementCount()

  • StartMonitorngElement(int i) – Adds the i’th element to the set of elements who post events to the event queue

  • StopMonitoringElement(int i) – Removes the i’th element from the set of elements who post events to the event queue.

IHIDElements, returned from GetElement(), have the following interface:

  • GetIndex() – Returns their index, ie: device->GetElement(n)->GetIndex() == n

  • unsigned int GetUsage() – Returns the USB HID Usage

  • unsigned int GetUsagePage() – Returns the USB HID Usage Page

  • int GetValue() – Returns the raw, untransformed value of the element at this moment.

  • int GetMinValue(), int GetMaxValue() – Return the min and max values that can be returned. See the caveat earlier, in the Python documentation, about this.

  • float GetNormalisedValue() – Returns the value of the element at this moment, as a float, scaled to (by default), the range 0.f to 1.f

  • SetNormalisedRange(float min, float max) – Changes the behaviour of GetNormalisedValue(), so that the result will be from min (when GetValue()==GetMinValue()) to max (when GetValue()==GetMaxValue()).

Error Handling

If an error occurs in the C++ portion of the library, a HIDException is raised. This includes a GetReason() method that returns a human-readable STL std::string explaining what went wrong. The Python translation layer, PyHID.cpp, catches these and raises a Python SystemError exception with the same message.

Known Issues

  • Currently, only reading simple (not compound) values is supported.

  • There is no system in place for detecting the addition/removal of devices. This might be added, or might be a new module that interacts with PyHID (because it’s useful to know when USB devices are added or removed whether or not they are in the USB HID category, and the functionality inside of Mac OS X is part of the general IO libraries, not the HID Manager)

Licensing

PyHID is licensed under similar terms to Python. Details are in the download pack.

© Wooji Juice 2023