From 08d0ee462661655106d01953e5a8dd730b2a4b41 Mon Sep 17 00:00:00 2001 From: RoarkGit <34554573+RoarkGit@users.noreply.github.com> Date: Thu, 26 Oct 2023 16:45:09 -0400 Subject: [PATCH] Initial commit. --- README.md | 64 ++++++++++++++++++++++++++++++++ pedal_mapper.py | 99 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 163 insertions(+) create mode 100644 README.md create mode 100644 pedal_mapper.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..c8bf474 --- /dev/null +++ b/README.md @@ -0,0 +1,64 @@ +# Pedal Mapper + +This is a simple program for mapping the buttons on an Elgato Stream Deck Pedal +to keyboard inputs. I use a pedal for push-to-talk when I play games with a +controller, and there wasn't anything that let me do that easily with the Elgato +Stream Deck Pedal. + +## Dependencies + +There aren't a lot of dependencies, but what you do need are: +- Python +- hidapi +- evdev + +## How it works + +The actual flow is really simple. You just configure three lists of key presses, +one for each button on the pedal, and then run the program. The program then +listens for button presses and sends the mapped keys to `/dev/uinput`. + +Run the Python script and press your pedal. You can run it as a `systemd` unit +or equivalent to have it always running in the background. + +You can find a list of all possible keycodes +[here](https://github.com/torvalds/linux/blob/master/include/uapi/linux/input-event-codes.h). + +## Possible issues + +If you're using Wayland it won't work if your voice chat client isn't focused. +However, if you have a means of forwarding keystrokes to Xwayland applications +and your client is running in Xwayland, then it will work fine. KDE has this +built-in and that's what I use. There are other solutions like [this +script](https://github.com/Rush/wayland-push-to-talk-fix/) for forwarding +keystrokes. + +There may be other issues. I haven't really run into any myself yet, but you +never know. + +## Possible additions + +This was just my first pass at implementing this specifically for my needs. The +code sucks, is basic as hell, and isn't very configurable. In no particular +order, things that I may implement in the future if I feel inspired are: +- Reading configuration from a file rather than being hardcoded into the + program. +- Configuring other types of devices. + [input-remapper](https://github.com/sezanzeb/input-remapper) is already a good + general purpose solution, but it's possible there are unsupported devices for + which people might want a similar solution to what is here. +- More advanced key combinations / macros. Right now the program just handles + basic hold keys and press keys. Adding something like callbacks to associate + with buttons could be neat, with a default one being how it works right now. +- Profiles/profile-switching would be neat. Being able to switch stuff on the + fly would be handy if someone wanted to use different bindings for different + situations. + +These are all highly aspirational and I don't actually need any of these myself +currently. I'm lazy and selfish so it's actually unlikely I'll implement these, +but they're here as ideas in case anyone else wants to contribute. + +## Contributing + +Feel free to submit merge requests. I'm not too scary and would happily accept +contributions. \ No newline at end of file diff --git a/pedal_mapper.py b/pedal_mapper.py new file mode 100644 index 0000000..b77459c --- /dev/null +++ b/pedal_mapper.py @@ -0,0 +1,99 @@ +from collections import namedtuple +from enum import Enum +from evdev import UInput, ecodes as e +import hid + +# Elgato Stream Deck Pedal Vendor / Product IDs +VENDOR_ID = 0x0FD9 +PRODUCT_ID = 0x0086 + + +# Represents a key combination of mods, which are held for the whole +# combination, and keys, which are pressed and released. +KeyCombo = namedtuple("KeyCombo", ["mods", "keys"], defaults=([], [])) + + +# Simple enum that represents the three buttons. +class Button(Enum): + LEFT = 0 + MIDDLE = 1 + RIGHT = 2 + + +class PedalMapper: + def __init__(self, left_keys=[], middle_keys=[], right_keys=[], polling_rate=10): + self.button_key_mappings = (left_keys, middle_keys, right_keys) + self.button_state = [False, False, False] + self.polling_rate = polling_rate + self.dev = hid.device() + self.dev.open(VENDOR_ID, PRODUCT_ID) + self.dev.set_nonblocking(1) + + # Register keys in UInput capabilities. + reg_keys = set() + for key_combos in self.button_key_mappings: + for combo in key_combos: + reg_keys.update(combo.mods) + reg_keys.update(combo.keys) + cap = {e.EV_KEY: reg_keys} + self.ui = UInput(cap, name="Pedal Mapper Virtual Input") + + def get_event(self): + # We're non-blocking, so wait for a poll. This could just be changed to + # blocking instead without issues, but it is non-blocking in case it's + # actually needed in the future (e.g. handling multiple devices). + read = self.dev.read(8, self.polling_rate) + if not read: + return + button = None + if read[4] != self.button_state[0]: + button = Button.LEFT + elif read[5] != self.button_state[1]: + button = Button.MIDDLE + elif read[6] != self.button_state[2]: + button = Button.RIGHT + else: + return + self.button_state[button.value] = not self.button_state[button.value] + return button, self.button_state[button.value] + + def handle_key(self, button, state): + key_combos = self.button_key_mappings[button.value] + for combo in key_combos: + # Press mods first + if state: + for mod in combo.mods: + self.write_key(mod, state) + for key in combo.keys: + self.write_key(key, state) + # Release mods last + else: + for key in combo.keys: + self.write_key(key, state) + for mod in combo.mods: + self.write_key(mod, state) + + # Writes a key to uinput and then syncs. + def write_key(self, key, state): + self.ui.write(e.EV_KEY, key, state) + self.ui.syn() + + +if __name__ == "__main__": + # Create Pedal + # Example mappings: + # Left: no-op + # Middle: right meta + F13 + # Right: shift + right meta + F13 + pm = PedalMapper( + middle_keys=[KeyCombo(mods=[e.KEY_RIGHTMETA], keys=[e.KEY_F13])], + right_keys=[ + KeyCombo(mods=[e.KEY_RIGHTSHIFT, e.KEY_RIGHTMETA], keys=[e.KEY_F13]) + ], + ) + + # Loop to get events and handle them accordingly. + while True: + ev = pm.get_event() + if ev: + pm.handle_key(ev[0], ev[1])