What Is a HID Device? USB HID (Human Interface Device) Explained

USB HID [Human Interface Device]

A HID (human interface device) is any device that people use to give input to a computer or get simple feedback from it — a keyboard, mouse, touchpad, game controller, barcode scanner or the volume buttons on a headset. “HID” is also the name of the standard those devices follow: a device that speaks HID describes itself to the computer, so Windows, Linux, macOS and web browsers can use it with a driver they already have, without a custom driver from the manufacturer.

Technically, USB HID is a device class defined by the USB Implementers Forum (USB-IF). A HID device sends a report descriptor that lists every data field it will exchange, then moves small packets called reports in each direction. This guide explains that model in plain terms first and then goes into the bytes: descriptors, reports, transfers, boot protocol, operating-system APIs and security. It finishes with a small, tested Node.js tool that decodes report descriptors.

Scope: this is an engineering explainer based on the USB-IF HID 1.11 class definition, the HID Usage Tables 1.7, the USB 2.0 specification and operating-system documentation from Microsoft, the Linux kernel and Apple. It is not a compliance guide. The USB-IF documents are free to download from usb.org but may not be republished, so this page explains and cites them rather than reproducing their tables or examples. Every example descriptor and report below was built for this article.

HID in plain language

Before HID, each new kind of input device tended to need its own driver. HID changed that by making the device describe itself. When you plug in a HID device, it tells the computer, in effect: “I have two buttons and a dial; the buttons are bits 0 and 1 of the first byte, and the dial is the next byte, from 0 to 255.” The computer’s built-in HID driver reads that description and knows how to interpret every packet that follows. Microsoft describes the result as support for standard boot devices plus room for new devices through “extensible, standardized, and easily programmable interfaces”.

Microsoft’s list of HID devices goes well beyond keyboards and mice: alphanumeric displays, bar code readers, headsets, sensors and vendors’ own proprietary devices all use it. And although HID began on USB, it was designed to be independent of the bus — USB is the cable, HID is the language spoken over it, and the same language now runs over Bluetooth, I²C and SPI too.

Why a HID device needs no custom driver

A USB device can contain several functions, and each function is an interface. An interface whose class code (bInterfaceClass) is 3 is a HID interface; HID 1.11 says the class is declared at the interface level, not in the device descriptor, which is why one gadget can be a HID keyboard and an audio headset at the same time. Inside that interface the device provides a class-specific HID descriptor (type 0x21) which tells the host the length of the report descriptor (type 0x22). The host fetches the report descriptor with a standard GET_DESCRIPTOR request addressed to the interface.

On Windows, Microsoft’s in-box HID-over-USB miniport (HIDUSB.SYS) is loaded through a USB class compatible-ID match, and the HID class driver (hidclass.sys) parses the descriptor and creates one device object per top-level collection. On Linux, transport drivers for USB, Bluetooth, I²C and user-space devices feed a common HID core that does the report parsing and loads generic drivers on top. The device firmware’s only job is to describe itself correctly.

USB device
 +-- Interface 0   bInterfaceClass = 3 (HID)
 |     +-- HID descriptor (0x21) ---- points to ----+
 |     +-- Endpoint: Interrupt IN   (required)      |
 |     +-- Endpoint: Interrupt OUT  (optional)      |
 |                                                  v
 |                                  Report descriptor (0x22)
 |                                  "what every bit means"
 +-- Endpoint 0: Control pipe (always present)
       GET_DESCRIPTOR, GET_REPORT, SET_REPORT, SET_IDLE ...

The USB HID protocol in one picture: descriptors, reports and pipes

The “USB HID protocol” is really three agreements:

  1. The report descriptor — a compact byte program, read once at enumeration, that declares fields, their sizes, value ranges and meaning.
  2. Reports — the actual data. Input reports go from device to host (key presses, readings). Output reports go from host to device (a keyboard’s Caps Lock LED). Feature reports are configuration values either side can read or write on request.
  3. Transfers and requests — Input reports normally travel on an interrupt IN endpoint, which the host polls. Output reports use an optional interrupt OUT endpoint or, if there is none, a SET_REPORT request on the control pipe. Feature reports always use GET_REPORT and SET_REPORT on the control pipe.

If the descriptor uses Report IDs, every report starts with a one-byte ID so that several report layouts can share one endpoint (HID 1.11 §5.6 and §8.2). If no Report ID appears anywhere, there is exactly one report of each type and no prefix byte. Report ID 0 is reserved.

How a report descriptor is encoded

A report descriptor is a sequence of items. Almost every item is a “short item”: one prefix byte followed by 0, 1, 2 or 4 data bytes, little-endian (HID 1.11 §6.2.2.2 and §5.8).

prefix byte:   7   6   5   4 | 3   2 | 1   0
               ---- bTag ----  bType   bSize
