Ok so my program was running for a night and it still works so I post the code. It is a bit fugly but works. I will also write about how I did it because it will be useful for people with not exactly my keyboard. The program needs a recent enough libpcap and wireshark. The debugfs needs to be mounted (mount -t debugfs none_debugs /sys/kernel/debug) and the usbmon module loaded (modprobe -v usbmon).
This is the program that runs in the background:
#!/usr/bin/python
# This program should be run as the logged in user. The user must have
# permissions to execute tshark as root.
from pexpect import spawn
from pexpect import TIMEOUT
from subprocess import PIPE
from subprocess import Popen
# Configuration variables
## Device ID from lsusb output
deviceID = "0458:0708"
## Output filter for tshark
filter = "usb.endpoint_number == 0x82 && usb.data != 00:00:00:00"
## Tshark command to execute
tsharkCmd = "/home/stribika/bin/tshark-wrapper"
## Keypress - command mapping
### Key: USB Application data in hex ":" between bytes "\r\n" at the end.
### Value: The command to execute. See subprocess.Popen.
commands = {
"00:00:20:00\r\n":[
"qdbus", "org.freedesktop.ScreenSaver", "/ScreenSaver",
"org.freedesktop.ScreenSaver.Lock"
],
"00:00:40:00\r\n":[
"qdbus", "org.kde.amarok", "/Player", "org.freedesktop.MediaPlayer.Prev"
],
"00:00:10:00\r\n":[
"qdbus", "org.kde.amarok", "/Player", "org.freedesktop.MediaPlayer.Next"
],
"02:00:00:00\r\n":[
"qdbus", "org.kde.amarok", "/Player", "org.freedesktop.MediaPlayer.Pause"
],
"04:00:00:00\r\n":[
"qdbus", "org.kde.amarok", "/Player", "org.freedesktop.MediaPlayer.Stop"
],
"00:04:00:00\r\n":[
"qdbus", "org.kde.amarok", "/Player", "org.freedesktop.MediaPlayer.Mute"
],
"20:00:00:00\r\n":[
"qdbus", "org.kde.kwin", "/KWin", "org.kde.KWin.setCurrentDesktop", "1"
],
"40:00:00:00\r\n":[
"qdbus", "org.kde.kwin", "/KWin", "org.kde.KWin.setCurrentDesktop", "2"
],
"00:00:80:00\r\n":[
"qdbus", "org.kde.kwin", "/KWin", "org.kde.KWin.setCurrentDesktop", "3"
],
"00:00:00:08\r\n":[
"qdbus", "org.kde.kwin", "/KWin", "org.kde.KWin.setCurrentDesktop", "4"
],
"00:00:00:20\r\n":[
"qdbus", "org.kde.kwin", "/KWin", "org.kde.KWin.setCurrentDesktop", "5"
],
"00:00:00:10\r\n":[
"qdbus", "org.kde.kwin", "/KWin", "org.kde.KWin.setCurrentDesktop", "6"
],
}
# USB interface names change across reboots. This determines what is the correct
# interface called this week. If this turns out to be the case with endpoint
# numbers lsusb can tell that too.
lsusbCmd = [ "lsusb", "-d", deviceID ]
sedCmd = [
"sed", "-r",
"s/^Bus ([0-9]{3}) Device [0-9]{3}: ID " + deviceID + ".*$/\\1/;s/^0+/usbmon/"
]
lsusb = Popen(lsusbCmd, stdin = PIPE, stdout = PIPE, stderr = PIPE)
sed = Popen(sedCmd, stdin = lsusb.stdout, stdout = PIPE, stderr = PIPE)
usbIface = sed.stdout.readline().rstrip()
# Arguments for Tshark
## -i is the interface (usbmon[0-9]+)
## -R is the output filter
tsharkArgs = [
"-T", "fields", "-e", "usb.data",
"-i", usbIface,
"-R", filter
]
# Start capturing
## pexpect is needed to disable buffering. (Nothing else actally disables it
## don't belive the lies about Popen's bufsize=0)
tshark = spawn(tsharkCmd, tsharkArgs, timeout = 3600)
line = "----"
# Read keypresses while tshark is running and execute the proper command.
while line != "":
try:
line = tshark.readline()
Popen(commands[line], stdin = PIPE, stdout = PIPE, stderr = PIPE)
# We do not care about timeout.
except TIMEOUT:
pass
As you can see there is a big commands array indexed with the application data from the USB packets. The values are the issued commands. I am using DBus to do what needs to be done but you can use xvkbd to generate real key press events (I found xvkbd very slow it takes seconds to send a simple key combination). tshark-wrapper is a simple wrapper around tshark it executes tshark as root and disables stderr.
#!/bin/sh
sudo tshark "$@" 2> /dev/null
There is the problem. The user needs permission to execute tshark as root without password. This is really really bad thing. The risk could be reduced by putting more aruments in the wrapper and less in the python script and allowing users to execute the wrapper as root.
Now about the process of doing this with other keyboards. I know almost nothing about USB and still it was not that hard. Most of my time was spent figuring out how to do unbuffered read from a pipe. I knew from lsusb output that my keyboard is on the 2nd USB interface. So I started capturing with wireshark on usbmon2. The mouse and other hardwares generate a lot of noise so unplug them or at least do not move the mouse.
The first thing I noticed was that the extra keys have endpoint ID 0x82 and the normal keys have endpoint ID 0x81. There were some packets at the beginning with 0x80. This is good it can be easily filtered:
usb.endpoint_number == 0x82
Normal key press:
Extra key press:
It was easy to see that a key press generates 4 USB packets: 2 for press, 2 for release. In every pair the first packet was sent by the keyboard to the PC and the second was the other way around. It seemed like ACK-s with TCP. The "ACK" was URB-SUBMIT and the normal packet was URB-COMPLETE type. So I decided to filter the "ACK"s and only show normal packets:
usb.urb_type == "C\x01\x82\x03\x02"
USB "ACK":
Now there were only 2 packets per key press. Every second had zero application value field and everything else had different values. So I filtered the zeroes and used the other values to identify keys.
usb.data != 00:00:00:00
Extra key release:
My keyboard is a Slimstar 220 (I hope this does not qualify as spam If it does I remove it.) If you have the same type chances are the unmodified program will work. Otherwise I think at least the application value stuff will be different.
If anyone feels like writing a real driver based on this data please let me know. I don't like my ugly hack.
Update: The code is now hopefully reboot-proof.
Best Answer
The answer is:
Explanition
With the tree command I have found this
Ok, so
/dev/input/event3
is my keyboard.The
od
command dumps files in octal and other formats.-x
option it dumps hexadecimal.--width=144
option it dumps only one line per keypress (one line is 144 Bytes long).--read-bytes=144
quitsod
after 144 Bytes.The
awk
command prints the 12th field out of the whole line. That only, if the number of fieldsNF
is greater then 1, because every second line is just a line break.The
while true
loop around the whole thing is because if I type some letter keys it breaks. I get no more results, only0000
. But theod
command quits reading after 144 Bytes (one key press). After that it is restarted. There is surely a better fix for that, but this is a good workaround.Example output (I pressed a few times Return, RightCtrl and Backspace, which gives me the correct numbers when comparing to this document from microsoft (PDF) or this text file document)