Skip to content

Instantly share code, notes, and snippets.

@TheNathannator
Last active May 19, 2024 08:30
Show Gist options
  • Save TheNathannator/c5d3b41a12db739b7ffc3d8d1a87c60a to your computer and use it in GitHub Desktop.
Save TheNathannator/c5d3b41a12db739b7ffc3d8d1a87c60a to your computer and use it in GitHub Desktop.

Xbox One Controller Protocol Notes

The Xbox One controller protocol is quite in-depth. This is a collection of all the information I've gathered over time about it, most of which is sourced from medusalix's xone driver for Linux. There's a little bit of my own research/reverse engineering in there too, but a majority of the information comes from xone.

The info here refers to the USB/wireless side of things, it does not fully apply to the interface the Xbox One controller driver on Windows exposes. That interface is covered here. The wireless receiver protocol is also not documented here, as that's its own beast to handle.

Struct definitions and code examples are not guaranteed to be valid C/C++ code, and are meant mainly for efficiently defining how things are structured or handled. All structs are assumed to be packed with 1-byte alignment.

Table of Contents

Basic Infrastructure

Messages

Individual sets of information that are sent between the device and host are referred to here as messages. These messages contain a header, which is followed by a data payload. Multiple messages can be sent in a single transfer by appending successive messages to the end of the previous, so individual transfers are not necessarily for a single message.

Header

The header is as follows:

struct GipHeader
{
    uint8_t command;
    uint8_t client : 4;
    bool needsAck : 1;
    bool system : 1;
    bool chunkStart : 1;
    bool chunked : 1;
    uint8_t sequence;
    leb128_t length;
};
  • command is a command/report ID that describes what action/request to carry out.
  • client is used to distinguish between multiple devices on a singular physical connection, such as audio or plug-in modules. Client 0 is typically the main device.
  • needsAck means the message must be responded to using a 0x01 command ID message (detailed later).
  • system means the message is one specified by the protocol and not the device itself.
  • chunkStart marks the start of a chunk sequence.
  • chunk marks a message as part of a chunk sequence.
  • sequence keeps track of which message is which between multiple messages of the same command. This starts out at 1, goes up to 255, and wraps back around to 1.
    • This needs to be maintained individually for each command ID.
    • The sequence count does not increase between messages of the same chunk sequence.
  • length is a little-endian variable-length number specifying the amount of bytes in the payload. In most messages this will be only one byte long; however, if the top-most bit of that byte is set to 1, it means there is another byte used to encode the length. This continues up to a length of 4 bytes.

Example Messages

-> 50-00-01-03 05-03-01

<- 09-00-05-09 00-0F-FF-FF-00-00-FF-00-EB

Chunk Sequences

To work around data transfer limits (such as the 64-byte payload limit of USB full-speed interrupt transfers), the protocol provides a way to split data into multiple messages. These are referred to as "chunk" sequences.

Chunk Header

Messages that have the chunk flag set have an additional length/index value added after the main header. The bytes for this value are not included in the length value of the main header.

struct GipChunkHeader : GipHeader
{
    leb128_t chunkLength;
};
  • In the first message of a chunk sequence, chunkLength is the size required to hold the data being chunked out. In the following messages, it is the index into that buffer that the message is starting at.
  • The header of a chunked message has a minimum length requirement of 6 bytes. If either the message length or chunk length/index only take up a single byte, one or the other must be padded by forcing it to be represented as 2 bytes (e.g. 0x05 -> 0x85 0x00). If the message length is 0, then the chunk length is padded; otherwise the message length is padded.

Chunk Handling

Chunk messages have a bit of special handling to consider.

The first message of a chunk sequence has the chunk, chunk start, and acknowledgement flags set. The acknowledgement flag is set in order to get information about the remaining size of the chunk buffer allocated on the other end from the acknowledgement message.

The messages following this also have the chunk flag set, and have the same sequence count. Occasionally, another acknowledgement will be requested in order to check on the buffer to ensure things are as expected. If they are not, it'll re-send everything from the previous acknowledgement request.

The last message of the chunk sequence will also set the acknowledgement flag to ensure all of the data has been received (once again restarting from the previous ack if not). After this, one last message is sent with no payload, and the chunk index set to the max size of the buffer.

Example Sequence

An example sequence, using 0x50 as an example command ID and with additional formatting to clarify:

-> 50-F0-01-3A    80-02 <3A bytes of data>
<- 01-20-01-09          00-50-20-3A-00-00-00-C6-00
-> 50-A0-01-BA-00 3A    <3A bytes of data>
-> 50-A0-01-BA-00 74    <3A bytes of data>
-> 50-A0-01-3A    AE-01 <3A bytes of data>
-> 50-A0-01-18    E8-01 <3A bytes of data>
-> 50-B0-01-00    80-02
<- 01-20-02-09          00-50-20-18-00-00-00-00-00

Oddity: Lone Chunk Messages

