|  |  |  | #!/usr/bin/env python3
 | 
					
						
							|  |  |  | # set up wheel
 | 
					
						
							|  |  |  | import array
 | 
					
						
							|  |  |  | import os
 | 
					
						
							|  |  |  | import struct
 | 
					
						
							|  |  |  | from fcntl import ioctl
 | 
					
						
							|  |  |  | from typing import NoReturn
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | # Iterate over the joystick devices.
 | 
					
						
							|  |  |  | print('Available devices:')
 | 
					
						
							|  |  |  | for fn in os.listdir('/dev/input'):
 | 
					
						
							|  |  |  |     if fn.startswith('js'):
 | 
					
						
							|  |  |  |         print(f'  /dev/input/{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 : 'trottle',
 | 
					
						
							|  |  |  |     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 = []
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def wheel_poll_thread(q: 'Queue[str]') -> NoReturn:
 | 
					
						
							|  |  |  |   # Open the joystick device.
 | 
					
						
							|  |  |  |   fn = '/dev/input/js0'
 | 
					
						
							|  |  |  |   print(f'Opening {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(f'Device name: {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, f'unknown(0x{_axis:02x})')
 | 
					
						
							|  |  |  |       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, f'unknown(0x{btn:03x})')
 | 
					
						
							|  |  |  |       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)))
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   # Enable FF
 | 
					
						
							|  |  |  |   import evdev  # pylint: disable=import-error
 | 
					
						
							|  |  |  |   from evdev import ecodes, InputDevice  # pylint: disable=import-error
 | 
					
						
							|  |  |  |   device = evdev.list_devices()[0]
 | 
					
						
							|  |  |  |   evtdev = InputDevice(device)
 | 
					
						
							|  |  |  |   val = 24000
 | 
					
						
							|  |  |  |   evtdev.write(ecodes.EV_FF, ecodes.FF_AUTOCENTER, val)
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   while True:
 | 
					
						
							|  |  |  |     evbuf = jsdev.read(8)
 | 
					
						
							|  |  |  |     value, mtype, number = struct.unpack('4xhBB', evbuf)
 | 
					
						
							|  |  |  |     # print(mtype, number, value)
 | 
					
						
							|  |  |  |     if mtype & 0x02:  # wheel & paddles
 | 
					
						
							|  |  |  |       axis = axis_map[number]
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       if axis == "z":  # gas
 | 
					
						
							|  |  |  |         fvalue = value / 32767.0
 | 
					
						
							|  |  |  |         axis_states[axis] = fvalue
 | 
					
						
							|  |  |  |         normalized = (1 - fvalue) * 50
 | 
					
						
							|  |  |  |         q.put(f"throttle_{normalized:f}")
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       elif axis == "rz":  # brake
 | 
					
						
							|  |  |  |         fvalue = value / 32767.0
 | 
					
						
							|  |  |  |         axis_states[axis] = fvalue
 | 
					
						
							|  |  |  |         normalized = (1 - fvalue) * 50
 | 
					
						
							|  |  |  |         q.put(f"brake_{normalized:f}")
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       elif axis == "x":  # steer angle
 | 
					
						
							|  |  |  |         fvalue = value / 32767.0
 | 
					
						
							|  |  |  |         axis_states[axis] = fvalue
 | 
					
						
							|  |  |  |         normalized = fvalue
 | 
					
						
							|  |  |  |         q.put(f"steer_{normalized:f}")
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     elif mtype & 0x01:  # buttons
 | 
					
						
							|  |  |  |       if value == 1: # press down
 | 
					
						
							|  |  |  |         if number in [0, 19]:  # X
 | 
					
						
							|  |  |  |           q.put("cruise_down")
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         elif number in [3, 18]:  # triangle
 | 
					
						
							|  |  |  |           q.put("cruise_up")
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         elif number in [1, 6]:  # square
 | 
					
						
							|  |  |  |           q.put("cruise_cancel")
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         elif number in [10, 21]:  # R3
 | 
					
						
							|  |  |  |           q.put("reverse_switch")
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | if __name__ == '__main__':
 | 
					
						
							|  |  |  |   from multiprocessing import Process, Queue
 | 
					
						
							|  |  |  |   q: Queue[str] = Queue()
 | 
					
						
							|  |  |  |   p = Process(target=wheel_poll_thread, args=(q,))
 | 
					
						
							|  |  |  |   p.start()
 |