Reverse-engineered from the official Windows (8BitDoAdvance.dll) and Android (libadvance-lib.so) applications, and validated with USB traffic captures.
Vendor ID: 0x2DC8 (11720)
| Controller | PID | PID (hex) | Notes |
|---|---|---|---|
| Ultimate 2.4G | 12306 | 0x3012 | Via 2.4G USB adapter |
| Ultimate 2.4G RR | 12307 | 0x3013 | Recovery mode |
| Ultimate 2.4G Wired | 12550 | 0x3106 | Direct USB-C, Xbox mode |
| Ultimate BT | 24583 | 0x6007 | Bluetooth / config mode after switch |
| Ultimate PC | 12305 | 0x3011 | Wired USB |
| Pro2 | 24579 | 0x6003 | |
| Pro2 CY | 24582 | 0x6006 | |
| Pro2 Wired | 12304 | 0x3010 | |
| USB Ultimate | 12544 | 0x3100 | Adapter |
| USB Ultimate2 | 12549 | 0x3105 | Adapter v2 |
| SN30Plus | 24578 | 0x6002 | |
| ArcadeStick | 36890 | 0x901A | |
| Xcloud | 8448 | 0x2100 | |
| Xcloud2 | 8449 | 0x2101 | |
| IDLE | 12553 | 0x3109 | Adapter standby / HID config mode |
| Start | 36900 | 0x9024 | |
| StartRR | 36899 | 0x9023 | Recovery |
When connected via USB-C, the Ultimate controller presents as an Xbox 360 controller (Vendor Specific Class, subclass 0x5D, protocol 0x01) with PID 0x3106. This is not HID class — the Linux xpad driver claims it.
The controller has two modes:
- Interface class: 0xFF (Vendor Specific) / subclass 0x5D (Xbox)
- Endpoints: EP 0x81 IN (32 bytes, interrupt), EP 0x02 OUT (32 bytes, interrupt)
- Sends 20-byte Xbox 360 gamepad reports continuously
- Claimed by
xpaddriver on Linux
- Interface class: 0x03 (HID)
- Creates
/dev/hidrawNviahid-genericdriver - Accepts 64-byte HID reports for configuration read/write
- Supports the full config protocol documented below
To configure the controller, it must be switched from Xbox mode to HID config mode. This was captured from the official Windows application.
- On Linux, the
xpadkernel driver must be detached from the device before sending the switch commands. The tool handles this automatically viadetach_kernel_driver. The mode switch sequence may need multiple attempts (up to 5).
Send the following commands on EP 0x02 OUT (interrupt transfer) while in Xbox mode (PID 0x3106):
Step Bytes Description
1 01 03 02 Xbox LED init
2 02 08 03 Xbox controller init
3 01 03 02 Xbox LED init (repeat)
4 01 03 06 Config mode trigger (LED cmd 0x06)
5 00 08 00 01 00 00 00 00 Rumble left motor
6 00 08 00 00 01 00 00 00 Rumble right motor
7 00 08 00 01 01 00 00 00 Rumble both motors
8 00 08 00 05 01 00 00 00 Rumble strong
9 00 08 00 00 00 00 00 00 Rumble off
Steps 5-9 (rumble sequence) appear to be required for the mode switch to complete. After the sequence, the device will:
- Disconnect from USB (~1 second)
- Re-enumerate as PID 0x6007 (Ultimate BT) with HID class
- Appear as
/dev/hidrawNviahid-genericdriver - Be ready for config protocol commands
The re-enumeration takes approximately 1-3 seconds.
LED commands: 01 03 XX (XX = LED pattern, 0x06 = config mode)
Rumble commands: 00 08 00 LL RR 00 00 00 (LL = left motor, RR = right motor)
Init command: 02 08 03
The controller returns to Xbox mode (0x3106) when physically unplugged and replugged.
All config communication uses 64-byte HID reports with report ID 0x81.
Before reading or writing config, send a report state command:
Request: 81 11 04 07 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ...
Response: 02 04 04 00 07 00 00 00 00 00 00 00 00 00 00 00 00 00 ...
Request: 81 05 00 21 01 00 ...
Response: 02 22 VL VH ...
Version = uint16_le(VL, VH) / 100. E.g., 6D 00 = 109 = version 1.09.
Corrected from USB traffic capture analysis. Fields marked with (pcap-verified).
Offset Size Field Notes
0 1 Report ID Always 0x81
1 1 Payload length Inner payload size (including 0x04 byte)
2 1 Transfer type Always 0x04
3 2 Command (uint16 LE) 1=write, 2=read (pcap-verified)
5 2 Sub-param (uint16 LE) Usually 0
7 2 Chunk length (uint16) Bytes in this packet, max 45 (pcap-verified)
9 2 CRC-16 CRC of payload data
11 4 Total size (uint32 LE) 0x674 = 1652 (pcap-verified)
15 4 Offset (uint32 LE) Current transfer position (pcap-verified)
19 45 Data payload Config data (write) or padding (read)
Offset Size Field Notes
0 1 Status 0x02 = success (pcap-verified)
1 1 Transfer type 0x04 (pcap-verified)
2 2 Command (uint16 LE) 4=read_response, 1=write_ack (pcap-verified)
4 2 Sub-param (uint16 LE) Echoed from request (2 for read)
6 2 Chunk length (uint16) Bytes of data in this response (pcap-verified)
8 2 CRC-16 CRC of payload data
10 4 Total size (uint32 LE) 1652 (pcap-verified)
14 4 Offset (uint32 LE) Matches request offset (pcap-verified)
18 45 Data payload Config data (read response)
Offset Size Field
0 1 Report ID (0x81)
1 1 Payload length (0x11 = 17)
2 1 Transfer type (0x04)
3 2 Command (uint16 LE)
5 2 Sub-command (uint16 LE)
7 57 Zero padding
Known simple commands:
- Report state (enter config mode): cmd=7, sub=0
- Save config: cmd=6, sub=0x15
Offset Size Field
0 1 Report ID (0x81)
1 1 Payload length
2 1 Command high byte
3 1 Command low byte
4+ N Parameters
Known direct commands:
- Version request:
81 05 00 21 01 00 ...
- Send report state command (cmd=7, sub=0)
- Loop (45 bytes per chunk, 37 iterations for 1652 bytes): a. Build read request: cmd=2, sub=0, chunk_len=45, total=1652, offset=N b. Send on HID write c. Read 64-byte response d. Verify: status=0x02, type=0x04, cmd=4, sub=2 e. Copy 45 bytes from response offset 18 to config buffer f. Advance offset by chunk_len (45) g. Repeat until offset >= 1652
- Send report state command (cmd=7, sub=0)
- Compute CRC-16 over config struct, store at offset 0x0C
- Loop (45 bytes per chunk): a. Build write packet: cmd=1, sub=0, chunk_len=45, total=1652, offset=N, data=chunk b. Send on HID write c. Read response, verify: status=0x02, type=0x04, cmd=1 d. Advance offset by response chunk_len e. Repeat until offset >= 1652
- Send save command: cmd=6, sub=0x15
Algorithm: CRC-16/KERMIT (reflected)
- Polynomial: 0x8408 (bit-reversed 0x1021)
- Initial value: 0xFFFF
- No final XOR
Two CRC-16 uses:
- Config struct CRC: Computed over the entire 1652-byte config, stored at offset 0x0C (
crc_valuefield). This uses theANSI_CRC_16_shfunction from the native library. - Packet CRC: Computed over the data payload of each transfer packet, stored in the packet header at offset 9-10.
fn crc16_kermit(data: &[u8]) -> u16 {
let mut crc: u16 = 0xFFFF;
for &byte in data {
crc ^= byte as u16;
for _ in 0..8 {
if crc & 1 != 0 {
crc = (crc >> 1) ^ 0x8408;
} else {
crc >>= 1;
}
}
}
crc
}Total size: 1652 bytes (0x674), verified by USB capture.
All Ultimate variants (2.4G, BT, PC) share the same struct. 3 config slots.
Offset Size Type Field
0x000 12 uint32[3] flag
0x00C 4 uint32 crc_value
0x010 2 uint16 gamepad_mode (enum GAMEPAD_MODE)
0x012 2 uint16 cur_slot (0-2)
0x014 96 config_file_name_t[3] file_name (32 bytes each)
0x074 36 rumble_params_record_t[3] rumble (12 bytes each)
0x098 24 joy_params_record_t[3] joystick (8 bytes each)
0x0B0 24 trigger_params_record_t[3] trigger (8 bytes each)
0x0C8 24 joy_trigger_special_feature_t[3] special_feature (8 bytes each)
0x0E0 252 mode_map_key_record_t_Pro2[3] map_key (84 bytes each)
0x1DC 1176 macro_fun_record_t_Pro2[3] macro_fun (392 bytes each)
Total: 1652 bytes
config_file_name_t (32 bytes):
name: byte[32] -- UTF-8 profile name, null-padded
rumble_params_record_t (12 bytes):
flag: uint32 -- enable/disable flags
zoom_out: float32[2] -- [left_intensity, right_intensity] (0.0-1.0)
action_range_t (2 bytes):
start_value: uint8
end_value: uint8
joy_params_record_t (8 bytes):
flag: uint32 -- enable/disable flags
range: action_range_t[2] -- [left_stick, right_stick]
trigger_params_record_t (8 bytes):
flag: uint32
range: action_range_t[2] -- [left_trigger, right_trigger]
joy_trigger_special_feature_t (8 bytes):
flag: uint32
featrue: uint32 -- bitmask of special feature flags (see below)
map_key_t_Pro2 (80 bytes):
key: uint32[20] -- button mapping values (indexed by MAP_KEY_E)
mode_map_key_record_t_Pro2 (84 bytes):
flag: uint32
mode: map_key_t_Pro2 -- 20 key mappings (single set, shared across gamepad modes)
macro_key_record_t_Pro2 (96 bytes, including 1 byte padding):
key_map: uint32 -- trigger button bitmask
interval_time_index: uint16[18] -- timing between key presses
key_index: uint16[18] -- key sequence
digtal_joy: uint8[18] -- digital/analog indicators
max_index: uint8 -- number of steps in macro
_padding: uint8 -- alignment to 96 bytes
macro_fun_record_t_Pro2 (392 bytes):
flag: uint32 -- enable/disable
total_cnt: uint8 -- number of active macros
_padding: uint8[3] -- alignment to 4 bytes
record: macro_key_record_t_Pro2[4] -- 4 macro slots
GAMEPAD_MODE (uint16):
0 = SWITCH_PRO_M (Nintendo Switch Pro Controller)
1 = GAMEPAD_J_M (DirectInput)
2 = MAC_M (macOS)
3 = XINPUT_M (XInput / Xbox)
4 = EXTEND_M (Extended)
5 = GAMEPAD_NONE
MAP_KEY_E (button indices, 0-19):
0=A, 1=B, 2=X, 3=Y, 4=L1, 5=R1, 6=L2, 7=R2,
8=L3, 9=R3, 10=SELECT, 11=START, 12=STAR/SHARE,
13=HOME, 14=UP, 15=DOWN, 16=LEFT, 17=RIGHT,
18=P1 (back paddle), 19=P2 (back paddle)
KEY_START_MAP = 0x00000001
KEY_L3_MAP = 0x00000002
KEY_R3_MAP = 0x00000004
KEY_SELECT_MAP = 0x00000008
KEY_X_MAP = 0x00000010
KEY_Y_MAP = 0x00000020
DIR_RIGHT_MAP = 0x00000040
DIR_LEFT_MAP = 0x00000080
DIR_DOWN_MAP = 0x00000100
DIR_UP_MAP = 0x00000200
KEY_L1_MAP = 0x00000400
KEY_R1_MAP = 0x00000800
KEY_B_MAP = 0x00001000
KEY_A_MAP = 0x00002000
KEY_L2_MAP = 0x00004000
KEY_R2_MAP = 0x00008000
KEY_MENU_MAP = 0x00010000
KEY_HOME_MAP = 0x00020000
KEY_BT_CON_MAP = 0x00040000
KEY_SCREENSHOT_MAP = 0x00400000
KEY_TURBO_SINGLE = 0x00800000
KEY_TURBO_AUTO = 0x01000000
KEY_P1_MAP = 0x02000000
KEY_P2_MAP = 0x04000000
KEY_NULL_MAP = 0x00000000
The special_feature.featrue field is a bitmask (RE'd from ConfidentialData.cs):
0x0001 — Invert Left Stick X
0x0002 — Invert Left Stick Y
0x0004 — Invert Right Stick X
0x0008 — Invert Right Stick Y
0x0010 — Swap Left/Right Sticks
0x0080 — Swap Left/Right Triggers
0x1000 — Enable Deadzone
0x2000 — 4-Way Mode (no diagonals)
The controller firmware stores one mapping array per profile slot (20 keys × 4 bytes = 80 bytes). This mapping is shared across all gamepad modes (XInput, Switch Pro, etc.).
The Windows app maintains two sets of mappings in local storage (mode[0] for XInput, mode[1] for Switch Pro) and swaps them when the user changes gamepad mode. This is a client-side feature — the controller itself only has one active mapping.
The Windows app's in-memory struct map_key_record_t defines map_key_t[2] mode (two 18-key arrays = 148 bytes), but the on-wire format is map_key_t_Pro2 (one 20-key array = 84 bytes including flag). The app translates between these formats during read/write. Verified: the 84-byte struct produces the correct 1652-byte config total.
P1/P2 (indices 18-19) can be remapped FROM but should not be remapped TO — the controller firmware does not support mapping other buttons to produce paddle output signals.
These are standalone function values used for the Star button's alternate mapping, NOT modifiers that can be OR'd with button bitmasks:
KEY_TURBO_SINGLE = 0x00800000— turbo function (Star button alternate)KEY_TURBO_AUTO = 0x01000000— auto-turbo / swap functionKEY_SCREENSHOT_MAP = 0x00400000— screenshot/capture function
Captured from official Windows app USB traffic (pcap-verified).
Uses the simple command format (0x04 wrapper) with cmd=0x08:
Request: 81 11 04 08 00 E8 03 LL RR 00 00 00 00 00 00 00 00 00 ...
Response: 02 04 08 00 ...
| Byte | Value | Description |
|---|---|---|
| 0 | 0x81 |
Report ID |
| 1 | 0x11 |
Simple command payload length (17) |
| 2 | 0x04 |
Transfer type |
| 3-4 | 0x08 0x00 |
Command: vibration test (uint16 LE) |
| 5-6 | 0xE8 0x03 |
Duration: 1000ms (uint16 LE) |
| 7 | LL |
Left motor intensity (0-255) |
| 8 | RR |
Right motor intensity (0-255) |
| 9+ | 0x00... |
Zero padding |
The controller acknowledges with a standard response and activates the motors for the specified duration.
Other vibration-related commands discovered during probing:
cmd=0x0B sub=0: reads current vibration parameters (4 bytes)cmd=0x0C sub=0 + 4B: writes vibration parameters (acknowledged, no motor activation)cmd=0x0C + 6B payload: causes controller disconnect — payload size sensitivecmd=0x0B sub=1: causes controller disconnectcmd=0x0D: controller reset/disconnect command
The Android app packs motor values into a single int:
int packed = (right << 8) | left | (l2_trigger << 16) | (r2_trigger << 24);
HIDChannel.setUltimateWiredVibration(packed);
RE'd from Calibration.cs in the Windows app. Calibration uses the direct command format (non-0x04).
The user must center both sticks before sending this command.
Request: 81 03 00 36 ...
Response: 02 37 01 ...
| Field | Value | Description |
|---|---|---|
| Command high | 0x00 |
|
| Command low | 0x36 |
Start calibration |
| Response[0] | 0x02 |
Success status |
| Response[1] | 0x37 |
Calibration response ID |
| Response[2] | 0x01 |
1 = success, other = failure |
Two variants depending on the controller model:
Ultimate PC (PID 0x3011):
Request: 81 04 00 36 01 ...
Response: 02 37 01 ...
Other devices (Ultimate 2.4G, BT, etc.):
Request: 81 04 00 38 01 ...
Response: 02 39 ...
| Field | Value | Description |
|---|---|---|
| Command high | 0x00 |
|
| Command low | 0x36 (PC) / 0x38 (others) |
Save calibration |
| Payload[0] | 0x01 |
Save flag |
| Response[1] | 0x37 (PC) / 0x39 (others) |
Save response ID |
1. User centers both sticks
2. Send start calibration (0x00, 0x36)
3. Wait for response [0x02, 0x37, 0x01]
4. Send save calibration (0x00, 0x38 or 0x36 for PC) with payload [0x01]
5. Wait for response [0x02, 0x39] or [0x02, 0x37, 0x01] for PC
The 8BitDo Firmware Updater (Windows, v252) downloads firmware from http://dl.8bitdo.com:8080.
List firmware versions:
POST http://dl.8bitdo.com:8080/firmware/select
Content-Type: application/x-www-form-urlencoded
Body: type={device_type}&beta=1
Response (JSON):
{
"msgState": 1,
"list": [
{
"fileName": "Ultimate 2.4g",
"filePathName": "/firmwareFile/upload/{uuid}.dat",
"fileSize": 356380,
"version": 2.04,
"md5": "...",
"type": 43,
"readme_en": "Fixed the abnormal joysticks issue when holding down the L3 / R3 button.",
"id": 1872,
"date": "2024-02-21"
}
]
}Download firmware:
GET http://dl.8bitdo.com:8080{filePathName}
| Type | Controller |
|---|---|
| 41 | Ultimate BT |
| 42 | Ultimate BT Receiver |
| 43 | Ultimate 2.4G |
| 44 | Ultimate 2.4G Receiver |
| 45 | Ultimate Wired (PC) |
| 48 | Ultimate C 2.4G |
| 50 | Ultimate C Wired |
See scripts/download_firmware.py for the complete list of all device types.
The firmware files use a Jieli SoC encrypted format (UFW). The file has a 204-byte header followed by encrypted payload.
Offset Size Description
0x000 4 Header size (0xCC = 204)
0x004 4 Load address (e.g. 0x3000)
0x008 4 Firmware data size
0x00C 4 PID (e.g. 0x3012 = Ultimate 2.4G)
0x01C 4 CRC/checksum
0x020 4 VID string "2DC8"
0x024 12 Device identifier bytes
0x030 4 Version/build info
0x0CC ... Encrypted firmware payload
The payload is encrypted with Jieli's proprietary encryption and cannot be statically analyzed. Flashing requires the proprietary firmwareUpgradeTools.dll (Jieli SDK).
For Jieli-based devices (Ultimate 2.4G, Ultimate BT, etc.):
- Download firmware .dat file from server
- Send "jump to boot" HID command to controller
- Controller re-enumerates as USB mass storage device (removable drive)
- Jieli SDK decrypts UFW, erases flash, writes firmware via USB mass storage protocol
- Controller reboots with new firmware
Note: Firmware updates are not implemented in this tool due to the proprietary Jieli encryption. Use the official 8BitDo Firmware Updater for updates.
The Android app communicates with the controller exclusively via Bluetooth Low Energy (BLE), never USB. When the user "connects via USB-C" on Android, the USB cable provides power only — configuration still happens over BLE.
Two UUID families are used depending on the controller model:
Family 1 — Pro2, Ultimate BT, Pro3, Micro:
| Device | BLE Name | Service UUID | Characteristic UUID |
|---|---|---|---|
| Pro 2 | 8ap |
0000FF10-0000-1000-8000-00805F9B34FB |
0000FF11-0000-1000-8000-00805F9B34FB |
| Pro 2 (boot) | 8bt |
0000FF20-0000-1000-8000-00805F9B34FB |
0000FF21-0000-1000-8000-00805F9B34FB |
| Pro 2 CY | - | 0000FF10-0000-1000-8000-00805F9B34FB |
0000FF13-0000-1000-8000-00805F9B34FB |
| Ultimate BT | 80NA |
0000FF10-0000-1000-8000-00805F9B34FB |
0000FF11-0000-1000-8000-00805F9B34FB, 0000FF13-0000-1000-8000-00805F9B34FB |
| Pro 3 | 80GQ |
0000FF10-0000-1000-8000-00805F9B34FB |
0000FF11-0000-1000-8000-00805F9B34FB |
| Micro | 80EL |
0000FF10-0000-1000-8000-00805F9B34FB |
0000FF11-0000-1000-8000-00805F9B34FB |
Family 2 — Ultimate Wired, Xbox Wired, Three/Gamesir:
| Device | BLE Name | Service UUID | Characteristic UUID |
|---|---|---|---|
| Ultimate Wired | 82CE |
00010203-0405-0607-0809-0A0B0C0D1910 |
00010203-0405-0607-0809-0A0B0C0D2B11 |
| Xbox Wired | 80GG |
00010203-0405-0607-0809-0A0B0C0D1910 |
00010203-0405-0607-0809-0A0B0C0D2B11 |
| Three/Gamesir | 81HB-X/D/W |
00010203-0405-0607-0809-0A0B0C0D1910 |
00010203-0405-0607-0809-0A0B0C0D2B11 |
1. Scan for BLE devices by name filter
Names: "8ap", "8bt", "80GG", "82CE", "80NA", "80NA_S", "80EL", "80GQ", "81HB-*"
RSSI threshold: -80 dBm
Scan timeout: 3000ms
2. Connect to GATT server (high priority)
3. Negotiate MTU
- Xbox/Ultimate Wired/Three (80GG, 82CE, 81HB-*): MTU = 67
- Others (Pro2, Ultimate BT, Pro3, Micro): MTU = 100
4. Discover services & characteristics
Match against known service/characteristic UUID pairs
5. Subscribe to notifications
Enable CCCD descriptor (UUID 00002902-0000-1000-8000-00805f9b34fb)
6. Ready for config read/write
The same config protocol commands are used over BLE, but the packet framing differs:
Request packets:
- USB HID:
[0x81, length, 0x04, cmd, ...]— report ID + length + transfer payload (64 bytes fixed) - BLE:
[0x04, cmd, ...]— transfer payload only, no report ID, no length byte
BLE writes skip bytes 0 and 1 of the USB packet (report ID + length prefix), sending only the protocol payload starting at the transfer type byte (0x04). Packet length is determined by the payload content, not padded to 64 bytes.
Response packets:
- USB HID:
[status(02), type(04), cmd_lo, cmd_hi, sub_lo, sub_hi, chunk_lo, chunk_hi, crc, crc, total(4), offset(4), data...] - BLE:
[type(04), 0x00, cmd_lo, cmd_hi, chunk_lo, chunk_hi, crc, crc, total(4), offset(4), data...]
BLE responses omit the status byte and have an extra 0x00 after the type byte. The cmd field echoes the request command (e.g. 0x02 for READ) rather than using CMD_READ_RESPONSE (0x04). The response is typically 61 bytes.
BLE-specific behavior:
CMD_REPORT_STATE(enter config mode) is not needed over BLE — go straight to read/write- BLE characteristic requires
WriteType::WithResponse(WRITE property, not WRITE_WITHOUT_RESPONSE) - Config read/write/vibration all work over BLE with the adjusted packet format
| Model | BLE Name | Scan Default |
|---|---|---|
| Pro 2 | 8ap/8apS |
Yes |
| Pro 3 | 80GQ |
Yes |
| Ultimate BT | 80NA/80NA_S |
Yes |
| Ultimate Wired | 82CE |
Yes |
| Xbox Wired | 80GG |
Yes |
| Micro | 80EL |
Yes |
| Three/Gamesir | 81HB-X/D/W |
Yes |
| USB 2.4G Adapter | - | No (USB only) |
On Linux, the xpad kernel driver automatically claims the controller in Xbox mode (PID 0x3106). xpad sends a "magic message" init sequence which fails (unable to receive magic message: -121), causing repeated USB resets.
The tool handles xpad automatically via per-device kernel driver detach (detach_kernel_driver). There is no need to run sudo rmmod xpad — only the specific device's driver attachment is released, leaving other controllers unaffected. The mode switch sequence may need multiple attempts (up to 5) before the device successfully re-enumerates as a HID device.
Install udev/99-8bitdo.rules for non-root hidraw access:
sudo cp udev/99-8bitdo.rules /etc/udev/rules.d/
sudo udevadm control --reload-rulesThis grants world read/write to 8BitDo USB and hidraw devices.