import os import sys import time import math import spidev import evdev import threading import RPi.GPIO as GPIO import usb4vc_ui from usb4vc_shared import * import usb4vc_gamepads """ sudo apt install stm32flash OPTIONAL git clone https://git.code.sf.net/p/stm32flash/code stm32flash-code cd stm32flash-code sudo make install HAVE TO ASSERT BOOT0 THE WHOLE TIME /usr/local/bin/stm32flash -r hhh -a 0x3b /dev/i2c-1 """ PCARD_BUSY_PIN = 20 SLAVE_REQ_PIN = 16 GPIO.setmode(GPIO.BCM) GPIO.setup(SLAVE_REQ_PIN, GPIO.IN, pull_up_down=GPIO.PUD_DOWN) GPIO.add_event_detect(SLAVE_REQ_PIN, GPIO.RISING) GPIO.setup(PCARD_BUSY_PIN, GPIO.IN, pull_up_down=GPIO.PUD_DOWN) pcard_spi = spidev.SpiDev(0, 0) pcard_spi.max_speed_hz = 2000000 opened_device_dict = {} led_device_path = '/sys/class/leds' input_device_path = '/dev/input/by-path/' """ https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/input-event-codes.h#L38 xbox gamepad: EVENT TYPE: EV_ABS dpad: ABS_HAT0X ABS_HAT1X buttons: https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/input-event-codes.h#L385 sticks: ABS_X, ABS_Y, ABS_RX, ABS_RY """ # https://github.com/torvalds/linux/blob/master/include/uapi/linux/input-event-codes.h EV_SYN = 0 EV_KEY = 1 EV_REL = 2 EV_ABS = 3 SYN_REPORT = 0 REL_X = 0x00 REL_Y = 0x01 REL_HWHEEL = 0x06 REL_WHEEL = 0x08 SPI_BUF_INDEX_MAGIC = 0 SPI_BUF_INDEX_SEQNUM = 1 SPI_BUF_INDEX_MSG_TYPE = 2 SPI_MOSI_MSG_TYPE_NOP = 0 SPI_MOSI_MSG_TYPE_INFO_REQUEST = 1 SPI_MOSI_MSG_TYPE_SET_PROTOCOL = 2 SPI_MOSI_MSG_TYPE_REQ_ACK = 3 SPI_MOSI_MSG_TYPE_KEYBOARD_EVENT = 8 SPI_MOSI_MSG_TYPE_MOUSE_EVENT = 9 SPI_MOSI_MSG_TYPE_GAMEPAD_EVENT_RAW_UNKNOWN = 10 SPI_MOSI_MSG_TYPE_GAMEPAD_EVENT_MAPPED = 11 SPI_MOSI_MSG_TYPE_GAMEPAD_EVENT_RAW_SUPPORTED = 12 SPI_MISO_MSG_TYPE_NOP = 0 SPI_MISO_MSG_TYPE_INFO_REQUEST = 128 SPI_MISO_MSG_TYPE_KB_LED_REQUEST = 129 SPI_MOSI_MAGIC = 0xde SPI_MISO_MAGIC = 0xcd BTN_SOUTH = 0x130 BTN_TR = 0x137 BTN_TL2 = 0x138 BTN_THUMBR = 0x13e BTN_TRIGGER = 0x120 BTN_THUMB = 0x121 BTN_THUMB2 = 0x122 BTN_TOP = 0x123 BTN_TOP2 = 0x124 BTN_PINKIE = 0x125 BTN_BASE = 0x126 BTN_BASE2 = 0x127 BTN_BASE3 = 0x128 BTN_BASE4 = 0x129 BTN_BASE5 = 0x12a BTN_BASE6 = 0x12b BTN_DEAD = 0x12f ABS_X = 0x00 ABS_HAT3Y = 0x17 BTN_LEFT = 0x110 BTN_RIGHT = 0x111 BTN_MIDDLE = 0x112 BTN_SIDE = 0x113 BTN_EXTRA = 0x114 BTN_FORWARD = 0x115 BTN_BACK = 0x116 BTN_TASK = 0x117 SPI_XFER_TIMEOUT = 0.025 def xfer_when_not_busy(data, drop=False): start_ts = time.time() while GPIO.input(PCARD_BUSY_PIN): # print(time.time(), "P-Card is busy!") if drop: return None if time.time() - start_ts > SPI_XFER_TIMEOUT: break return pcard_spi.xfer(data) def is_gamepad_button(event_code): name_result = code_value_to_name_lookup.get(event_code) if name_result is None: return False for item in name_result: if item in gamepad_event_code_name_list: return True return False # some gamepad buttons report as keyboard keys when connected via bluetooth, need to account for those gamepad_buttons_as_kb_codes = { 373, # KEY_MODE, Xbox logo button, xbox one controller when connected via bluetooth on xpadneo } nop_spi_msg_template = [SPI_MOSI_MAGIC] + [0]*31 info_request_spi_msg_template = [SPI_MOSI_MAGIC, 0, SPI_MOSI_MSG_TYPE_INFO_REQUEST] + [0]*29 keyboard_event_spi_msg_template = [SPI_MOSI_MAGIC, 0, SPI_MOSI_MSG_TYPE_KEYBOARD_EVENT] + [0]*29 mouse_event_spi_msg_template = [SPI_MOSI_MAGIC, 0, SPI_MOSI_MSG_TYPE_MOUSE_EVENT] + [0]*29 gamepad_event_ibm_ggp_spi_msg_template = [SPI_MOSI_MAGIC, 0, SPI_MOSI_MSG_TYPE_GAMEPAD_EVENT_MAPPED, 0, 0, 0, 0, 0, 127, 127, 127, 127] + [0]*20 raw_usb_unknown_gamepad_event_spi_msg_template = [SPI_MOSI_MAGIC, 0, SPI_MOSI_MSG_TYPE_GAMEPAD_EVENT_RAW_UNKNOWN] + [0]*7 + [127]*8 + [0]*14 raw_usb_supported_gamepad_event_spi_msg_template = [SPI_MOSI_MAGIC, 0, SPI_MOSI_MSG_TYPE_GAMEPAD_EVENT_RAW_SUPPORTED] + [0]*7 + [127]*4 + [0, 0, 127, 127] + [0]*14 def make_spi_msg_ack(): return [SPI_MOSI_MAGIC, 0, SPI_MOSI_MSG_TYPE_REQ_ACK] + [0]*29 def make_keyboard_spi_packet(input_data, kbd_id): result = list(keyboard_event_spi_msg_template) result[3] = kbd_id result[4] = input_data[2] result[5] = input_data[3] result[6] = input_data[4] return result def dict_get_if_exist(this_dict, this_key): if this_key not in this_dict: return 0; return this_dict[this_key]; def make_mouse_spi_packet(mouse_dict, mouse_id): to_transfer = list(mouse_event_spi_msg_template) to_transfer[3] = mouse_id to_transfer[4:6] = mouse_dict['x'] to_transfer[6:8] = mouse_dict['y'] to_transfer[8] = mouse_dict['scroll'] to_transfer[9] = mouse_dict['hscroll'] to_transfer[13] = mouse_dict[BTN_LEFT] to_transfer[14] = mouse_dict[BTN_RIGHT] to_transfer[15] = mouse_dict[BTN_MIDDLE] to_transfer[16] = mouse_dict[BTN_SIDE] to_transfer[17] = mouse_dict[BTN_EXTRA] return to_transfer PID_PROTOCOL_OFF = 0 PID_GENERIC_GAMEPORT_GAMEPAD = 7 PID_BBC_MICRO_JOYSTICK = 14 PID_RAW_USB_GAMEPAD = 127 def get_range_max_and_midpoint(axes_dict, axis_key): try: return axes_dict[axis_key]['max'] - axes_dict[axis_key]['min'], int((axes_dict[axis_key]['max'] + axes_dict[axis_key]['min']) / 2) except Exception as e: print('exception get_range_max_and_midpoint:', e) return None, None def convert_to_8bit_midpoint127(value, axes_dict, axis_key): rmax, rmid = get_range_max_and_midpoint(axes_dict, axis_key) if rmax is None: return 127 value -= rmid result = int((value / rmax) * 255) + 127 if result == 254: result = 255 return result IBMPC_GGP_SPI_LOOKUP = { 'IBM_GGP_BTN_1':4, 'IBM_GGP_BTN_2':5, 'IBM_GGP_BTN_3':6, 'IBM_GGP_BTN_4':7, 'IBM_GGP_JS1_X':8, 'IBM_GGP_JS1_Y':9, 'IBM_GGP_JS2_X':10, 'IBM_GGP_JS2_Y':11, } MOUSE_SPI_LOOKUP = { BTN_LEFT:13, BTN_RIGHT:14, BTN_MIDDLE:15, BTN_SIDE:16, BTN_EXTRA:17, REL_X:4, REL_Y:6, REL_WHEEL:8, } def translate_dict(old_mapping_dict, lookup_dict): translated_map_dict = dict(old_mapping_dict) for key in old_mapping_dict: lookup_result = lookup_dict.get(key) if lookup_result is not None: translated_map_dict[lookup_result] = old_mapping_dict[key] del translated_map_dict[key] return translated_map_dict def find_keycode_in_mapping(source_code, mapping_dict, usb_gamepad_type): # print(source_code, mapping_dict, usb_gamepad_type) map_dict_copy = dict(mapping_dict) if 'Generic' in usb_gamepad_type and map_dict_copy.get("MAPPING_TYPE") == "DEFAULT_15PIN": map_dict_copy = usb4vc_gamepads.GENERIC_DEFAULT_MAPPING elif 'DualSense' in usb_gamepad_type and map_dict_copy.get("MAPPING_TYPE") == "DEFAULT_15PIN": map_dict_copy = usb4vc_gamepads.PS5_DEFAULT_MAPPING elif 'DualShock 4' in usb_gamepad_type and map_dict_copy.get("MAPPING_TYPE") == "DEFAULT_15PIN": map_dict_copy = usb4vc_gamepads.PS4_DEFAULT_MAPPING elif 'Xbox' in usb_gamepad_type and map_dict_copy.get("MAPPING_TYPE") == "DEFAULT_15PIN": map_dict_copy = usb4vc_gamepads.XBOX_DEFAULT_MAPPING elif 'DualSense' in usb_gamepad_type and map_dict_copy.get("MAPPING_TYPE") == "DEFAULT_MOUSE_KB": map_dict_copy = usb4vc_gamepads.PS5_DEFAULT_KB_MOUSE_MAPPING elif 'DualShock 4' in usb_gamepad_type and map_dict_copy.get("MAPPING_TYPE") == "DEFAULT_MOUSE_KB": map_dict_copy = usb4vc_gamepads.PS4_DEFAULT_KB_MOUSE_MAPPING elif 'Xbox' in usb_gamepad_type and map_dict_copy.get("MAPPING_TYPE") == "DEFAULT_MOUSE_KB": map_dict_copy = usb4vc_gamepads.XBOX_DEFAULT_KB_MOUSE_MAPPING source_name = code_value_to_name_lookup.get(source_code) if source_name is None: return None, None if 'Xbox' in usb_gamepad_type: map_dict_copy = translate_dict(map_dict_copy, usb4vc_gamepads.xbox_one_to_linux_ev_code_dict) elif 'DualSense' in usb_gamepad_type: map_dict_copy = translate_dict(map_dict_copy, usb4vc_gamepads.ps5_to_linux_ev_code_dict) elif 'DualShock 4' in usb_gamepad_type: map_dict_copy = translate_dict(map_dict_copy, usb4vc_gamepads.ps4_to_linux_ev_code_dict) target_info = None for item in source_name: if item in map_dict_copy: target_info = map_dict_copy[item] if target_info is None or 'code' not in target_info: return None, None target_info = dict(target_info) # make a copy so the lookup table itself won't get modified lookup_result = code_name_to_value_lookup.get(target_info['code']) if lookup_result is None: return None, None source_type = None if ABS_X <= source_code <= ABS_HAT3Y: source_type = 'usb_abs_axis' elif is_gamepad_button(source_code) or source_code in gamepad_buttons_as_kb_codes: source_type = 'usb_gp_btn' if source_type is None: return None, None target_info['code'] = lookup_result[0] if 'code_neg' in target_info: target_info['code_neg'] = code_name_to_value_lookup.get(target_info['code_neg'])[0] target_info['type'] = lookup_result[1] return source_type, target_info def find_furthest_from_midpoint(this_set): curr_best = None diff_max = 0 for item in this_set: this_diff = abs(item - 127) if this_diff >= diff_max: diff_max = this_diff curr_best = item return curr_best def apply_curve(x_0_255, y_0_255): x_neg127_127 = x_0_255 - 127 y_neg127_127 = y_0_255 - 127 try: real_amount = int(math.sqrt(abs(x_neg127_127) ** 2 + abs(y_neg127_127) ** 2)) if real_amount > 127: real_amount = 127 lookup_result = usb4vc_ui.get_joystick_curve().get(real_amount, real_amount) ratio = real_amount / lookup_result xxxxx = int(x_neg127_127 / ratio) yyyyy = int(y_neg127_127 / ratio) return xxxxx + 127, yyyyy + 127 except Exception as e: # print('exception apply_curve:', e) pass return x_0_255, y_0_255 prev_gp_output = {} prev_kb_output = {} curr_mouse_output = {} def make_15pin_gamepad_spi_packet(gp_status_dict, this_device_info, mapping_info): global prev_gp_output global prev_kb_output global curr_mouse_output # print(gp_status_dict) axes_info = this_device_info['axes_info'] this_gamepad_name = this_device_info.get('name', '').lower().strip() usb_gamepad_type = this_device_info['gamepad_type'] gp_id = this_device_info['id'] this_gp_dict = gp_status_dict[gp_id] # print(this_gp_dict) curr_gp_output = { 'IBM_GGP_BTN_1':set([0]), 'IBM_GGP_BTN_2':set([0]), 'IBM_GGP_BTN_3':set([0]), 'IBM_GGP_BTN_4':set([0]), 'IBM_GGP_JS1_X':set([127]), 'IBM_GGP_JS1_Y':set([127]), 'IBM_GGP_JS2_X':set([127]), 'IBM_GGP_JS2_Y':set([127]), } curr_kb_output = {} curr_mouse_output = { 'mouse_id':gp_id, 'is_modified':False, BTN_LEFT:set([0]), BTN_MIDDLE:set([0]), BTN_RIGHT:set([0]), BTN_SIDE:set([0]), BTN_EXTRA:set([0]), REL_X:0, REL_Y:0, REL_WHEEL:0, } for source_code in this_gp_dict: source_type, target_info = find_keycode_in_mapping(source_code, mapping_info['mapping'], usb_gamepad_type) if target_info is None: continue source_value = this_gp_dict[source_code] target_code = target_info['code'] target_type = target_info['type'] # print(source_code, source_type, target_code, target_type) # print("----------") # usb gamepad button to generic gamepad button if source_type == 'usb_gp_btn' and target_type == 'ibm_ggp_btn' and target_code in curr_gp_output: curr_gp_output[target_code].add(source_value) # usb gamepad analog axes to generic gamepad analog axes if source_type == 'usb_abs_axis' and target_type == 'ibm_ggp_axis' and target_code in curr_gp_output and usb4vc_gamepads.is_analog_trigger(source_code, usb_gamepad_type) is False: curr_gp_output[target_code].add(source_value) # usb gamepad analog axes to generic gamepad analog half axes # only use to map USB analog trigger to generic gameport gamepad half axes if source_type == 'usb_abs_axis' and target_type == 'ibm_ggp_half_axis' and target_code[:-1] in curr_gp_output: axis_value = 127 if target_code.endswith('P'): axis_value += abs(source_value)//2 elif target_code.endswith('N'): axis_value -= abs(source_value)//2 curr_gp_output[target_code[:-1]].add(axis_value) # usb gamepad button to generic gamepad analog axes if source_type == 'usb_gp_btn' and target_type == 'ibm_ggp_half_axis' and target_code[:-1] in curr_gp_output and source_value: if target_code.endswith('P'): axis_value = 255 elif target_code.endswith('N'): axis_value = 0 curr_gp_output[target_code[:-1]].add(axis_value) # usb gamepad button to keyboard key if source_type == 'usb_gp_btn' and target_type == 'kb_key': if target_code not in curr_kb_output: curr_kb_output[target_code] = set() curr_kb_output[target_code].add(source_value) # usb gamepad analog axes to keyboard key if source_type == 'usb_abs_axis' and target_type == 'kb_key': if target_code not in curr_kb_output: curr_kb_output[target_code] = set() is_activated = 0 deadzone_amount = 50 try: deadzone_amount = int(127 * target_info['deadzone_percent'] / 100) except Exception: pass if usb4vc_gamepads.is_analog_trigger(source_code, usb_gamepad_type): if source_value > deadzone_amount: is_activated = 1 curr_kb_output[target_code].add(is_activated) else: if source_value > 127 + deadzone_amount: is_activated = 1 curr_kb_output[target_code].add(is_activated) if target_info['code_neg'] not in curr_kb_output: curr_kb_output[target_info['code_neg']] = set() is_activated = 0 if source_value < 127 - deadzone_amount: is_activated = 1 curr_kb_output[target_info['code_neg']].add(is_activated) # usb gamepad button to mouse buttons if source_type == 'usb_gp_btn' and target_type == 'mouse_btn' and target_code in curr_mouse_output: curr_mouse_output[target_code].add(source_value) curr_mouse_output['is_modified'] = True # usb gamepad analog axes to mouse axes if source_type == 'usb_abs_axis' and target_type == 'usb_rel_axis' and target_code in curr_mouse_output and usb4vc_gamepads.is_analog_trigger(source_code, usb_gamepad_type) is False: movement = source_value - 127 deadzone_amount = 20 try: deadzone_amount = int(127 * target_info['deadzone_percent'] / 100) except Exception: pass if abs(movement) <= deadzone_amount: movement = 0 joystick_to_mouse_slowdown = 10 curr_mouse_output[target_code] = int(movement / joystick_to_mouse_slowdown) curr_mouse_output['is_modified'] = True for key in curr_kb_output: key_status_set = curr_kb_output[key] curr_kb_output[key] = 0 if 1 in key_status_set: curr_kb_output[key] = 1 for key in curr_mouse_output: if type(key) is int and BTN_LEFT <= key <= BTN_TASK: mouse_button_status_set = curr_mouse_output[key] curr_mouse_output[key] = 0 if 1 in mouse_button_status_set: curr_mouse_output[key] = 1 # if curr axes set is more than prev, apply the new one, else apply the one furest from midpoint gp_spi_msg = list(gamepad_event_ibm_ggp_spi_msg_template) gp_spi_msg[3] = gp_id; for key in curr_gp_output: if "IBM_GGP_BTN_" in key: this_value = 0 if 1 in curr_gp_output[key]: this_value = 1 gp_spi_msg[IBMPC_GGP_SPI_LOOKUP[key]] = this_value if 'IBM_GGP_JS' in key: if key in prev_gp_output: new_value_set = curr_gp_output[key] - prev_gp_output[key] if 0 and len(new_value_set) > 0: gp_spi_msg[IBMPC_GGP_SPI_LOOKUP[key]] = list(new_value_set)[0] else: gp_spi_msg[IBMPC_GGP_SPI_LOOKUP[key]] = find_furthest_from_midpoint(curr_gp_output[key]) else: gp_spi_msg[IBMPC_GGP_SPI_LOOKUP[key]] = find_furthest_from_midpoint(curr_gp_output[key]) kb_spi_msg = None new_keys = curr_kb_output.keys() - prev_kb_output.keys() if len(new_keys) > 0: this_key = list(new_keys)[0] kb_spi_msg = list(keyboard_event_spi_msg_template) kb_spi_msg[3] = gp_id kb_spi_msg[4] = this_key kb_spi_msg[6] = curr_kb_output[this_key] for key in curr_kb_output: if key in prev_kb_output and curr_kb_output[key] != prev_kb_output[key]: kb_spi_msg = list(keyboard_event_spi_msg_template) kb_spi_msg[3] = gp_id kb_spi_msg[4] = key kb_spi_msg[6] = curr_kb_output[key] mouse_spi_msg = None if curr_mouse_output['is_modified']: mouse_spi_msg = list(mouse_event_spi_msg_template) mouse_spi_msg[3] = gp_id for mcode in curr_mouse_output: if type(mcode) is int: if BTN_LEFT <= mcode <= BTN_TASK: mouse_spi_msg[MOUSE_SPI_LOOKUP[mcode]] = curr_mouse_output[mcode] elif REL_X <= mcode <= REL_WHEEL: mouse_spi_msg[MOUSE_SPI_LOOKUP[mcode]:MOUSE_SPI_LOOKUP[mcode]+2] = list((curr_mouse_output[mcode] & 0xffff).to_bytes(2, byteorder='little')) prev_gp_output = curr_gp_output prev_kb_output = curr_kb_output gp_spi_msg[8], gp_spi_msg[9] = apply_curve(gp_spi_msg[8], gp_spi_msg[9]) gp_spi_msg[10], gp_spi_msg[11] = apply_curve(gp_spi_msg[10], gp_spi_msg[11]) return gp_spi_msg, kb_spi_msg, mouse_spi_msg def make_unknown_raw_gamepad_spi_packet(gp_status_dict, this_device_info): gp_id = this_device_info['id'] this_msg = list(raw_usb_unknown_gamepad_event_spi_msg_template) this_msg[3] = gp_id this_msg[4:6] = this_device_info['vendor_id'].to_bytes(2, byteorder='little') this_msg[6:8] = this_device_info['product_id'].to_bytes(2, byteorder='little') for event_code in gp_status_dict[gp_id]: if BTN_SOUTH <= event_code <= BTN_TR: this_msg[8] |= (gp_status_dict[gp_id][event_code] << (event_code - BTN_SOUTH)) elif BTN_TL2 <= event_code <= BTN_THUMBR: this_msg[9] |= (gp_status_dict[gp_id][event_code] << (event_code - BTN_TL2)) elif BTN_TRIGGER <= event_code <= BTN_BASE2: this_msg[18] |= (gp_status_dict[gp_id][event_code] << (event_code - BTN_TRIGGER)) elif BTN_BASE3 <= event_code <= BTN_BASE6: this_msg[19] |= (gp_status_dict[gp_id][event_code] << (event_code - BTN_BASE3)) elif ABS_X <= event_code <= ABS_HAT3Y: msg_index = usb4vc_gamepads.raw_usb_gamepad_abs_axes_to_spi_msg_index_lookup.get(event_code) if msg_index is not None: this_msg[msg_index] = gp_status_dict[gp_id][event_code] # print('{:08b}'.format(this_msg[9])) return this_msg, None, None def xbname_to_ev_codename(xb_btn): ev_name = usb4vc_gamepads.xbox_one_to_linux_ev_code_dict.get(xb_btn) if ev_name is None: return None return code_name_to_value_lookup[ev_name][0] def ps5name_to_ev_codename(ps5_btn): ev_name = usb4vc_gamepads.ps5_to_linux_ev_code_dict.get(ps5_btn) if ev_name is None: return None return code_name_to_value_lookup[ev_name][0] def ps4name_to_ev_codename(ps4_btn): ev_name = usb4vc_gamepads.ps4_to_linux_ev_code_dict.get(ps4_btn) if ev_name is None: return None return code_name_to_value_lookup[ev_name][0] xbox_to_generic_dict = { xbname_to_ev_codename('XB_A'):'FACE_BUTTON_SOUTH', xbname_to_ev_codename('XB_B'):'FACE_BUTTON_EAST', xbname_to_ev_codename('XB_X'):'FACE_BUTTON_WEST', xbname_to_ev_codename('XB_Y'):'FACE_BUTTON_NORTH', xbname_to_ev_codename('XB_LB'):'SHOULDER_BUTTON_LEFT', xbname_to_ev_codename('XB_RB'):'SHOULDER_BUTTON_RIGHT', xbname_to_ev_codename('XB_LSB'):'STICK_BUTTON_LEFT', xbname_to_ev_codename('XB_RSB'):'STICK_BUTTON_RIGHT', xbname_to_ev_codename('XB_VIEW'):'MISC_BUTTON_1', xbname_to_ev_codename('XB_MENU'):'MISC_BUTTON_2', xbname_to_ev_codename('XB_LOGO'):'LOGO_BUTTON', } ps5_to_generic_dict = { ps5name_to_ev_codename('PS_CROSS'):'FACE_BUTTON_SOUTH', ps5name_to_ev_codename('PS_CIRCLE'):'FACE_BUTTON_EAST', ps5name_to_ev_codename('PS_SQUARE'):'FACE_BUTTON_WEST', ps5name_to_ev_codename('PS_TRIANGLE'):'FACE_BUTTON_NORTH', ps5name_to_ev_codename('PS_L1'):'SHOULDER_BUTTON_LEFT', ps5name_to_ev_codename('PS_R1'):'SHOULDER_BUTTON_RIGHT', ps5name_to_ev_codename('PS_L2_BUTTON'):'BUMPER_BUTTON_LEFT', ps5name_to_ev_codename('PS_R2_BUTTON'):'BUMPER_BUTTON_RIGHT', ps5name_to_ev_codename('PS_LSB'):'STICK_BUTTON_LEFT', ps5name_to_ev_codename('PS_RSB'):'STICK_BUTTON_RIGHT', ps5name_to_ev_codename('PS_CREATE'):'MISC_BUTTON_1', ps5name_to_ev_codename('PS_OPTION'):'MISC_BUTTON_2', ps5name_to_ev_codename('PS_MUTE'):'MISC_BUTTON_3', ps5name_to_ev_codename('PS_TOUCHPAD_BUTTON'):'MISC_BUTTON_4', ps5name_to_ev_codename('PS_LOGO'):'LOGO_BUTTON', } ps4_to_generic_dict = { ps4name_to_ev_codename('PS_CROSS'):'FACE_BUTTON_SOUTH', ps4name_to_ev_codename('PS_CIRCLE'):'FACE_BUTTON_EAST', ps4name_to_ev_codename('PS_SQUARE'):'FACE_BUTTON_WEST', ps4name_to_ev_codename('PS_TRIANGLE'):'FACE_BUTTON_NORTH', ps4name_to_ev_codename('PS_L1'):'SHOULDER_BUTTON_LEFT', ps4name_to_ev_codename('PS_R1'):'SHOULDER_BUTTON_RIGHT', ps4name_to_ev_codename('PS_L2_BUTTON'):'BUMPER_BUTTON_LEFT', ps4name_to_ev_codename('PS_R2_BUTTON'):'BUMPER_BUTTON_RIGHT', ps4name_to_ev_codename('PS_LSB'):'STICK_BUTTON_LEFT', ps4name_to_ev_codename('PS_RSB'):'STICK_BUTTON_RIGHT', ps4name_to_ev_codename('PS_SHARE'):'MISC_BUTTON_1', ps4name_to_ev_codename('PS_OPTION'):'MISC_BUTTON_2', ps4name_to_ev_codename('PS_TOUCHPAD_BUTTON'):'MISC_BUTTON_4', ps4name_to_ev_codename('PS_LOGO'):'LOGO_BUTTON', } def make_supported_raw_gamepad_spi_packet(gp_status_dict, this_device_info): gp_id = this_device_info['id'] this_gp_status = gp_status_dict[gp_id] supported_gamepad_status_dict = { 'FACE_BUTTON_SOUTH':0, 'FACE_BUTTON_EAST':0, 'FACE_BUTTON_WEST':0, 'FACE_BUTTON_NORTH':0, 'SHOULDER_BUTTON_LEFT':0, 'SHOULDER_BUTTON_RIGHT':0, 'BUMPER_BUTTON_LEFT':0, 'BUMPER_BUTTON_RIGHT':0, 'STICK_BUTTON_LEFT':0, 'STICK_BUTTON_RIGHT':0, 'LOGO_BUTTON':0, 'MISC_BUTTON_1':0, 'MISC_BUTTON_2':0, 'MISC_BUTTON_3':0, 'MISC_BUTTON_4':0, 'MISC_BUTTON_5':0, "LEFT_STICK_X":127, "LEFT_STICK_Y":127, "RIGHT_STICK_X":127, "RIGHT_STICK_Y":127, "ANALOG_TRIGGER_LEFT":0, "ANALOG_TRIGGER_RIGHT":0, "DPAD_X":127, "DPAD_Y":127, } if this_device_info['gamepad_type'] == 'Xbox': for item in this_gp_status: generic_name = xbox_to_generic_dict.get(item) if generic_name is not None: supported_gamepad_status_dict[generic_name] = this_gp_status[item] supported_gamepad_status_dict["LEFT_STICK_X"] = this_gp_status.get(xbname_to_ev_codename('XB_LSX'), 127) supported_gamepad_status_dict["LEFT_STICK_Y"] = this_gp_status.get(xbname_to_ev_codename('XB_LSY'), 127) supported_gamepad_status_dict["RIGHT_STICK_X"] = this_gp_status.get(xbname_to_ev_codename('XB_RSX'), 127) supported_gamepad_status_dict["RIGHT_STICK_Y"] = this_gp_status.get(xbname_to_ev_codename('XB_RSY'), 127) supported_gamepad_status_dict["ANALOG_TRIGGER_LEFT"] = this_gp_status.get(xbname_to_ev_codename('XB_LT'), 0) supported_gamepad_status_dict["ANALOG_TRIGGER_RIGHT"] = this_gp_status.get(xbname_to_ev_codename('XB_RT'), 0) supported_gamepad_status_dict["DPAD_X"] = this_gp_status.get(xbname_to_ev_codename('XB_DPX'), 127) supported_gamepad_status_dict["DPAD_Y"] = this_gp_status.get(xbname_to_ev_codename('XB_DPY'), 127) elif this_device_info['gamepad_type'] == usb4vc_gamepads.GAMEPAD_TYPE_PS5_GEN1: for item in this_gp_status: generic_name = ps5_to_generic_dict.get(item) if generic_name is not None: supported_gamepad_status_dict[generic_name] = this_gp_status[item] supported_gamepad_status_dict["LEFT_STICK_X"] = this_gp_status.get(ps5name_to_ev_codename('PS_LSX'), 127) supported_gamepad_status_dict["LEFT_STICK_Y"] = this_gp_status.get(ps5name_to_ev_codename('PS_LSY'), 127) supported_gamepad_status_dict["RIGHT_STICK_X"] = this_gp_status.get(ps5name_to_ev_codename('PS_RSX'), 127) supported_gamepad_status_dict["RIGHT_STICK_Y"] = this_gp_status.get(ps5name_to_ev_codename('PS_RSY'), 127) supported_gamepad_status_dict["ANALOG_TRIGGER_LEFT"] = this_gp_status.get(ps5name_to_ev_codename('PS_L2_ANALOG'), 0) supported_gamepad_status_dict["ANALOG_TRIGGER_RIGHT"] = this_gp_status.get(ps5name_to_ev_codename('PS_R2_ANALOG'), 0) supported_gamepad_status_dict["DPAD_X"] = this_gp_status.get(ps5name_to_ev_codename('PS_DPX'), 127) supported_gamepad_status_dict["DPAD_Y"] = this_gp_status.get(ps5name_to_ev_codename('PS_DPY'), 127) elif 'DualShock 4' in this_device_info['gamepad_type']: for item in this_gp_status: generic_name = ps4_to_generic_dict.get(item) if generic_name is not None: supported_gamepad_status_dict[generic_name] = this_gp_status[item] supported_gamepad_status_dict["LEFT_STICK_X"] = this_gp_status.get(ps4name_to_ev_codename('PS_LSX'), 127) supported_gamepad_status_dict["LEFT_STICK_Y"] = this_gp_status.get(ps4name_to_ev_codename('PS_LSY'), 127) supported_gamepad_status_dict["RIGHT_STICK_X"] = this_gp_status.get(ps4name_to_ev_codename('PS_RSX'), 127) supported_gamepad_status_dict["RIGHT_STICK_Y"] = this_gp_status.get(ps4name_to_ev_codename('PS_RSY'), 127) supported_gamepad_status_dict["ANALOG_TRIGGER_LEFT"] = this_gp_status.get(ps4name_to_ev_codename('PS_L2_ANALOG'), 0) supported_gamepad_status_dict["ANALOG_TRIGGER_RIGHT"] = this_gp_status.get(ps4name_to_ev_codename('PS_R2_ANALOG'), 0) supported_gamepad_status_dict["DPAD_X"] = this_gp_status.get(ps4name_to_ev_codename('PS_DPX'), 127) supported_gamepad_status_dict["DPAD_Y"] = this_gp_status.get(ps4name_to_ev_codename('PS_DPY'), 127) this_msg = list(raw_usb_supported_gamepad_event_spi_msg_template) this_msg[3] = gp_id this_msg[4:6] = this_device_info['vendor_id'].to_bytes(2, byteorder='little') this_msg[6:8] = this_device_info['product_id'].to_bytes(2, byteorder='little') for index, item in enumerate(list(supported_gamepad_status_dict.items())): # print(index, item) if 0 <= index <= 7: this_msg[8] |= item[1] << (index % 8) if 8 <= index <= 15: this_msg[9] |= item[1] << (index % 8) this_msg[10] = supported_gamepad_status_dict["LEFT_STICK_X"] this_msg[11] = supported_gamepad_status_dict["LEFT_STICK_Y"] this_msg[12] = supported_gamepad_status_dict["RIGHT_STICK_X"] this_msg[13] = supported_gamepad_status_dict["RIGHT_STICK_Y"] this_msg[14] = supported_gamepad_status_dict["ANALOG_TRIGGER_LEFT"] this_msg[15] = supported_gamepad_status_dict["ANALOG_TRIGGER_RIGHT"] this_msg[16] = supported_gamepad_status_dict["DPAD_X"] this_msg[17] = supported_gamepad_status_dict["DPAD_Y"] return this_msg, None, None def make_gamepad_spi_packet(gp_status_dict, this_device_info): current_protocol = usb4vc_ui.get_gamepad_protocol() try: if current_protocol['pid'] in [PID_GENERIC_GAMEPORT_GAMEPAD, PID_PROTOCOL_OFF]: return make_15pin_gamepad_spi_packet(gp_status_dict, this_device_info, current_protocol) # elif current_protocol['pid'] == PID_RAW_USB_GAMEPAD: else: if this_device_info['gamepad_type'] == usb4vc_gamepads.GAMEPAD_TYPE_UNKNOWN: return make_unknown_raw_gamepad_spi_packet(gp_status_dict, this_device_info) else: return make_supported_raw_gamepad_spi_packet(gp_status_dict, this_device_info) except Exception as e: print("exception make_15pin_gamepad_spi_packet:", e) return list(nop_spi_msg_template), None, None def change_kb_led(scrolllock, numlock, capslock): led_file_list = os.listdir(led_device_path) capslock_list = [os.path.join(led_device_path, x) for x in led_file_list if 'capslock' in x] numlock_list = [os.path.join(led_device_path, x) for x in led_file_list if 'numlock' in x] scrolllock_list = [os.path.join(led_device_path, x) for x in led_file_list if 'scrolllock' in x] for item in scrolllock_list: with open(os.path.join(item, 'brightness'), 'w') as led_file: led_file.write(str(scrolllock)) for item in numlock_list: with open(os.path.join(item, 'brightness'), 'w') as led_file: led_file.write(str(numlock)) for item in capslock_list: with open(os.path.join(item, 'brightness'), 'w') as led_file: led_file.write(str(capslock)) def clear_mouse_movement(mdict): mdict['x'] = [0, 0] mdict['y'] = [0, 0] mdict['scroll'] = 0 mdict['hscroll'] = 0 def joystick_hold_update(): needs_update = 0 abs_value_multiplier = 1.5 for key in curr_mouse_output: if type(key) is int and REL_X <= key <= REL_WHEEL and curr_mouse_output[key]: needs_update = 1 if needs_update and curr_mouse_output['is_modified']: mouse_spi_msg = list(mouse_event_spi_msg_template) mouse_spi_msg[3] = curr_mouse_output['mouse_id'] for mcode in curr_mouse_output: if type(mcode) is int: if BTN_LEFT <= mcode <= BTN_TASK: mouse_spi_msg[MOUSE_SPI_LOOKUP[mcode]] = curr_mouse_output[mcode] elif REL_X <= mcode <= 0x08: movement_value = int(curr_mouse_output[mcode] * abs_value_multiplier) & 0xffff mouse_spi_msg[MOUSE_SPI_LOOKUP[mcode]:MOUSE_SPI_LOOKUP[mcode]+2] = list(movement_value.to_bytes(2, byteorder='little')) xfer_when_not_busy(mouse_spi_msg) def multiply_round_up_0(number, multi): new_number = number * multi if 0 < new_number < 1: new_number = 1 elif -1 < new_number < 0: new_number = -1 return int(new_number) USB_GAMEPAD_STICK_AXES_NAMES = ["ABS_X", "ABS_Y", "ABS_RX", "ABS_RY"] gamepad_status_dict = {} gamepad_hold_check_interval = 0.02 def raw_input_event_worker(): last_usb_event = 0 mouse_status_dict = {'x': [0, 0], 'y': [0, 0], 'scroll': 0, 'hscroll': 0, BTN_LEFT:0, BTN_RIGHT:0, BTN_MIDDLE:0, BTN_SIDE:0, BTN_EXTRA:0, BTN_FORWARD:0, BTN_BACK:0, BTN_TASK:0} next_gamepad_hold_check = time.time() + gamepad_hold_check_interval last_mouse_msg = [] last_gamepad_msg = None last_mouse_button_msg = None print("raw_input_event_worker started") while 1: now = time.time() if now > next_gamepad_hold_check: joystick_hold_update() next_gamepad_hold_check = now + gamepad_hold_check_interval # give other threads some breathing room if # no USB input events if now - last_usb_event > 1: time.sleep(0.005) for key in list(opened_device_dict): this_device = opened_device_dict[key] this_id = this_device['id'] try: data = this_device['file'].read(16) except OSError: print("Device disappeared:", this_device['name']) this_device['file'].close() del opened_device_dict[key] continue if data is None: continue usb4vc_ui.my_oled.kick() last_usb_event = now if this_device['is_gp'] and this_id not in gamepad_status_dict: gamepad_status_dict[this_id] = {} data = list(data[8:]) event_code = data[3] * 256 + data[2] # event is a key press if data[0] == EV_KEY: # keyboard keys if 0x1 <= event_code <= 248 and event_code not in gamepad_buttons_as_kb_codes: xfer_when_not_busy(make_keyboard_spi_packet(data, this_id)) # Mouse buttons elif 0x110 <= event_code <= 0x117: mouse_status_dict[event_code] = data[4] next_gamepad_hold_check = now + gamepad_hold_check_interval if data[4] != 2: clear_mouse_movement(mouse_status_dict) last_mouse_button_msg = make_mouse_spi_packet(mouse_status_dict, this_id) xfer_when_not_busy(list(last_mouse_button_msg)) # Gamepad buttons elif is_gamepad_button(event_code) or event_code in gamepad_buttons_as_kb_codes: this_btn_status = data[4] if this_btn_status != 0: this_btn_status = 1 gamepad_status_dict[this_id][event_code] = this_btn_status # event is relative axes AKA mouse elif data[0] == EV_REL and event_code == REL_X: rawx = int.from_bytes(data[4:6], byteorder='little', signed=True) rawx = multiply_round_up_0(rawx, usb4vc_ui.get_mouse_sensitivity()) & 0xffff mouse_status_dict["x"] = list(rawx.to_bytes(2, byteorder='little')) elif data[0] == EV_REL and event_code == REL_Y: rawy = int.from_bytes(data[4:6], byteorder='little', signed=True) rawy = multiply_round_up_0(rawy, usb4vc_ui.get_mouse_sensitivity()) & 0xffff mouse_status_dict["y"] = list(rawy.to_bytes(2, byteorder='little')) elif data[0] == EV_REL and event_code == REL_WHEEL: mouse_status_dict['scroll'] = data[4] elif data[0] == EV_REL and event_code == REL_HWHEEL: mouse_status_dict['hscroll'] = data[4] # event is absolute axes AKA joystick elif this_device['is_gp'] and data[0] == EV_ABS: abs_value = int.from_bytes(data[4:8], byteorder='little', signed=True) abs_value_midpoint127 = convert_to_8bit_midpoint127(abs_value, this_device['axes_info'], event_code) gamepad_status_dict[this_id][event_code] = abs_value_midpoint127 # SYNC event elif data[0] == EV_SYN and event_code == SYN_REPORT: if this_device['is_mouse']: this_mouse_msg = make_mouse_spi_packet(mouse_status_dict, this_id) if this_mouse_msg == last_mouse_button_msg: pass # send spi mouse message if there is moment, or the button is not typematic elif (max(this_mouse_msg[13:18]) != 2 or sum(this_mouse_msg[4:10]) != 0) and (this_mouse_msg[4:] != last_mouse_msg[4:] or sum(this_mouse_msg[4:]) != 0): xfer_when_not_busy(list(this_mouse_msg)) next_gamepad_hold_check = now + gamepad_hold_check_interval clear_mouse_movement(mouse_status_dict) last_mouse_msg = list(this_mouse_msg) if this_device['is_gp']: for axis_name in USB_GAMEPAD_STICK_AXES_NAMES: axis_code = code_name_to_value_lookup.get(axis_name)[0] this_gp_dict = gamepad_status_dict[this_device['id']] if axis_code in this_gp_dict and 127 - 10 <= this_gp_dict[axis_code] <= 127 + 10: this_gp_dict[axis_code] = 127 gamepad_output = make_gamepad_spi_packet(gamepad_status_dict, this_device) if gamepad_output != last_gamepad_msg: # print(gamepad_output) gp_to_transfer, kb_to_transfer, mouse_to_transfer = gamepad_output xfer_when_not_busy(list(gp_to_transfer)) if kb_to_transfer is not None: time.sleep(0.001) xfer_when_not_busy(list(kb_to_transfer)) if mouse_to_transfer is not None: time.sleep(0.001) xfer_when_not_busy(list(mouse_to_transfer)) next_gamepad_hold_check = now + gamepad_hold_check_interval last_gamepad_msg = gamepad_output # ----------------- PBOARD INTERRUPT ----------------- if GPIO.event_detected(SLAVE_REQ_PIN): # send out ACK to turn off P-Card interrupt slave_result = xfer_when_not_busy(make_spi_msg_ack()) time.sleep(0.001) # send another to shift response into RPi slave_result = xfer_when_not_busy(make_spi_msg_ack()) print(int(time.time()), slave_result) if slave_result[SPI_BUF_INDEX_MAGIC] == SPI_MISO_MAGIC and slave_result[SPI_BUF_INDEX_MSG_TYPE] == SPI_MISO_MSG_TYPE_KB_LED_REQUEST: try: change_kb_led(slave_result[3], slave_result[4], slave_result[5]) change_kb_led(slave_result[3], slave_result[4], slave_result[5]) except Exception as e: print('exception change_kb_led:', e) def check_is_gamepad(capability_dict): keys_and_buttons = capability_dict.get(('EV_KEY', 1)) if keys_and_buttons is None: return False key_name_set = set() for item in [x[0] for x in keys_and_buttons]: if isinstance(item, str): key_name_set.add(item) if isinstance(item, list): key_name_set.update(item) for item in gamepad_event_code_name_list: if item in key_name_set: return True return False def get_input_devices(): result = [] available_devices = [evdev.InputDevice(path) for path in evdev.list_devices()] for this_device in available_devices: if 'motion' in this_device.name.lower(): continue dev_dict = { 'path':this_device.path, 'name':this_device.name, 'vendor_id':this_device.info.vendor, 'product_id':this_device.info.product, 'axes_info':{}, 'gamepad_type':'Generic', 'is_kb':False, 'is_mouse':False, 'is_gp':False, } this_cap = this_device.capabilities(verbose=True) cap_str = str(this_cap) if 'BTN_LEFT' in cap_str and "EV_REL" in cap_str: dev_dict['is_mouse'] = True if 'KEY_ENTER' in cap_str and "KEY_Y" in cap_str: dev_dict['is_kb'] = True if check_is_gamepad(this_cap): dev_dict['is_gp'] = True try: for item in this_device.capabilities()[EV_ABS]: dev_dict['axes_info'][item[0]] = item[1]._asdict() except Exception as e: print('exception get_input_devices:', e) dev_dict['gamepad_type'] = usb4vc_gamepads.gamepad_type_lookup(this_device.info.vendor, this_device.info.product) result.append(dev_dict) return result def get_device_count(): mouse_count = 0 kb_count = 0 gp_count = 0 for key in opened_device_dict: if opened_device_dict[key]['is_mouse']: mouse_count += 1 elif opened_device_dict[key]['is_kb']: kb_count += 1 elif opened_device_dict[key]['is_gp']: gp_count += 1 return (mouse_count, kb_count, gp_count) def usb_device_scan_worker(): print("usb_device_scan_worker started") while 1: time.sleep(0.75) try: device_list = get_input_devices() except Exception as e: print('exception get_input_devices:', e) continue if len(device_list) == 0: print('No input devices found') continue for item in device_list: if item['path'] in opened_device_dict: continue try: this_file = open(item['path'], "rb") os.set_blocking(this_file.fileno(), False) item['file'] = this_file try: item['id'] = int(item['path'][len(item['path'].rstrip('0123456789')):]) except: item['id'] = 255 opened_device_dict[item['path']] = item print("opened device:", hex(item['vendor_id']), hex(item['product_id']), item['name']) except Exception as e: print("exception Device open:", e, item) raw_input_event_parser_thread = threading.Thread(target=raw_input_event_worker, daemon=True) usb_device_scan_thread = threading.Thread(target=usb_device_scan_worker, daemon=True) def get_pboard_info(): # send request this_msg = list(info_request_spi_msg_template) this_msg[3] = RPI_APP_VERSION_TUPLE[0] this_msg[4] = RPI_APP_VERSION_TUPLE[1] this_msg[5] = RPI_APP_VERSION_TUPLE[2] xfer_when_not_busy(this_msg) time.sleep(0.05) # send an empty message to allow response to be shifted into RPi response = xfer_when_not_busy(list(nop_spi_msg_template)) time.sleep(0.05) response = xfer_when_not_busy(list(nop_spi_msg_template)) return response def set_protocol(raw_msg): time.sleep(0.05) xfer_when_not_busy(list(raw_msg)) time.sleep(0.05)