initial commit
This commit is contained in:
commit
401a65cd32
4 changed files with 224 additions and 0 deletions
45
README.md
Normal file
45
README.md
Normal 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
168
blututto.py
Executable 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
2
pyproject.toml
Normal file
|
@ -0,0 +1,2 @@
|
|||
[tool.black]
|
||||
line-length=79
|
9
sample-config.json
Normal file
9
sample-config.json
Normal 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"]
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue