I have an Arduino Pro Micro (ATMega32u4) that I want to use as an IR receiver for a media PC and need to send codes like Volume Up, Media Home, Play, etc. But when I look up the scan codes for those and send them to Keyboard.press() it just prints out '0' every time.

Any way to send those keyboard commands?

  • 1
    Include your sketch. Provide a ref for where you looked up the scan codes etc, as most charts don't have media keys listed Commented Feb 28, 2015 at 18:43
  • I used an app I wrote in C# a long time ago to capture scan codes. Media Home is 128 apparently.
    – Adam Haile
    Commented Mar 2, 2015 at 5:40

5 Answers 5


You have to do some tweaks to the usbkeyboard libraries that comes with the IDE.

For a video tutorial, take a look at this:

Arduino Leonardo as a multimedia keyboard

Which uses the information from this site, if you prefer text only:

Arduino Leonardo multimedia keys

The Leonardo and ProMicro both use the ATMega32u4 and should both work with theses tweaks. If you need more functionality than the ones that this tweak gives you, you have to dig down into usb descriptors on usb.org for the commands you need.

  • What is the meaning of "4" for the first parameter to HID_SendReport()? Is it for pretending to be an IR remote control? Commented May 5, 2020 at 0:42
  • OK, after a long, long search I found it: 4 is for "Consumer control" (for "report ID"). A more complete list (only 3 is left out) is in a library, with symbolic names: 1: Mouse, 2: Keyboard, 4: Consumer control (with the volume controls/multimedia control), 5: System control, 6: Gamepad, 7: Mouse absolute, 8: NKRO keyboard, 9: Teensy keyboard, 10: Surface dial Commented May 5, 2020 at 15:26
  • Though the report IDs may be arbitrary (self-defined)? Commented May 6, 2020 at 11:51

It is indeed possible and also without the requirement to modify the Arduino environment itself. Just register an HID descriptor for the "report ID" for "Consumer Control" (the code is 4 (constant kHID_ReportID_ConsumerControl), where the "report ID" code for keyboard and mouse are 2 and 1, respectively):

