Skip to content

Commit

Permalink
Initial commit.
Browse files Browse the repository at this point in the history
  • Loading branch information
RoarkGit committed Oct 26, 2023
0 parents commit 08d0ee4
Show file tree
Hide file tree
Showing 2 changed files with 163 additions and 0 deletions.
64 changes: 64 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -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.
99 changes: 99 additions & 0 deletions pedal_mapper.py
Original file line number Diff line number Diff line change
@@ -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])

0 comments on commit 08d0ee4

Please sign in to comment.