flycast/shell/linux/tools/reicast-joyconfig.py

248 lines
8.7 KiB
Python
Executable File

#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import print_function
import sys
import re
import os
if sys.version_info < (3, 0):
import ConfigParser as configparser
INPUT_FUNC = raw_input
else:
import configparser
INPUT_FUNC = input
try:
import evdev
except ImportError:
print("Can't import evdev module. Please install it.")
print("You can do this via:")
print(" pip install evdev")
sys.exit(1)
DEV_ID_PATTERN = re.compile('(\d+)')
DREAMCAST_BUTTONS = ['A', 'B', 'C', 'D', 'X', 'Y', 'Z', 'START']
DREAMCAST_DPAD = [('X', 'LEFT', 'RIGHT'), ('Y', 'UP', 'DOWN')]
DREAMCAST_TRIGGERS = ['TRIGGER_LEFT', 'TRIGGER_RIGHT']
DREAMCAST_STICK_AXES = [('X', 'left'), ('Y', 'up')]
SECTIONS = ["emulator", "dreamcast", "compat"]
def list_devices():
devices = [(int(DEV_ID_PATTERN.findall(fn)[0]), evdev.InputDevice(fn))
for fn in evdev.list_devices()]
dev_id_len = len(str(max([dev_id for dev_id, dev in devices])))
for dev_id, dev in sorted(devices, key=lambda x: x[0]):
print("%s: %s (%s, %s)" %
(str(dev_id).rjust(dev_id_len), dev.name, dev.fn, dev.phys))
def clear_events(dev):
try:
event = dev.read_one()
while(event is not None):
event = dev.read_one()
except (OSError, IOError):
# BlockingIOErrors should only occur if someone uses the evdev
# module < v0.4.4. BlockingIOError inherits from OSError, so we
# catch that for Python 2 compatibility. We also catch IOError,
# just in case.
pass
def read_button(dev):
for event in dev.read_loop():
if event.type == evdev.ecodes.EV_KEY and event.value == 0:
break
return event
def read_axis(dev, absinfos):
axis_inverted = False
for event in dev.read_loop():
if event.type == evdev.ecodes.EV_ABS and \
event.value in (absinfos[event.code].min, absinfos[event.code].max):
if event.value == absinfos[event.code].max:
axis_inverted = True
break
return (event, axis_inverted)
def read_axis_or_key(dev, absinfos):
axis_inverted = False
for event in dev.read_loop():
if event.type == evdev.ecodes.EV_KEY and event.value == 0:
break
elif (event.type == evdev.ecodes.EV_ABS and event.value in
(absinfos[event.code].min, absinfos[event.code].max)):
if event.value == absinfos[event.code].max:
axis_inverted = True
break
return (event, axis_inverted)
def print_mapped_button(name, event):
try:
code_id = evdev.ecodes.BTN[event.code]
except (IndexError, KeyError):
try:
code_id = evdev.ecodes.KEY[event.code]
except (IndexError, KeyError):
code_id = None
if type(code_id) is list:
code_id = code_id[0]
code_id = (' (%s)' % code_id) if code_id else ''
print("%s mapped to %d%s." % (name, event.code, code_id))
def print_mapped_axis(name, event, axis_inverted=False):
try:
code_id = evdev.ecodes.ABS[event.code]
except (IndexError, KeyError):
code_id = None
if type(code_id) is list:
code_id = code_id[0]
code_id = (' (%s)' % code_id) if code_id else ''
inv = (' (inverted)' if axis_inverted else '')
print("%s mapped to %d%s%s." % (name, event.code, code_id, inv))
def setup_device(dev_id):
print("Using device %d..." % dev_id)
fn = "/dev/input/event%d" % dev_id
dev = evdev.InputDevice(fn)
print("Name: %s" % dev.name)
print("File: %s" % dev.fn)
print("Phys: %s" % dev.phys)
cap = dev.capabilities(verbose=False, absinfo=True)
try:
absinfos = dict(cap[evdev.ecodes.EV_ABS])
except KeyError:
absinfos = dict()
mapping = configparser.RawConfigParser()
for section in SECTIONS:
mapping.add_section(section)
mapping.set("emulator", "mapping_name", dev.name)
# Emulator escape button
if ask_yes_no("Do you want to map a button to exit the emulator"):
clear_events(dev)
print("Press the that button now...")
event = read_button(dev)
mapping.set("emulator", "btn_escape", event.code)
print_mapped_button("emulator escape button", event)
# Regular dreamcast buttons
for button in DREAMCAST_BUTTONS:
if ask_yes_no("Do you want to map the %s button?" % button):
clear_events(dev)
print("Press the %s button now..." % button)
event = read_button(dev)
mapping.set("dreamcast", "btn_%s" % button.lower(), event.code)
print_mapped_button("%s button" % button, event)
# DPads
for i in range(1, 3):
if ask_yes_no("Do you want to map DPad %d?" % i):
for axis, button1, button2 in DREAMCAST_DPAD:
clear_events(dev)
print("Press the %s button of DPad %d now..." % (button1, i))
event, axis_inverted = read_axis_or_key(dev, absinfos)
if event.type == evdev.ecodes.EV_ABS:
axisname = "axis_dpad%d_%s" % (i, axis.lower())
mapping.set("compat", axisname, event.code)
mapping.set("compat", "%s_inverted" % axisname, "yes" if axis_inverted else "no")
print_mapped_axis("%s axis of DPad %d" % (axis, i), event, axis_inverted)
else:
buttonconf = "btn_dpad%d_%%s" % i
mapping.set("dreamcast", buttonconf % button1.lower(), event.code)
print_mapped_button("%s button of DPad %d" % (button1, i), event)
clear_events(dev)
print("Press the %s button of DPad %d now..." % (button2, i))
event = read_button(dev)
mapping.set("dreamcast", buttonconf % button2.lower(), event.code)
print_mapped_button("%s button of DPad %d" % (button2, i), event)
# Triggers
for trigger in DREAMCAST_TRIGGERS:
if ask_yes_no("Do you want to map %s?" % trigger):
clear_events(dev)
print("Press the %s now..." % trigger)
event, axis_inverted = read_axis_or_key(dev, absinfos)
axis_inverted = not axis_inverted
if event.type == evdev.ecodes.EV_ABS:
axisname = "axis_%s" % trigger.lower()
mapping.set("dreamcast", axisname, event.code)
mapping.set("compat", "%s_inverted" % axisname, "yes" if axis_inverted else "no")
print_mapped_axis("analog %s" % trigger, event, axis_inverted)
else:
mapping.set("compat", "btn_%s" % trigger.lower(), event.code)
print_mapped_button("digital %s" % trigger, event)
# Stick
if ask_yes_no("Do you want to map the analog stick?"):
for axis, axis_dir in DREAMCAST_STICK_AXES:
clear_events(dev)
print("Please move the analog stick as far %s as possible now..." % axis_dir)
event, axis_inverted = read_axis(dev, absinfos)
axisname = "axis_%s" % axis.lower()
mapping.set("dreamcast", axisname, event.code)
mapping.set("compat", "%s_inverted" % axisname, "yes" if axis_inverted else "no")
print_mapped_axis(axis, event, axis_inverted)
for section in SECTIONS:
if not mapping.options(section):
mapping.remove_section(section)
return mapping
def ask_yes_no(question, default=True):
valid = {"yes": True, "y": True, "ye": True,
"no": False, "n": False}
prompt = "Y/n" if default else "y/N"
while True:
print("%s [%s] " % (question, prompt), end='')
choice = INPUT_FUNC().lower()
if choice == '':
return default
if choice in valid:
return valid[choice]
else:
print("Please respond with 'yes' or 'no'")
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(
description='Reicast evdev controller mapping editor')
parser.add_argument('-d', '--device-id', action='store', type=int,
default=-1, help='device-id to map.')
parser.add_argument('-f', '--file', action='store', default=None,
help='write mapping to this file')
args = parser.parse_args()
if args.device_id < 0:
list_devices()
dev_id = int(input("Please enter the device id: "))
else:
dev_id = args.device_id
mapping = setup_device(dev_id)
if args.file:
with open(args.file, "w") as f:
mapping.write(f)
print("\nMapping file saved to: %s\n" % os.path.abspath(args.file))
else:
print("\nHere's your mapping file:\n")
mapping.write(sys.stdout)
sys.exit(0)