A year ago, I bought a Logitech Wireless Solar Keyboard K750. I'm particularly picky on keyboards, but this one is good. It has an incredible useful feature: while being wireless, it has no need for disposable or rechargeable batteries, it uses solar power!
My problem is that there's obviously no way to know the battery status from Linux, the provided application only working on Windows.
And one dark night, while fragging on QuakeLive, my keyboard stopped working: it had no battery left. This activity being quite energy consuming, it emptied the whole battery.
Someone should write code to get the battery status and light meter from Linux: challenge accepted!
How the keyboard works
This keyboard, like many of the new wireless devices from Logitech, uses the Unifying interface. It's an USB receiver that can be attached up to 6 differents devices (mouse, keyboards…). On old Linux kernel, the Unifying receiver is recognized as only one keyboard and/or one mouse device.
Recently, a driver called hid-logitech-dj has been added to the Linux kernel. With this driver, each device attached to the receiver is recognized as one different device.
What the Logitech application does
The Logitech application under Windows works that way: you launch it, and it displays the battery charge level. On the keyboard, there's a special "light" button (up right). When pressed, a LED will light up on the keyboard: green if the keyboard is receiving enough light and is charging, red if the keyboard does not receive enough light and is therefore discharging. Pushing this same button while the application is running will makes the light meter activated: the application will tell you how much lux your keyboard is receiving.
Let's reverse engineer this
As far as I know, there's nothing in the USB HID protocol that handles this kind of functionality (battery status, light meter…) in a standard way. So the first task to accomplish is, unfortunately, to reverse engineer the program.
I discovered a bit too late that Drew Fisher did a good presentation on USB reverse engineering at 28c3. You might want to take a look at it if you want to reverse engineer on USB. I did not need it, but I learned a few things.
Anyway, my plan was the following: run the Logitech application inside a virtual machine running Windows, give it direct access to the USB keyboard, and sniff what happens on the USB wire.
To achieve that, you need a virtual machine emulator that can do USB pass-through. Both KVM and VirtualBox can do that, but VirtualBox works much better with USB and allow hot(un)plugging of devices, so I used it.
To sniff what happens on the USB, you need to load the usbmon Linux kernel
module. Simply doing
modprobe usbmon will work. You can then
use Wireshark which know how to use usbmon
devices and understand the USB protocol.
USB stuff you need to know
You don't need to know much about USB to understand what I'll write about below, but for the sake of comprehensibility I'll write a couple of things here before jumping in.
To communicate with an USB device, we communicate with one of its endpoints. Endpoints are regrouped into an interface. Interfaces are regrouped into a configuration. A device might contains one or several configurations.
There's also several types of packets in the USB wire protocol, and at least two of them interest us there, they are:
- Interrupt packets, a packet send spontaneously;
- Controls packets, used for command and status operations.
Once everything was set-up, I ran my beloved Wireshark. There's a an URB of type interrupt sent each time you press any key with some data in it. Therefore I advise you to plug another keyboard (or use the laptop keyboard if you're doing this on a laptop), otherwise you'll get crazy trying to sniff the keyboard you're typing on.
At this point, just launching the application does a bunch of USB traffic. Pressing the "light" button on the keyboard makes even more USB packets coming in and out. Here's the interesting packets that I noticed once I excluded the noise:
- When pressing the "light" button, an URB of type interrupt is sent by the keyboard to the computer;
- An URB control packet is sent by the computer to the keyboard in response;
- Regularly URB interrupt packets are sent just after.
With all this, the next step was clear: understand the packets and reproduce that exchange under Linux.
What the packets mean
The "go for the light meter" packet
The packet sent from the computer to the keyboard is the following.
What's here interesting is the last part representing the data. wLength
says that the length of the data is 7 bytes, so let's take a look at those 7
10 01 09 03 78 01 00.
Well, actually, you can't decode them like that, unless you're a freak or a Logitech engineer. And I have actually no idea what they mean. But sending this to the keyboard will trigger an interesting thing: the keyboard will start sending URB interrupt with some data without you pressing any more key.
The "light meter and battery values" packet
This is most interesting packet. This is the one sent by the keyboard to the host and that contains the data we want to retrieve.
This packets come in regularly (1 per second) on the wire for some time once you sent the "go for the light meter" packet. At one point they are emitted less often and do not contain the value for the light meter anymore, suggesting that the control packet sent earlier triggers the activation of the light meter for a defined period.
Now you probably wonder where the data are in this. They're in the 20 bytes
leftover in the capture data part, indicated by Wireshark, at the end of the
11 02 09 10 39 00 0c 06 1d 47 4f 4f 44 00 00 00 00 00 00 00.
Fortunately, it was easy to decode. Knowing we're looking for 2 values (battery charge and light meter), we just need to observe and compare the packet emitted on the wire with the values displayed by the Logitech Solar App.
To achieve this, I looked both at the Logitech Solar App and Wireshark while bringing more and more light near the keyboard, increasing the lux value received by the meter on the Solar App, and saw that the fields represented in blue (see below) where changing in Wireshark. Since 2 bytes were changing, I guessed that it was coded on 16 bits, and therefore it was easy to correlate the value with the Solar App.
In this example, the battery has a charge of
0x39 = 57 % and
the light meter receives
0x0c = 12 lux of light. It's basically
dark, and that makes sense: it was night and the light was off in my office,
the only light being the one coming from my screen.
I've no idea what the
GOOD part of the packet is about, but
it's present in every packet and it's actually very handy to recognize such
a packet. Therefore I'm considering this as some sort of useful mark for
For the other bytes, they were always the same (
0x11 0x2 0x9
0x10 at the beginning, 7 times
0x00 at the end). The 2
bytes between the light meter and GOOD probably mean something, but I've no
idea what for now.
Building our solar app
Now we've enough information to build our own very basic solar application. We know how to triggers the light meter, and we know how to decode the packets.
We're going to write a small application using libusb. Here's a quick example. It's not perfect and does not check for error codes, be careful.
What the program is doing is the following:
- Request for the Unifying Receiver device based on vendor and product ID
- Get the HID interface
- Detach the HID interface from the kernel driver
- Claim the interface
- Send a control packets, were parameters are defined using the same data we captured earlier
- Read interrupt packets coming in until we receive one we recognize (length 20 containing the "GOOD" string)
- Decode the content (battery charge & light meter)
- Release the interface
- Reattach the kernel driver to the interface
This gives the following:
Found keyboard 0x0x24ec8e0 Charge: 64 % Light: 21 lux
To be continued
Unfortunately, this approach has at least one major drawback. We have to disconnect the Logitech Unifying Receiver from the kernel. That means that while we're waiting for the packet, we're dropping packets corresponding to other events from every connected device (key presses, pointer motions…).
In order to solve that, I sent a request for help on the linux-input mailing list. That way, I learned that Logitech is actually using the HID++ protocol to communicate with the devices using the Unifying Receiver. Lars-Dominik Braun managed to get the HID++ specifications from Logitech and published them with their authorization.
This opens a whole new world. With that document, I may be able to understand the part I reverse engineered and convert this to a more useful and generic library using the hidraw interface (so we don't have to disconnect the devices from the kernel driver).