Fiddling with a CNC machine or a 3d printer requires sending arbitrary commands. Setting up a workpiece, adjusting the head, running calibration or touch probe, all gets rather annoying when it involves a disco dance between the machine and the laptop. Dedicated terminal is not always worth the cost.
A regular USB keyboard gets attached to the machine. A python script (what else these days) is written to take the HID events from selected devices, track key press/release events to facilitate key combinations, and measure time from key press to release to differentiate between short, long, and even longer keypresses.
The HID-class devices, or human-interface devices, are standardized devices providing events from the user interface. An example of an event is a key press, key repeat or key release, a mouse button press or release, a touchscreen touch, a mouse move, or a joystick move.
Such input device manifests in the linux system as a /dev/input/eventX (or similar) device node. Events (small binary data chunks) and device descriptors (giving meaning to the binary mess) can be acquired from the device.
Many devices have multiple sub-devices in them. A keyboard controller can easily become four /dev/input devices, one for regular keypresses, one for multimedia keys (consumer control), one for power/sleep buttons (system control), and one for mouse events. Often the full set of supported functions claimed is not realized in hardware, as the same controller may be missing keys or other peripherals. Sometimes these can be hacked into the hardware.
The keys have assigned numbers, paired to names. Partial example:
('KEY_ESC', 1) ('KEY_1', 2) ('KEY_2', 3) ('KEY_3', 4) ('KEY_4', 5) ('KEY_5', 6) ...
('KEY_J', 36) ('KEY_K', 37) ('KEY_L', 38) ('KEY_SEMICOLON', 39) ('KEY_APOSTROPHE', 40) ('KEY_GRAVE', 41) ('KEY_LEFTSHIFT', 42) ('KEY_BACKSLASH', 43) ...
('KEY_CHANNELUP', 402) ('KEY_CHANNELDOWN', 403) ('KEY_LAST', 405) ('KEY_NEXT', 407) ('KEY_RESTART', 408)
On the raw events level, there is no difference between keys. A key "A" is the same KEY_A whether pressed together with other keys or alone. The modifier keys like ctrl or shift or so have no special meaning here, that is handled later downstream in the processing.
The keypress events are of three types:
The python-evdev library is chosen as the core process.
The evdev library provides three pieces of information for each device, via evdev.list_devices():
Multiple devices can be opened at once:
Examples:
Raspberry Pi 3B with a single USB keyboard
path: /dev/input/event3 addr: usb-3f980000.usb-1.4/input1 name: NOVATEK USB Keyboard path: /dev/input/event2 addr: usb-3f980000.usb-1.4/input1 name: NOVATEK USB Keyboard Consumer Control path: /dev/input/event1 addr: usb-3f980000.usb-1.4/input1 name: NOVATEK USB Keyboard System Control path: /dev/input/event0 addr: usb-3f980000.usb-1.4/input0 name: NOVATEK USB Keyboard
Raspberry Pi 2B with a wireless 2.4GHz keyboard dongle, a Logitech wireless dongle, and a touchscreen
path: /dev/input/event5 phys: usb-3f980000.usb-1.3.4/input1 name: 2.4G Composite Devic System Control path: /dev/input/event4 phys: usb-3f980000.usb-1.3.4/input1 name: 2.4G Composite Devic Consumer Control path: /dev/input/event3 phys: usb-3f980000.usb-1.3.4/input1 name: 2.4G Composite Devic Mouse path: /dev/input/event2 phys: usb-3f980000.usb-1.3.4/input0 name: 2.4G Composite Devic path: /dev/input/event1 phys: usb-3f980000.usb-1.3.3/input2:1 name: Logitech K230 path: /dev/input/event0 phys: usb-3f980000.usb-1.3.1/input0 name: 深圳市全动电子技术有限公司 ByQDtech 触控USB鼠标
a give-or-take standard PC with a keyboard and some internal peripherals
path: /dev/input/event5 addr: ALSA name: HDA Intel PCH Headphone path: /dev/input/event4 addr: eeepc-wmi/input0 name: Eee PC WMI hotkeys path: /dev/input/event3 addr: isa0061/input0 name: PC Speaker path: /dev/input/event2 addr: LNXPWRBN/button/input0 name: Power Button path: /dev/input/event1 addr: PNP0C0C/button/input0 name: Power Button path: /dev/input/event0 addr: isa0060/serio0/input0 name: AT Translated Set 2 keyboard
The raspberry pi boards have a single USB controller, with usb-3f980000.usb-1. as the constant part, followed by dot-separated number of port (one number on the raspi port itself, then additional ones for the eventual chain of hubs). This allows addressing of devices via physical port location; different keyboards can be plugged into the same port and still operate properly.
When raspi board is detected, the constant part is set to the phys prefix, so devices can be addressed as "4" (for the usb-1.4 ones in the 3B example), or "3.4", "3.3" or "3.1" for the ones from the 2B example. The part after the end slash is ignored; eg. the 3B example opens both input0 and input1 devices, or, all four /dev/input/eventX ones.
The above examples are obtained by listing the available HID devices.
It is also possible to list the available events, reported by the HID descriptor:
For tracing the real appearance of events, an option is provided that skips loading of the execute commands, and instead just shows the event name and the device originating it.
To do something useful, the keypress event has to not be just recognized, but also followed up with an action. This is achieved by pairing a command to execute.
Combo keys can be useful. Different functionalities can be assigned to combination of SHIFT-key, CTRL-key, ALT-SHIFT-key, or even CAPS-WIN-CTRL-key. Caps Lock, otherwise pretty useless, can be used as another modifier key, eg. for various macros.
The kbdassist code handles the modifier keys by adding them into a set-type variable when pressed, removed when released.
When a key is pressed, a timestamp is remembered in order to measure the keypress length. When the key is released, a handlekey() function is invoked, with the keypress length, and the list of pressed and not-yet-released keys. Presence of modifier keys can be checked in the list, and the active ones are prepended to the key name as prefixes.
The main key name gets the KEY_ prefix stripped, to simplify the naming.
The modifier keys are as follows:
The order of the keys is fixed. All of them together with KEY_X result in CAPS_WIN_ALT_CTRL_SHIFT_X.
The key modifiers are usual but can be done as arbitrary. For use of a numeric keypad, the +,-,*,/,Enter and other keys can be assigned as modifiers, whether by separate names or by adding them to definitions of existing ones. MOD: and MOD_CLEAR can be used for modifying the default table. The -C option lists, inter alia, the configured modifiers and their prefix order.
On release of a modifier key, the keypress handler is invoked on it too. Just do not assign anything to execute with these alone.
The code recognizes three brackets of keypress durations; normal, long, verylong. The length is added to the key event name as another prefix, the very first from the left:
A long ctrl-X event will therefore be LONG_CTRL_X. A very long press of key 'C' will be VLONG_C.
Unless strict timing is requested (-T option), event not defined for a longer keypress will be attempted with shorter definition; VLONG_C if not found will be tried as LONG_C and then as C.
In that case, action defined for C will be executed for LONG_C and VLONG_C as well. Action defined for LONG_D will be executed also with VLONG_D, but ignored by event D.
KEY: UP | device /dev/input/event2, name "2.4G Composite Devic", phys "usb-3f980000.usb-1.3.4/input0" KEY: ENTER | device /dev/input/event2, name "2.4G Composite Devic", phys "usb-3f980000.usb-1.3.4/input0" KEY: DOWN | device /dev/input/event2, name "2.4G Composite Devic", phys "usb-3f980000.usb-1.3.4/input0" KEY: SEARCH | device /dev/input/event4, name "2.4G Composite Devic Consumer Control", phys "usb-3f980000.usb-1.3.4/input1" KEY: HOMEPAGE | device /dev/input/event4, name "2.4G Composite Devic Consumer Control", phys "usb-3f980000.usb-1.3.4/input1" KEY: BTN_LEFT | device /dev/input/event3, name "2.4G Composite Devic Mouse", phys "usb-3f980000.usb-1.3.4/input1" KEY: BTN_RIGHT | device /dev/input/event3, name "2.4G Composite Devic Mouse", phys "usb-3f980000.usb-1.3.4/input1" KEY: LONG_BTN_RIGHT | device /dev/input/event3, name "2.4G Composite Devic Mouse", phys "usb-3f980000.usb-1.3.4/input1" KEY: VLONG_BTN_RIGHT | device /dev/input/event3, name "2.4G Composite Devic Mouse", phys "usb-3f980000.usb-1.3.4/input1" KEY: W | device /dev/input/event2, name "2.4G Composite Devic", phys "usb-3f980000.usb-1.3.4/input0" KEY: E | device /dev/input/event2, name "2.4G Composite Devic", phys "usb-3f980000.usb-1.3.4/input0" KEY: R | device /dev/input/event2, name "2.4G Composite Devic", phys "usb-3f980000.usb-1.3.4/input0" KEY: SHIFT_W {'KEY_W', 'KEY_LEFTSHIFT'} | device /dev/input/event2, name "2.4G Composite Devic", phys "usb-3f980000.usb-1.3.4/input0" KEY: SHIFT_E {'KEY_E', 'KEY_W', 'KEY_LEFTSHIFT'} | device /dev/input/event2, name "2.4G Composite Devic", phys "usb-3f980000.usb-1.3.4/input0" KEY: SHIFT_R {'KEY_E', 'KEY_R', 'KEY_W', 'KEY_LEFTSHIFT'} | device /dev/input/event2, name "2.4G Composite Devic", phys "usb-3f980000.usb-1.3.4/input0" KEY: LONG_LEFTSHIFT | device /dev/input/event2, name "2.4G Composite Devic", phys "usb-3f980000.usb-1.3.4/input0" KEY: CAPS_W {'KEY_CAPSLOCK', 'KEY_W'} | device /dev/input/event2, name "2.4G Composite Devic", phys "usb-3f980000.usb-1.3.4/input0" KEY: CAPS_E {'KEY_CAPSLOCK', 'KEY_E', 'KEY_W'} | device /dev/input/event2, name "2.4G Composite Devic", phys "usb-3f980000.usb-1.3.4/input0" KEY: LONG_CAPSLOCK | device /dev/input/event2, name "2.4G Composite Devic", phys "usb-3f980000.usb-1.3.4/input0" KEY: VLONG_CAPS_SHIFT_LEFTSHIFT {'KEY_CAPSLOCK', 'KEY_LEFTSHIFT'} | device /dev/input/event2, name "2.4G Composite Devic", phys "usb-3f980000.usb-1.3.4/input0" KEY: VLONG_CAPSLOCK | device /dev/input/event2, name "2.4G Composite Devic", phys "usb-3f980000.usb-1.3.4/input0" KEY: SHIFT_G {'KEY_G', 'KEY_LEFTSHIFT'} | device /dev/input/event2, name "2.4G Composite Devic", phys "usb-3f980000.usb-1.3.4/input0" KEY: LONG_LEFTSHIFT | device /dev/input/event2, name "2.4G Composite Devic", phys "usb-3f980000.usb-1.3.4/input0" KEY: CAPS_G {'KEY_CAPSLOCK', 'KEY_G'} | device /dev/input/event2, name "2.4G Composite Devic", phys "usb-3f980000.usb-1.3.4/input0" KEY: LONG_CAPSLOCK | device /dev/input/event2, name "2.4G Composite Devic", phys "usb-3f980000.usb-1.3.4/input0" KEY: CTRL_SHIFT_G {'KEY_LEFTCTRL', 'KEY_G', 'KEY_LEFTSHIFT'} | device /dev/input/event2, name "2.4G Composite Devic", phys "usb-3f980000.usb-1.3.4/input0" KEY: CAPS_SHIFT_Y {'KEY_Y', 'KEY_CAPSLOCK', 'KEY_T', 'KEY_LEFTSHIFT'} | device /dev/input/event2, name "2.4G Composite Devic", phys "usb-3f980000.usb-1.3.4/input0" KEY: VLONG_CAPS_SHIFT_LEFTSHIFT {'KEY_Y', 'KEY_CAPSLOCK', 'KEY_T', 'KEY_LEFTSHIFT'} | device /dev/input/event2, name "2.4G Composite Devic", phys "usb-3f980000.usb-1.3.4/input0" KEY: VLONG_CAPSLOCK | device /dev/input/event2, name "2.4G Composite Devic", phys "usb-3f980000.usb-1.3.4/input0" KEY: CTRL_C {'KEY_C', 'KEY_LEFTCTRL'} | device /dev/input/event2, name "2.4G Composite Devic", phys "usb-3f980000.usb-1.3.4/input0" Ctrl-C, exitingBeware, some more unusual key combinations may not be handled by the scanning matrix of a given keyboard device. Test, test, test!
This is what the -E (and -M) option is for.
obtained via -F -A (full list, all devices)
---- device /dev/input/event2, name "2.4G Composite Devic", phys "usb-3f980000.usb-1.5.2/input0" type 4, code 4, val 0x0007001b | event at 1669771513.161219, code 04, type 04, val 458779, MSC_SCAN type 1, code 45, val 1 | key event at 1669771513.161219, 45 (KEY_X), down type 0, code 0, val 0 | synchronization event at 1669771513.161219, SYN_REPORT ---- device /dev/input/event2, name "2.4G Composite Devic", phys "usb-3f980000.usb-1.5.2/input0" type 1, code 45, val 2 | key event at 1669771513.413038, 45 (KEY_X), hold type 0, code 0, val 1 | synchronization event at 1669771513.413038, SYN_REPORT ---- device /dev/input/event2, name "2.4G Composite Devic", phys "usb-3f980000.usb-1.5.2/input0" type 1, code 45, val 2 | key event at 1669771513.863037, 45 (KEY_X), hold type 0, code 0, val 1 | synchronization event at 1669771513.863037, SYN_REPORT type 4, code 4, val 0x0007001b | event at 1669771513.863037, code 04, type 04, val 458779, MSC_SCAN type 1, code 45, val 0 | key event at 1669771513.863037, 45 (KEY_X), up type 0, code 0, val 0 | synchronization event at 1669771513.863037, SYN_REPORT ---- device /dev/input/event3, name "2.4G Composite Devic Mouse", phys "usb-3f980000.usb-1.5.2/input1" type 2, code 1, val 3 | relative axis event at 1669771593.527032, REL_Y type 0, code 0, val 0 | synchronization event at 1669771593.527032, SYN_REPORT ---- device /dev/input/event3, name "2.4G Composite Devic Mouse", phys "usb-3f980000.usb-1.5.2/input1" type 2, code 0, val -1 | relative axis event at 1669771593.544034, REL_X type 2, code 1, val 2 | relative axis event at 1669771593.544034, REL_Y type 0, code 0, val 0 | synchronization event at 1669771593.544034, SYN_REPORT ---- device /dev/input/event3, name "2.4G Composite Devic Mouse", phys "usb-3f980000.usb-1.5.2/input1" type 4, code 4, val 0x00090001 | event at 1669771602.661007, code 04, type 04, val 589825, MSC_SCAN type 1, code 272, val 1 | key event at 1669771602.661007, 272 (['BTN_LEFT', 'BTN_MOUSE']), down type 0, code 0, val 0 | synchronization event at 1669771602.661007, SYN_REPORT ---- device /dev/input/event3, name "2.4G Composite Devic Mouse", phys "usb-3f980000.usb-1.5.2/input1" type 4, code 4, val 0x00090001 | event at 1669771602.806020, code 04, type 04, val 589825, MSC_SCAN type 1, code 272, val 0 | key event at 1669771602.806020, 272 (['BTN_LEFT', 'BTN_MOUSE']), up type 0, code 0, val 0 | synchronization event at 1669771602.806020, SYN_REPORT ---- device /dev/input/event0, name "深圳市全动电子技术有限公司 ByQDtech 触控USB鼠标", phys "usb-3f980000.usb-1.3.1/input0" type 3, code 47, val 2 | absolute axis event at 1669771626.693009, ABS_MT_SLOT type 3, code 57, val 59 | absolute axis event at 1669771626.693009, ABS_MT_TRACKING_ID type 3, code 53, val 638 | absolute axis event at 1669771626.693009, ABS_MT_POSITION_X type 3, code 54, val 388 | absolute axis event at 1669771626.693009, ABS_MT_POSITION_Y type 3, code 58, val 24 | absolute axis event at 1669771626.693009, ABS_MT_PRESSURE type 1, code 330, val 1 | key event at 1669771626.693009, 330 (BTN_TOUCH), down type 3, code 0, val 638 | absolute axis event at 1669771626.693009, ABS_X type 3, code 1, val 388 | absolute axis event at 1669771626.693009, ABS_Y type 3, code 24, val 24 | absolute axis event at 1669771626.693009, ABS_PRESSURE type 4, code 5, val 0x00000000 | event at 1669771626.693009, code 05, type 04, val 00, MSC_TIMESTAMP type 0, code 0, val 0 | synchronization event at 1669771626.693009, SYN_REPORT ---- device /dev/input/event0, name "深圳市全动电子技术有限公司 ByQDtech 触控USB鼠标", phys "usb-3f980000.usb-1.3.1/input0" type 4, code 5, val 0x00827800 | event at 1669771626.704959, code 05, type 04, val 8550400, MSC_TIMESTAMP type 0, code 0, val 0 | synchronization event at 1669771626.704959, SYN_REPORT ---- device /dev/input/event0, name "深圳市全动电子技术有限公司 ByQDtech 触控USB鼠标", phys "usb-3f980000.usb-1.3.1/input0" type 4, code 5, val 0x00ff779c | event at 1669771626.715957, code 05, type 04, val 16742300, MSC_TIMESTAMP type 0, code 0, val 0 | synchronization event at 1669771626.715957, SYN_REPORT ---- device /dev/input/event0, name "深圳市全动电子技术有限公司 ByQDtech 触控USB鼠标", phys "usb-3f980000.usb-1.3.1/input0" type 4, code 5, val 0x018ffe70 | event at 1669771626.734969, code 05, type 04, val 26214000, MSC_TIMESTAMP type 0, code 0, val 0 | synchronization event at 1669771626.734969, SYN_REPORT
Use -q to silence the SYN_REPORT events.
Values for type 0 (SYN), 1 (KEY, keypress), 2 (REL, relative pointer move) and 3 (ABS, absolute pointer coordinates) are shown as decimal.
Values for type 4 (MSC, misc), 5 (SW, switch) and greater are shown as hexadecimal, as that's easier to look-and-see decode the position-encoded and binary values. MSC_ descriptions are hardcoded, evdev did not decode, may improve in later versions.
Ctrl-C is detected via scancodes, to facilitate termination of the process in order to avoid locking the hapless admin out of the terminal. (Other remedy, connect in via SSH or so and kill the process. Or reboot the device.)
The actions are specified in a config file. By default, it can be in ~/.kbdassist.cfg, or /etc/kbdassist.cfg.
The actions are apecified as pairs of key name, '=', and a command.
For control of 3d printers and other Octoprint-based devices, sw_8control is leveraged.
Example:
# cancel print VLONG_C = 8cancel LONG_C = 8cancel CAPS_C = 8cancel CTRL_C = 8cancel # start print VLONG_CAPS_P = 8print # pause/resume P = 8pause VLONG_P = 8resume # move and calibrate # calibrate square, goto center VLONG_CAPS_C = 8gmulti g29 g0x0y0 # zero to bed VLONG_CAPS_Z = 8gmulti g30 g92z0 g0z10f2000 CAPS_Z = 8gmulti g30 g92z0 g0z10f2000 # home VLONG_H = 8home
For moving the head, many repeated commands with minor variants have to be used. To avoid spamming the config, two separate tables are used:
Example:
# JOGPREF: prefix = distance JOGPREF: CTRL_ = 0.1 JOGPREF: = 1 JOGPREF: LONG_ = 5 JOGPREF: SHIFT_ = 10 JOGPREF: VLONG_ = 20 JOGPREF: CAPS_ = 50 # JOGKEY: key = command, # substituted for distance JOGKEY: LEFT = 8jog # -0 -0 JOGKEY: RIGHT = 8jog -# -0 -0 JOGKEY: UP = 8jog -0 -# -0 JOGKEY: DOWN = 8jog -0 # -0 JOGKEY: PAGEUP = 8jog -# JOGKEY: HOME = 8jog -# JOGKEY: PAGEDOWN = 8jog # JOGKEY: END = 8jog #
This generates the head movement commands for six distances from 0.1 to 50 mm per step, and for all three axes (with Z axis having both pgup/pgdn and home/end keys as synonymous, to allow for different keyboard layouts). Much less annoying to write 6+8 lines than to explicitly handle 6*8=48 ones.
For specifying different keypress lengths, the defaults can be overriden by the config file:
# VLONG, LONG: time in milliseconds VLONG: 1000 LONG: 300
For modifying the default modkey prefixes, use directives MOD_CLEAR (for clearing the default mod table if needed), and MOD: for setting the key/prefix pair:
MOD_CLEAR MOD: KEY_TAB = TAB MOD: KEY_RIGHTSHIFT = RSHIFTBeware of the order. The prefixes, as defined, are added right to left. The keys are stored in a dict, so overwriting an existing key will keep the original dict order and may be confusing.
A selection of device by name can be done in the config file as well:
# fraction of device name, to match DEVNAME:NOVATEKThis can be overriden by the commandline options.
For defining the key combination to exit the process, when running from the console:
# key combination for process exiting, empty to disable, make sure the combo is valid! EXITPROCESS: ALT_CTRL_SHIFT_C
Normally, for a notch better performance, subprocess.Popen() call is used. This needs the command name and arguments specified as a list. This is parsed from the configfile string via shlex.split() call.
Some commands have to be invoked via /bin/sh shell. When the command contains pipe, redirect, or semicolon, the os.system() call is used instead.
The command is run as a parallel process, non-blocking.
usage: kbdassist.py [-h] [-d DEVICE] [-dn DEVNAME] [-da DEVADDR] [-dp DEVPREFIX] [-A] [-l] [-L] [-E] [-M] [-C] [-q] [-v] [-D] [-T] [-c CONFIG] [--printdefaultconfig] [-F] executes commands on keypresses and their combinations optional arguments: -h, --help show this help message and exit -d DEVICE, --device DEVICE /dev/input device -dn DEVNAME, --devname DEVNAME fraction of device name, default="" -da DEVADDR, --devaddr DEVADDR device phys address or suffix (eg. 4, 2.1, 3.2.3), match to last slash -dp DEVPREFIX, --devprefix DEVPREFIX device address prefix, default="" -A, --matchall match all devices -l, --list list HID devices present -L, --listevents list events for matching device(s) -E, --showevents show detected event names and source devices, ignore config -M, --forcemods like -E, but force reading of config/modifier -C, --listcommands list set commands -q, --quiet suppress most output -v, --verbose print more details -D, --dryrun show command instead of executing -T, --stricttime do not try VLONG-LONG-normal if longer command not found -c CONFIG, --config CONFIG keys config file, default=['~/.kbdassist.cfg', '~/kbdassist.cfg', './kbdassist.cfg', '/etc/kbdassist.cfg'] --printdefaultconfig print default config file content -F, --showallevents show ALL detected events (not just keypresses), ignore config; -q to suppress SYN_REPORT
Use -v to see what the program is doing, what files are being looked for where, etc.; can save a headscratcher of debugging.