static HIDSubDescriptor node(HID_ReportDescriptor_ConsumerControl,


After the registration, HID().SendReport() now works for "report ID" equal to 4 (the first parameter), and the audio volume can be changed, etc.

The registration needs to done once and can be put into the init() function, for instance, after the Keyboard.begin(); and Mouse.begin(); calls.


Example usage, increasing the audio volume by one step:

uint8_t data[2];
data[0] = kVolumeUp;
data[1] = 0;
HID().SendReport(kHID_ReportID_ConsumerControl, data, 2);

The constants are at the end.


"Consumer Control" has the multimedia controls Volume up, Volume down, Mute, Play, etc.

The registration is similar to the way the constructors for the built-in keyboard and mouse libraries do it. HID_ReportDescriptor_ConsumerControl and constants are listed at the end because it is quite long, but they must come first (for instance, in the beginning of the file).

All this could be packaged into a reusable form. Here I have demonstrated it in a paste-and-forget form to keep it short.

HID().SendReport() looks strange, but HID() is a global function that returns an instance of the class HID_ (all built into the standard Arduino libraries, presumably to avoid some initialisation order problems with global/static objects - I thought Scott Meyers had solved that a long time ago (47. in the first book)).

HID().SendReport() can also be used to circumvent the limitation of a presumption of a US keyboard. E.g. using code 100, page 56 (and kHID_ReportID_Keyboard, like in the standard class keyboard_) to emulate the 102th key on European keyboards (e.g. a Spanish keyboard).

Only volume up and volume down were tested.

Definition of HID_ReportDescriptor_ConsumerControl and constants:

// For direct use of USB HID (yes, 3 is left out)
const uint8_t kHID_ReportID_Mouse           =  1;
const uint8_t kHID_ReportID_Keyboard        =  2;
const uint8_t kHID_ReportID_ConsumerControl =  4;
const uint8_t kHID_ReportID_SystemControl   =  5;
const uint8_t kHID_ReportID_Gamepad         =  6;
const uint8_t kHID_ReportID_MouseAbsolute   =  7;
const uint8_t kHID_ReportID_NKRO_keyboard   =  8;
const uint8_t kHID_ReportID_TeensyKeyboard  =  9;
const uint8_t kHID_ReportID_SurfaceDial     = 10;

// For use in "Consumer Control". They are bit coded.
const uint8_t kVolumeUp   = 1 << 0; // Bit 0
const uint8_t kVolumeDown = 1 << 1; // Bit 1
const uint8_t kVolumeMute = 1 << 2; // Bit 2

static const uint8_t HID_ReportDescriptor_ConsumerControl[] PROGMEM =
    0x05, 0x0c,                           // Usage Page ("Consumer Devices")
    0x09, 0x01,                           // Usage (Consumer Control)
    0xa1, 0x01,                           // Collection (Application)
    0x85, kHID_ReportID_ConsumerControl,  // "Report ID"
    0x15, 0x00,                           // Logical Minimum (0)
    0x25, 0x01,                           // Logical Maximum (1)
    0x09, 0xe9,                           // Usage (Volume Up)
    0x09, 0xea,                           // Usage (Volume Down)
    0x75, 0x01,                           // Report Size (1)
    0x95, 0x02,                           // Report Count (2)
    0x81, 0x06,                           // Input (Data, Variable, Relative)

    0x09, 0xe2,                           // Usage (Mute)
    0x95, 0x01,                           // Report Count (1)
    0x81, 0x06,                           // Input (Data, Variable, Relative)

    0x09, 0xb0,                           // Usage (Play)
    0x95, 0x01,                           // Report Count (1)
    0x81, 0x06,                           // Input (Data, Variable, Relative)

    0x09, 0xb1,                           // Usage (Pause)
    0x95, 0x01,                           // Report Count (1)
    0x81, 0x06,                           // Input (Data, Variable, Relative)

    0x09, 0xb7,                           // Usage (Stop)
    0x95, 0x01,                           // Report Count (1)
    0x81, 0x06,                           // Input (Data, Variable, Relative)

    0x09, 0xb5,                           // Usage (Next)
    0x95, 0x01,                           // Report Count (1)
    0x81, 0x06,                           // Input (Data, Variable, Relative)

    0x09, 0xb6,                           // Usage (Previous)
    0x95, 0x01,                           // Report Count (1)
    0x81, 0x06,                           // Input (Data, Variable, Relative)

    0x09, 0xb3,                           // Usage (Fast Forward)
    0x95, 0x01,                           // Report Count (1)
    0x81, 0x06,                           // Input (Data, Variable, Relative)

    0x09, 0xb4,                           // Usage (Rewind)
    0x95, 0x01,                           // Report Count (1)
    0x81, 0x06,                           // Input (Data, Variable, Relative)

    0x95, 0x06,                           // Report Count (6) Number of bits remaining in byte
    0x81, 0x07,                           // Input (Constant, Variable, Relative)
    0xc0                                  // End Collection
}; // Table for USB HID "Consumer Control" (for audio volume control, etc.)


This was tested with:

  • Arduino Leonardo (already used as a macro keyboard, using an old PS/2 keyboard. Two new macro keys were assigned for testing audio volume up / down).
  • The Arduino Leonardo connected to an extra current port of a D-Link USB hub (necessary, for unknown reasons - probably a local problem)
  • Arduino IDE v. 1.18.10
  • Linux, Ubuntu 19.10 (Eoan Ermine), but with the buggy GNOME replaced by Cinnamon.
  • I had the same problem, and the existing information here was not sufficient. Later, I found and tested a solution. And here is the result! Commented May 6, 2020 at 0:08
  • Some trivia: This started because the volume wheel on the keyboard exhibited strange behaviour. The emulation was an effort to try to isolate the problem. It turned out it is most likely a mechanical issue (it has had problems before with some keys (that resoldering solved)). Commented May 6, 2020 at 1:34
  • Though, are the report IDs entirely arbitrary (self-defined)? Do they just happen to be 2 and 1 for keyboard and mouse in the Arduino standard library? Or are there some conventions/default values/standard? Commented May 6, 2020 at 12:10
  • 2
    Thank you for this recipe, it was the only one that worked for me! Using an Arduino Micro (not Pro) with an IR receiver+remote, working well on Ubuntu 20.04.
    – miguev
    Commented Sep 27, 2020 at 16:04
  • The Spanish keyboard reference was meant to be a reference to this how-to guide (on this site), but I will not bump it at this time. Commented Mar 4, 2022 at 6:10

If you are getting zeros, the most probably cause is it's the library for the IR. Reinstall or update the library. Sometimes you have to reinstall more than one time. That should work.

About media keys, the keyboard library doesn't have media keys functionality, so you need to use another one. This one worked for me: https://github.com/NicoHood/HID

After you install this library, you have to change your code. The code for pressing media keys is different. You have to use Consumer.write, for example, Consumer.write(MEDIA_VOLUME_UP) for volume up. You can find the name of every key here: https://github.com/NicoHood/HID/wiki/Consumer-API