bType: 0 = Main   1 = Global   2 = Local   3 = reserved
bSize: 0 -> no data   1 -> 1 byte   2 -> 2 bytes   3 -> 4 bytes

Example 0x95 = 1001 01 01  ->  tag 9, Global, 1 data byte  ->  Report Count

The prefix 0xFE marks a long item (a size byte and tag byte follow), but HID 1.11 defines no long items, so in practice a parser can treat it as an error (§6.2.2.3). The three item types behave differently, and that difference is the key to reading any descriptor:

  • Main items create or group fields: Input, Output, Feature, Collection and End Collection. The data byte of Input/Output/Feature is a set of flags: bit 0 Data/Constant, bit 1 Array/Variable, bit 2 Absolute/Relative, plus wrap, linearity, preferred state, null state, volatility (Output and Feature only) and buffered bytes (§6.2.2.4).
  • Global items set state that persists for every following Main item until changed: Usage Page, Logical Minimum/Maximum, Physical Minimum/Maximum, Unit Exponent, Unit, Report Size (bits per field), Report ID, Report Count (number of fields), and Push/Pop to save and restore that state (§6.2.2.7).
  • Local items apply only to the next Main item and are then cleared: Usage, Usage Minimum/Maximum, designator and string indexes, and Delimiter (§6.2.2.8).

A usage says what a field means. It is a 32-bit value: the high 16 bits are the usage page, the low 16 bits the usage ID. Usually a Usage Page item sets the page once and 1- or 2-byte Usage items give just the ID; a 4-byte Usage item carries both halves at once (an “extended” usage) (§6.2.2.8; HUT §3.1).

Item Type Tag Prefix with 1 data byte What it does
Input Main 8 81 Adds device-to-host fields
Output Main 9 91 Adds host-to-device fields
Feature Main 11 B1 Adds configuration fields
Collection / End Collection Main 10 / 12 A1 / C0 (no data) Opens / closes a group
Usage Page Global 0 05 Sets the page for later usages
Logical Minimum / Maximum Global 1 / 2 15 / 25 Value range (signed)
Report Size Global 7 75 Bits per field
Report ID Global 8 85 Starts a numbered report
Report Count Global 9 95 Number of fields
Usage Local 0 09 Meaning of the next field(s)
Usage Minimum / Maximum Local 1 / 2 19 / 29 A range of usages, e.g. buttons 1–8

Prefix bytes are computed from the tag/type/size layout above; add 1 to the prefix for 2 data bytes and 2 for 4 data bytes (for example 06 is a 2-byte Usage Page).

Signed or unsigned? The 0xFF trap

Logical and physical extents are two’s-complement numbers sized by the item, and a field is treated as signed unless both Logical Minimum and Logical Maximum are zero or positive (§5.8 and §6.2.2.7). That has a practical consequence that catches many firmware authors: 25 FF means Logical Maximum = −1, not 255. To declare 0–255 you need two data bytes, 26 FF 00. Local items, by contrast, are always unsigned.

Worked example: designing a descriptor for a small custom device

Suppose you are building a USB accessory with two push-buttons and an 8-bit sensor reading, plus a way for the PC to send it a 63-byte command block and to read or write a signed calibration offset. You want an application on the PC to talk to it directly, without the operating system treating it as a keyboard or mouse. A sensible design:

  • Put everything in an application collection on a vendor-defined usage page (0xFF00–0xFFFF are reserved for vendors in the Usage Tables). Windows opens keyboard and mouse collections exclusively for its input stack; a vendor collection is left for applications.
  • Use three Report IDs, one per report type, so each report has a predictable length.

Built with the encoder from the code section below, the descriptor is 68 bytes:

// An original example: a vendor-defined HID device with three reports.
//   ID 1  Input   : 2 buttons + 6 padding bits + one 8-bit reading (0..255)
//   ID 2  Output  : 63-byte opaque command block (64 bytes with the ID)
//   ID 3  Feature : one signed 16-bit calibration offset (-1000..1000)
import { encode } from './hid-descriptor.mjs';

export const exampleDescriptor = encode([
  ['Usage Page', 0xff00],          // vendor-defined page: no OS input stack claims it
  ['Usage', 0x01],
  ['Collection', 0x01],            // Application
  ['Report ID', 1],
  ['Usage Page', 0x09],            // Button
  ['Usage Minimum', 1],
  ['Usage Maximum', 2],
  ['Logical Minimum', 0],
  ['Logical Maximum', 1],
  ['Report Size', 1],
  ['Report Count', 2],
  ['Input', 0x02],                 // Data,Var,Abs
  ['Report Size', 6],
  ['Report Count', 1],
  ['Input', 0x01],                 // Const: padding to a byte boundary
  ['Usage Page', 0xff00],
  ['Usage', 0x02],
  ['Logical Maximum', 255],        // needs 2 bytes: 0xFF alone would mean -1
  ['Report Size', 8],
  ['Input', 0x02],
  ['Report ID', 2],
  ['Usage', 0x03],
  ['Report Count', 63],
  ['Output', 0x02],
  ['Report ID', 3],
  ['Usage', 0x04],
  ['Logical Minimum', -1000],
  ['Logical Maximum', 1000],
  ['Report Size', 16],
  ['Report Count', 1],
  ['Feature', 0x02],
  ['End Collection'],
]);

