Skip to content

Instantly share code, notes, and snippets.

@rdb
Last active October 1, 2023 16:10
  • Star 68 You must be signed in to star a gist
  • Fork 23 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save rdb/8864666 to your computer and use it in GitHub Desktop.
Access joysticks/game controllers from Python in Linux via the joystick driver. See https://discourse.panda3d.org/t/game-controllers-on-linux-without-pygame/14128
# Released by rdb under the Unlicense (unlicense.org)
# Based on information from:
# https://www.kernel.org/doc/Documentation/input/joystick-api.txt
import os, struct, array
from fcntl import ioctl
# Iterate over the joystick devices.
print('Available devices:')
for fn in os.listdir('/dev/input'):
if fn.startswith('js'):
print(' /dev/input/%s' % (fn))
# We'll store the states here.
axis_states = {}
button_states = {}
# These constants were borrowed from linux/input.h
axis_names = {
0x00 : 'x',
0x01 : 'y',
0x02 : 'z',
0x03 : 'rx',
0x04 : 'ry',
0x05 : 'rz',
0x06 : 'throttle',
0x07 : 'rudder',
0x08 : 'wheel',
0x09 : 'gas',
0x0a : 'brake',
0x10 : 'hat0x',
0x11 : 'hat0y',
0x12 : 'hat1x',
0x13 : 'hat1y',
0x14 : 'hat2x',
0x15 : 'hat2y',
0x16 : 'hat3x',
0x17 : 'hat3y',
0x18 : 'pressure',
0x19 : 'distance',
0x1a : 'tilt_x',
0x1b : 'tilt_y',
0x1c : 'tool_width',
0x20 : 'volume',
0x28 : 'misc',
}
button_names = {
0x120 : 'trigger',
0x121 : 'thumb',
0x122 : 'thumb2',
0x123 : 'top',
0x124 : 'top2',
0x125 : 'pinkie',
0x126 : 'base',
0x127 : 'base2',
0x128 : 'base3',
0x129 : 'base4',
0x12a : 'base5',
0x12b : 'base6',
0x12f : 'dead',
0x130 : 'a',
0x131 : 'b',
0x132 : 'c',
0x133 : 'x',
0x134 : 'y',
0x135 : 'z',
0x136 : 'tl',
0x137 : 'tr',
0x138 : 'tl2',
0x139 : 'tr2',
0x13a : 'select',
0x13b : 'start',
0x13c : 'mode',
0x13d : 'thumbl',
0x13e : 'thumbr',
0x220 : 'dpad_up',
0x221 : 'dpad_down',
0x222 : 'dpad_left',
0x223 : 'dpad_right',
# XBox 360 controller uses these codes.
0x2c0 : 'dpad_left',
0x2c1 : 'dpad_right',
0x2c2 : 'dpad_up',
0x2c3 : 'dpad_down',
}
axis_map = []
button_map = []
# Open the joystick device.
fn = '/dev/input/js0'
print('Opening %s...' % fn)
jsdev = open(fn, 'rb')
# Get the device name.
#buf = bytearray(63)
buf = array.array('B', [0] * 64)
ioctl(jsdev, 0x80006a13 + (0x10000 * len(buf)), buf) # JSIOCGNAME(len)
js_name = buf.tobytes().rstrip(b'\x00').decode('utf-8')
print('Device name: %s' % js_name)
# Get number of axes and buttons.
buf = array.array('B', [0])
ioctl(jsdev, 0x80016a11, buf) # JSIOCGAXES
num_axes = buf[0]
buf = array.array('B', [0])
ioctl(jsdev, 0x80016a12, buf) # JSIOCGBUTTONS
num_buttons = buf[0]
# Get the axis map.
buf = array.array('B', [0] * 0x40)
ioctl(jsdev, 0x80406a32, buf) # JSIOCGAXMAP
for axis in buf[:num_axes]:
axis_name = axis_names.get(axis, 'unknown(0x%02x)' % axis)
axis_map.append(axis_name)
axis_states[axis_name] = 0.0
# Get the button map.
buf = array.array('H', [0] * 200)
ioctl(jsdev, 0x80406a34, buf) # JSIOCGBTNMAP
for btn in buf[:num_buttons]:
btn_name = button_names.get(btn, 'unknown(0x%03x)' % btn)
button_map.append(btn_name)
button_states[btn_name] = 0
print('%d axes found: %s' % (num_axes, ', '.join(axis_map)))
print('%d buttons found: %s' % (num_buttons, ', '.join(button_map)))
# Main event loop
while True:
evbuf = jsdev.read(8)
if evbuf:
time, value, type, number = struct.unpack('IhBB', evbuf)
if type & 0x80:
print("(initial)", end="")
if type & 0x01:
button = button_map[number]
if button:
button_states[button] = value
if value:
print("%s pressed" % (button))
else:
print("%s released" % (button))
if type & 0x02:
axis = axis_map[number]
if axis:
fvalue = value / 32767.0
axis_states[axis] = fvalue
print("%s: %.3f" % (axis, fvalue))
@jwells4
Copy link

