HID is simple enough for a trained engineer to assume it can be authored by hand. This is wrong. Unfortunately, HID is hard enough for most bugs to be forgotten in a final product, and when a product ships bug-free, most of the time, there is room for improvement.
HID is error prone. Because of mix of local and global items, some parameters have to be reset between data items, others may not. Moreover, some items (i.e. Physical range) have an implicit value that depend on others if set to 0.
Some examples may be of some interest.
This report descriptor is a excerpt from an existing product. It has been extracted directly from device. This is the first main collection (there are others collections, irrelevant for the example).
Hex form:
05 01 09 05 A1 01 85 01 05 09 19 01 29 0F 15 00 25 01 95 0F 75 01 81 02 75 01 95 01 81 03 05 01 09 39 15 01 25 08 35 00 46 3B 01 66 14 00 75 04 95 01 81 42 75 04 95 01 81 03 05 01 09 01 A1 00 09 30 09 31 15 00 26 FF FF 35 00 46 FF FF 95 02 75 10 81 02 C0 09 32 09 35 15 00 26 FF FF 35 00 46 FF FF 95 02 75 10 81 02 05 02 09 C5 09 C4 15 00 26 FF FF 35 00 46 FF FF 95 02 75 10 81 02 C0
Corresponding code with textual form, from a sibling tool:
1 $ hidrd-convert -i hex -o code broken.hex
2 0x05, 0x01, /* Usage Page (Desktop), */
3 0x09, 0x05, /* Usage (Gamepad), */
4 0xA1, 0x01, /* Collection (Application), */
5 0x85, 0x01, /* Report ID (1), */
6 0x05, 0x09, /* Usage Page (Button), */
7 0x19, 0x01, /* Usage Minimum (01h), */
8 0x29, 0x0F, /* Usage Maximum (0Fh), */
9 0x15, 0x00, /* Logical Minimum (0), */
10 0x25, 0x01, /* Logical Maximum (1), */
11 0x95, 0x0F, /* Report Count (15), */
12 0x75, 0x01, /* Report Size (1), */
13 0x81, 0x02, /* Input (Variable), */
14 0x75, 0x01, /* Report Size (1), */
15 0x95, 0x01, /* Report Count (1), */
16 0x81, 0x03, /* Input (Constant, Variable), */
17 0x05, 0x01, /* Usage Page (Desktop), */
18 0x09, 0x39, /* Usage (Hat Switch), */
19 0x15, 0x01, /* Logical Minimum (1), */
20 0x25, 0x08, /* Logical Maximum (8), */
21 0x35, 0x00, /* Physical Minimum (0), */
22 0x46, 0x3B, 0x01, /* Physical Maximum (315), */
23 0x66, 0x14, 0x00, /* Unit (Degrees), */
24 0x75, 0x04, /* Report Size (4), */
25 0x95, 0x01, /* Report Count (1), */
26 0x81, 0x42, /* Input (Variable, Null State), */
27 0x75, 0x04, /* Report Size (4), */
28 0x95, 0x01, /* Report Count (1), */
29 0x81, 0x03, /* Input (Constant, Variable), */
30 0x05, 0x01, /* Usage Page (Desktop), */
31 0x09, 0x01, /* Usage (Pointer), */
32 0xA1, 0x00, /* Collection (Physical), */
33 0x09, 0x30, /* Usage (X), */
34 0x09, 0x31, /* Usage (Y), */
35 0x15, 0x00, /* Logical Minimum (0), */
36 0x26, 0xFF, 0xFF, /* Logical Maximum (-1), */
37 0x35, 0x00, /* Physical Minimum (0), */
38 0x46, 0xFF, 0xFF, /* Physical Maximum (-1), */
39 0x95, 0x02, /* Report Count (2), */
40 0x75, 0x10, /* Report Size (16), */
41 0x81, 0x02, /* Input (Variable), */
42 0xC0, /* End Collection, */
43 0x09, 0x32, /* Usage (Z), */
44 0x09, 0x35, /* Usage (Rz), */
45 0x15, 0x00, /* Logical Minimum (0), */
46 0x26, 0xFF, 0xFF, /* Logical Maximum (-1), */
47 0x35, 0x00, /* Physical Minimum (0), */
48 0x46, 0xFF, 0xFF, /* Physical Maximum (-1), */
49 0x95, 0x02, /* Report Count (2), */
50 0x75, 0x10, /* Report Size (16), */
51 0x81, 0x02, /* Input (Variable), */
52 0x05, 0x02, /* Usage Page (Simulation), */
53 0x09, 0xC5, /* Usage (Brake), */
54 0x09, 0xC4, /* Usage (Accelerator), */
55 0x15, 0x00, /* Logical Minimum (0), */
56 0x26, 0xFF, 0xFF, /* Logical Maximum (-1), */
57 0x35, 0x00, /* Physical Minimum (0), */
58 0x46, 0xFF, 0xFF, /* Physical Maximum (-1), */
59 0x95, 0x02, /* Report Count (2), */
60 0x75, 0x10, /* Report Size (16), */
61 0x81, 0x02, /* Input (Variable), */
62 0xC0 /* End Collection */
This defines a gamepad, 15 buttons, a hat switch, two thumb sticks and two analog triggers.
There are some broken constructs (despite in some third party recommandation documents) a compiler could do nothing about:
- line 43, the second thumb stick using Z and Rz as axis Usage codes instead of X and Y in another Physical Collection (see HUT1_12v2, A.5, p. 132);
- line 53, analog triggers use specific usages even if nothing enforces using those two triggers for "Accelerator" and "Brake". Actually, specification explicitly says "Button" usages should be preferred over specific usages (see HID1_11, 6.2.2.8, in footnote, p. 40).
There are other constructs where a compiler could have been useful:
- line 35 onwards, range for analog controls is broken, it goes from 0 to -1. Logical minimum and Logical maximum are signed, value is 16 bits, maximum should have been defined with a 4-byte item (most HID parsers are tolerant about this one);
- finally, this descriptor is suboptimal. It repeats physical bounds that are the same as logical ones. Repeating them is not needed as physical range is meant to be the same as logical one when both physical minimum and physical maximum are 0 (see HID1_11, 6.2.2.7, p. 38).
But they are not the worst thing. There is a blatant error. With extractor, it may become clearer:
$ python3 -m hrdc.descriptor.extractor -i hex broken.hex
1 from hrdc.usage import *
2 from hrdc.descriptor import *
3
4 descriptor = TopLevel(
5 Report(1,
6 Collection(Collection.Application, desktop.Gamepad,
7 Value(Value.Input, button.Button(1), 1, logicalMin = 0, logicalMax = 1),
8 Value(Value.Input, button.Button(2), 1, logicalMin = 0, logicalMax = 1),
9 Value(Value.Input, button.Button(3), 1, logicalMin = 0, logicalMax = 1),
10 Value(Value.Input, button.Button(4), 1, logicalMin = 0, logicalMax = 1),
11 Value(Value.Input, button.Button(5), 1, logicalMin = 0, logicalMax = 1),
12 Value(Value.Input, button.Button(6), 1, logicalMin = 0, logicalMax = 1),
13 Value(Value.Input, button.Button(7), 1, logicalMin = 0, logicalMax = 1),
14 Value(Value.Input, button.Button(8), 1, logicalMin = 0, logicalMax = 1),
15 Value(Value.Input, button.Button(9), 1, logicalMin = 0, logicalMax = 1),
16 Value(Value.Input, button.Button(10), 1, logicalMin = 0, logicalMax = 1),
17 Value(Value.Input, button.Button(11), 1, logicalMin = 0, logicalMax = 1),
18 Value(Value.Input, button.Button(12), 1, logicalMin = 0, logicalMax = 1),
19 Value(Value.Input, button.Button(13), 1, logicalMin = 0, logicalMax = 1),
20 Value(Value.Input, button.Button(14), 1, logicalMin = 0, logicalMax = 1),
21 Value(Value.Input, button.Button(15), 1, logicalMin = 0, logicalMax = 1),
22 Padding(Value.Input, 1),
23 Value(Value.Input, desktop.HatSwitch, 4, flags = Value.Variable|Value.NullState, logicalMax = 8, physicalMin = 0, physicalMax = 315, unit = Unit.Degree),
24 Padding(Value.Input, 4),
25 Collection(Collection.Physical, desktop.Pointer,
26 Value(Value.Input, desktop.X, 16, logicalMin = 0, logicalMax = -1, unit = Unit.Degree),
27 Value(Value.Input, desktop.Y, 16, logicalMin = 0, logicalMax = -1, unit = Unit.Degree),
28 ),
29 Value(Value.Input, desktop.Z, 16, logicalMin = 0, logicalMax = -1, unit = Unit.Degree),
30 Value(Value.Input, desktop.Rz, 16, logicalMin = 0, logicalMax = -1, unit = Unit.Degree),
31 Value(Value.Input, simulation.Brake, 16, logicalMin = 0, logicalMax = -1, unit = Unit.Degree),
32 Value(Value.Input, simulation.Accelerator, 16, logicalMin = 0, logicalMax = -1, unit = Unit.Degree),
33 ),
34 ),
35 )
36
37 if __name__ == "__main__":
38 compile_main(descriptor)
After the Hat switch definition, line 23, all subsequent values have Degree as unit. This is most probably not wanted.
Why did this happen ? Because Unit is a global item, but this may easily be forgotten about. A Unit() item should have reset the unit somewhere after line 26 of descriptor above.
Again, here is a binary descriptor from an actual device:
05 01 09 05 a1 01 05 01 09 01 a1 00 05 09 19 01 29 0c 15 00 25 01 75 01 95 0c 81 02 75 08 95 01 81 01 05 01 09 39 25 07 35 00 46 0e 01 66 40 00 75 04 81 42 09 30 09 31 15 80 25 7f 46 ff 00 66 00 00 75 08 95 02 81 02 09 35 95 01 81 02 09 36 16 00 00 26 ff 00 81 02 09 bb 15 00 26 ff 00 35 00 46 ff 00 75 08 95 04 91 02 c0 c0
Spec-annotated code:
1 $ hidrd-convert -i hex -o code broken2.hex
2 0x05, 0x01, /* Usage Page (Desktop), */
3 0x09, 0x05, /* Usage (Gamepad), */
4 0xA1, 0x01, /* Collection (Application), */
5 0x05, 0x01, /* Usage Page (Desktop), */
6 0x09, 0x01, /* Usage (Pointer), */
7 0xA1, 0x00, /* Collection (Physical), */
8 0x05, 0x09, /* Usage Page (Button), */
9 0x19, 0x01, /* Usage Minimum (01h), */
10 0x29, 0x0C, /* Usage Maximum (0Ch), */
11 0x15, 0x00, /* Logical Minimum (0), */
12 0x25, 0x01, /* Logical Maximum (1), */
13 0x75, 0x01, /* Report Size (1), */
14 0x95, 0x0C, /* Report Count (12), */
15 0x81, 0x02, /* Input (Variable), */
16 0x75, 0x08, /* Report Size (8), */
17 0x95, 0x01, /* Report Count (1), */
18 0x81, 0x01, /* Input (Constant), */
19 0x05, 0x01, /* Usage Page (Desktop), */
20 0x09, 0x39, /* Usage (Hat Switch), */
21 0x25, 0x07, /* Logical Maximum (7), */
22 0x35, 0x00, /* Physical Minimum (0), */
23 0x46, 0x0E, 0x01, /* Physical Maximum (270), */
24 0x66, 0x40, 0x00, /* Unit (40h), */
25 0x75, 0x04, /* Report Size (4), */
26 0x81, 0x42, /* Input (Variable, Null State), */
27 0x09, 0x30, /* Usage (X), */
28 0x09, 0x31, /* Usage (Y), */
29 0x15, 0x80, /* Logical Minimum (-128), */
30 0x25, 0x7F, /* Logical Maximum (127), */
31 0x46, 0xFF, 0x00, /* Physical Maximum (255), */
32 0x66, 0x00, 0x00, /* Unit, */
33 0x75, 0x08, /* Report Size (8), */
34 0x95, 0x02, /* Report Count (2), */
35 0x81, 0x02, /* Input (Variable), */
36 0x09, 0x35, /* Usage (Rz), */
37 0x95, 0x01, /* Report Count (1), */
38 0x81, 0x02, /* Input (Variable), */
39 0x09, 0x36, /* Usage (Slider), */
40 0x16, 0x00, 0x00, /* Logical Minimum (0), */
41 0x26, 0xFF, 0x00, /* Logical Maximum (255), */
42 0x81, 0x02, /* Input (Variable), */
43 0x09, 0xBB, /* Usage (BBh), */
44 0x15, 0x00, /* Logical Minimum (0), */
45 0x26, 0xFF, 0x00, /* Logical Maximum (255), */
46 0x35, 0x00, /* Physical Minimum (0), */
47 0x46, 0xFF, 0x00, /* Physical Maximum (255), */
48 0x75, 0x08, /* Report Size (8), */
49 0x95, 0x04, /* Report Count (4), */
50 0x91, 0x02, /* Output (Variable), */
51 0xC0, /* End Collection, */
52 0xC0 /* End Collection */
With extractor:
$ python3 -m hrdc.descriptor.extractor -i hex broken2.hex
1 from hrdc.usage import *
2 from hrdc.descriptor import *
3
4 descriptor = TopLevel(
5 Report(0,
6 Collection(Collection.Application, desktop.Gamepad,
7 Collection(Collection.Physical, desktop.Pointer,
8 Value(Value.Input, button.Button(1), 1, logicalMin = 0, logicalMax = 1),
9 Value(Value.Input, button.Button(2), 1, logicalMin = 0, logicalMax = 1),
10 Value(Value.Input, button.Button(3), 1, logicalMin = 0, logicalMax = 1),
11 Value(Value.Input, button.Button(4), 1, logicalMin = 0, logicalMax = 1),
12 Value(Value.Input, button.Button(5), 1, logicalMin = 0, logicalMax = 1),
13 Value(Value.Input, button.Button(6), 1, logicalMin = 0, logicalMax = 1),
14 Value(Value.Input, button.Button(7), 1, logicalMin = 0, logicalMax = 1),
15 Value(Value.Input, button.Button(8), 1, logicalMin = 0, logicalMax = 1),
16 Value(Value.Input, button.Button(9), 1, logicalMin = 0, logicalMax = 1),
17 Value(Value.Input, button.Button(10), 1, logicalMin = 0, logicalMax = 1),
18 Value(Value.Input, button.Button(11), 1, logicalMin = 0, logicalMax = 1),
19 Value(Value.Input, button.Button(12), 1, logicalMin = 0, logicalMax = 1),
20 Padding(Value.Input, 8),
21 Value(Value.Input, desktop.HatSwitch, 4, flags = Value.Variable|Value.NullState, logicalMin = 0, logicalMax = 7, physicalMin = 0, physicalMax = 270, unit = 64),
22 Value(Value.Input, desktop.X, 8, logicalMin = -128, logicalMax = 127, physicalMin = 0, physicalMax = 255),
23 Value(Value.Input, desktop.Y, 8, logicalMin = -128, logicalMax = 127, physicalMin = 0, physicalMax = 255),
24 Value(Value.Input, desktop.Rz, 8, logicalMin = -128, logicalMax = 127, physicalMin = 0, physicalMax = 255),
25 Value(Value.Input, desktop.Slider, 8, logicalMin = 0, logicalMax = 255),
26 Value(Value.Output, 0x100bb, 8, logicalMin = 0, logicalMax = 255),
27 Value(Value.Output, 0x100bb, 8, logicalMin = 0, logicalMax = 255),
28 Value(Value.Output, 0x100bb, 8, logicalMin = 0, logicalMax = 255),
29 Value(Value.Output, 0x100bb, 8, logicalMin = 0, logicalMax = 255),
30 ),
31 ),
32 ),
33 )
34
35 if __name__ == "__main__":
36 compile_main(descriptor)
Broken constructs:
- Hat switch unit is 0x40, which decodes to No system, None^4 (Length). This is totally invalid (the person who did this probably mixed rows and columns in table from page 37 in HID1_11);
- Hat switch goes from 0 to 7 (logical) and from 0° to 270° (physical), that means decoded values are 0°, 38.571°, 77.143°, 115.714°, 154.286°, 192.857°, 231.429° and 270°. On a compliant host, user may not be able to point North-West. How could this go to the field uncatched ?
HID is hard to optimize by hand. Once optimized, a report descriptor is detious to edit by hand because author has to think about interactions between local and global items.
HRDC contains an optimizer. It can be used to rewrite existing descriptors with identical meaning (including bugs above), but with the canonical representation.
Let's try this on various report descriptors found in the wild. I took various HID report descriptors from various devices, ran the optimizer on them, compared report descriptor sizes. There is always some difference.
Size before | Size after | Gain |
---|---|---|
32 | 30 | - 6 % |
61 | 60 | - 1 % |
73 | 61 | - 16 % |
97 | 45 | - 53 % |
98 | 73 | - 25 % |
98 | 73 | - 25 % |
101 | 85 | - 15 % |
103 | 87 | - 15 % |
106 | 102 | - 3 % |
108 | 91 | - 15 % |
108 | 91 | - 15 % |
116 | 113 | - 2 % |
117 | 82 | - 29 % |
119 | 92 | - 22 % |
119 | 100 | - 15 % |
142 | 112 | - 21 % |
146 | 116 | - 20 % |
148 | 118 | - 20 % |
148 | 118 | - 20 % |
148 | 118 | - 20 % |
148 | 142 | - 4 % |
149 | 135 | - 9 % |
152 | 98 | - 35 % |
156 | 104 | - 33 % |
166 | 164 | - 1 % |
174 | 125 | - 28 % |
178 | 155 | - 12 % |
184 | 159 | - 13 % |
196 | 168 | - 14 % |
214 | 204 | - 4 % |
214 | 211 | - 1 % |
217 | 199 | - 8 % |
246 | 234 | - 4 % |
259 | 232 | - 10 % |
266 | 237 | - 10 % |
275 | 234 | - 14 % |
326 | 293 | - 10 % |
326 | 293 | - 10 % |
409 | 350 | - 14 % |
Average gain: 15%
There is no good reason we had to write HID report descriptors by hand for so long!