Decoding it gives an annotated listing, plus the length of every report exactly as the host will see it:

68 bytes: 06 00 ff 09 01 a1 01 85 01 05 09 19 01 29 02 15 00 25 01 75 01 95 02 81 02 75 06 95 01 81 01 06 00 ff 09 02 26 ff 00 75 08 81 02 85 02 09 03 95 3f 91 02 85 03 09 04 16 18 fc 26 e8 03 75 10 95 01 b1 02 c0

   0  06 00 ff        Usage Page (Vendor-defined 0xFF00)
   3  09 01           Usage (0x01)
   5  a1 01           Collection (Application)
   7  85 01             Report ID (1)
   9  05 09             Usage Page (Button)
  11  19 01             Usage Minimum (0x01)
  13  29 02             Usage Maximum (0x02)
  15  15 00             Logical Minimum (0)
  17  25 01             Logical Maximum (1)
  19  75 01             Report Size (1)
  21  95 02             Report Count (2)
  23  81 02             Input (Data,Var,Abs)
  25  75 06             Report Size (6)
  27  95 01             Report Count (1)
  29  81 01             Input (Const,Array,Abs)
  31  06 00 ff          Usage Page (Vendor-defined 0xFF00)
  34  09 02             Usage (0x02)
  36  26 ff 00          Logical Maximum (255)
  39  75 08             Report Size (8)
  41  81 02             Input (Data,Var,Abs)
  43  85 02             Report ID (2)
  45  09 03             Usage (0x03)
  47  95 3f             Report Count (63)
  49  91 02             Output (Data,Var,Abs)
  51  85 03             Report ID (3)
  53  09 04             Usage (0x04)
  55  16 18 fc          Logical Minimum (-1000)
  58  26 e8 03          Logical Maximum (1000)
  61  75 10             Report Size (16)
  63  95 01             Report Count (1)
  65  b1 02             Feature (Data,Var,Abs)
  67  c0              End Collection

Report lengths (bytes on the wire, including Report ID):
  Input   ID 1: 16 bits -> 3 bytes
  Output  ID 2: 504 bits -> 64 bytes
  Feature ID 3: 16 bits -> 3 bytes

Points worth noticing in that listing:

  • The six-bit Constant Input at offset 29 is padding, so the reading starts on a byte boundary. HID packs fields at bit level with no automatic alignment; a field with no usage acts as padding (§6.2.2.9 and §8.1).
  • Report Size 8 and Logical Maximum 255 are Global, so the Output report at offset 49 inherits them; only the Usage and Report Count had to change.
  • The Report ID adds one byte to each report: the 504-bit Output report is 63 data bytes plus the ID, which makes exactly 64 bytes.

What the reports look like on the wire

Bits are filled from the least significant bit of each byte upward, and multi-byte values are little-endian (§5.8). With button 1 pressed, button 2 released and a reading of 200, the device’s Input report is:

01 01 C8
|  |  +-- reading = 0xC8 = 200
|  +----- bit 0 = button 1 (pressed), bit 1 = button 2 (released), bits 2-7 padding
+-------- Report ID 1

To set a calibration offset of −250 the host sends Feature report 3. In 16-bit two’s complement −250 is 0xFF06, sent low byte first: 03 06 FF. Because Feature reports use the control pipe, the 8-byte SETUP packet for reading that report back is built from the HID class-request format (§7.2): request type A1 (class, interface, device-to-host), GET_REPORT = 01, wValue = report type in the high byte (Input 1, Output 2, Feature 3) and Report ID in the low byte, wIndex = interface number, wLength = report length:

A1 01 03 03 00 00 03 00
|  |  |  |  +-+-- wIndex  = interface 0
|  |  |  +------- wValue high byte: report type 3 (Feature)
|  |  +---------- wValue low byte:  Report ID 3
|  +------------- bRequest GET_REPORT
+---------------- bmRequestType: class request to interface, device-to-host
                  (last two bytes: wLength = 3, little-endian)
Class request bRequest Direction Required?
GET_REPORT 0x01 Device → host All HID devices
GET_IDLE 0x02 Device → host Keyboards
GET_PROTOCOL 0x03 Device → host Boot devices
SET_REPORT 0x09 Host → device Optional; required for Output reports if the device declares any
SET_IDLE 0x0A Host → device Keyboards
SET_PROTOCOL 0x0B Host → device Boot devices

Request codes from HID 1.11 §7.2; the “Required?” column summarises its Appendix G for boot and non-boot keyboards and mice.

Polling interval, packet size and idle rate

An interrupt endpoint is not an interrupt in the CPU sense: the host polls it. The endpoint descriptor’s bInterval sets the period. For low- and full-speed interrupt endpoints it is a value from 1 to 255 in 1 ms frames; for high-speed endpoints it is an exponent, giving a period of 2^(bInterval − 1) microframes of 125 µs each (USB 2.0 §9.6.6). Hosts may round it: Microsoft documents that Windows polls full-speed interrupt endpoints at a power-of-two period (1, 2, 4, 8, 16 or 32 ms) and low-speed ones at 8, 16 or 32 ms, depending on the requested value.

Packet size limits come from USB itself: an interrupt packet carries at most 8 bytes at low speed, 64 bytes at full speed and 1024 bytes at high speed (USB 2.0 §5.7.3). A report larger than one packet is simply split across several transactions (HID 1.11 §8.4), so 64 bytes is a convenient size, not a HID limit. Windows’ HID class driver accepts reports of just under 8 KB (8 KB minus one bit).

The idle rate controls repetition when nothing changes. SET_IDLE makes the device NAK polls while its report is unchanged, either indefinitely (duration 0) or until a timeout in 4 ms steps up to 1.02 s. HID 1.11 recommends a default of 500 ms for keyboards and infinity for mice and joysticks (§7.2.4).

Boot protocol vs report protocol

A PC’s firmware setup screen cannot afford a full descriptor parser, so HID defines a fixed fallback. An interface with subclass 1 (boot interface) and protocol 1 (keyboard) or 2 (mouse) promises to support a predefined report layout; with subclass 0, the protocol field is 0 (§4.2–4.3). The host can switch between the fixed layout (boot protocol) and the device’s own descriptor (report protocol) with SET_PROTOCOL. Devices start in report protocol, but the specification tells hosts not to assume this and to set the protocol they want (§7.2.6).

The boot keyboard report is 8 bytes: a modifier bitmap (bit 0 Left Ctrl, bit 1 Left Shift, bit 2 Left Alt, bit 3 Left GUI, bits 4–7 the right-hand equivalents), a reserved byte, and up to six key codes from the Keyboard/Keypad usage page (§8.3 and Appendix B). Typing a capital A with the left Shift key looks like this:

02 00 04 00 00 00 00 00   Left Shift held + usage 0x04 ("Keyboard a and A")
00 00 00 00 00 00 00 00   all keys released

The report lists the keys that are currently down, not key-down/key-up events; the host works out presses and releases by comparing reports, and the host, not the keyboard, implements auto-repeat. If more keys are held than there are slots, the keyboard fills every slot with 0x01 (ErrorRollOver) (Appendix C). A boot mouse report is three bytes: buttons, then signed X and Y movement.

Usage pages you will meet in practice

Page Name (HUT 1.7) Typical use
0x01 Generic Desktop Mouse, keyboard, joystick and gamepad collections; X, Y, wheel axes
0x07 Keyboard/Keypad Key codes and modifier keys
0x08 LED Num/Caps/Scroll Lock indicators
0x09 Button Numbered buttons
0x0C Consumer Media and volume keys (Windows treats this collection as shared)
0x0D Digitizers Pens, touchscreens, precision touchpads
0x20 Sensors Ambient light, motion and other sensors
0x84 Power UPS and battery devices
0x8C Barcode Scanner Native (non-keyboard) barcode scanners
0x8E Magnetic Stripe Reader Native card-swipe readers
0xF1D0 FIDO Alliance Security keys (U2F authenticator device)
0xFF00–0xFFFF Vendor-defined Custom devices and vendor-specific functions

Page numbers and names from HID Usage Tables 1.7 Table 3.1; “typical use” from the same document and Microsoft’s list of HID clients supported in Windows.

HID beyond USB

The report descriptor and report model do not depend on USB. Windows ships HID transport minidrivers for USB (Hidusb.sys), Bluetooth (Hidbth.sys), Bluetooth LE (HidBthLE.dll), I²C (Hidi2c.sys, introduced in Windows 8 for SoC platforms), GPIO buttons and, since Windows 11, SPI. Linux’s HID subsystem is likewise a bus that USB, Bluetooth, I²C and user-space transport drivers plug into. For an application the practical effect is that the same descriptor and the same reports arrive whether the keyboard is wired, wireless or built into a laptop on an I²C bus.

How each operating system exposes HID to your code

Platform Find and open Read / write reports Notes
Windows SetupDi* to find the collection, CreateFile to open it ReadFile / WriteFile; HidD_GetFeature, HidD_SetFeature, HidD_GetInputReport; HidP_* to parse fields Buffers start with the Report ID (0 if unused). Keyboard and mouse collections are opened exclusively by the system.
Linux /dev/hidrawN (locate with libudev) read() / write(); ioctls for the report descriptor, raw info and feature reports Report descriptor also at /sys/bus/hid/devices/<id>/report_descriptor. /dev/uhid creates virtual HID devices from user space.
macOS IOHIDManager (IOKit) with device-matching criteria Input-report and input-value callbacks The newer Core HID framework (macOS 15 and later) also supports virtual devices for testing.
Web browsers WebHID navigator.hid.requestDevice() Input-report events, send output and feature reports Draft specification. The user must pick the device in a chooser; a blocklist restricts sensitive devices and reports.

Two details save debugging time. First, Microsoft warns that polling with HidD_GetInputReport can lose reports and that some devices hang on it; use ReadFile for streaming input and the HidD_ calls only for current state. Second, on Windows the first byte of a report buffer is always the Report ID — 0 for a device without numbered reports — with the report data starting at the second byte. Linux hidraw follows the same rule for write(), but read() only prefixes the Report ID when the device uses numbered reports; otherwise the data starts at the first byte.

Decode a report descriptor in Node.js

The Linux kernel documentation recommends using an existing parser rather than decoding descriptors by hand, and it shows where to get the raw bytes (sysfs or the hidraw HIDIOCGRDESC ioctl). The module below is a deliberately small, dependency-free decoder for exactly that job: it walks the short items, names them, applies the signed/unsigned rules, rejects long items and truncated input with the byte offset, and totals the bits per report type and Report ID so you can check the buffer sizes your application must use. It also includes an encoder that picks the smallest data size for each value, which is how the example descriptor above was built.

// HID report descriptor decoder/encoder (short items only).
// Item encoding per USB-IF "Device Class Definition for HID" 1.11:
//   section 5.3 / 6.2.2.2  prefix byte = bTag(7..4) | bType(3..2) | bSize(1..0)
//   bSize 0,1,2,3 -> 0,1,2,4 data bytes, little-endian (section 5.8)
//   section 6.2.2.3        long item prefix 0xFE; no long items are defined
//   sections 6.2.2.4/7/8   Main, Global and Local tag numbers
// Written by the Ambimat engineering team. No dependencies.

const TYPES = ['Main', 'Global', 'Local', 'Reserved'];
const DATA_BYTES = [0, 1, 2, 4];

const TAGS = {
  Main: { 8: 'Input', 9: 'Output', 10: 'Collection', 11: 'Feature', 12: 'End Collection' },
  Global: {
    0: 'Usage Page', 1: 'Logical Minimum', 2: 'Logical Maximum', 3: 'Physical Minimum',
    4: 'Physical Maximum', 5: 'Unit Exponent', 6: 'Unit', 7: 'Report Size', 8: 'Report ID',
    9: 'Report Count', 10: 'Push', 11: 'Pop',
  },
  Local: {
    0: 'Usage', 1: 'Usage Minimum', 2: 'Usage Maximum', 3: 'Designator Index',
    4: 'Designator Minimum', 5: 'Designator Maximum', 7: 'String Index',
    8: 'String Minimum', 9: 'String Maximum', 10: 'Delimiter',
  },
};

// Global items whose data is a two's-complement number (sections 5.8 and 6.2.2.7).
// Everything else is unsigned; section 6.2.2.8 says all Local items are unsigned.
const SIGNED = new Set(['Logical Minimum', 'Logical Maximum', 'Physical Minimum',
  'Physical Maximum', 'Unit Exponent']);

const COLLECTIONS = ['Physical', 'Application', 'Logical', 'Report', 'Named Array',
  'Usage Switch', 'Usage Modifier'];

// A few Usage Page names for readable output (USB-IF HID Usage Tables 1.7, Table 3.1).
const PAGES = { 0x01: 'Generic Desktop', 0x07: 'Keyboard/Keypad', 0x08: 'LED',
  0x09: 'Button', 0x0c: 'Consumer', 0x0d: 'Digitizers', 0xf1d0: 'FIDO Alliance' };

const hex = (n, w = 2) => '0x' + n.toString(16).toUpperCase().padStart(w, '0');

function readValue(bytes, signed) {
  let v = 0;
  for (let i = bytes.length - 1; i >= 0; i--) v = v * 256 + bytes[i]; // little-endian
  const bits = bytes.length * 8;
  if (signed && bits && v >= 2 ** (bits - 1)) v -= 2 ** bits;
  return v;
}

const FLAG_NAMES = [['Data', 'Const'], ['Array', 'Var'], ['Abs', 'Rel'], [null, 'Wrap'],
  [null, 'NonLinear'], [null, 'NoPreferred'], [null, 'Null'], [null, 'Volatile'],
  [null, 'Buffered']];

function describe(name, value, size) {
  if (name === 'Input' || name === 'Output' || name === 'Feature') {
    const out = [];
    FLAG_NAMES.forEach(([off, on], bit) => {
      if (name === 'Input' && bit === 7) return; // bit 7 is reserved for Input
      const label = value & (1 << bit) ? on : off;
      if (label) out.push(label);
    });
    return out.join(',');
  }
  if (name === 'Collection') {
    if (value < COLLECTIONS.length) return COLLECTIONS[value];
    return value >= 0x80 ? `Vendor ${hex(value)}` : `Reserved ${hex(value)}`;
  }
  if (name === 'Usage Page') {
    if (value >= 0xff00 && value <= 0xffff) return `Vendor-defined ${hex(value, 4)}`;
    return PAGES[value] ? `${PAGES[value]}` : hex(value, 4);
  }
  if (name.startsWith('Usage') && size === 4) { // extended usage: page in high 16 bits
    return `${hex(Math.floor(value / 0x10000), 4)}:${hex(value % 0x10000, 4)}`;
  }
  if (name.startsWith('Usage') || name === 'Unit') return hex(value);
  return String(value);
}

// decode(bytes) -> array of { offset, raw, type, tag, name, size, value, text }
// Throws RangeError on truncated items and Error on long items.
export function decode(bytes) {
  const items = [];
  let i = 0;
  while (i < bytes.length) {
    const prefix = bytes[i];
    if (prefix === 0xfe) {
      throw new Error(`long item at offset ${i}: HID 1.11 defines no long items (section 6.2.2.3)`);
    }
    const size = DATA_BYTES[prefix & 0x03];
    const type = TYPES[(prefix >> 2) & 0x03];
    const tag = prefix >> 4;
    if (i + 1 + size > bytes.length) {
      throw new RangeError(`item at offset ${i} needs ${size} data byte(s), only ${bytes.length - i - 1} left`);
    }
    const data = Array.from(bytes.slice(i + 1, i + 1 + size));
    const name = TAGS[type]?.[tag] ?? `Reserved ${type} tag ${tag}`;
    let value = readValue(data, SIGNED.has(name));
    // Unit Exponent: a 1-byte value 0x0-0xF is the 4-bit code from the section 6.2.2.7 table.
    if (name === 'Unit Exponent' && size === 1 && data[0] <= 0x0f) {
      value = data[0] >= 8 ? data[0] - 16 : data[0];
    }
    // Main items with no data bytes still carry a value: zero (section 6.2.2.4 remarks).
    const showValue = size > 0 || ['Input', 'Output', 'Feature', 'Collection'].includes(name);
    items.push({ offset: i, raw: Array.from(bytes.slice(i, i + 1 + size)), type, tag, name, size,
      value, text: showValue ? `${name} (${describe(name, value, size)})` : name });
    i += 1 + size;
  }
  return items;
}

// format(items) -> human-readable listing, indented by collection depth.
export function format(items) {
  let depth = 0;
  return items.map((it) => {
    if (it.name === 'End Collection') depth = Math.max(0, depth - 1);
    const hexBytes = it.raw.map((b) => b.toString(16).padStart(2, '0')).join(' ');
    const line = `${String(it.offset).padStart(4)}  ${hexBytes.padEnd(15)} ${'  '.repeat(depth)}${it.text}`;
    if (it.name === 'Collection') depth++;
    return line;
  }).join('\n');
}

// reportBits(items) -> { Input: {id: bits}, Output: {...}, Feature: {...} }
// Sums Report Size x Report Count per Main item, honouring Report ID and Push/Pop
// (sections 5.4, 6.2.2.7, 8.1). Report ID 0 means "no Report IDs used".
export function reportBits(items) {
  const out = { Input: {}, Output: {}, Feature: {} };
  let g = { size: 0, count: 0, id: 0 };
  const stack = [];
  for (const it of items) {
    if (it.name === 'Report Size') g.size = it.value;
    else if (it.name === 'Report Count') g.count = it.value;
    else if (it.name === 'Report ID') g.id = it.value;
    else if (it.name === 'Push') stack.push({ ...g });
    else if (it.name === 'Pop') g = stack.pop() ?? g;
    else if (it.name in out) out[it.name][g.id] = (out[it.name][g.id] ?? 0) + g.size * g.count;
  }
  return out;
}

// Report length in bytes as seen on the wire: data padded to a byte boundary
// (section 8.4) plus one byte for the Report ID prefix when IDs are used (section 8.2).
export const reportBytes = (bits, id) => Math.ceil(bits / 8) + (id ? 1 : 0);

const NAME_TO_PREFIX = {};
for (const [t, tags] of Object.entries(TAGS)) {
  for (const [tag, name] of Object.entries(tags)) NAME_TO_PREFIX[name] = (Number(tag) << 4) | (TYPES.indexOf(t) << 2);
}

// encode([[name, value?], ...]) -> Uint8Array, choosing the smallest data size
// that holds the value (signed range for SIGNED items, unsigned otherwise).
export function encode(list) {
  const out = [];
  for (const [name, value] of list) {
    const base = NAME_TO_PREFIX[name];
    if (base === undefined) throw new Error(`unknown item name: ${name}`);
    if (value === undefined) { out.push(base); continue; }
    const signed = SIGNED.has(name);
    if (!Number.isInteger(value) || (!signed && value < 0)) throw new RangeError(`${name}: bad value ${value}`);
    const n = [1, 2, 4].find((b) => signed
      ? value >= -(2 ** (8 * b - 1)) && value < 2 ** (8 * b - 1)
      : value < 2 ** (8 * b));
    if (!n) throw new RangeError(`${name}: ${value} does not fit in 4 bytes`);
    let v = value < 0 ? value + 2 ** (8 * n) : value;
    out.push(base | (n === 4 ? 3 : n));
    for (let k = 0; k < n; k++) { out.push(v % 256); v = Math.floor(v / 256); }
  }
  return Uint8Array.from(out);
}

The accompanying test file (node --test, 14 tests) checks the prefix split, all four data sizes, two’s-complement extents at 1, 2 and 4 bytes (including the 25 FF = −1 case), unsigned Local items and extended usages, both Unit Exponent encodings, every Main-item flag, reserved tags, long-item rejection, truncated input, encoder round-trips and Push/Pop handling. To decode a real device on Linux, read /sys/bus/hid/devices/<id>/report_descriptor with fs.readFileSync and pass the bytes to decode().

Security: a keyboard is trusted input

Keyboard and mouse events are treated as a proxy for what the user intends — the WebHID specification calls them “trusted input”. A device that can generate them can act with the user’s authority, which is the root of most HID security issues.

  • Keystroke injection. In 2014 SRLabs (“BadUSB — On accessories that turn evil”) showed that the firmware of USB thumb drives could be reprogrammed so a device re-enumerates as a different device, and that keyboard emulation alone was enough to infect a machine and escalate privileges without a software vulnerability. An unknown USB device that suddenly announces a keyboard interface deserves suspicion.
  • Card readers that “type”. Some card readers run as keyboard emulators so that applications built for keyboard input can receive card data. MagTek’s manual for its keyboard-emulation swipe reader warns that a key pressed on another keyboard during a swipe can corrupt the data, and recommends its vendor-defined HID mode for new development, where the application talks to the reader directly.
  • Configuration and firmware channels. The WebHID specification notes that HID devices commonly accept firmware updates through custom reports, that untrusted updates could make a device masquerade as a different device, and that programmable macros on keyboards and mice must not be re-programmable by a malicious app. Device makers should authenticate firmware and protect against downgrades.

Mitigations are platform-specific. Windows opens keyboard and mouse collections exclusively so other software cannot intercept global input. Linux can deauthorise new USB devices by default through the authorized_default sysfs attribute and let user space approve them. Browsers that implement WebHID require an explicit user choice per device and block sensitive device classes. For your own device, prefer a vendor-defined collection over keyboard emulation when an application, not a person, is the consumer of the data.

Frequently asked questions

What is a HID device?

A HID (human interface device) is a device that follows the HID standard for describing its own controls and data — most commonly a keyboard, mouse, touchpad, game controller or media-key panel, but also barcode scanners, sensors, security keys and many custom gadgets. Because it describes itself, the operating system can use it with built-in drivers.

What does “HID-compliant device” mean in Windows Device Manager?

It means the device presented a valid HID descriptor, so Windows loaded its own HID minidriver and HID class driver instead of needing a vendor driver; Microsoft’s driver documentation uses “HID-compliant” in exactly this sense. Windows creates a separate device for each top-level collection in the report descriptor, which is why one physical product (for example a keyboard with media keys, or a touchscreen) often shows several “HID-compliant” entries.

Is a HID device safe? Can I disable it?

An entry under Human Interface Devices is normal and is not malware in itself; most are your keyboard, touchpad, touchscreen, headset buttons or similar. The real risk is a device that pretends to be a keyboard (see the security section above). Disabling an entry in Device Manager leaves the hardware connected but disables its driver until you re-enable it, so disabling the wrong one can switch off your touchscreen, touchpad or keyboard. Check which physical device an entry belongs to before disabling it.

What is the difference between HID and USB?

USB is a bus that carries many classes of device (storage, audio, video, HID and more). HID is one of those classes, identified by interface class code 3 on USB. HID is not tied to USB: the same model runs over Bluetooth, Bluetooth LE, I²C and SPI.

What is the USB HID protocol?

It is the combination of a report descriptor that the device sends once, Input/Output/Feature reports that carry data, and a small set of class requests (GET_REPORT, SET_REPORT, GET/SET_IDLE, GET/SET_PROTOCOL) on the control pipe, with input normally delivered on a polled interrupt IN endpoint. Boot-capable keyboards and mice also support a fixed “boot protocol” layout for firmware that cannot parse descriptors.