jwells4 commented Sep 19, 2019

This code gives me a lot of syntax errors, specifically on the print statements. I'm new to python and Raspberry pi. Is there something I'm doing wrong? I'm using Pi4 and python 3.7.3.

@rdb
Copy link
Author

rdb commented Sep 19, 2019

This code gives me a lot of syntax errors, specifically on the print statements. I'm new to python and Raspberry pi. Is there something I'm doing wrong? I'm using Pi4 and python 3.7.3.

@jwells4 I have just updated the gist to use Python 3 print functions instead of Python 2 print statements.

@jwells4
Copy link

jwells4 commented Sep 20, 2019

Thanks so much for making the changes! I was actually doing those myself, but it's nice when it's done by someone more familiar with the language than me lol. I'm still getting errors, but not with the prints. Now I'm getting the following output when running the code:

Available devices:
/dev/input/js0
Opening /dev/input/js0...
Traceback (most recent call last):
File "controllerTest.py", line 101, in
buf = array.array('c', ['\0'] * 64)
ValueError: bad typecode (must be b, B, u, h, H, i, I, l, L, q, Q, f or d)

From what I've been able to find online, this is because 'c' is not a valid type as of 3.5? Based on some reading, I replaced the 'c' with 'u' (for unicode) and the code now seems to work. There may be a more-correct or more elegant way of doing this, but I wanted to point it out anyway. Thanks for the response and the code update!

@rdb
Copy link
Author

rdb commented Sep 20, 2019

@jwells4 I think that is not correct; I have just updated the code to what I think should be right, though I have not tested it with Python 3 yet.

@jwells4
Copy link

jwells4 commented Sep 20, 2019

I just tested it on my setup and it works well. The difference between your update and my noob solution was that the device name now properly displays. Again, thanks so much for updating this. It's been super helpful.

@emdeex
Copy link

emdeex commented Dec 28, 2019

@franferri
Copy link

Got this to work on python3 here:

https://gist.github.com/emdeex/97b771b264bebbd1e18dd897404040be

Thanks!

@SergioMOrozco
Copy link

Works right out of the box with an xbox one controller on my raspberry pi. Thanks man. Saved me a headache.

@d-wiles
Copy link

d-wiles commented Feb 14, 2021

I think the JSIOCGBTNMAP IOCTL call has the wrong magic number.

If I'm reading the kernel source correctly, buf should be 0x200 unsigned shorts ((KEY_MAX - BTN_MISC) + 1), not decimal 200.

Also, len(buf) gives the number of elements in buf (0x200). (buf.buffer_info()[1] * buf.itemsize) returns the size in bytes (0x400).

If both assumptions are correct, the magic number should be 0x84006a34, not 0x80406a34.

Also, "throttle" is misspelled as "trottle".

@rdb
Copy link
Author

rdb commented Apr 21, 2021

@d-wiles that really depends on which version of the headers you have; I just tried in CentOS 5 and it's even 0x82006a34. Linux is flexible about this, masking out the size bits:
https://github.com/torvalds/linux/blob/6417f03132a6952cd17ddd8eaddbac92b61b17e0/drivers/input/joydev.c#L578

KEY_MAX also used to have a lower value.

@Ju-Hyeon4804
Copy link

Thank you for your work, but you don't have to wait for this command until there's an event on the joystick
I would like to proceed
evbuf = jsdev.read(8).
I want to look at the camera while controlling the RC car. However, there must be an event for the camera to work.

@rdb
Copy link
Author

rdb commented Nov 3, 2022

You can probably make it non-blocking by using the lower-level os.open() instead of open() with the os.O_RDONLY | os.O_NONBLOCK flags.

Another way is to use a thread. It will be woken up when there is data available.

@Ju-Hyeon4804
Copy link

Thank you for your feed pack. But I get an error.
(AttributeError: 'int' object has no attribute 'read')
I'm sorry I'm a Python beginner.

@rdb
Copy link
Author

rdb commented Nov 5, 2022

It's not that trivial: the os.open() call is different than built-in open(), it returns a fd, you have to use it with the other functions from the os module, change all the calls to those instead.

@Ju-Hyeon4804
Copy link

Ju-Hyeon4804 commented Nov 5, 2022

Thank you for answer. But I'm a Python beginner, sorry. I'm not sure exactly which part I need to change. Can you give more detailed feedback?

@rdb
Copy link
Author

rdb commented Nov 5, 2022

I checked and instead of os.open there may be an easier alternative, by calling this on the file after the open call:

os.set_blocking(jsdev.fileno(), False)

Of course you'll need to deal with the exception you will probably get when calling read() without data being available.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment