Wherein I learn more than I ever wanted to learn about USB descriptors and start using pyusb to send raw USB commands to my mouse, to try and learn even more about the inner workings of it. Partly so I can make sense of the firmware disassembly, and partly so I can maybe have a hope of unbricking it!
A long long time ago, in Part 1, I decided I wanted to make my mouse's fancy features work outside of Windows, and figured out the most useless part of the protocol. I extracted the firmware in Part 2, disassembled it in Part 3, and figured out more of the protocol and subsequently bricked the mouse in Part 4. Ouch.
Where do we go from here? Well, ideally we'd like to have this mouse fully working again so that we can play with it - writing a tool for the vendor-specific commands is useless if it won't actually accept any of them...!
The inbuilt firmware updater tool doesn't work, of course, since it just tries to send one of those commands that the mouse is straight up ignoring.
I wanted to get a better understanding of what was going on behind the scenes, so I started delving into the disassembly. Remember how I mentioned in part 3 that Holtek supplied some sample code for a USB Gaming Mouse... with a suspiciously similar feature set?
I started looking through my primitive disassembly and comparing it to the sample code. It was most definitely very suspiciously similar. The microcontroller assigns a reset vector at address 0, and then places interrupt vectors at addresses 4, 8, 0xC, 0x10, and so forth -- the locations that the CPU will jump to when a particular kind of interrupt occurs. Each address maps to one word, or one instruction, so you can actually fit 4 instructions into one interrupt vector.
The sample code's main.asm
starts as follows (skipping past a ton of extern
declarations to reference code/data defined in other files):
main .section AT 00000H 'code'
;-----------------------------------------------------------------------------;
;Watchdot Time-out or Reset
SZ PDF ;power down flag
SNZ TO ;Watchdot time-out flag
JMP main_start ;reset
JMP Watch_Wake ;Watchdot Time-out
;------------------------------------------------------------------------------
ORG 0004H ;External Interrupt 0
RETI
public DelayXuSec
DelayXuSec:
sdz ACC
jmp DelayXuSec
ret
;------------------------------------------------------------------------------
ORG 0008H ;External Interrupt 1
RETI
public Delay100uSec
Delay100uSec:
mov A,132
jmp DelayXuSec
;------------------------------------------------------------------------------
ORG 000CH ;usb interrut subroutine
JMP USB_ISR_BEGIN ;usb_int.asm
public Delay12uSec
Delay12uSec:
mov a,14
jmp DelayXuSec
;-----------------------------------------------------------------------------;
ORG 0010H ;Multi-function 0 Interrupt subroutine
JMP TMR0_ISR
public Delay4uSec
Delay4uSec:
mov A,05
jmp DelayXuSec
;-----------------------------------------------------------------------------;
ORG 0014H ;Multi-function 1 Interrupt subroutine
RETI
public Delay50uSec
Delay50uSec:
mov A,66
jmp DelayXuSec
;-----------------------------------------------------------------------------;
ORG 0018H ;Multi-function 2 Interrupt subroutine
RETI
;-----------------------------------------------------------------------------;
ORG 001CH ;Multi-function 3 Interrupt subroutine
RETI
;-----------------------------------------------------------------------------;
ORG 0020H ;SIM Interrupt subroutine
RETI
;-----------------------------------------------------------------------------;
ORG 0024H ;SPIA Interrupt subroutine
RETI
;-----------------------------------------------------------------------------;
ORG 0028H ;LVD Interrupt subroutine
RETI
The reset vector is handled by a couple of conditions which jump to either main_start
or Watch_Wake
depending on the state of certain registers. This takes up the full four instructions allotted between 0 (the reset vector) and 4 (the first interrupt vector).
The interrupt vectors at addresses 4, 8, 0x14, 0x18, 0x1C, 0x20, 0x24 and 0x28 are all empty handlers -- you get reti
(return from interrupt) and that's it. The two at 0xC and 0x10 are non-empty, but just contain a single jmp
instruction to another place where the meat is.
They've taken advantage of this to fill the 3-instruction spaces between the vectors with some stuff. DelayXuSec
is a very simple function which spins in a loop until ACC (the other name for the accumulator in this architecture) reaches zero, decrementing ACC each iteration.
Then, there's a few very badly-named variants of this: Delay100uSec
loads 132 into the accumulator and jumps to DelayXuSec
. Delay12uSec
does 14. Delay4uSec
does 5. Delay50uSec
does 66.
My mouse's firmware contains the exact same function, in the exact same place, and a bunch of similar variants, but with slightly different values (and there's more of them). That's suspicious; did they base it off the sample code? This similarity is not conclusive, and it's not really useful on its own, but it is enough to make me go looking further.
The code at main_start
isn't identical, but there's definite similarities - though admittedly there's bound to be some sort of similarity in places like this; after all, they're both going to need to do things like set up USB communications using the registers that control this chip's built-in USB functionality.
I decided to have a read through the sample code so I could better familiarise myself with how it was structured. Something that immediately caught my eye was the descriptors, located in the aptly named DESCRIPTOR.ASM
in the sample code.
These are standard data structures (defined in the USB specifications) which describe the device and how it communicates. The host machine asks the device things like "what are you?" and "what do you offer?"; the device replies with the appropriate descriptors, and the host parses them to learn how to deal with that device. There's a few different kinds that show up in the sample code, which I'll describe here.
If you're feeling as masochistic as I was, you can find the structure definitions for general USB descriptors on pages 261-274 of the USB 2.0 specification and for HID descriptors on pages 21-47 of the USB HID Device Class Definition.
Only one of these exists! Stores general information about the device as a whole. On our Sample Mouse, it contains the following:
Devices are allowed to have more than one of these, but the Sample Mouse only has one. This allows devices to change their capabilities on-the-fly (what interfaces they expose, how much power they promise to draw).
This one says that there are 3 interfaces, the number of the configuration is 1, the device is bus-powered and supports remote wakeup, and it claims to draw 100mA of power at most.
This is kind of like a 'sub-device'. In the case of the Sample Mouse, there's three!
Class 3 means that it's a HID class device, unsurprisingly. The subclass specifies that the keyboard and mouse interfaces are usable as boot devices -- this means that they support a predefined protocol for either keyboard or mice (surprise!), so that a BIOS can use them without having to parse a complex Report specification (more on this shortly).
These tell us which endpoints (communication channels, essentially) are used by each interface. Each endpoint has an address defined (number + transfer direction), attributes (specifying what kind of transfer occurs), and a couple of transfer-type-specific parameters. Thankfully, we don't need to know much about these particular details to understand what's going on in our mouse.
The Keyboard's got one: endpoint address 1 (in / to host), interrupt transfer type, maximum packet size 8 bytes.
The Mouse's got one: endpoint address 2 (in / to host), interrupt transfer type, maximum packet size 8 bytes.
The Mystery Interface's got two: one has endpoint address 3 (in / to host), the second has endpoint address 4 (out / to device). Both have a maximum packet size of 32 bytes.
There's also Endpoint 0, which always exists. This is used for core USB control commands (including the ones that fetch the descriptors - so that's necessary to bootstrap the whole system) and can be implicitly used by any interface without this interface having to create an endpoint descriptor that goes "hey folks, I wanna use endpoint 0, thank".
These allow the device to serve up text; mostly just used for the purpose of showing a name in Device Manager and similar. The sample mouse includes three of these: the first one just tells the host what languages it supports (code 0x0409: English), the second one is "Holtek", and the third is "USB Gaming Mouse".
The Device, Configuration and Interface Descriptors include fields that can refer to strings by using either 0 for "no string", or a value higher than 0 to specify the index of a particular string descriptor.
Last but not least come the HID-specific descriptors. All the ones we've described up to now are part of the main USB specification; every USB device uses them. These, on the other hand, are exclusively for HID class devices.
(It's easy to say "HID device", but when you say that, what you're really saying is "Human Interface Device device". That's nasty. The HID specification says "HID class device", so I'll defer to them and use that. Still feels kinda ugly to me, but they know more about HID dev-- HID class devices than I do!)
The HID Descriptor is basically a little bit of metadata that gets attached to each interface (not device!), with fields relating to the HID spec. It describes what version of the HID spec the device abides by (1.10 for the Sample Mouse), what country code the device is localised for (optional, but useful for e.g keyboard layouts) and which extra HID-specific descriptors are included.
The Report Descriptor is a complex, variable-size beast. It's not really a structure, it's more like a domain-specific language. Each HID Descriptor contains one Report Descriptor. These essentially tell the host device how to parse the data coming from the device, or in some cases, how to format data being sent to the device. This is why you can have mice with different amounts of buttons, or game controllers with different amounts of controls.
The report descriptor contains stuff like "I'm gonna send you the on/off state of 8 buttons, followed by a 16-bit X position in the range -32768 to 32767, followed by an 8-bit Y position in the range 0 to 255" (but in a machine-readable, formally specified way), and it's the host's job to make sense out of that. More on this shortly, where I investigate my beloved TeckNet mouse's reports.
The Physical Descriptor is optional, and includes information on how the reports map to physical properties of the device. The sample mouse code doesn't use this.
I'm going to admit I made a mistake here when I was working on this. I decided to try and parse the descriptor data by hand, straight from my disassembly. In retrospect it would have been much easier to just grab it straight from the device and feed it into a parser, but hindsight is 20/20... For your sake, I'm going to go for the latter method here.
First, I hooked up my mouse to an Arch Linux VM and installed pyusb and IPython. I feel silly for not trying this earlier, but this library combined with the REPL makes a wonderful little tool for poking at USB devices ad-hoc.
(Note: You may need to run ipython as superuser to be able to write to USB devices directly.)
In [1]: import usb.core, usb.control, usb.util, binascii
In [2]: dev = usb.core.find(idVendor=0x4D9, idProduct=0xA118)
In [3]: dev
Out[3]: <DEVICE ID 04d9:a118 on Bus 001 Address 004>
In [4]: binascii.hexlify(usb.control.get_descriptor(dev, 0x400, usb.util.DESC_TYPE_DEVICE, 0))
Out[4]: b'1201000200000008d90418a1010100020001'
In [5]: binascii.hexlify(usb.control.get_descriptor(dev, 0x400, usb.util.DESC_TYPE_CONFIG, 0))
Out[5]: b'09025b00030100a03209040000010301020009211001000122470007058103080001090401000103000100092110010001228c00070582031000040904020002030000000921100100012220000705830340000407050403400004'
This library exposes a nice API for dealing with configurations and endpoints, but I'm going to mostly ignore it in the sake of poking at the raw data, because I want to see how it works. I load the device, verify it's been connected to, then use pyusb's helper functions to send two Get_Descriptor
requests. One gets me the Device Descriptor, and the second gets me the Configuration Descriptor as well as the bits it contains (Interface, Endpoint and HID Descriptors).
There's an extremely handy parser I found online here: USB Descriptor and Request Parser - you can just paste the hex blobs and click 'USB Standard Descriptor' and it'll output what they contain. It's excellent. Here's the output I've gotten from my mouse for those:
0x12, // bLength
0x01, // bDescriptorType (Device)
0x00, 0x02, // bcdUSB 2.00
0x00, // bDeviceClass (Use class information in the Interface Descriptors)
0x00, // bDeviceSubClass
0x00, // bDeviceProtocol
0x08, // bMaxPacketSize0 8
0xD9, 0x04, // idVendor 0x04D9
0x18, 0xA1, // idProduct 0xA118
0x01, 0x01, // bcdDevice 2.01
0x00, // iManufacturer (String Index)
0x02, // iProduct (String Index)
0x00, // iSerialNumber (String Index)
0x01, // bNumConfigurations 1
0x09, // bLength
0x02, // bDescriptorType (Configuration)
0x5B, 0x00, // wTotalLength 91
0x03, // bNumInterfaces 3
0x01, // bConfigurationValue
0x00, // iConfiguration (String Index)
0xA0, // bmAttributes Remote Wakeup
0x32, // bMaxPower 100mA
0x09, // bLength
0x04, // bDescriptorType (Interface)
0x00, // bInterfaceNumber 0
0x00, // bAlternateSetting
0x01, // bNumEndpoints 1
0x03, // bInterfaceClass
0x01, // bInterfaceSubClass
0x02, // bInterfaceProtocol
0x00, // iInterface (String Index)
0x09, // bLength
0x21, // bDescriptorType (HID)
0x10, 0x01, // bcdHID 1.10
0x00, // bCountryCode
0x01, // bNumDescriptors
0x22, // bDescriptorType[0] (HID)
0x47, 0x00, // wDescriptorLength[0] 71
0x07, // bLength
0x05, // bDescriptorType (Endpoint)
0x81, // bEndpointAddress (IN/D2H)
0x03, // bmAttributes (Interrupt)
0x08, 0x00, // wMaxPacketSize 8
0x01, // bInterval 1 (unit depends on device speed)
0x09, // bLength
0x04, // bDescriptorType (Interface)
0x01, // bInterfaceNumber 1
0x00, // bAlternateSetting
0x01, // bNumEndpoints 1
0x03, // bInterfaceClass
0x00, // bInterfaceSubClass
0x01, // bInterfaceProtocol
0x00, // iInterface (String Index)
0x09, // bLength
0x21, // bDescriptorType (HID)
0x10, 0x01, // bcdHID 1.10
0x00, // bCountryCode
0x01, // bNumDescriptors
0x22, // bDescriptorType[0] (HID)
0x8C, 0x00, // wDescriptorLength[0] 140
0x07, // bLength
0x05, // bDescriptorType (Endpoint)
0x82, // bEndpointAddress (IN/D2H)
0x03, // bmAttributes (Interrupt)
0x10, 0x00, // wMaxPacketSize 16
0x04, // bInterval 4 (unit depends on device speed)
0x09, // bLength
0x04, // bDescriptorType (Interface)
0x02, // bInterfaceNumber 2
0x00, // bAlternateSetting
0x02, // bNumEndpoints 2
0x03, // bInterfaceClass
0x00, // bInterfaceSubClass
0x00, // bInterfaceProtocol
0x00, // iInterface (String Index)
0x09, // bLength
0x21, // bDescriptorType (HID)
0x10, 0x01, // bcdHID 1.10
0x00, // bCountryCode
0x01, // bNumDescriptors
0x22, // bDescriptorType[0] (HID)
0x20, 0x00, // wDescriptorLength[0] 32
0x07, // bLength
0x05, // bDescriptorType (Endpoint)
0x83, // bEndpointAddress (IN/D2H)
0x03, // bmAttributes (Interrupt)
0x40, 0x00, // wMaxPacketSize 64
0x04, // bInterval 4 (unit depends on device speed)
0x07, // bLength
0x05, // bDescriptorType (Endpoint)
0x04, // bEndpointAddress (OUT/H2D)
0x03, // bmAttributes (Interrupt)
0x40, 0x00, // wMaxPacketSize 64
0x04, // bInterval 4 (unit depends on device speed)
// 109 bytes
Lots to take in there, but it's all handily annotated. There's one configuration, as per the Device Descriptor. It's got three interfaces, just like our sample code.
Where things get different is in the interfaces themselves. Interface 0 has Protocol 2 (means it can pretend to be a generic mouse) and uses Endpoint 1. Interface 1 has Protocol 1 (so it's a keyboard) and uses Endpoint 2. This is the opposite order to the sample code, where interface 0 was a keyboard and interface 1 was a mouse. My mouse's keyboard interface (that just sounds wrong) has a maximum packet size of 16 bytes, in contrast to the sample code's keyboard interface which supplies a measly 8 bytes at most.
The third interface is bidirectional, like the sample code's third interface, using Endpoint 3 as input (device-to-host) and Endpoint 4 as output (host-to-device). It's got a max packet size of 64 bytes. Hey, doesn't that seem suspiciously identical to the bulk data transfers used by the mouse to read/write its configuration? 🤔
(spoiler: yes, that's precisely how they're transmitted)
Finally, let's dig into the report descriptors. We can dump the ones from our mouse using pyusb. Sadly, we can't use the helper methods for that because they only let us call Get_Descriptor
on a device, whereas we need to send that command in an interface context. That's OK, we can build our own command!
Page 49 of the HID specification tells us precisely how.. or you can just look at that convenient screenshot of the page. Whatever works.
⚠️ Important note: Your mileage may vary, but on my test system (an Arch Linux VM), I got 'Pipe error' when I tried to send a control message directly to the HID interfaces. To get this to work, I had to first sudo rmmod usbhid
. I could do this safely because I was working in a VM and was just SSHed into it from the host, but you probably don't want to unload usbhid
on a regular system, lest you lose your input devices...!
pyusb provides the following method: ctrl_transfer(bmRequestType, bRequest, wValue=0, wIndex=0, data_or_wLength=None, timeout=None)
Going by that document, we need to set bmRequestType to 0x81, bRequest to 6, wValue's high byte to 0x22, wValue's low byte to 0, wIndex to 0 and wLength to the amount of data we want to read.
We've got three interfaces, so we'll need to do it three times. Doing this, I finally gain my prize: the binary descriptor data!
In [20]: for i in range(3):
...: print(binascii.hexlify(dev.ctrl_transfer(0x81, 6, 0x2200, i, 0x400)))
...:
b'05010902a1010901a1000509150025011901290575019505810295038101050116018026ff7f093009317510950281061581257f0938750895018106050c0a380295018106c0c0'
b'05010906a1018501050719e029e71500250175019508810219002aff00150026ff007508950e8100c005010980a1018502198129831500250175019503810295058101c0050c0901a101850319002aff02150026ff02751095018100c005010902a1018504150026ff7f09300931751095028102c00601ff0901a1018506150026ff00092f750895038100c0'
b'0600ff0a00ffa101150026ff0009207508954081020921910209229508b102c0'
Once again, I can feed this into Frank Zhao's handy USB Descriptor and Request Parser for some insight.
0x05, 0x01, // Usage Page (Generic Desktop Ctrls)
0x09, 0x02, // Usage (Mouse)
0xA1, 0x01, // Collection (Application)
0x09, 0x01, // Usage (Pointer)
0xA1, 0x00, // Collection (Physical)
0x05, 0x09, // Usage Page (Button)
0x15, 0x00, // Logical Minimum (0)
0x25, 0x01, // Logical Maximum (1)
0x19, 0x01, // Usage Minimum (0x01)
0x29, 0x05, // Usage Maximum (0x05)
0x75, 0x01, // Report Size (1)
0x95, 0x05, // Report Count (5)
0x81, 0x02, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
0x95, 0x03, // Report Count (3)
0x81, 0x01, // Input (Const,Array,Abs,No Wrap,Linear,Preferred State,No Null Position)
0x05, 0x01, // Usage Page (Generic Desktop Ctrls)
0x16, 0x01, 0x80, // Logical Minimum (-32767)
0x26, 0xFF, 0x7F, // Logical Maximum (32767)
0x09, 0x30, // Usage (X)
0x09, 0x31, // Usage (Y)
0x75, 0x10, // Report Size (16)
0x95, 0x02, // Report Count (2)
0x81, 0x06, // Input (Data,Var,Rel,No Wrap,Linear,Preferred State,No Null Position)
0x15, 0x81, // Logical Minimum (-127)
0x25, 0x7F, // Logical Maximum (127)
0x09, 0x38, // Usage (Wheel)
0x75, 0x08, // Report Size (8)
0x95, 0x01, // Report Count (1)
0x81, 0x06, // Input (Data,Var,Rel,No Wrap,Linear,Preferred State,No Null Position)
0x05, 0x0C, // Usage Page (Consumer)
0x0A, 0x38, 0x02, // Usage (AC Pan)
0x95, 0x01, // Report Count (1)
0x81, 0x06, // Input (Data,Var,Rel,No Wrap,Linear,Preferred State,No Null Position)
0xC0, // End Collection
0xC0, // End Collection
// 71 bytes
This one's pretty straightforward. It defines five binary inputs for mouse buttons, three constant padding bits (all that gets packed into one byte), followed by two signed 16-bit values for relative X and Y movement, followed by one signed 8-bit value for relative wheel movement, followed by one signed 8-bit value for relative "AC Pan" (horizontal scrolling).
0x05, 0x01, // Usage Page (Generic Desktop Ctrls)
0x09, 0x06, // Usage (Keyboard)
0xA1, 0x01, // Collection (Application)
0x85, 0x01, // Report ID (1)
0x05, 0x07, // Usage Page (Kbrd/Keypad)
0x19, 0xE0, // Usage Minimum (0xE0)
0x29, 0xE7, // Usage Maximum (0xE7)
0x15, 0x00, // Logical Minimum (0)
0x25, 0x01, // Logical Maximum (1)
0x75, 0x01, // Report Size (1)
0x95, 0x08, // Report Count (8)
0x81, 0x02, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
0x19, 0x00, // Usage Minimum (0x00)
0x2A, 0xFF, 0x00, // Usage Maximum (0xFF)
0x15, 0x00, // Logical Minimum (0)
0x26, 0xFF, 0x00, // Logical Maximum (255)
0x75, 0x08, // Report Size (8)
0x95, 0x0E, // Report Count (14)
0x81, 0x00, // Input (Data,Array,Abs,No Wrap,Linear,Preferred State,No Null Position)
0xC0, // End Collection
0x05, 0x01, // Usage Page (Generic Desktop Ctrls)
0x09, 0x80, // Usage (Sys Control)
0xA1, 0x01, // Collection (Application)
0x85, 0x02, // Report ID (2)
0x19, 0x81, // Usage Minimum (Sys Power Down)
0x29, 0x83, // Usage Maximum (Sys Wake Up)
0x15, 0x00, // Logical Minimum (0)
0x25, 0x01, // Logical Maximum (1)
0x75, 0x01, // Report Size (1)
0x95, 0x03, // Report Count (3)
0x81, 0x02, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
0x95, 0x05, // Report Count (5)
0x81, 0x01, // Input (Const,Array,Abs,No Wrap,Linear,Preferred State,No Null Position)
0xC0, // End Collection
0x05, 0x0C, // Usage Page (Consumer)
0x09, 0x01, // Usage (Consumer Control)
0xA1, 0x01, // Collection (Application)
0x85, 0x03, // Report ID (3)
0x19, 0x00, // Usage Minimum (Unassigned)
0x2A, 0xFF, 0x02, // Usage Maximum (0x02FF)
0x15, 0x00, // Logical Minimum (0)
0x26, 0xFF, 0x02, // Logical Maximum (767)
0x75, 0x10, // Report Size (16)
0x95, 0x01, // Report Count (1)
0x81, 0x00, // Input (Data,Array,Abs,No Wrap,Linear,Preferred State,No Null Position)
0xC0, // End Collection
0x05, 0x01, // Usage Page (Generic Desktop Ctrls)
0x09, 0x02, // Usage (Mouse)
0xA1, 0x01, // Collection (Application)
0x85, 0x04, // Report ID (4)
0x15, 0x00, // Logical Minimum (0)
0x26, 0xFF, 0x7F, // Logical Maximum (32767)
0x09, 0x30, // Usage (X)
0x09, 0x31, // Usage (Y)
0x75, 0x10, // Report Size (16)
0x95, 0x02, // Report Count (2)
0x81, 0x02, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
0xC0, // End Collection
0x06, 0x01, 0xFF, // Usage Page (Vendor Defined 0xFF01)
0x09, 0x01, // Usage (0x01)
0xA1, 0x01, // Collection (Application)
0x85, 0x06, // Report ID (6)
0x15, 0x00, // Logical Minimum (0)
0x26, 0xFF, 0x00, // Logical Maximum (255)
0x09, 0x2F, // Usage (0x2F)
0x75, 0x08, // Report Size (8)
0x95, 0x03, // Report Count (3)
0x81, 0x00, // Input (Data,Array,Abs,No Wrap,Linear,Preferred State,No Null Position)
0xC0, // End Collection
// 140 bytes
A lot is going on here! This one combines multiple 'Collections' to pull from multiple different kinds of input types, and also defines multiple HID Report IDs, allowing the device to tell the OS that it's offering a particular kind of report.
Note that Report ID 6 is what we saw used back in Part 1 for the mouse to supply notifications to the config tool!
The last interface has a super-simple report descriptor with some interesting features... and it'll be immediately obvious why.
0x06, 0x00, 0xFF, // Usage Page (Vendor Defined 0xFF00)
0x0A, 0x00, 0xFF, // Usage (0xFF00)
0xA1, 0x01, // Collection (Application)
0x15, 0x00, // Logical Minimum (0)
0x26, 0xFF, 0x00, // Logical Maximum (255)
0x09, 0x20, // Usage (0x20)
0x75, 0x08, // Report Size (8)
0x95, 0x40, // Report Count (64)
0x81, 0x02, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
0x09, 0x21, // Usage (0x21)
0x91, 0x02, // Output (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile)
0x09, 0x22, // Usage (0x22)
0x95, 0x08, // Report Count (8)
0xB1, 0x02, // Feature (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile)
0xC0, // End Collection
// 32 bytes
Whereas the other reports were exclusively Input reports (data being sent from the device to the host), this one provides multiple kinds. It's got vendor-defined usage FF00:FF00, which you may remember as being used by the control channel for this mouse's configuration commands.
The specifications here corroborate what we already figured out and implemented using hidapi. Feature reports form 8 bytes; these are our main commands and their replies. Input and output reports form 64 bytes; these are the individual blocks of our bulk transfers.
There's a lot to go through here, but now we're in a much better place to figure out how this mouse handles commands. We know, for instance, that Endpoints 3 and 4 are used to transmit the bulk data.
Inside the USBReport.asm
file, we can find the code that deals with reports. There's a jump table near the start inside a block called GetFeatureReport which appears to handle the equivalent of the 'read data' commands. There's also a block called FeatureCommand which seems like an equivalent to my mouse's 'write data'/'set something' commands. Here's the tables for both...
GetFeatureReport_PCL21:
ADDM A,PCL
JMP GetFirmwareVersion ;00H
JMP GetReportRate ;01H
JMP GetProfileValue ;02H
JMP GetSensorOption ;03H
JMP ToStallPipe0 ;04H
JMP ToStallPipe0 ;05H
JMP ToStallPipe0 ;06H
JMP ToStallPipe0 ;07H
JMP ToStallPipe0 ;08H
JMP ToStallPipe0 ;09H
JMP ToStallPipe0 ;0AH
JMP GetDPIStage ;0BH
JMP GetLEDColor ;0CH
JMP GetLEDEffect ;0DH
JMP GetXYSensitivity ;0EH
JMP ToStallPipe0 ;0FH
JMP ToStallPipe0 ;10H
JMP GetDPIStageValue ;11H
JMP GetKeyMatrixValue ;12H
JMP GetMacroKeyValue ;13H
FeatureCommand_PCL21:
ADDM A,PCL
JMP SetUSBAccessIndex ;00H
JMP SetReportRate ;01H
JMP SetProfileValue ;02H
JMP SetSensorOption ;03H
JMP ToStallPipe0 ;04H
JMP ToStallPipe0 ;05H
JMP ToStallPipe0 ;06H
JMP ToStallPipe0 ;07H
JMP SetForceMacroStop ;08H
jmp ToStallPipe0 ;09H
JMP ToStallPipe0 ;0AH
JMP SetDPIStage ;0BH
JMP SetLEDColor ;0CH
JMP SetLEDEffect ;0DH
JMP SetXYSensitivity ;0EH
JMP ToStallPipe0 ;0FH
JMP SetLEDColorStageValue ;10H
JMP SetDPIStageValue ;11H
JMP SetKeyMatrixValue ;12H
JMP SetMacroKeyValue ;13H
It's similar to what my mouse has, but definitely not identical. This sample code uses command 0x13 to set macros, but that's command 0xF on mine. This sample code says that command 0xC sets the LED colours, but I already know that command 0xC writes the configuration on mine. So, the sample code won't really help us here. Time to go look at something else...?
I didn't expect to write this much about USB descriptors, but it just kind of happened...
In part 6, I'm going to be trying to reflash the mouse in an attempt to get it back to fully working order, and writing an IDA processor module for the microcontroller. Stay tuned :3
If you've really enjoyed this series of posts and want to help fund my hot chocolate habit, feel free to send me a couple of pounds: Ko-fi | Monzo.me (UK) | PayPal.me
Previous Post: Mouse Adventures #4: Writing a custom tool
Next Post: Mouse Adventures #6: Enabling the Bootloader