Can a USB HID device send more than 64 bytes?

Yes. 64 bytes is the full-speed interrupt packet limit, not a HID limit; a longer report is split across several transactions, and high-speed endpoints allow up to 1024 bytes per packet. Windows’ HID class driver handles reports of just under 8 KB.

Why does my custom HID device not appear to my application on Windows?

If its top-level collection uses a keyboard or mouse usage, Windows opens it exclusively for system input, so your application cannot read it. Put the data your application needs in a vendor-defined collection (usage page 0xFF00–0xFFFF).

Related reading on Ambimat

Sources

  • USB Implementers Forum, Device Class Definition for Human Interface Devices (HID), Firmware Specification, version 1.11, 27 June 2001 — §§4.1–4.4, 5.3–5.10, 6.2.1–6.2.2.9, 7.1–7.2.6, 8.1–8.4, Appendices B, C and G. usb.org
  • USB Implementers Forum, HID Usage Tables for Universal Serial Bus (USB), version 1.7, 26 January 2026 — Table 3.1 (Usage Page Summary), §3.1, Keyboard/Keypad, Magnetic Stripe Reader and FIDO Alliance pages. usb.org
  • USB Implementers Forum, Universal Serial Bus Specification, revision 2.0, 27 April 2000 — §5.7.3 (interrupt packet sizes) and §9.6.6, Table 9-13 (bInterval). usb.org
  • Microsoft, Develop Windows device drivers for Human Interface Devices (HID), Microsoft Learn, updated 2024-10-22. learn.microsoft.com
  • Microsoft, HID architecture, Microsoft Learn, updated 2024-06-27 — HID clients supported in Windows, access modes. learn.microsoft.com
  • Microsoft, HID Transport Overview, Microsoft Learn, updated 2024-06-27 — in-box transports and report-length limits. learn.microsoft.com
  • Microsoft, Architecture and overview (HID over USB), Microsoft Learn, updated 2024-10-22. learn.microsoft.com
  • Microsoft, Introduction to HID over I2C, Microsoft Learn, updated 2024-01-11. learn.microsoft.com
  • Microsoft, Top-level collections, Microsoft Learn, updated 2024-06-27. learn.microsoft.com
  • Microsoft, Opening HID collections, Microsoft Learn, updated 2024-10-22. learn.microsoft.com
  • Microsoft, Obtaining HID Reports and Sending HID Reports, Microsoft Learn, updated 2024-09-20. learn.microsoft.com
  • Microsoft, USB_ENDPOINT_DESCRIPTOR structure (usbspec.h), Microsoft Learn, updated 2021-04-01 — bInterval polling-period tables. learn.microsoft.com
  • Microsoft, HIDUSBFX2 sample driver (Windows driver samples), Microsoft Learn, 2024-07-02 — “Theory of operation”. learn.microsoft.com
  • Microsoft, Disable a device, Windows Server 2003 product help (previous versions), 2009-10-08. learn.microsoft.com
  • Linux kernel documentation, Introduction to HID report descriptors (Documentation/hid/hidintro.rst). docs.kernel.org
  • Linux kernel documentation, HIDRAW – Raw Access to USB and Bluetooth Human Interface Devices (Documentation/hid/hidraw.rst). docs.kernel.org
  • Linux kernel documentation, UHID – User-space I/O driver support for HID subsystem (Documentation/hid/uhid.rst). docs.kernel.org
  • Linux kernel documentation, HID I/O Transport Drivers (Documentation/hid/hid-transport.rst). docs.kernel.org
  • Linux kernel documentation, Authorizing (or not) your USB devices to connect to the system (Documentation/usb/authorization.rst). docs.kernel.org
  • Apple, IOHIDManager.h, Apple Developer Documentation (IOKit). developer.apple.com
  • Apple, Core HID, Apple Developer Documentation (macOS 15.0+). developer.apple.com
  • M. Reynolds (ed.), WebHID API, W3C Web Platform Incubator Community Group, Draft Community Group Report, 13 September 2024 — §3 Security and privacy considerations, §15 The HID Blocklist. wicg.github.io
  • K. Nohl, S. Krißler, J. Lell (SRLabs), BadUSB — On accessories that turn evil, Black Hat USA 2014 slides — slides 6, 8 and 11 (Wayback Machine capture of srlabs.de). web.archive.org
  • MagTek, USB KB SureSwipe & USB KB Swipe Reader Technical Reference Manual, D99875206 Rev 20, April 2020 — introduction. magtek.com
  • MagTek, USB HID SureSwipe & USB HID Swipe Reader Technical Reference Manual, D99875191 Rev 20, April 2020 — introduction. magtek.com

Written by the Ambimat engineering team. Code tested on Node.js 25.8.2.