For normal keys, just use Keyboard.write(). If you want, I can send you the code I made to do this.


Keyboard.press() presses a key and “holds it down” until a Keyboard.release() or releaseAll() is issued. That is, Keyboard.press() isn't appropriate for simulation of just tapping and releasing a key, unless you follow it with a delay and a release.

Also see Keyboard.write(). But note the following, from Keyboard.write()'s documentation:

Only ASCII characters that are on the keyboard are supported. For example, ASCII 8 (backspace) would work, but ASCII 25 (Substitution) would not.

On my linux system, xev (used via xev -event keyboard) shows the following data for media keys labeled as shown.

  Excerpt from info displayed by xev           Picture
keycode 180 (keysym 0x1008ff18, XF86HomePage)   house
keycode 179 (keysym 0x1008ff81, XF86Tools)   quarter-notes
keycode 163 (keysym 0x1008ff19, XF86Mail)      envelope
keycode 172 (keysym 0x1008ff14, XF86AudioPlay)  >/||
keycode 122 (keysym 0x1008ff11, XF86AudioLowerVolume)  speaker
keycode 123 (keysym 0x1008ff13, XF86AudioRaiseVolume)  speaker
keycode 121 (keysym 0x1008ff12, XF86AudioMute)  x'd-speaker
keycode 148 (keysym 0x1008ff1d, XF86Calculator)  Calculator
  • 1
    I know... I'm doing press();delay();releaseAll(). But if I give it code 128 for example, it does nothing.
    – Adam Haile
    Commented Mar 2, 2015 at 5:41

Peter's answer probably is the way to go from scratch. But if you want to start from somewhere there's also another library by BleKeyboard https://github.com/T-vK/ESP32-BLE-Keyboard/blob/master/BleKeyboard.cpp. It shows you pretty much the whole process how the media report is created.

// ------------------------------------------------- Media Keys
  USAGE_PAGE(1),      0x0C,          // USAGE_PAGE (Consumer)
  USAGE(1),           0x01,          // USAGE (Consumer Control)
  COLLECTION(1),      0x01,          // COLLECTION (Application)
  REPORT_ID(1),       MEDIA_KEYS_ID, //   REPORT_ID (3)
  USAGE_PAGE(1),      0x0C,          //   USAGE_PAGE (Consumer)
  LOGICAL_MINIMUM(1), 0x00,          //   LOGICAL_MINIMUM (0)
  LOGICAL_MAXIMUM(1), 0x01,          //   LOGICAL_MAXIMUM (1)
  REPORT_SIZE(1),     0x01,          //   REPORT_SIZE (1)
  REPORT_COUNT(1),    0x10,          //   REPORT_COUNT (16)
  USAGE(1),           0xB5,          //   USAGE (Scan Next Track)     ; bit 0: 1
  USAGE(1),           0xB6,          //   USAGE (Scan Previous Track) ; bit 1: 2
  USAGE(1),           0xB7,          //   USAGE (Stop)                ; bit 2: 4
  USAGE(1),           0xCD,          //   USAGE (Play/Pause)          ; bit 3: 8
  USAGE(1),           0xE2,          //   USAGE (Mute)                ; bit 4: 16
  USAGE(1),           0xE9,          //   USAGE (Volume Increment)    ; bit 5: 32
  USAGE(1),           0xEA,          //   USAGE (Volume Decrement)    ; bit 6: 64
  USAGE(2),           0x23, 0x02,    //   Usage (WWW Home)            ; bit 7: 128
  USAGE(2),           0x94, 0x01,    //   Usage (My Computer) ; bit 0: 1
  USAGE(2),           0x92, 0x01,    //   Usage (Calculator)  ; bit 1: 2
  USAGE(2),           0x2A, 0x02,    //   Usage (WWW fav)     ; bit 2: 4
  USAGE(2),           0x21, 0x02,    //   Usage (WWW search)  ; bit 3: 8
  USAGE(2),           0x26, 0x02,    //   Usage (WWW stop)    ; bit 4: 16
  USAGE(2),           0x24, 0x02,    //   Usage (WWW back)    ; bit 5: 32
  USAGE(2),           0x83, 0x01,    //   Usage (Media sel)   ; bit 6: 64
  USAGE(2),           0x8A, 0x01,    //   Usage (Mail)        ; bit 7: 128
  HIDINPUT(1),        0x02,          //   INPUT (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
  END_COLLECTION(0)                  // END_COLLECTION

If you are curious why these media button works, you can checkout the USB doc https://www.usb.org/sites/default/files/documents/hut1_12v2.pdf, Page 75, where it states why each key usage is used.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.