306 lines
11 KiB
Python
Executable File
306 lines
11 KiB
Python
Executable File
#!/usr/bin/env python
|
|
# -*- coding: utf-8 -*-
|
|
|
|
from __future__ import print_function
|
|
|
|
import sys
|
|
import re
|
|
import os
|
|
import select
|
|
import contextlib
|
|
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 one of the following:")
|
|
print(" pip install evdev")
|
|
print(" sudo apt install python-evdev")
|
|
sys.exit(1)
|
|
|
|
try:
|
|
import termios
|
|
except ImportError:
|
|
termios = None
|
|
|
|
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:
|
|
# This is kinda hacky, but fixes issue #962:
|
|
# https://github.com/reicast/reicast-emulator/issues/962
|
|
# First, we read all available input events, then we wait for up to a
|
|
# quarter second, then we attempt to read event more input events. This
|
|
# should make sure that all available input events have been read (and
|
|
# discarded).
|
|
for event in iter(dev.read_one, None):
|
|
pass
|
|
select.select([dev], [], [], 0.50)
|
|
for event in iter(dev.read_one, None):
|
|
pass
|
|
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
|
|
|
|
@contextlib.contextmanager
|
|
def noecho():
|
|
# This function is largely based on unix_getpass():
|
|
# https://github.com/python/cpython/blob/master/Lib/getpass.py#L30
|
|
try:
|
|
fd = os.open('/dev/tty', os.O_RDWR | os.O_NOCTTY)
|
|
stream = os.fdopen(fd, 'w+', 1)
|
|
except EnvironmentError:
|
|
try:
|
|
fd = sys.stdin.fileno()
|
|
stream = sys.stderr
|
|
except (AttributeError, ValueError):
|
|
fd = None
|
|
stream = None
|
|
if termios is None or fd is None:
|
|
yield
|
|
else:
|
|
old = termios.tcgetattr(fd) # a copy to save
|
|
new = old[:]
|
|
new[3] &= ~termios.ECHO # 3 == 'lflags'
|
|
tcsetattr_flags = termios.TCSAFLUSH
|
|
if hasattr(termios, 'TCSASOFT'):
|
|
tcsetattr_flags |= termios.TCSASOFT
|
|
|
|
try:
|
|
termios.tcsetattr(fd, tcsetattr_flags, new)
|
|
yield
|
|
finally:
|
|
termios.tcsetattr(fd, tcsetattr_flags, old)
|
|
if stream is not None:
|
|
stream.flush() # issue7208
|
|
|
|
|
|
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"):
|
|
with noecho():
|
|
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):
|
|
with noecho():
|
|
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:
|
|
with noecho():
|
|
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):
|
|
with noecho():
|
|
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:
|
|
with noecho():
|
|
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):
|
|
# Flush stdin (if possible)
|
|
if termios is not None:
|
|
termios.tcflush(sys.stdin, termios.TCIFLUSH)
|
|
|
|
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:")
|
|
print("Save this as \"~/.local/share/reicast/mappings/%s.cfg\"\n" % mapping.get("emulator", "mapping_name"))
|
|
mapping.write(sys.stdout)
|
|
|
|
sys.exit(0)
|