Sometimes a chunk message is sent without a preceding chunk start message, specifically during authentication. I'm unsure of why this is.

Protocol Command Details

Protocol Command IDs

There are several commands built into the protocol:

  • 0x01: Acknowledge a previous message
  • 0x02: Device arrival
  • 0x03: Device status
  • 0x04: Descriptor
  • 0x05: Power mode
  • 0x06: Authentication
  • 0x07: Virtual keycode
  • 0x08: Audio control
  • 0x0A: LED control
  • 0x0B: HID report
  • 0x0C: Firmware data
  • 0x1E: Serial number
  • 0x60: Audio sample data

0x01: Acknowledgement

This message must be used in response to messages that have the acknowledgement flag set.

struct GipAcknowledge
{
    uint8_t unk1;
    uint8_t innerCommand;
    uint8_t innerClient : 4;
    bool innerNeedsAck : 1;
    bool innerSystem : 1;
    bool innerChunkStart : 1;
    bool innerChunked : 1;
    uint16_t bytesReceived;
    uint16_t unk2;
    uint16_t remainingBuffer;
};
  • The sequence count in the header must match the sequence count of the message that is being acknowledged!
  • unk1 seems to almost always be 0. I've seen it be 0x20 in some cases though.
  • innerCommand is the command ID of the message being responded to.
  • innerClient and the inner flag values reflect the client number and flags of the message being responded to. However, the host seems to only respect the innerSystem flag. All of the other flags are always left unset, and only controllers will set those flags when sending responses.
  • bytesReceived is the number of bytes received for the message being responded to. For chunk sequences, this is the cumulative number of bytes received across all of the chunk packets that have been received.
  • unk2 seems to always be 0x0000. It may be part of bytesReceived, but given that remainingBuffer is only a 16-bit value, it wouldn't make sense for more data than that to be sendable in a sequence (unless remainingBuffer can expand to a 32-bit number?).
  • remainingBuffer is used in chunk message responses to indicate the remaining size of the chunk buffer.

0x02: Device Arrival

Devices send this message to announce their presence. It'll be sent periodically until the host requests its descriptor.

struct GipArrival
{
    uint64_t serial;
    uint16_t vendorId;
    uint16_t productId;
    struct {
        uint16_t major;
        uint16_t minor;
        uint16_t build;
        uint16_t revision;
    } firmwareVersion, hardwareVersion;
};

Take caution: the serial number provided here is used as a unique identifier for the device.

0x03: Device Status

Devices send this message periodically to report their status.

struct GipStatus
{
    uint8_t batteryLevel : 2;
    uint8_t batteryType : 2;
    uint8_t unk1 : 4;
    uint8_t unk2[3];
};
  • batteryLevel is the current battery level, from 0-3.
  • batteryType is the currently-used battery type:
    • 0: No battery
    • 1: Standard AA batteries
    • 2: Rechargable battery pack
    • Not sure if 3 is used for anything

0x04: Device Descriptor

This message is sent to request and send descriptor information.

This message works in the following ways:

  • A message of this type with no length or data should be sent to a device to request its descriptor:

0x04 0x20 <message count> 0x00

  • The controller will then respond with a chunk sequence that encodes its descriptor, which when decoded is structured like the following:
struct GipDescriptor
{
    struct {
        uint16_t headerLength;
        uint8_t unk[12];
        uint16_t dataLength;
    } header;

    union {
        struct {
            uint16_t customCommands;
            uint16_t firmwareVersions;
            uint16_t audioFormats;
            uint16_t outputCommands;
            uint16_t inputCommands;
            uint16_t classNames;
            uint16_t interfaceGuids;
            uint16_t hidDescriptor;
        } offsets;

        uint8_t data[];
    };
};

The descriptor starts out with its own header:

  • headerLength is the length of the header, including the bytes for this length.
  • unk[0] is typically 0x01, the rest of unk is typically 0s.
  • dataLength is the length of the data buffer following the header.

After the header comes the data block. This contains both a set of offsets, and the data itself. The offsets are relative to the start of the data block (i.e. the start of the offsets).

Each offset points to a different type of data. The first byte at each offset is a count of how many elements are located at this offset. If either the offset or the count byte are 0, the device does not contain any elements for that type of data.

This struct should hopefully illustrate how it's laid out, though it by no means can be used directly. The data pointed to by the offsets needs to be handled manually.

template <typename T>
struct GipDescriptorElement
{
    uint8_t count;
    T elements[]; // std::size(elements) == count
}

Custom Commands

This offset specifies custom commands that a device supports which are not part of the core protocol.

struct GipCustomCommand
{
    uint16_t length;
    uint8_t commandId;
    uint16_t maxMessageLength;
    uint16_t unk1;

    // flags
    uint8_t : 2;
    bool headerIncludedInMaxLength : 1;
    bool outputCommand : 1;
    bool inputCommand : 1;
    uint8_t : 3;

    uint8_t unk2[15];
};

