Итак, моя программа работала в течение ночи, и она все еще работает, поэтому я публикую код. Это немного неопрятно, но работает. Я также напишу о том, как я это сделал, потому что это будет полезно для людей с не совсем моей клавиатурой. Программа нуждается в достаточно недавних libpcap и wireshark. Необходимо отладить debugfs (mount -t debugfs none_debugs /sys /kernel /debug) и загрузить модуль usbmon (modprobe -v usbmon).
Это программа, которая работает в фоновом режиме:
#!/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
Как вы можете видеть, существует большой массив команд, индексируемый данными приложения из пакетов USB. Значения являются выданными командами. Я использую DBus, чтобы делать то, что должно быть сделано, но вы можете использовать xvkbd для генерации реальных событий нажатия клавиш (я обнаружил, что xvkbd очень медленный, для отправки простой комбинации клавиш требуются секунды). tshark-wrapper - это простая оболочка вокруг tshark, она запускает tshark от имени пользователя root и отключает stderr.
#!/bin/sh
sudo tshark "$@" 2> /dev/null
Есть проблема. Пользователю необходимо разрешение для запуска tshark от имени пользователя root без пароля. Это действительно очень плохо. Риск может быть уменьшен, если поместить больше оболочек в оболочку и меньше в скрипт Python и позволить пользователям запускать оболочку от имени пользователя root.
Теперь о том, как это сделать с другими клавиатурами. Я почти ничего не знаю о USB, и все же это было не так сложно. Большая часть моего времени была потрачена на выяснение того, как выполнять небуферизованное чтение из канала. Из вывода lsusb я знал, что моя клавиатура находится на втором USB-интерфейсе. Поэтому я начал захватывать с помощью wireshark на usbmon2. Мышь и другое оборудование создают много шума, поэтому отключите их или, по крайней мере, не двигайте мышь.
Первое, что я заметил, было то, что дополнительные ключи имеют идентификатор конечной точки 0x82, а обычные ключи имеют идентификатор конечной точки 0x81. В начале было несколько пакетов с 0x80. Это хорошо, это может быть легко отфильтровано:
usb.endpoint_number == 0x82
Обычное нажатие клавиши:
Дополнительное нажатие клавиши:
Было легко увидеть, что нажатие клавиши генерирует 4 USB-пакета: 2 для нажатия, 2 для отпускания. В каждой паре первый пакет отправлялся с клавиатуры на ПК, а второй - наоборот. Казалось, ACK-ы с TCP. "ACK" был URB-SUBMIT, а обычный пакет был URB-COMPLETE. Поэтому я решил отфильтровать "ACK" и показывать только нормальные пакеты:
usb.urb_type == "C\x01\x82\x03\x02"
USB "ACK":
Теперь было только 2 пакета на нажатие клавиши. Каждая секунда имела нулевое поле значения приложения, а все остальные имели разные значения. Поэтому я отфильтровал нули и использовал другие значения для идентификации ключей.
usb.data != 00:00:00:00
Выпуск дополнительного ключа:
Моя клавиатура - Slimstar 220 (надеюсь, это не квалифицируется как спам, если она удаляется). Если у вас есть шансы того же типа, то немодифицированная программа будет работать. В противном случае, я думаю, что, по крайней мере, ценность приложения будет другой.
Если кому-то захочется написать настоящий драйвер на основе этих данных, пожалуйста, дайте мне знать. Мне не нравится мой уродливый хак.
Обновление: код теперь, надеюсь, защищен от перезагрузки.