Linux – How to get USB keyboard extra buttons working

keyboardlinuxusb

I have recently bought an USB keyboard. It has 12 extra buttons but only 5 of them are working. There are no "unknown scancode" messages in the logs. Evtest can't detect them it can't even detect the working 5 (only regular keys). Xev detects the working 5 but not the others. "cat /dev/input/by-path/pci-0000:00:02.0-usb-0:4:1.0-event-kbd" is the same as evtest with uglier output. In fact the only way I was able to detect the other 7 keys is USB sniffing with wireshark. So my keyboard is not defective.

I am using Gentoo Linux with gentoo-sources-2.6.30-r4 kernel, xorg-server-1.6.2-r1 and the xf86-input-evdev driver version 1.6.2-r1. Here is the relevant xorg.conf section:

Section "InputDevice"
    Identifier  "Keyboard0"
    Driver      "evdev"    
    Option      "Device"        "/dev/input/by-path/pci-0000:00:02.0-usb-0:4:1.0-event-kbd"
    Option      "XkbLayout"     "hu"                                                       
EndSection

I tried finding out more about options like XkbModel but the man pages are not very helpful. I have searched for every [keyboard] question here but only found something similar on windows.

What can I do to get the keys working? If it is a bug where should I report it?

Update: Here is the showkeys -s output. The X server was not running when I did this.

kb mode was UNICODE
[ if you are trying this under X, it might not work
since the X server is also reading /dev/console ]

press any key (program terminates 10s after last keypress)...
0xe0 0x22
0xe0 0xa2
0xe0 0x24
0xe0 0xa4
0xe0 0x20
0xe0 0xa0
0xe0 0x32
0xe0 0xb2
0xe0 0x6c
0xe0 0xec

I pressed the extra keys from left to right. Each key has 2 lines (press and release I guess) and only the working 5 are detected.

Update: I came up with a really bad way of doing this. I can run tshark (wireshark's command line interface) in the background parsing the output and executing arbitrary programs on the proper USB packets. There is a serious security issue: any user who is allowed to use the extra keys will have the ability to see any USB and network traffic. The only advantage of this approach is that it works. I will post the full program for this after some cleanup.

Best Answer

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:

Normal key press

Extra 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":

alt text

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:

alt text

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.

Related Question