typedef GipDescriptorElement<GipCustomCommand> GipCustomCommands;
  • length is the length of the data in the descriptor, including the length bytes.
  • commandId is the command ID that the custom command uses.
    • This can be set to the same ID as one of the core commands; the system flag in the command header is used to differentiate between the core command and the custom one.
  • maxMessageLength is the maximum length that the message will use. If headerIncludedInMaxLength is set, subtract 4 (the size of the typical header) to get the true maximum length.
  • outputCommand and inputCommand specify whether a command is sent to or received from the device (or possibly both, though I haven't seen one with both set yet).
  • unk1 and unk2 are typically all 0s.

Firmware Versions

This offset specifies the firmware versions that a device supports.

struct GipFirmwareVersion
{
    uint16_t major;
    uint16_t minor;
};

typedef GipDescriptorElement<GipFirmwareVersion> GipFirmwareVersions;

Audio Formats

This offset specifies the audio formats that a device supports. This is not set on the base client of a device, it's only set on the client that provides audio support.

enum GipAudioFormatType
{
    GIP_AUDIO_CHAT_24KHz = 0x04,
    GIP_AUDIO_CHAT_16KHz = 0x05,
    GIP_AUDIO_HEADSET_MONO_24KHz = 0x09,
    GIP_AUDIO_HEADSET_STEREO_48KHz = 0x10,
};

struct GipAudioFormat
{
    uint8_t input;
    uint8_t output;
};

typedef GipDescriptorElement<GipAudioFormat> GipAudioFormats;

Output Commands

This offset specifies the built-in commands that it supports receiving, stored as a byte array.

typedef GipDescriptorElement<uint8_t> GipOutputCommands;

Input Commands

This offset specifies the built-in commands that it supports sending, stored as a byte array.

typedef GipDescriptorElement<uint8_t> GipInputCommands;

Class Names

This offset specifies class strings for the interfaces the device supports. This one is a bit more involved than the other data elements, as its individual elements are variable-length. The following struct is pseudocode only!

struct GipClassString
{
    uint16_t length;
    char string[length];
};

typedef GipDescriptorElement<GipClassString> GipClassStrings;

Interface Guids

This offset specifies GUIDs for the interfaces the device supports.

typedef GipDescriptorElement<GUID> GipInterfaceGuids;

HID Descriptor

This offset specifies an HID descriptor for the device as a simple byte array.

typedef GipDescriptorElement<uint8_t> GipHidDescriptor;

0x05: Device Configuration/Power Mode

This message is sent to configure the controller, including its power mode. It has many sub-commands, which may or may not have additional data (most do not).

struct GipSetConfiguration
{
    uint8_t subcommand;
};
  • subcommand is any of the following:
    • 0x00: Power on

    • 0x01: Sleep

    • 0x04: Power off

    • 0x05: Unknown

      • This one seems to be followed by or grouped with other configuration commands of different command IDs, such as setting the Xbox LED or turning vibration off on gamepads.
    • 0x06: Wireless pairing

      struct GipSetWirelessPairing
      {
          uint8_t subcommand = 0x06;
          uint8_t pairingAddress[6]; // For wireless pairing; all 0s if no receiver is connected
          char countryCode[2]; // "US" and "AU" are known valid codes, unsure what other ones exist
          uint8_t unknown[6]; // All 0s if no receiver; otherwise 00 0f 00 00 00 1f
      };
    • 0x07: Reset

0x06: Authentication

Authentication is a whole ordeal that probably won't be figured out for quite some time lol, not detailing anything here (nor do I plan to, as it's a touchy thing and easy enough to bypass by just passing through from a real controller).

0x07: Virtual Keycode

This message is used to report keystroke inputs.

struct GipKeystroke
{
    struct
    {
        bool pressed : 1;
        uint8_t : 7;
        uint8_t keycode;
    } keystrokes[];
};
  • pressed is 1 when pressed, 0 when released.
  • keycode is the virtual keycode for the keystroke.

Multiple keystrokes can be reported at once within the same message.

Controllers use this to report the guide button, using the virtual keycode of 0x5B (Left Windows key).

0x0A: LED Control

This message is used to control the Xbox button LED.

enum GipLedMode
{
	GIP_LED_OFF = 0x00,
	GIP_LED_ON = 0x01,
	GIP_LED_BLINK_FAST = 0x02,
	GIP_LED_BLINK_NORMAL = 0x03,
	GIP_LED_BLINK_SLOW = 0x04,
	GIP_LED_FADE_SLOW = 0x08,
	GIP_LED_FADE_FAST = 0x09,
};

struct GipLedControl
{
    uint8_t unk = 0x00; // subcommand?
    uint8_t mode;
    uint8_t brightness;
};

TODO Commands

0x08: Audio Control

0x0B: HID report

0x0C: Firmware data

0x1E: Serial Number

0x60: Audio Samples

Where are 0x09 and 0x20?

These commands are technically device-defined and aren't part of the main protocol. Thus, they aren't covered here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment