commit 401a65cd32d3876b62f7a1c8f4af265011e9de5a Author: boyska Date: Sun Jul 19 17:28:28 2020 +0200 initial commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..fa18783 --- /dev/null +++ b/README.md @@ -0,0 +1,45 @@ +blututto +=========== + +`blututto` is a simple python script to run commands from generic keypressure. Differently than other (better) +tools like `xchainkeys`, `xbindkeys`, `sxhkd`, etc. It supports specifying the *device*. +This is extremely useful in companion with special remotes like the `AB Shutter3` or probably many others + +Setup +-------- + +I will not digress on how to setup bluetooth, pairing, etc. Bluetooth is a mess, and I'm not the one +understanding it. Try hard, and at some point you will find it + + +Quick run +----------- + +`sudo python3 ev.py -n 'AB Shutter3 Consumer Control' --debug` + +will tell you something. Please note that **not** all the shutters you will find online are programmed to send +the same keycode. So look at the debug messages, and see what happens if you press any key. This is the output +I have: + +``` +INFO:root:Found device: /dev/input/event7 +DEBUG:root:device /dev/input/event7, name "AB Shutter3 Consumer Control", phys "6C:71:D9:57:7F:48" +INFO:root:start loop +DEBUG:root:received key event at 1595169257.015336, 115 (KEY_VOLUMEUP), down +DEBUG:root:received key event at 1595169257.195282, 115 (KEY_VOLUMEUP), up +DEBUG:root:(115, 'short') +``` + +You can see that the shutter I bought sends the key "volume up", whose code is `115`. So that's what you need +to put in your configuration file. + + +Better run +------------- + +You might have noticed that I ran the script with `sudo`. That's not good, and not strictly needed. First +solution: `sudo useradd $USER input` and voilĂ ! But maybe that's still too much... + +better adding a udevrule. Match on +` ATTRS{name}=="AB Shutter3 Consumer Control"` +and apply mode 0640 and set the group to `$(id -gn)` diff --git a/blututto.py b/blututto.py new file mode 100755 index 0000000..9931f55 --- /dev/null +++ b/blututto.py @@ -0,0 +1,168 @@ +#!/usr/bin/env python3 +import json +import logging +import os +import sys +import time +from argparse import ArgumentParser +from subprocess import Popen + +import evdev + +INPUTDEV_BASEDIR = "/dev/input/" + +# TODO: handle sighup +# TODO: prevent event to do other things + + +def listall(): + for f in os.listdir(INPUTDEV_BASEDIR): + try: + print(f, evdev.InputDevice(os.path.join(INPUTDEV_BASEDIR, f))) + except: + pass + + +def find_dev_by_name(name): + for f in os.listdir(INPUTDEV_BASEDIR): + try: + ev = evdev.InputDevice(os.path.join(INPUTDEV_BASEDIR, f)) + except Exception: + continue + if ev.name == name: + return os.path.join(INPUTDEV_BASEDIR, f) + raise ValueError("device with such name not found") + + +class KeymapTable: + def __init__(self): + self.table = {} + + @classmethod + def from_config_file(cls, data): + kt = cls() + kt.table = {int(key): value for key, value in data.items()} + kt.validate() + return kt + + def validate(self): + errors = 0 + for key in self.table: + if type(key) is not int: + logging.error("Keycode %r is not of valid type", key) + errors += 1 + if type(self.table[key]) is str: + # Autofixing key key + self.table[key] = { + "short": self.table[key], + "long": self.table[key], + } + if type(self.table[key]) is not dict: + logging.error("Keycode %r is not of valid type", key) + errors += 1 + if errors: + raise ValueError("%d validation errors found" % errors) + + def __contains__(self, x): + code, pressure_type = x + return code in self.table and pressure_type in self.table[code] + + def __getitem__(self, x): + code, pressure_type = x + return self.table[code][pressure_type] + + +def loop(device, args): + pressed = None + for event in device.read_loop(): + if event.type != evdev.ecodes.EV_KEY: + continue + # for attr in dir(event): + # if not attr.startswith("_"): + # logging.debug(" ", attr, str(getattr(event, attr))) + logging.debug("received %s", str(evdev.categorize(event))) + if event.value == 1: # keydown + pressed = {"code": event.code, "ts": event.timestamp()} + if event.value == 0 and pressed is not None: + pressure_time = event.timestamp() - pressed["ts"] + if pressure_time * 1000 <= args.short_threshold: + pressure_type = "short" + else: + pressure_type = "long" + yield (pressed["code"], pressure_type) + + +def main(): + p = ArgumentParser() + p.add_argument("--config", type=open) + input_p = p.add_argument_group("input device options") + input_p.add_argument("-d", "--device") + input_p.add_argument( + "-n", "--name", help="Look at `xinput list` to have an idea" + ) + input_p.add_argument( + "--short-threshold", + default=300, + help="Threshold to distinguish a short and a long press, " + "in milliseconds", + ) + output_p = p.add_argument_group("output options") + output_p.add_argument( + "-v", + "--verbose", + dest="logging_level", + action="store_const", + const=logging.INFO, + ) + output_p.add_argument( + "--debug", + dest="logging_level", + action="store_const", + const=logging.DEBUG, + ) + args = p.parse_args() + + logging.basicConfig(level=args.logging_level) + if args.config: + config = json.load(args.config) + else: + config = {} + + keymaps = KeymapTable.from_config_file(config.get("keys", {})) + + if args.name: + if args.device: + print("Cannot specify both -d and -n", file=sys.stderr) + sys.exit(1) + while True: + try: + args.device = find_dev_by_name(args.name) + except ValueError: + logging.debug("Could not find device; trying again") + time.sleep(2) + continue + else: + logging.info("Found device: %s", args.device) + break + + device = evdev.InputDevice(args.device) + logging.debug("%s", device) + logging.info("start loop") + for highlevel_event in loop(device, args): + logging.debug(highlevel_event) + if highlevel_event not in keymaps: + continue + + cmd = keymaps[highlevel_event] + if type(cmd) is str: + cmd = cmd.split() + cmd[0] = os.path.expanduser(cmd[0]) + try: + Popen(cmd) + except Exception as exc: + logging.error("Error running %s: %s" % (cmd, str(exc))) + + +if __name__ == "__main__": + # listall() + main() diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..93721f8 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,2 @@ +[tool.black] +line-length=79 diff --git a/sample-config.json b/sample-config.json new file mode 100644 index 0000000..686c4ae --- /dev/null +++ b/sample-config.json @@ -0,0 +1,9 @@ +{ + "keys": { + "115": "~/bin/on/bt-shutter-pressed-short", + "42": { + "short": ["~/bin/on/bt-answer-pressed", "short"], + "long": ["~/bin/on/bt-answer-pressed", "long"] + } + } +}