diff --git a/scripts/json2vdf.py b/scripts/json2vdf.py index ab89818..2812ab6 100755 --- a/scripts/json2vdf.py +++ b/scripts/json2vdf.py @@ -24,40 +24,13 @@ import json -def json2vdf(stream): - - """ - Read a json file and return a string in Steam vdf format - """ - - def _istr(ident, string): - return (ident * '\t') + string - - data = json.loads(stream.read(), object_pairs_hook=list) - - def _json2vdf(data, indent): - out = '' - for k, val in data: - if isinstance(val, list): - if indent: - out += '\n' - out += _istr(indent, '"{}"\n'.format(k)) - out += _istr(indent, '{\n') - out += _json2vdf(val, indent + 1) - out += _istr(indent, '}\n') - else: - out += _istr(indent, '"{}" "{}"\n'.format(k, val)) - return out - - return _json2vdf(data, 0) - - def main(): """ Read json and write Steam vdf conversion """ import sys import argparse + import steamcontroller.config parser = argparse.ArgumentParser(prog='json2vdf', description=main.__doc__) parser.add_argument('-i', '--input', default=sys.stdin, @@ -69,7 +42,7 @@ def main(): help='output vdf file (stdout if not specified)') args = parser.parse_args() - args.output.write(json2vdf(args.input)) + args.output.write(steamcontroller.config.json2vdf(args.input)) if __name__ == '__main__': main() diff --git a/scripts/sc-configurable.py b/scripts/sc-configurable.py new file mode 100755 index 0000000..55db5d7 --- /dev/null +++ b/scripts/sc-configurable.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 + +# The MIT License (MIT) +# +# Copyright (c) 2016 Mike Cronce +# Stany MARCEL +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +"""Steam Controller VDF-configurable mode""" + +from steamcontroller import SteamController +from steamcontroller.config import Configurator +from steamcontroller.daemon import Daemon + +import gc + +class SCDaemon(Daemon): + def __init__(self, pidfile, config_file): + self.pidfile = pidfile + self.config_file = config_file + self.logfile = '/var/log/steam-controller.log' + + def run(self): + config = Configurator('Steam Controller', self.config_file) + sc = SteamController(callback = config.evm.process) + sc.run() + del sc + del config + gc.collect() + +if __name__ == '__main__': + import argparse + + def _main(): + parser = argparse.ArgumentParser(description = __doc__) + parser.add_argument('command', type = str, choices = ['start', 'stop', 'restart', 'debug']) + parser.add_argument('-c', '--config-file', type = str, required = True) + parser.add_argument('-i', '--index', type = int, choices = [0,1,2,3], default = None) + args = parser.parse_args() + + if args.index != None: + daemon = SCDaemon('/tmp/steamcontroller{:d}.pid'.format(args.index), args.config_file) + else: + daemon = SCDaemon('/tmp/steamcontroller.pid', args.config_file) + + if 'start' == args.command: + daemon.start() + elif 'stop' == args.command: + daemon.stop() + elif 'restart' == args.command: + daemon.restart() + elif 'debug' == args.command: + try: + config = Configurator('Steam Controller', args.config_file) + sc = SteamController(callback = config.evm.process) + sc.run() + except KeyboardInterrupt: + return + + _main() diff --git a/scripts/vdf2json.py b/scripts/vdf2json.py index 9ccff6d..4a2e374 100755 --- a/scripts/vdf2json.py +++ b/scripts/vdf2json.py @@ -22,51 +22,13 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -from shlex import shlex - -def vdf2json(stream): - - """ - Read a Steam vdf file and return a string in json format - """ - - def _istr(ident, string): - return (ident * ' ') + string - - jbuf = '{\n' - lex = shlex(stream) - indent = 1 - - while True: - tok = lex.get_token() - if not tok: - return jbuf + '}\n' - if tok == '}': - indent -= 1 - jbuf += _istr(indent, '}') - ntok = lex.get_token() - lex.push_token(ntok) - if ntok and ntok != '}': - jbuf += ',' - jbuf += '\n' - else: - ntok = lex.get_token() - if ntok == '{': - jbuf += _istr(indent, tok + ': {\n') - indent += 1 - else: - jbuf += _istr(indent, tok + ': ' + ntok) - ntok = lex.get_token() - lex.push_token(ntok) - if ntok != '}': - jbuf += ',' - jbuf += '\n' def main(): """ Read Steam vdf and write json compatible conversion """ import sys import argparse + import steamcontroller.config parser = argparse.ArgumentParser(prog='vdf2json', description=main.__doc__) parser.add_argument('-i', '--input', @@ -79,7 +41,7 @@ def main(): help='output json file (stdout if not specified)') args = parser.parse_args() - args.output.write(vdf2json(args.input)) + args.output.write(steamcontroller.config.vdf2json(args.input)) if __name__ == '__main__': main() diff --git a/src/config.py b/src/config.py new file mode 100644 index 0000000..9fc4237 --- /dev/null +++ b/src/config.py @@ -0,0 +1,544 @@ +# The MIT License (MIT) +# +# Copyright (c) 2017 Mike Cronce +# Stany MARCEL +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import json +import os +import shlex + +from steamcontroller import SCButtons +from steamcontroller.events import EventMapper, Modes, StickModes, PadModes, Pos, TrigModes +from steamcontroller.uinput import Axes, Keys, Scans + +def vdf2json(stream): # {{{ + """ + Read a Steam vdf file and return a string in json format + """ + + def _istr(ident, string): + return (ident * ' ') + string + + jbuf = '{\n' + lex = shlex.shlex(stream) + indent = 1 + + while True: + tok = lex.get_token() + if not tok: + return jbuf + '}\n' + if tok == '}': + indent -= 1 + jbuf += _istr(indent, '}') + ntok = lex.get_token() + lex.push_token(ntok) + if ntok and ntok != '}': + jbuf += ',' + jbuf += '\n' + else: + ntok = lex.get_token() + if ntok == '{': + jbuf += _istr(indent, tok + ': {\n') + indent += 1 + else: + jbuf += _istr(indent, tok + ': ' + ntok) + ntok = lex.get_token() + lex.push_token(ntok) + if ntok != '}': + jbuf += ',' + jbuf += '\n' +# }}} + +def json2vdf(stream): # {{{ + + """ + Read a json file and return a string in Steam vdf format + """ + + def _istr(ident, string): + return (ident * '\t') + string + + data = json.loads(stream.read(), object_pairs_hook=list) + + def _json2vdf(data, indent): + out = '' + for k, val in data: + if isinstance(val, list): + if indent: + out += '\n' + out += _istr(indent, '"{}"\n'.format(k)) + out += _istr(indent, '{\n') + out += _json2vdf(val, indent + 1) + out += _istr(indent, '}\n') + else: + out += _istr(indent, '"{}" "{}"\n'.format(k, val)) + return out + return _json2vdf(data, 0) +# }}} + +def join_duplicate_keys(ordered_pairs): # {{{ + d = {} + for k, v in ordered_pairs: + if k in d: + if(type(d[k]) == list): + d[k].append(v) + else: + newlist = [] + newlist.append(d[k]) + newlist.append(v) + d[k] = newlist + else: + d[k] = v + return d +# }}} + +def load_vdf(path): # {{{ + f = open(path, 'r') + obj = json.loads(vdf2json(f), object_pairs_hook = join_duplicate_keys) + + # Since /controller_mappings/group is a key duplicated numerous times, it + # makes it cumbersome to use. This changes /controller_mappings/group + # to be a single-use key with a dict in it; each object in the dict is a + # one of these separate "group" objects, and the keys to the dict are + # the "id" fields of these objects. + obj['controller_mappings']['group'] = {group['id'] : group for group in obj['controller_mappings']['group']} + + # ID -> binding doesn't really do us any good. Flip it. + obj['controller_mappings']['preset']['group_source_bindings'] = {value : key for key, value in obj['controller_mappings']['preset']['group_source_bindings'].items()} + + return obj +# }}} + +def get_binding(group_inputs, input_name, activator): # {{{ + # This is the list of key "names" from Valve's VDF files (keys) that need + # to be replaced with libinput constants (values) + replace = { + 'PERIOD' : 'DOT', + 'ESCAPE' : 'ESC', + 'DASH' : 'MINUS', + 'EQUALS' : 'EQUAL' + } + + try: + activator = group_inputs[input_name]['activators'][activator] + # TODO: Proper support for multiples + if(type(activator) != list): + activator = [activator] + binding = activator[0]['bindings']['binding'].split() + except KeyError: + return None + + # TODO: mode_shift ... maybe more? + if(binding[0] == 'key_press'): + # Ugly + binding[1] = binding[1].replace('_ARROW', '') + binding[1] = binding[1].replace('_', '') + binding[1] = binding[1].replace(',', '') # Items such as "key_press W, w"; everything after the comma is already trimmed by split() above, ignore trailing items for now' + + if(binding[1] in replace): + binding[1] = replace[binding[1]] + + # Holy crap, the hacks don't get much uglier than this. Add 0x100 to + # all KEY_ constants, because the keyboard ends at 0xff and that + # seems to make the uinput subsystem happy about considering this + # to be a gamepad/joystick. + return Keys.__getattr__('KEY_' + binding[1]) + 0x100 + elif(binding[0] == 'mouse_wheel'): + # TODO: Figure out if we actually need this; if so, add support + return None + elif(binding[0] == 'mouse_button'): + return Keys.__getattr__('BTN_' + binding[1]) + elif(binding[0] == 'mode_shift'): + return [a['bindings']['binding'].split()[1] for a in activator] + + return None +# }}} + +def get_dpad_inputs(group): # {{{ + return { + 'north' : get_binding(group, 'dpad_north', 'Full_Press'), + 'west' : get_binding(group, 'dpad_west', 'Full_Press'), + 'south' : get_binding(group, 'dpad_south', 'Full_Press'), + 'east' : get_binding(group, 'dpad_east', 'Full_Press') + } +# }}} + +def get_diamond_inputs(group): # {{{ + return { + 'north' : get_binding(group, 'button_y', 'Full_Press'), + 'west' : get_binding(group, 'button_x', 'Full_Press'), + 'south' : get_binding(group, 'button_b', 'Full_Press'), + 'east' : get_binding(group, 'button_a', 'Full_Press') + } +# }}} + +def parse_trackpad_config(group, pos): # {{{ + config = {} + if(group['mode'] == 'absolute_mouse'): + config['mode'] = PadModes.MOUSE + config['buttons'] = {'click' : get_binding(group['inputs'], 'click', 'Full_Press')} + elif(group['mode'] == 'mouse_region'): + # TODO: Implement + config['mode'] = PadModes.NOACTION + pass + elif(group['mode'] == 'scrollwheel'): + config['mode'] = PadModes.MOUSESCROLL + config['buttons'] = {'click' : get_binding(group['inputs'], 'click', 'Full_Press')} + elif(group['mode'] == 'dpad'): + config['mode'] = PadModes.BUTTONCLICK + config['buttons'] = get_dpad_inputs(group['inputs']) + elif(group['mode'] == 'four_buttons'): + config['mode'] = PadModes.BUTTONCLICK + config['buttons'] = get_diamond_inputs(group['inputs']) + elif(group['mode'] == 'mouse_joystick'): + config['mode'] = PadModes.AXIS + config['buttons'] = {'click' : get_binding(group['inputs'], 'click', 'Full_Press')} + axes = [Axes.ABS_HAT0X, Axes.ABS_HAT0Y] if pos == Pos.LEFT else [Axes.ABS_HAT1X, Axes.ABS_HAT1Y] + config['axes'] = [(axis, -32768, 32767, 16, 128) for axis in axes] + return config +# }}} + +def parse_joystick_config(group): # {{{ + config = {} + if(group['mode'] == 'joystick_mouse'): + config['mode'] = StickModes.AXIS + config['buttons'] = {'click' : get_binding(group['inputs'], 'click', 'Full_Press')} + config['axes'] = [(axis, -32768, 32767, 16, 128) for axis in [Axes.ABS_X, Axes.ABS_Y]] + elif(group['mode'] == 'scrollwheel'): + # TODO: Implement + config['mode'] = StickModes.NOACTION + pass + elif(group['mode'] == 'dpad'): + config['mode'] = StickModes.BUTTON + config['buttons'] = get_dpad_inputs(group['inputs']) + elif(group['mode'] == 'buttons'): + config['mode'] = StickModes.BUTTON + config['buttons'] = get_diamond_inputs(group['inputs']) + return config +# }}} + +def parse_trigger_config(group): # {{{ + config = {} + if(group['mode'] == 'trigger'): + config['mode'] = TrigModes.BUTTON + config['buttons'] = {'click' : get_binding(group['inputs'], 'click', 'Full_Press')} + return config +# }}} + +def parse_config(config): # {{{ + groups = config['controller_mappings']['group'] + bindings = config['controller_mappings']['preset']['group_source_bindings'] + + # TODO: Check/respect all possible "mode" entries in each group + + output_config = { + 'left_trackpad' : {}, + 'right_trackpad' : {}, + 'joystick' : {}, + 'button_diamond' : {}, + 'switch' : {}, + 'left_trigger' : {}, + 'right_trigger' : {} + } + + if('left_trackpad active' in bindings): + output_config['left_trackpad']['active'] = parse_trackpad_config(groups[bindings['left_trackpad active']], Pos.LEFT) + if('left_trackpad active modeshift' in bindings): + output_config['left_trackpad']['modeshift'] = parse_trackpad_config(groups[bindings['left_trackpad active modeshift']], Pos.LEFT) + print('--- Left trackpad (active) loaded') + + if('right_trackpad active' in bindings): + output_config['right_trackpad']['active'] = parse_trackpad_config(groups[bindings['right_trackpad active']], Pos.RIGHT) + if('right_trackpad active modeshift' in bindings): + output_config['right_trackpad']['modeshift'] = parse_trackpad_config(groups[bindings['right_trackpad active modeshift']], Pos.RIGHT) + print('--- Right trackpad (active) loaded') + + if('joystick active' in bindings): + group = groups[bindings['joystick active']] + output_config['joystick']['active'] = parse_joystick_config(group) + output_config['joystick']['active']['buttons']['click'] = get_binding(group['inputs'], 'click', 'Full_Press') + if('joystick active modeshift' in bindings): + group = groups[bindings['joystick active modeshift']] + output_config['joystick']['modeshift'] = parse_joystick_config(group) + output_config['joystick']['modeshift']['buttons']['click'] = get_binding(group['inputs'], 'click', 'Full_Press') + print('--- Joystick (active) loaded') + + if('button_diamond active' in bindings): + inputs = groups[bindings['button_diamond active']]['inputs'] + output_config['button_diamond']['active'] = {'buttons' : { + 'a' : get_binding(inputs, 'button_a', 'Full_Press'), + 'b' : get_binding(inputs, 'button_b', 'Full_Press'), + 'x' : get_binding(inputs, 'button_x', 'Full_Press'), + 'y' : get_binding(inputs, 'button_y', 'Full_Press') + }} + if('button_diamond active modeshift' in bindings): + inputs = groups[bindings['button_diamond active modeshift']]['inputs'] + output_config['button_diamond']['modeshift'] = {'buttons' : { + 'a' : get_binding(inputs, 'button_a', 'Full_Press'), + 'b' : get_binding(inputs, 'button_b', 'Full_Press'), + 'x' : get_binding(inputs, 'button_x', 'Full_Press'), + 'y' : get_binding(inputs, 'button_y', 'Full_Press') + }} + print('--- Button diamond (active) loaded') + + if('switch active' in bindings): + inputs = groups[bindings['switch active']]['inputs'] + output_config['switch']['active'] = {'buttons' : { + 'left_bumper' : get_binding(inputs, 'left_bumper', 'Full_Press'), + 'right_bumper' : get_binding(inputs, 'right_bumper', 'Full_Press'), + 'start' : get_binding(inputs, 'button_escape', 'Full_Press'), + 'back' : get_binding(inputs, 'button_menu', 'Full_Press'), + 'left_grip' : get_binding(inputs, 'button_back_left', 'Full_Press'), + 'right_grip' : get_binding(inputs, 'button_back_right', 'Full_Press') + }} + if('switch active modeshift' in bindings): + inputs = groups[bindings['switch active modeshift']]['inputs'] + output_config['switch']['modeshift'] = {'buttons' : { + 'left_bumper' : get_binding(inputs, 'left_bumper', 'Full_Press'), + 'right_bumper' : get_binding(inputs, 'right_bumper', 'Full_Press'), + 'start' : get_binding(inputs, 'button_escape', 'Full_Press'), + 'back' : get_binding(inputs, 'button_menu', 'Full_Press'), + 'left_grip' : get_binding(inputs, 'button_back_left', 'Full_Press'), + 'right_grip' : get_binding(inputs, 'button_back_right', 'Full_Press') + }} + print('--- Switches (active) loaded') + + if('left_trigger active' in bindings): + group_id = bindings['left_trigger active'] + output_config['left_trigger']['active'] = parse_trigger_config(groups[bindings['left_trigger active']]) + if('left_trigger active modeshift' in bindings): + group_id = bindings['left_trigger active modeshift'] + output_config['left_trigger']['modeshift'] = parse_trigger_config(groups[bindings['left_trigger active modeshift']]) + print('--- Left trigger (active) loaded') + + if('right_trigger active' in bindings): + group_id = bindings['right_trigger active'] + output_config['right_trigger']['active'] = parse_trigger_config(groups[bindings['right_trigger active']]) + if('right_trigger active modeshift' in bindings): + group_id = bindings['right_trigger active modeshift'] + output_config['right_trigger']['modeshift'] = parse_trigger_config(groups[bindings['right_trigger active modeshift']]) + print('--- Right trigger (active) loaded') + + return output_config +# }}} + +class Configurator(): + vdf_path = None + config = None + evm = None + + # These are all used for gamepad definition as passed down the stack, + # through EventMapper, to the Gamepad subclass of UInput + name = None + vendor = 0x28de + product = 0x1142 + version = 0x1 + + def __init__(self, name, vdf_path = None, vendor = 0x28de, product = 0x1142, version = 0x1): # {{{ + self.name = name + self.vdf_path = vdf_path + if(self.vdf_path != None): + self.load_config() + self.vendor = vendor + self.product = product + self.version = version + # }}} + + def load_config(self): # {{{ + self.import_config(parse_config(load_vdf(self.vdf_path))) + # }}} + + def import_config(self, config): # {{{ + self.config = config + self.generate_eventmapper() + # }}} + + def generate_gamepad_definition(self): # {{{ + return { + 'vendor' : self.vendor, + 'product' : self.product, + 'version' : self.version, + 'name' : self.get_gamepad_name(), + 'keys' : self.get_keys(), + 'axes' : self.get_axes(), + 'rels' : [] + } + # }}} + + def generate_eventmapper(self): # {{{ + assert self.config != None + self.evm = EventMapper(gamepad_definition = self.generate_gamepad_definition(), modes = self.get_modes()) + + if('left_trackpad' in self.config and 'active' in self.config['left_trackpad']): + self.set_trackpad_config(Pos.LEFT, 'active') + print('--- Left trackpad configured') + + if('right_trackpad' in self.config and 'active' in self.config['right_trackpad']): + self.set_trackpad_config(Pos.RIGHT, 'active') + print('--- Right trackpad configured') + + if('joystick' in self.config and 'active' in self.config['joystick']): + self.set_joystick_config('active') + print('--- Joystick configured') + + if('button_diamond' in self.config and 'active' in self.config['button_diamond']): + self.set_diamond_config('active') + print('--- Button diamond configured') + + if('switch' in self.config and 'active' in self.config['switch']): + self.set_switches_config('active', True) + print('--- Switches configured') + + if('left_trigger' in self.config and 'active' in self.config['left_trigger']): + self.set_trigger_config(Pos.LEFT, 'active') + print('--- Left trigger configured') + + if('right_trigger' in self.config and 'active' in self.config['right_trigger']): + self.set_trigger_config(Pos.RIGHT, 'active') + print('--- Right trigger configured') + + # This cannot be configured from the Steam UI. Should we extend that file + # to support configuring it? + self.evm.setButtonAction(SCButtons.STEAM, Keys.KEY_ESC) + # }}} + + def get_gamepad_name(self): # {{{ + name = self.name + ((' [' + os.path.basename(self.vdf_path) + ']') if self.vdf_path != None else '') + try: + # In python3, we need to do this to get a string of 8-bit chars + return bytes(name) + except TypeError: + # But in python2, str consists of 8-bit chars; it's the unicode type + # that consists of wide chars + return name + # }}} + + def get_keys(self): # {{{ + buttons = [] + for group in self.config.values(): + for mode in group.values(): + if('buttons' in mode): + for button in mode['buttons'].values(): + if(button != None and type(button) != list): + buttons.append(button) + buttons = list(set(buttons)) + return buttons + # }}} + + def get_axes(self): # {{{ + axes = [] + for group in self.config.values(): + for mode in group.values(): + if('axes' in mode): + for axis in mode['axes']: + if(axis != None): + axes.append(axis) + axes = list(set(axes)) + return axes + # }}} + + def get_modes(self): # {{{ + # TODO: Figure out if this is actually useful or not + return [Modes.GAMEPAD, Modes.MOUSE, Modes.KEYBOARD] + # }}} + + def modeshift(self, sections, pressed): # {{{ + group = 'modeshift' if pressed else 'active' + if('left_trackpad' in sections and group in self.config['left_trackpad']): + self.set_trackpad_config(Pos.LEFT, group) + if('right_trackpad' in sections and group in self.config['right_trackpad']): + self.set_trackpad_config(Pos.RIGHT, group) + if('joystick' in sections and group in self.config['joystick']): + self.set_joystick_config(group) + if('button_diamond' in sections and group in self.config['button_diamond']): + self.set_diamond_config(group) + if('switch' in sections and group in self.config['switch']): + self.set_switches_config(group, False) + if('left_trigger' in sections and group in self.config['left_trigger']): + self.set_trigger_config(Pos.LEFT, group) + if('right_trigger' in sections and group in self.config['right_trigger']): + self.set_trigger_config(Pos.RIGHT, group) + # }}} + + def set_trackpad_config(self, pos, mode): # {{{ + button = SCButtons.RPAD if pos == Pos.RIGHT else SCButtons.LPAD + group = self.config['right_trackpad'][mode] if pos == Pos.RIGHT else self.config['left_trackpad'][mode] + if(group['mode'] == PadModes.MOUSE): + self.evm.setPadMouse(pos) + self.evm.setButtonAction(button, group['buttons']['click']) + elif(group['mode'] == PadModes.MOUSESCROLL): + # TODO: Support configuration for scroll directions + self.evm.setPadScroll(pos) + self.evm.setButtonAction(button, group['buttons']['click']) + elif(group['mode'] == PadModes.BUTTONCLICK): + buttons = group['buttons'] + self.evm.setPadButtons(pos, [buttons['north'], buttons['west'], buttons['south'], buttons['east']], clicked = True) + elif(group['mode'] == PadModes.BUTTONTOUCH): + buttons = group['buttons'] + self.evm.setPadButtons(pos, [buttons['north'], buttons['west'], buttons['south'], buttons['east']], clicked = False) + self.evm.setButtonAction(button, group['buttons']['click']) + elif(group['mode'] == PadModes.AXIS): + self.evm.setPadAxes(pos, *[axis[0] for axis in group['axes']]) + self.evm.setButtonAction(button, group['buttons']['click']) + # }}} + + def set_joystick_config(self, mode): # {{{ + group = self.config['joystick'][mode] + if(group['mode'] == StickModes.BUTTON): + self.evm.setStickButtons([group['buttons']['north'], group['buttons']['west'], group['buttons']['south'], group['buttons']['east']]) + if('click' in group['buttons'] and group['buttons']['click'] != None): + self.evm.setButtonAction(SCButtons.LPAD, group['buttons']['click']) + elif(group['mode'] == StickModes.AXIS): + self.evm.setStickAxes(*[axis[0] for axis in group['axes']]) + # }}} + + def set_diamond_config(self, mode): # {{{ + group = self.config['button_diamond'][mode] + self.evm.setButtonAction(SCButtons.A, group['buttons']['a']) + self.evm.setButtonAction(SCButtons.B, group['buttons']['b']) + self.evm.setButtonAction(SCButtons.X, group['buttons']['x']) + self.evm.setButtonAction(SCButtons.Y, group['buttons']['y']) + # }}} + + def set_switches_config(self, mode, assign_modeshifts): # {{{ + group = self.config['switch'][mode] + self.evm.setButtonAction(SCButtons.LB, group['buttons']['left_bumper']) + self.evm.setButtonAction(SCButtons.RB, group['buttons']['right_bumper']) + self.evm.setButtonAction(SCButtons.START, group['buttons']['start']) + self.evm.setButtonAction(SCButtons.BACK, group['buttons']['back']) + if(type(group['buttons']['left_grip']) == list): + if(assign_modeshifts): + self.evm.setButtonCallback(SCButtons.LGRIP, lambda evm, btn, pressed: self.modeshift(group['buttons']['left_grip'], pressed)) + else: + self.evm.setButtonAction(SCButtons.LGRIP, group['buttons']['left_grip']) + if(type(group['buttons']['right_grip']) == list): + if(assign_modeshifts): + self.evm.setButtonCallback(SCButtons.RGRIP, lambda evm, btn, pressed: self.modeshift(group['buttons']['right_grip'], pressed)) + else: + self.evm.setButtonAction(SCButtons.RGRIP, group['buttons']['right_grip']) + # }}} + + def set_trigger_config(self, pos, mode): # {{{ + group = self.config['right_trigger'][mode] if pos == Pos.RIGHT else self.config['left_trigger'][mode] + if(group['mode'] == TrigModes.BUTTON): + self.evm.setTrigButton(pos, group['buttons']['click']) + # }}} + diff --git a/src/daemon.py b/src/daemon.py index 964c765..56aa8d2 100644 --- a/src/daemon.py +++ b/src/daemon.py @@ -18,8 +18,9 @@ class Daemon(object): Usage: subclass the daemon class and override the run() method.""" - def __init__(self, pidfile): + def __init__(self, pidfile, logfile = os.devnull): self.pidfile = pidfile + self.logfile = logfile def daemonize(self): """Deamonize class. UNIX double fork mechanism.""" @@ -42,7 +43,6 @@ def daemonize(self): try: pid = os.fork() if pid > 0: - # exit from second parent sys.exit(0) except OSError as err: @@ -53,8 +53,8 @@ def daemonize(self): sys.stdout.flush() sys.stderr.flush() stdi = open(os.devnull, 'r') - stdo = open(os.devnull, 'a+') - stde = open(os.devnull, 'a+') + stdo = open(self.logfile, 'a+') + stde = open(self.logfile, 'a+') os.dup2(stdi.fileno(), sys.stdin.fileno()) os.dup2(stdo.fileno(), sys.stdout.fileno()) diff --git a/src/events.py b/src/events.py index ecfdb89..3357e3c 100644 --- a/src/events.py +++ b/src/events.py @@ -82,11 +82,14 @@ class EventMapper(object): callback to be registered to a SteamController instance """ - def __init__(self): - - self._uip = (sui.Gamepad(), - sui.Keyboard(), - sui.Mouse()) + def __init__(self, gamepad_definition = None, modes = [Modes.GAMEPAD, Modes.MOUSE, Modes.KEYBOARD]): + self._uip = {} + if(Modes.GAMEPAD in modes): + self._uip[Modes.GAMEPAD] = sui.Gamepad(gamepad_definition) + if(Modes.KEYBOARD in modes): + self._uip[Modes.KEYBOARD] = sui.Keyboard() + if(Modes.MOUSE in modes): + self._uip[Modes.MOUSE] = sui.Mouse() self._btn_map = {x : (None, 0) for x in list(SCButtons)} @@ -204,26 +207,29 @@ def _keyreleased(mode, ev): time() - self._steam_pressed_time > EXIT_PRESS_DURATION): sc.addExit() - # Manage buttons + # Manage buttons {{{ for btn, (mode, ev) in self._btn_map.items(): - if mode is None: continue if btn & btn_add: - if mode is Modes.CALLBACK: - ev(self, btn, True) - else: - _keypressed(mode, ev) + # This hack makes me want to go take a shower + if(not (btn == SCButtons.LPAD and (sci.buttons & SCButtons.LPADTOUCH))): + if mode is Modes.CALLBACK: + ev(self, btn, True) + else: + _keypressed(mode, ev) elif btn & btn_rem: - - if mode is Modes.CALLBACK: - ev(self, btn, False) - else: - _keyreleased(mode, ev) - # Manage pads + # This hack still makes me want to go take a shower + if(not (btn == SCButtons.LPAD and (sci_p.buttons & SCButtons.LPADTOUCH))): + if mode is Modes.CALLBACK: + ev(self, btn, False) + else: + _keyreleased(mode, ev) + # }}} + + # Manage pads {{{ for pos in [Pos.LEFT, Pos.RIGHT]: - if pos == Pos.LEFT: x, y = sci.lpad_x, sci.lpad_y x_p, y_p = sci_p.lpad_x, sci_p.lpad_y @@ -236,16 +242,17 @@ def _keyreleased(mode, ev): click = SCButtons.RPAD if sci.buttons & touch == touch: - # Compute mean pos - try: - xm_p = int(sum(self._xdq[pos]) / len(self._xdq[pos])) - except ZeroDivisionError: - xm_p = 0 - - try: - ym_p = int(sum(self._ydq[pos]) / len(self._ydq[pos])) - except ZeroDivisionError: - ym_p = 0 + if(sci_p.buttons & touch == touch): + # Compute mean pos + try: + xm_p = int(sum(self._xdq[pos]) / len(self._xdq[pos])) + except ZeroDivisionError: + xm_p = 0 + + try: + ym_p = int(sum(self._ydq[pos]) / len(self._ydq[pos])) + except ZeroDivisionError: + ym_p = 0 self._xdq[pos].append(x) self._ydq[pos].append(y) @@ -259,11 +266,12 @@ def _keyreleased(mode, ev): except ZeroDivisionError: ym = 0 - if not sci_p.buttons & touch == touch: + if(not sci_p.buttons & touch == touch): xm_p, ym_p = xm, ym + xm = 0 + ym = 0 - - # Mouse and mouse scroll modes + # Mouse and mouse scroll modes {{{ if self._pad_modes[pos] in (PadModes.MOUSE, PadModes.MOUSESCROLL): _free = True _dx = 0 @@ -287,14 +295,13 @@ def _keyreleased(mode, ev): # FIXME: make haptic configurable if not _free: sc.addFeedback(pos, amplitude=256) + # }}} - - # Axis mode + # Axis mode {{{ elif self._pad_modes[pos] == PadModes.AXIS: revert = self._pad_revs[pos] (xmode, xev), (ymode, yev) = self._pad_evts[pos] if xmode is not None: - # FIXME: make haptic configurable if sci.buttons & touch == touch: self._moved[pos] += sqrt((xm - xm_p)**2 + (ym - ym_p)**2) @@ -302,17 +309,23 @@ def _keyreleased(mode, ev): sc.addFeedback(pos, amplitude=100) self._moved[pos] %= 4000 - if x != x_p: - self._uip[xmode].axisEvent(xev, x) - syn.add(xmode) - if y != y_p: - self._uip[ymode].axisEvent(yev, y if not revert else -y) - syn.add(ymode) - - # Button touch mode - elif (self._pad_modes[pos] == PadModes.BUTTONTOUCH or - self._pad_modes[pos] == PadModes.BUTTONCLICK): - + if x != x_p: + self._uip[xmode].axisEvent(xev, x) + syn.add(xmode) + if y != y_p: + self._uip[ymode].axisEvent(yev, y if not revert else -y) + syn.add(ymode) + else: + if(x_p != 0): + self._uip[xmode].axisEvent(xev, 0) + syn.add(xmode) + if(y_p != 0): + self._uip[ymode].axisEvent(yev, 0) + syn.add(ymode) + # }}} + + # Button touch mode {{{ + elif (self._pad_modes[pos] == PadModes.BUTTONTOUCH or self._pad_modes[pos] == PadModes.BUTTONCLICK): if self._pad_modes[pos] == PadModes.BUTTONTOUCH: on_test = touch off_test = touch @@ -378,8 +391,7 @@ def _keyreleased(mode, ev): else: haptic |= _absreleased(xev) - if (sci.buttons & off_test != off_test and - sci_p.buttons & on_test == on_test): + if (sci.buttons & off_test != off_test and sci_p.buttons & on_test == on_test): if len(self._pad_evts[pos]) == 4: for mode, ev in self._pad_evts[pos]: haptic |= _keyreleased(mode, ev) @@ -389,14 +401,15 @@ def _keyreleased(mode, ev): if haptic and self._pad_modes[pos] == PadModes.BUTTONTOUCH: sc.addFeedback(pos, amplitude=300) + # }}} if sci.buttons & touch != touch: xm_p, ym_p, xm, ym = 0, 0, 0, 0 self._xdq[pos].clear() self._ydq[pos].clear() + # }}} - - # Manage Trig + # Manage Trig {{{ for pos in [Pos.LEFT, Pos.RIGHT]: trigval = sci.ltrig if pos == Pos.LEFT else sci.rtrig trigval_prev = sci_p.ltrig if pos == Pos.LEFT else sci_p.rtrig @@ -414,9 +427,9 @@ def _keyreleased(mode, ev): elif self._trig_s[pos] is not None and trigval <= self._trig_s[pos]: self._trig_s[pos] = None _keyreleased(mode, ev) + # }}} - - # Manage Stick + # Manage Stick {{{ if sci.buttons & SCButtons.LPADTOUCH != SCButtons.LPADTOUCH: x, y = sci.lpad_x, sci.lpad_y x_p, y_p = sci_p.lpad_x, sci_p.lpad_y @@ -435,7 +448,6 @@ def _keyreleased(mode, ev): self._uip[ymode].axisEvent(yev, y if not revert else -y) elif self._stick_mode == StickModes.BUTTON: - tmode, tev = self._stick_evts[0] lmode, lev = self._stick_evts[1] bmode, bev = self._stick_evts[2] @@ -475,7 +487,7 @@ def _keyreleased(mode, ev): if sci.buttons & SCButtons.LPAD == SCButtons.LPAD: if self._stick_pressed_callback is not None: self._stick_pressed_callback(self) - + # }}} if len(_pressed): self._uip[Modes.KEYBOARD].pressEvent(_pressed) @@ -487,8 +499,16 @@ def _keyreleased(mode, ev): self._uip[i].synEvent() - def setButtonAction(self, btn, key_event): - for mode in Modes: + def setButtonAction(self, btn, key_event, mode = None): + if(key_event == None and btn in self._btn_map): + self._btn_map[btn] = (None, 0) + return + + if(mode != None): + self._btn_map[btn] = (mode, key_event) + return + + for mode in self._uip.keys(): if self._uip[mode].keyManaged(key_event): self._btn_map[btn] = (mode, key_event) return @@ -506,7 +526,7 @@ def setButtonCallback(self, btn, callback): self._btn_map[btn] = (Modes.CALLBACK, callback) - def setPadButtons(self, pos, key_events, deadzone=0.6, clicked=False): + def setPadButtons(self, pos, key_events, deadzone=0.6, clicked=False, mode = None): """ Set pad as buttons @@ -523,10 +543,13 @@ def setPadButtons(self, pos, key_events, deadzone=0.6, clicked=False): self._pad_evts[pos] = [] for ev in key_events: - for mode in Modes: - if self._uip[mode].keyManaged(ev): - self._pad_evts[pos].append((mode, ev)) - break + if(mode != None): + self._pad_evts[pos].append((mode, ev)) + else: + for mode in self._uip.keys(): + if self._uip[mode].keyManaged(ev): + self._pad_evts[pos].append((mode, ev)) + break self._pad_dzones[pos] = 32768 * deadzone @@ -613,9 +636,19 @@ def setPadAxes(self, pos, abs_x_event, abs_y_event, revert=True): (Modes.GAMEPAD, abs_y_event)] self._pad_revs[pos] = revert - def setTrigButton(self, pos, key_event): + def setTrigButton(self, pos, key_event, mode = None): + if(key_event == None): + self._trig_evts[pos] = (None, 0) + self._trig_modes[pos] = TrigModes.NOACTION + return + self._trig_modes[pos] = TrigModes.BUTTON - for mode in Modes: + + if(mode != None): + self._trig_evts[pos] = (mode, key_event) + return + + for mode in self._uip.keys(): if self._uip[mode].keyManaged(key_event): self._trig_evts[pos] = (mode, key_event) return @@ -644,7 +677,7 @@ def setStickAxesCallback(self, callback): self._stick_axes_callback = callback - def setStickButtons(self, key_events): + def setStickButtons(self, key_events, mode = None): """ Set stick as buttons @@ -657,10 +690,13 @@ def setStickButtons(self, key_events): self._stick_evts = [] for ev in key_events: - for mode in Modes: - if self._uip[mode].keyManaged(ev): - self._stick_evts.append((mode, ev)) - break + if(mode != None): + self._stick_evts.append((mode, ev)) + else: + for mode in self._uip.keys(): + if self._uip[mode].keyManaged(ev): + self._stick_evts.append((mode, ev)) + break def setStickPressedCallback(self, callback): """ diff --git a/src/uinput.py b/src/uinput.py index 684a018..292d2be 100644 --- a/src/uinput.py +++ b/src/uinput.py @@ -366,31 +366,40 @@ class Gamepad(UInput): Gamepad uinput class, create a Xbox360 gamepad device """ - def __init__(self): - super(Gamepad, self).__init__(vendor=0x045e, - product=0x028e, - version=0x110, - name=b"Microsoft X-Box 360 pad", - keys=[Keys.BTN_START, - Keys.BTN_MODE, - Keys.BTN_SELECT, - Keys.BTN_A, - Keys.BTN_B, - Keys.BTN_X, - Keys.BTN_Y, - Keys.BTN_TL, - Keys.BTN_TR, - Keys.BTN_THUMBL, - Keys.BTN_THUMBR], - axes=[(Axes.ABS_X, -32768, 32767, 16, 128), - (Axes.ABS_Y, -32768, 32767, 16, 128), - (Axes.ABS_RX, -32768, 32767, 16, 128), - (Axes.ABS_RY, -32768, 32767, 16, 128), - (Axes.ABS_Z, 0, 255, 0, 0), - (Axes.ABS_RZ, 0, 255, 0, 0), - (Axes.ABS_HAT0X, -1, 1, 0, 0), - (Axes.ABS_HAT0Y, -1, 1, 0, 0)], - rels=[]) + def __init__(self, gamepad_definition = None): + if(gamepad_definition == None): + gamepad_definition = { + 'vendor' : 0x045e, + 'product' : 0x028e, + 'version' : 0x110, + 'name' : b"Microsoft X-Box 360 pad", + 'keys' : [ + Keys.BTN_START, + Keys.BTN_MODE, + Keys.BTN_SELECT, + Keys.BTN_A, + Keys.BTN_B, + Keys.BTN_X, + Keys.BTN_Y, + Keys.BTN_TL, + Keys.BTN_TR, + Keys.BTN_THUMBL, + Keys.BTN_THUMBR + ], + 'axes' : [ + (Axes.ABS_X, -32768, 32767, 16, 128), + (Axes.ABS_Y, -32768, 32767, 16, 128), + (Axes.ABS_RX, -32768, 32767, 16, 128), + (Axes.ABS_RY, -32768, 32767, 16, 128), + (Axes.ABS_Z, 0, 255, 0, 0), + (Axes.ABS_RZ, 0, 255, 0, 0), + (Axes.ABS_HAT0X, -1, 1, 0, 0), + (Axes.ABS_HAT0Y, -1, 1, 0, 0) + ], + 'rels' : [] + } + + super(Gamepad, self).__init__(**gamepad_definition) class Mouse(UInput):