hid to osc

A Human Interface Device (HID) to Open Sound Control (OSC) converter for macOS written in Python3.

The program can send OSC to any IP and port but by default it will send to 127.0.0.1 (localhost) on port 57120 (SuperCollider).

Here is a binary build... HIDtoOSC.zip (macOS, 64bit, 5.3mb)

To run it start Terminal.app and type...

cd ~/Downloads
./HIDtoOSC

That should list available HID devices on your system. After that you will probably see a warning that it failed to open the default device.

So to open and start reading data from any of your own devices you will need to give the correct vendor id and product id as arguments.

usage: HIDtoOSC [-h] [-V] [--vid VID] [--pid PID] [--ip IP] [--port PORT]
                [--rate RATE] [--debug]

optional arguments:
  -h, --help     show this help message and exit
  -V, --version  show program version
  --vid VID      set HID vendor id
  --pid PID      set HID product id
  --ip IP        set OSC destination IP
  --port PORT    set OSC destination port
  --rate RATE    update rate in milliseconds
  --debug        post incoming HID data

 
example - usb keyboard

Here is an example that show how to connect to an external generic usb keyboard with the vendor and product id 6700, 2.

NOTE: for security reasons sudo is needed when accessing keyboards but not for accessing most other devices (gamepads, joysticks, mice)

Some times you will need to run the program a few times before the HID is claimed. Stop the program with ctrl+c

In the example the key A is pressed and released, but all HID will use their own data format. The --debug flag makes the program print out the incoming data.

 
receiving osc

In SuperCollider we can receive the data as an OSC message. Test it by running the line...

OSCFunc.trace;

Here is what the example above generates...

OSC Message Received:
        time: 75956.941459501
        address: a NetAddr(127.0.0.1, 56073)
        recvPort: 57120
        msg: [ /hid, 6700, 2, Int8Array[ 0, 0, 4, 0, 0, 0, 0, 0 ] ]

example - usb mouse

And here is another example connecting to an optical mouse. (no sudo needed)

This mouse example sends OSC to port 9999 and the data format is slightly different than in the keyboard example above.

 
building

If you do not trust the binary above (and you should not - specially when running it with sudo) you can run the python code directly.

homebrew and python3 is required.

First install some libraries...

brew install hidapi liblo

Next create an virtual environment...

cd ~/python3
mkdir HIDtoOSC && cd HIDtoOSC
python3 -m venv env
source env/bin/activate  #to later leave the virtual environment type: deactivate

Then install some python libraries...

pip install pyliblo
pip install hid pyusb
pip install pyinstaller

Finally copy the main python program below, put it in a file called HIDtoOSC.py and test it with for example python HIDtoOSC.py -h (stop with ctrl+c)

#f.olofsson 2019

# required python libraries: pyliblo, hid, pyusb
# example: sudo python HIDtoOSC.py --vid 6700 --pid 2 --port 12002 --debug
# note: macOS must be running this as root if the device is a keyboard

import sys, argparse
import usb.core, hid, liblo

#--settings
version= 0.1

#--arguments
parser= argparse.ArgumentParser()
parser.add_argument('-V', '--version', action= 'store_true', help= 'show program version')
parser.add_argument('--vid', type= int, dest= 'vid', help= 'set HID vendor id')
parser.add_argument('--pid', type= int, dest= 'pid', help= 'set HID product id')
parser.add_argument('--ip', dest= 'ip', help= 'set OSC destination IP')
parser.add_argument('--port', type= int, dest= 'port', help= 'set OSC destination port')
parser.add_argument('--rate', type= int, dest= 'rate', help= 'update rate in milliseconds')
parser.add_argument('--debug', dest= 'debug', action= 'store_true', help= 'post incoming HID data')

parser.set_defaults(vid= 1452)
parser.set_defaults(pid= 517)
parser.set_defaults(ip= '127.0.0.1')
parser.set_defaults(port= 57120)
parser.set_defaults(rate= 100)
parser.set_defaults(debug= False)
args= parser.parse_args()

if args.version:
  sys.exit('HIDtoOSC version %s'%version)

print('Available HID devices:')
for d in hid.enumerate():
  print('  device: %s, %s'%(d['manufacturer_string'], d['product_string']))
  print('  vid: %s, pid: %s'%(d['vendor_id'], d['product_id']))

def main():
  dev= usb.core.find(idVendor= args.vid, idProduct= args.pid)

  if dev is None:
    sys.exit('Could not find device %s, %s'%(args.vid, args.pid))

  for config in dev:
    for i in range(config.bNumInterfaces):
      if dev.is_kernel_driver_active(i):
        dev.detach_kernel_driver(i)

  try:
    dev.set_configuration()
  except usb.core.USBError as e:
    sys.exit('Could not set configuration: %s'%str(e))

  endpoint= dev[0][(0, 0)][0]
  try:
    dev= hid.Device(args.vid, args.pid)
    print('Device %s, %s open'%(args.vid, args.pid))
  except:
    sys.exit('Could not open device %s, %s'%(args.vid, args.pid))

  print(endpoint)
  target= (args.ip, args.port)
  noerror= True
  while noerror:
    try:
      data= dev.read(endpoint.wMaxPacketSize, args.rate)
    except:
      noerror= False
      try:
        dev.close()
        print('Closed the hid device')
      except:
        pass

    if data:
      if args.debug:
        print([x for x in data])
      try:
        msg= liblo.Message('/hid', args.vid, args.pid, ('b', data))
        liblo.send(target, msg)
      except:
        print('WARNING: Could not send osc to %s'%(target, ))

if __name__=='__main__':
  main()

Later you can build your own binary with the command...

pyinstaller HIDtoOSC.py --onefile --hidden-import=libhidapi --add-binary='/usr/local/lib/libhidapi.dylib:.'