Skip to content

Instantly share code, notes, and snippets.

@walchko
Last active October 18, 2020 18:04
Show Gist options
  • Save walchko/23d1e6480511892b4e0951254ce5f380 to your computer and use it in GitHub Desktop.
Save walchko/23d1e6480511892b4e0951254ce5f380 to your computer and use it in GitHub Desktop.
python Linux PS4 joystick
#!/usr/bin/env python
#-----------------------------------------
# MIT License
# Taken from: https://github.com/autorope/donkeycar/blob/dev/donkeycar/parts/controller.py
# 3 Jan 2020
# all of the button/axes names were wrong, but it did work
# I am using the USB cable and the touch pad/button dives my mouse ...
# maybe if I fix that, then the buttons will go back to what they were?
#------------------------------------------
# kernel doc: https://www.kernel.org/doc/Documentation/input/joystick-api.txt
#
# 2. Event Reading
# ~~~~~~~~~~~~~~~~
#
# struct js_event e;
# read (fd, &e, sizeof(e));
#
# where js_event is defined as
#
# struct js_event {
# __u32 time; /* event timestamp in milliseconds */
# __s16 value; /* value */
# __u8 type; /* event type */
# __u8 number; /* axis/button number */
# };
#
# 2.4 js_event.time
# ~~~~~~~~~~~~~~~~~
#
# The time an event was generated is stored in ``js_event.time''. It's a time
# in milliseconds since ... well, since sometime in the past. This eases the
# task of detecting double clicks, figuring out if movement of axis and button
# presses happened at the same time, and similar.
import os
import array
import time
import struct
import logging
from pprint import pprint
class Joystick(object):
'''
An interface to a physical joystick
'''
def __init__(self, dev_fn='/dev/input/js0'):
# states hold the current value of the joystick
self.axis_states = {}
self.button_states = {}
# names match button codes to human button understanding
self.axis_names = {}
self.button_names = {}
# mapping of names to buttons/axes for printing
self.axis_map = []
self.button_map = []
self.jsdev = None # joystick file descriptor
self.dev_fn = dev_fn # joystick file name
def __del__(self):
# if still open, close it
if self.jsdev:
self.jsdev.close()
self.jsdev = None
def init(self):
try:
from fcntl import ioctl
except ModuleNotFoundError:
self.num_axes = 0
self.num_buttons = 0
print("no support for fnctl module. joystick not enabled.")
return False
if not os.path.exists(self.dev_fn):
print(self.dev_fn, "is missing")
return False
'''
call once to setup connection to device and map buttons
'''
# Open the joystick device.
# print('Opening %s...' % self.dev_fn)
self.jsdev = open(self.dev_fn, 'rb')
# Get the device name.
buf = array.array('B', [0] * 64)
ioctl(self.jsdev, 0x80006a13 + (0x10000 * len(buf)), buf) # JSIOCGNAME(len)
self.js_name = buf.tobytes().decode('utf-8')
# print('Device name: %s' % self.js_name)
# Get number of axes and buttons.
buf = array.array('B', [0])
ioctl(self.jsdev, 0x80016a11, buf) # JSIOCGAXES
self.num_axes = buf[0]
buf = array.array('B', [0])
ioctl(self.jsdev, 0x80016a12, buf) # JSIOCGBUTTONS
self.num_buttons = buf[0]
# Get the axis map.
buf = array.array('B', [0] * 0x40)
ioctl(self.jsdev, 0x80406a32, buf) # JSIOCGAXMAP
for axis in buf[:self.num_axes]:
axis_name = self.axis_names.get(axis, 'unknown(0x%02x)' % axis)
self.axis_map.append(axis_name)
self.axis_states[axis_name] = 0.0
# Get the button map.
buf = array.array('H', [0] * 200)
ioctl(self.jsdev, 0x80406a34, buf) # JSIOCGBTNMAP
for btn in buf[:self.num_buttons]:
btn_name = self.button_names.get(btn, 'unknown(0x%03x)' % btn)
self.button_map.append(btn_name)
self.button_states[btn_name] = 0
#print('btn', '0x%03x' % btn, 'name', btn_name)
self.show_map()
return True
def show_map(self):
'''
list the buttons and axis found on this joystick
'''
print('='*70)
print(" {}".format(self.js_name))
print(" File Descriptor: {}".format(self.dev_fn))
print("-"*70)
print (' %d axes found:' % (self.num_axes))
print (' %s' % (', '.join(self.axis_map)))
print (' %d buttons found:' % (self.num_buttons))
print (' %s' % (', '.join(self.button_map)))
print("-"*70)
def poll(self):
'''
query the state of the joystick, returns button which was pressed, if any,
and axis which was moved, if any. button_state will be None, 1, or 0 if no changes,
pressed, or released. axis_val will be a float from -1 to +1. button and axis will
be the string label determined by the axis map in init.
'''
button = None
button_state = None
axis = None
axis_val = None
if self.jsdev is None:
print("self.jsdev == None")
return button, button_state, axis, axis_val
# read the joystick, it will que up events until you read them. If
# you do not read them fast enough, the driver will reset and get a
# 0x80 below
evbuf = self.jsdev.read(8)
# print(">> evbuf", evbuf)
if evbuf:
tval, value, typev, number = struct.unpack('IhBB', evbuf)
if typev & 0x80:
# ignore initialization event, either because the joystick just
# started OR you are not reading it fast enough and the internal
# queue is over flowing
print(">> initialization event ... ignore values")
return button, button_state, axis, axis_val
if typev & 0x01:
button = self.button_map[number]
#print(tval, value, typev, number, button, 'pressed')
if button:
self.button_states[button] = value
button_state = value
logging.info("button: %s state: %d" % (button, value))
if typev & 0x02:
axis = self.axis_map[number]
if axis:
fvalue = value / 32767.0
self.axis_states[axis] = fvalue
axis_val = fvalue
logging.debug("axis: %s val: %f" % (axis, fvalue))
return button, button_state, axis, axis_val
class PS4Joystick(Joystick):
'''
An interface to a physical PS4 joystick available at /dev/input/js0
'''
def __init__(self, *args, **kwargs):
super(PS4Joystick, self).__init__(*args, **kwargs)
self.axis_names = {
# 16b floats: -1.0 to 1.0
# Left stick (LS)
0x00 : 'LS_x',
0x01 : 'LS_y',
# Right stick (RS)
0x03 : 'RS_x',
0x04 : 'RS_y',
0x02 : 'L2_axis',
0x05 : 'R2_axis',
0x10 : 'dpad_leftright',
0x11 : 'dpad_updown',
0x19 : 'tilt_a',
0x1a : 'tilt_b',
0x1b : 'tilt_c',
0x06 : 'motion_a',
0x07 : 'motion_b',
0x08 : 'motion_c',
}
self.button_names = {
# 0 - unpushed, 1 - pushed
0x134 : 'square',
0x130 : 'cross',
0x131 : 'circle',
0x133 : 'triangle',
0x136 : 'L1',
0x137 : 'R1',
0x138 : 'L2',
0x139 : 'R2',
0x13d : 'L3',
0x13e : 'R3',
0x13a : 'share',
0x13b : 'options',
0x13c : 'PS',
}
js = PS4Joystick()
js.init()
# try:
# while True:
# button, button_state, axis, axis_val = js.poll()
# pprint(js.axis_states)
# pprint(js.button_states)
# # print(axis)
# # print(axis_val)
# # time.sleep(0.05)
# if js.button_states['PS']:
# break
# except KeyboardInterrupt:
# print("\n\nctrl-C")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment