From 2f9f69ec77313e917debaaa47f04e384b9f4edac Mon Sep 17 00:00:00 2001 From: MasterofJOKers Date: Tue, 6 Sep 2022 20:47:01 +0200 Subject: [PATCH] Add udev-device-plug-handler This command runs in the background and listens for udev bind events, comparing the attributes of those events to known implemented devices. If any match, we call their handle() method. The same handle() method is called on startup if the presence of the device is detected via the check_available() method. Currently implemented devices are my Logitech G930 headset and my TEX Shinobi keyboard. --- udev-device-plug-handler/setup.cfg | 15 +++ udev-device-plug-handler/setup.py | 4 + .../udev-device-plug-handler | 105 ++++++++++++++++++ 3 files changed, 124 insertions(+) create mode 100644 udev-device-plug-handler/setup.cfg create mode 100644 udev-device-plug-handler/setup.py create mode 100644 udev-device-plug-handler/udev-device-plug-handler diff --git a/udev-device-plug-handler/setup.cfg b/udev-device-plug-handler/setup.cfg new file mode 100644 index 0000000..17824c8 --- /dev/null +++ b/udev-device-plug-handler/setup.cfg @@ -0,0 +1,15 @@ +[metadata] +name = udev-device-plug-handler +version = 0.1 +description = Supposed to be background application waiting for devices to change and running scripts for them + +[options] +scripts = udev-device-plug-handler +install_requires = + click + pyudev + +[flake8] +max-line-length = 120 +exclude = .git,__pycache__,*.egg-info,*lib/python* +ignore = E241,E741,W503,W504 diff --git a/udev-device-plug-handler/setup.py b/udev-device-plug-handler/setup.py new file mode 100644 index 0000000..056ba45 --- /dev/null +++ b/udev-device-plug-handler/setup.py @@ -0,0 +1,4 @@ +import setuptools + + +setuptools.setup() diff --git a/udev-device-plug-handler/udev-device-plug-handler b/udev-device-plug-handler/udev-device-plug-handler new file mode 100644 index 0000000..f1a5860 --- /dev/null +++ b/udev-device-plug-handler/udev-device-plug-handler @@ -0,0 +1,105 @@ +#!/usr/bin/env python3 +import subprocess +from typing import Dict, List, Optional, Union + +import click +import pyudev # type: ignore + + +class KnownDevice(): + """Base class for implementing any device""" + # a map of udev-provided attributes and their values this device should match to + UDEV_PROPS: Dict[str, str] = {} + + @staticmethod + def check_available() -> bool: + """Check if that device is connected. + + This method will be run on startup to check if we need to handle the device. + """ + raise NotImplementedError + + @staticmethod + def handle(device: Optional[pyudev.Device] = None) -> None: + """Handle a device being plugged or available + + This method will be called on finding an device to be bound by udev with the `device` argument. It will also be + called on startup without the device argument. + """ + raise NotImplementedError + + +class UsbHidKeyboardMouse(KnownDevice): + """Represent the mechanical thinkpack-like keyboard's trackpoint""" + UDEV_PROPS = {'ID_MODEL': 'USB-HID_Keyboard'} + + @staticmethod + def check_available() -> bool: + cmd = "xinput | grep pointer | grep 'USB-HID Keyboard Mouse'" + cp = subprocess.run(cmd, shell=True, stdout=subprocess.DEVNULL) + return cp.returncode == 0 + + @staticmethod + def handle(device: Optional[pyudev.Device] = None) -> None: + subcmd = "xinput | grep pointer | grep 'USB-HID Keyboard Mouse' | " \ + r"sed -r 's/.*[[:space:]]id=([[:digit:]]+)[[:space:]].*/\1/'" + cmd: Union[str, List[str]] = f"xinput set-prop $({subcmd}) 'libinput Middle Emulation Enabled' 1" + subprocess.run(cmd, shell=True) + cmd = ['xset', 'r', 'rate', '195', '25'] + subprocess.run(cmd) + print("Handled USB-HID_Keyboard") + + +class UsbLogitechG930(KnownDevice): + """Represent the Logitech G930 headset's keys""" + UDEV_PROPS = {'ID_MODEL': 'Logitech_G930_Headset'} + + @staticmethod + def check_available() -> bool: + cmd = "xinput | grep keyboard | grep 'Logitech G930 Headset'" + cp = subprocess.run(cmd, shell=True, stdout=subprocess.DEVNULL) + return cp.returncode == 0 + + @staticmethod + def handle(device: Optional[pyudev.Device] = None) -> None: + cmd = ['/home/joker/scripting/remap_headset_keys.sh'] + subprocess.run(cmd) + print("Handled Logitech G930 Headset") + + +@click.command() +@click.option('--debug', is_flag=True, help='Show all device changes instead of only handled.') +def main(debug: bool) -> None: + # do all actions at startup for devices we might have missed + for known_device in KnownDevice.__subclasses__(): + if not known_device.check_available(): + continue + known_device.handle() + + context = pyudev.Context() + monitor = pyudev.Monitor.from_netlink(context) + monitor.filter_by(subsystem='usb') + monitor.start() + + device: pyudev.Device + for device in iter(monitor.poll, None): + if device.action != 'bind': + continue + + for known_device in KnownDevice.__subclasses__(): + if not all(device.properties.get(prop) == prop_value + for prop, prop_value in known_device.UDEV_PROPS.items()): + continue + known_device.handle(device=device) + break + else: + if debug: + print(dict(device)) + + +if __name__ == '__main__': + import sys + try: + sys.exit(main()) + except KeyboardInterrupt: + pass