initial commit

This commit is contained in:
boyska 2020-07-19 17:28:28 +02:00
commit 401a65cd32
4 changed files with 224 additions and 0 deletions

45
README.md Normal file
View file

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

168
blututto.py Executable file
View file

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

2
pyproject.toml Normal file
View file

@ -0,0 +1,2 @@
[tool.black]
line-length=79

9
sample-config.json Normal file
View file

@ -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"]
}
}
}