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.
This commit is contained in:
MasterofJOKers 2022-09-06 20:47:01 +02:00
parent 7729a8a9db
commit 2f9f69ec77
3 changed files with 124 additions and 0 deletions

View File

@ -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

View File

@ -0,0 +1,4 @@
import setuptools
setuptools.setup()

View File

@ -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