Mac Special Keys

If you’ve used a Mac in the last decade (or two?) you’ve noticed that the function keys are repurposed to do things like previous/next track, volume, brightness, and more.

Modern Macs deal with this by having a function key map for specific keyboards, so macOS knows that your Macbook internal keyboard has certain keys that map to certain functions. Prior to that they (sensibly) used USB HID usages to define the keys.

Is there a way to mimic this with our own USB device?

Usage Pages and Usages

USB HID devices (keyboards, mice, game controllers, etc) describe their capabilities to the host using Usage Pages. These are defined in HID Usage Tables 1.12.

There are many, many possibilities available to use and it would have made sense to just use some of the extra keycodes already there, e.g. use F19 for launchpad. But why bother with that when you could make it more complicated and define your very own set of vendor-specific usages?


Thankfully there are a few resources to find out what usages the keys map to. Running

ioreg -l | grep FnFunctionUsageMap

(thanks to this page) shows the map used for an existing keyboard. The first value is the keyboard usage for F1, F2, etc. and the second is the alternate usage. FF01 is the vendor-specific page, 000C is the consumer devices page and is used for volume controls, play/pause, etc.

We can also reference the open source pieces of macOS, the most useful part being AppleHIDUsageTables.h. This shows all of the Apple-specific pages. The file is blank since 10.14. The relevant one for us is the keyboard page, 0xff01. Others are also worth looking into - what’s AppleVendorLisa?

The relevant usages for modern macOS are:

  • 0x03 Function - sends the Fn keycode but has no other effect in this case
  • 0x04 Launchpad
  • 0x10 Exposé All (now Mission Control)
  • 0x20 Brightness Up
  • 0x21 Brightness Down

I already had a HID report for the consumer controls so I just added these onto that, here’s part of the HID descriptor as a C array. It defines a report of one byte with each bit corresponding to a specific usage:

0x05, 0x0c,             // USAGE_PAGE (Consumer Devices)
0x09, 0x01,             // USAGE (consumer control)
0xA1, 0x01,             // COLLECTION (Application)
    0x85, 0x01,         //  REPORT ID 1
    0x15, 0x00,         //   Logical min
    0x25, 0x01,         //   Logical max
    0x75, 0x01,         //   Report size (1)
    0x95, 0x08,         //   Report count (8)
    0x09, 0xb5,         //   Usage: next track
    0x09, 0xb6,         //   Usage: prev track
    0x09, 0xcd,         //   Usage: play/pause
    0x09, 0xe9,         //   Usage: vol up
    0x09, 0xea,         //   Usage: vol down
    0x06, 0x01, 0xff,   // USAGE_PAGE (Vendor-defined, Apple Keyboard)
    0x09, 0x21,         //   Usage: bright down
    0x09, 0x20,         //   Usage: bright up
    0x09, 0x04,         //   Usage: Launchpad
    0x81, 0x02,         // INPUT data, var, abs
0x0c                    // End collection

This works… on a Mac. If that’s all you care about then this is fine, but if you plug it into a Windows machine none of them work. Windows doesn’t know what to do with the vendor-specific piece, so it ignores the entire collection. I ended up using one report for the standard consumer controls and a separate report for the Mac-specific ones.