Logitech K750 article
authorJulien Danjou <julien@danjou.info>
Thu, 28 Jun 2012 15:22:10 +0000 (17:22 +0200)
committerJulien Danjou <julien@danjou.info>
Mon, 9 Jul 2012 16:27:34 +0000 (18:27 +0200)
Signed-off-by: Julien Danjou <julien@danjou.info>

content/blog/2012/logitech-k750-linux-support.html [new file with mode: 0644]
content/media/images/logitech-solar-app.png [new file with mode: 0644]
content/media/images/logitech-unifying.jpg [new file with mode: 0644]
content/media/images/logitech-wireless-solar-keyboard-k750.png [new file with mode: 0644]

diff --git a/content/blog/2012/logitech-k750-linux-support.html b/content/blog/2012/logitech-k750-linux-support.html
new file mode 100644 (file)
index 0000000..bf658b1
--- /dev/null
@@ -0,0 +1,407 @@
+---
+title: Logitech K750 keyboard and Unifying Receiver Linux support
+created: !!timestamp '2012-07-09 17:48:00'
+tags:
+    - linux
+    - usb
+    - logitech
+---
+
+{% mark excerpt %}
+
+A year ago, I bought a [Logitech Wireless Solar Keyboard
+K750](http://www.logitech.com/keyboards/keyboards/k750-keyboard). 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!
+
+![Logitech Wireless Solar Keyboard K750](/media/images/logitech-wireless-solar-keyboard-k750.png)
+
+My problem is that there's obviously no way to know the battery status from
+Linux, the provided application only working on Windows.
+
+{% endmark %}
+
+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
+
+<span class="pull-right">
+![Logitech Unifying Receiver](/media/images/logitech-unifying.jpg)
+</span>
+
+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
+
+<span class="pull-left">
+![Logitech Solar App](/media/images/logitech-solar-app.png)
+</span>
+
+The Logitech application under Windows works that way: you launch it, and it
+displays you 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](http://en.wikipedia.org/wiki/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](http://www.youtube.com/watch?v=jMf55KVDPaE).
+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](http://www.linux-kvm.org/page/Main_Page) and
+[VirtualBox](https://www.virtualbox.org/) 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 <code>modprobe usbmon</code> will work. You can then
+use [Wireshark](http://www.wireshark.org/) 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.
+
+All of this and more is well (and better) explained in the [chapter
+13](http://lwn.net/images/pdf/LDD3/ch13.pdf) of [Linux Device Drivers, Third
+Edition](http://lwn.net/Kernel/LDD3/).
+
+## Sniffed data
+
+Once everything was set-up, I ran my beloved Wireshark. There's a
+an <abbr title="USB Request Block">URB</abbr> 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.
+
+<figure>
+<pre>
+Frame 17: 71 bytes on wire (568 bits), 71 bytes captured (568 bits)
+    Frame Length: 71 bytes (568 bits)
+    Capture Length: 71 bytes (568 bits)
+USB URB
+    URB id: 0xffff880161997240
+    URB type: URB_SUBMIT ('S')
+    URB transfer type: URB_CONTROL (0x02)
+    Endpoint: 0x00, Direction: OUT
+        0... .... = Direction: OUT (0)
+        .000 0000 = Endpoint value: 0
+    Device: 6
+    URB bus id: 1
+    Device setup request: relevant (0)
+    Data: present (0)
+    URB sec: 1340124450
+    URB usec: 495643
+    URB status: Operation now in progress (-EINPROGRESS) (-115)
+    URB length [bytes]: 7
+    Data length [bytes]: 7
+    [Response in: 18]
+    [bInterfaceClass: HID (0x03)]
+    URB setup
+        bmRequestType: 0x21
+            0... .... = Direction: Host-to-device
+            .01. .... = Type: Class (0x01)
+            ...0 0001 = Recipient: Interface (0x01)
+    bRequest: SET_REPORT (0x09)
+    wValue: 0x0210
+        ReportID: 16
+        ReportType: Output (2)
+    wIndex: 2
+    wLength: 7
+0000  40 72 99 61 01 88 ff ff 53 02 00 06 01 00 00 00   @r.a....S.......
+0010  22 ad e0 4f 00 00 00 00 1b 90 07 00 8d ff ff ff   "..O............
+0020  07 00 00 00 07 00 00 00 21 09 10 02 02 00 07 00   ........!.......
+0030  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00   ................
+0040  10 01 09 03 78 01 00                              ....x..
+</pre>
+</figure>
+
+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
+bytes: <code>10 01 09 03 78 01 00</code>.
+
+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.
+
+<figure>
+<pre>
+Frame 1467: 84 bytes on wire (672 bits), 84 bytes captured (672 bits)
+    Frame Length: 84 bytes (672 bits)
+    Capture Length: 84 bytes (672 bits)
+USB URB
+    URB id: 0xffff88010c43c380
+    URB type: URB_COMPLETE ('C')
+    URB transfer type: URB_INTERRUPT (0x01)
+    Endpoint: 0x83, Direction: IN
+        1... .... = Direction: IN (1)
+        .000 0011 = Endpoint value: 3
+    Device: 2
+    URB bus id: 6
+    Device setup request: not relevant ('-')
+    Data: present (0)
+    URB sec: 1334953309
+    URB usec: 728740
+    URB status: Success (0)
+    URB length [bytes]: 20
+    Data length [bytes]: 20
+    [Request in: 1466]
+    [Time from request: 0.992374000 seconds]
+    [bInterfaceClass: Unknown (0xffff)]
+Leftover Capture Data: 1102091039000c061d474f4f4400000000000000
+
+0000  80 c3 43 0c 01 88 ff ff 43 01 83 02 06 00 2d 00   ..C.....C.....-.
+0010  5d c5 91 4f 00 00 00 00 a4 1e 0b 00 00 00 00 00   ]..O............
+0020  14 00 00 00 14 00 00 00 00 00 00 00 00 00 00 00   ................
+0030  02 00 00 00 00 00 00 00 00 02 00 00 00 00 00 00   ................
+0040  11 02 09 10 39 00 0c 06 1d 47 4f 4f 44 00 00 00   ....9....GOOD...
+0050  00 00 00 00                                       ....
+
+</pre>
+</figure>
+
+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
+packet: <code>11 02 09 10 39 00 0c 06 1d 47 4f 4f 44 00 00 00 00 00 00 00</code>.
+
+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.
+
+<figure>
+<pre>
+[ ....<span style="color:red;">9</span><span style="color: blue;">..</span>..<span style="color: green;">GOOD</span>....... ]
+11 02 09 10 <span style="color: red;">39</span> <span style="color: blue;">00 0c</span> 06 1d <span style="color: green;">47 4f 4f 44</span> 00 00 00 00 00 00 00
+4 bytes - <span style="color: red">1 byte for battery charge</span> - <span style="color: blue;">2 bytes for light meter</span> - 2 bytes - <span style="color: green">4 bytes for GOOD</span> - 7 bytes
+</pre>
+</figure>
+
+In this example, the battery has a charge of <code>0x39 = 57 %</code> and
+the light meter receives <code>0x0c = 12 lux</code> 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 <code>GOOD</code> 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
+now.
+
+For the other bytes, they were always the same (<code>0x11 0x2 0x9
+0x10</code> at the beginning, 7 times <code>0x00</code> 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](http://www.libusb.org/). Here's a quick example. It's not perfect
+and does not check for error codes, be careful.
+
+<figure>
+<pre class="prettyprint">
+/* Written by Julien Danjou <julien@danjou.info> in 2012 */
+
+#include &lt;libusb.h&gt;
+#include &lt;stdio.h&gt;
+#include &lt;stdlib.h&gt;
+#include &lt;string.h&gt;
+
+int main(void)
+{
+    libusb_context *ctx;
+    libusb_init(&ctx);
+    libusb_set_debug(ctx, 3);
+
+    /* Look at the keyboard based on vendor and device id */
+    libusb_device_handle *device_handle = libusb_open_device_with_vid_pid(ctx, 0x046d, 0xc52b);
+
+    fprintf(stderr, "Found keyboard 0x%p\n", device_handle);
+
+    libusb_device *device = libusb_get_device(device_handle);
+
+    struct libusb_device_descriptor desc;
+
+    libusb_get_device_descriptor(device, &desc);
+
+    for(uint8_t config_index = 0; config_index < desc.bNumConfigurations; config_index++)
+    {
+        struct libusb_config_descriptor *config;
+
+        libusb_get_config_descriptor(device, config_index, &config);
+
+        /* We know we want interface 2 */
+        int iface_index = 2;
+        const struct libusb_interface *iface = &config->interface[iface_index];
+
+        for (int altsetting_index = 0; altsetting_index < iface->num_altsetting; altsetting_index++)
+        {
+            const struct libusb_interface_descriptor *iface_desc = &iface->altsetting[al>tsetting_index];
+
+            if (iface_desc->bInterfaceClass == LIBUSB_CLASS_HID)
+            {
+                libusb_detach_kernel_driver(device_handle, iface_index);
+                libusb_claim_interface(device_handle, iface_index);
+
+                unsigned char ret[65535];
+
+                unsigned char payload[] = "\x10\x02\x09\x03\x78\x01\x00";
+
+                if(libusb_control_transfer(device_handle,
+                                           LIBUSB_REQUEST_TYPE_CLASS | LIBUSB_RECIPIENT_INTERFACE,
+                                           LIBUSB_REQUEST_SET_CONFIGURATION,
+                                           0x0210, 2, payload, sizeof(payload) - 1, 10000))
+                {
+                    int actual_length = 0;
+
+                    while(actual_length != 20 || strncmp((const char *) &ret[9], "GOOD", 4))
+                        libusb_interrupt_transfer(device_handle,
+                                                  iface_desc->endpoint[0].bEndpointAddress,
+                                                  ret, sizeof(ret), &actual_length, 100000);
+
+                    uint16_t lux = ret[5] << 8 | ret[6];
+
+                    fprintf(stderr, "Charge: %d %%\nLight: %d lux\n", ret[4], lux);
+                }
+
+                libusb_release_interface(device_handle, iface_index);
+                libusb_attach_kernel_driver(device_handle, iface_index);
+            }
+        }
+    }
+
+    libusb_close(device_handle);
+    libusb_exit(ctx);
+}
+
+</pre>
+</figure>
+
+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:
+
+<pre>
+Found keyboard 0x0x24ec8e0
+Charge: 64 %
+Light: 21 lux
+</pre>
+
+Challenge accomplished!
+
+# 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](http://vger.kernel.org/vger-lists.html#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](http://6xq.net) managed to get the HID++ specifications from Logitech
+and [published
+them](http://6xq.net/git/lars/lshidpp.git/plain/doc/logitech_hidpp_2.0_specification_draft_2012-06-04.pdf)
+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).
diff --git a/content/media/images/logitech-solar-app.png b/content/media/images/logitech-solar-app.png
new file mode 100644 (file)
index 0000000..d9b62b6
Binary files /dev/null and b/content/media/images/logitech-solar-app.png differ
diff --git a/content/media/images/logitech-unifying.jpg b/content/media/images/logitech-unifying.jpg
new file mode 100644 (file)
index 0000000..7c1f581
Binary files /dev/null and b/content/media/images/logitech-unifying.jpg differ
diff --git a/content/media/images/logitech-wireless-solar-keyboard-k750.png b/content/media/images/logitech-wireless-solar-keyboard-k750.png
new file mode 100644 (file)
index 0000000..ee7205d
Binary files /dev/null and b/content/media/images/logitech-wireless-solar-keyboard-k750.png differ