Fanatec wheelbases communicate with the host PC over USB HID (Human Interface Device). All commands and data flow through HID reports organized into separate collections, each with its own endpoint, report size, and purpose.
Status — how to read this. This reference describes the Fanatec HID protocol as pieced together from observation and experimentation — not an official specification. Some details are confirmed against hardware; others are inferred and may be incomplete or wrong.
- USB Identifiers
- HID Collections
- Report Framing
- Collection Routing
- col01 Reference
- 0x09 — General Control
- 0x13 / 0x14 — Base Rev LEDs
- col03 Reference
- Cross-Reference Topics
- Appendix
| Field | Value |
|---|---|
| Vendor ID | 0x0EB7 (Endor AG / Fanatec) |
| Product ID | Varies by wheelbase (e.g., 0x0020 = ClubSport DD+) |
Fanatec devices expose three HID collections:
| Collection | Report Size | Direction | Purpose |
|---|---|---|---|
| col01 | 8 bytes | Bidirectional | Legacy LED control, 7-segment display, configuration commands |
| col02 | 8 bytes | Device → Host | Input reports (buttons, encoders, axes) |
| col03 | 64 bytes | Bidirectional | Modern LED control, ITM display, tuning menu |
The original control channel, used by older wheels and for commands that predate the 64-byte protocol. See col01 Reference for all subcmds.
Device-to-host input reports carrying button states, encoder positions, and axis values. Not covered in detail here — this channel is read by the OS HID driver and exposed as a standard game controller.
The extended control channel, used by newer devices for features requiring larger payloads. Not all devices support col03. Older wheelbases and rims operate exclusively through col01. See col03 Reference for all command classes.
All col01 output reports are 8 bytes. The first byte is the report ID, followed by a command header:
Byte: [0] [1] [2] [3] [4] [5] [6] [7]
ReportID 0xF8 0x09 subcmd data...
The report ID byte (0x00 or 0x01) is device-specific and assigned during initialization: use 0x01 on col03-capable devices and 0x00 on col01-only devices. The 0xF8 byte marks a Fanatec control command, and the bytes after it address it in tiers:
- byte[2] — command class.
0x09for most LED/display/config commands; a few dedicated classes such as0x13/0x14(base rev LEDs). - byte[3] — subcommand (within class
0x09). Most subcommands are complete here — e.g.0x08= rev LED data. - byte[4] — operation. One subcommand,
0x01, is a group: rather than acting on its own it selects a further set of operations in byte[4].
[ReportID, 0xF8, 0x09, 0x01, operation, data...] ← group 0x01
All col03 output reports are 64 bytes, zero-padded. The first byte is always 0xFF:
Byte: [0] [1] [2] [3..63]
0xFF cmd_class subcmd payload (zero-padded)
The command class (byte[1]) determines the protocol domain:
| Command Class | Protocol |
|---|---|
0x01 |
LED control |
0x02 |
ITM enable |
0x03 |
Tuning menu |
0x05 |
ITM display |
0x08 |
System report (identity) |
col01 and col03 are separate HID interfaces — the host opens and writes to each one independently. The two are distinguished by their framing:
| Collection | First Byte | Report Size |
|---|---|---|
| col03 | 0xFF |
64 bytes |
| col01 | Device report ID | 8 bytes |
Note: The Fanatec SDK provides a single high-level send path that uses the first byte of the application buffer as a routing hint (
0xFF→ col03, anything else → col01 with the first byte replaced by the device report ID). This is an SDK convenience, not a wire-level mechanism.
The col03 collection is a separate HID interface from col01. On Windows, col03 device paths typically contain "col03" in the path string and can also be identified by a 64-byte maximum output report length.
Whether col03 availability is determined by the wheelbase (always present on modern bases) or the wheel (only present when a col03-capable rim is connected) has not been verified. In practice, col03 should be discovered dynamically at connection time rather than assumed.
Most col01 commands use the [ReportID, 0xF8, 0x09, ...] framing described in Report Framing. The sections below are grouped by command class (byte[2]): the general 0x09 class, then the base rev-LED classes 0x13/0x14.
The catch-all control class — rev and flag LEDs, displays, and configuration. The subcommand sits in byte[3]; most subcommands are complete there (listed below). The exception is subcommand 0x01, a group whose operations are selected by byte[4].
Turns a single white LED on the rim on or off. It has no effect on rev LED output; the rev LEDs are driven entirely by 0x08 and need no enable command.
[ReportID, 0xF8, 0x09, 0x02, enable, 0x00, 0x00, 0x00]
| Byte | Field | Values |
|---|---|---|
| 4 | enable | 0x01 = on, 0x00 = off |
Thought to correspond to the small status LED next to the XBox button on some wheels, but this has not yet been verified.
Enables or disables the RevStripe LED strip. Found on three rims: CSLESWP1X, CSLESWP1PS4, CSLESWWRC.
Inverted semantics — 0x00 means ON:
Enable: [ReportID, 0xF8, 0x09, 0x06, 0x00, 0x00, 0x00, 0x00]
Disable: [ReportID, 0xF8, 0x09, 0x06, 0x01, 0x00, 0x00, 0x00]
| Byte | Field | Values |
|---|---|---|
| 4 | enable | 0x00 = on, 0x01 = off (inverted) |
Typical RevStripe sequence, as captured from official software driving the strip:
1. Base rev LEDs off: [RID, F8, 14, FF, 00, 00, 00, 00]
2. Set color (red): [RID, F8, 09, 08, 00, 38, 00, 00] (repeated per color)
3. Base rev LEDs on: [RID, F8, 14, 00, 00, 00, 00, 00]
Selects how the rim interprets subsequent 0x08 writes.
[ReportID, 0xF8, 0x09, 0x07, mode, 0x00, 0x00, 0x00]
| Byte | Field | Values |
|---|---|---|
| 4 | mode | 0x00 = 0x08 payloads are colors, 0x01 = payloads are on/off patterns |
The driver stack sends this command on your behalf. On CSL Elite P1 (Xbox), CSL Elite P1 (PS4) and CSL Elite WRC, Fanatec's driver watches 0x08 writes and injects a mode change in response to specific payloads:
| Payload written | Driver injects | Resulting mode |
|---|---|---|
01 00 |
0x07 with 0x01 |
pattern |
00 07 (blue) |
0x07 with 0x00 |
color |
00 38 (red) |
0x07 with 0x00 |
color |
This is edge-triggered, so it fires only on transitions. 0x08 is ambiguous — 01 00 is both a valid color and the "LED 0 only" pattern — so the driver infers intent from the payload. RGB333 r=0, g=4, b=0 encodes to 01 00, which puts the rim into pattern mode. Later color writes are then read as bitmasks: on a RevStripe rim the strip lights in its fixed pattern color whenever data_lo bit 0 is set (green, cyan, yellow, white) and stays dark otherwise. Pure red or pure blue returns it to color mode.
Sends LED data — interpreted as either a bitmask or a color depending on the connected rim.
[ReportID, 0xF8, 0x09, 0x08, data_lo, data_hi, 0x00, 0x00]
Non-RGB rims (e.g., CSSWBMWV2): 9-bit bitmask. LED 0 sits alone in data_lo bit 0; LEDs 1-8 pack into data_hi most-significant-first, so LED 1 is bit 7 and LED 8 is bit 0. Note this differs from the 0x13 base ordering.
Example: LEDs 0, 1, 2 on, rest off
data_lo = 0x01, data_hi = 0xC0 (LED0; LED1 = bit7, LED2 = bit6)
Example: All 9 LEDs on
data_lo = 0x01, data_hi = 0xFF
RevStripe rims (CSLESWP1X, CSLESWP1PS4, CSLESWWRC): RGB333 color value controlling the entire strip as one unit:
Example: Red → data_lo = 0x00, data_hi = 0x38
Example: Green → data_lo = 0x01, data_hi = 0xC0
Per-LED color for RGB rims without col03, one bit per channel — seven colors plus off.
[ReportID, 0xF8, 0x09, 0x0A, data0, data1, data2, data3]
27 bits packed LSB-first across the four data bytes, three bits per LED for nine LEDs, in the order [LED0.R, LED0.G, LED0.B, LED1.R, ...]. So LED n's red bit is 3n, green 3n+1, blue 3n+2.
Example: LED 0 red → data0 = 0x01
Example: LED 0 blue → data0 = 0x04
Example: all nine green → 92 24 49 02
Note the channel order here is R, G, B — the only conventional ordering in this protocol, and different from both 0x08 (G, R, B) and the col03 color paths (B, G, R).
Per-LED 3-bit color for flag LEDs on col01, packed the same way as 0x0A but for six flag LEDs (18 bits). Only a subset of wheels have flag LEDs — see the devices reference for the support matrix.
[ReportID, 0xF8, 0x09, 0x0B, data0, data1, data2, data3]
Sets overall brightness for the legacy RGB rev LEDs driven by 0x0A.
[ReportID, 0xF8, 0x09, 0x0C, brightness, dirty_flag, 0x00, 0x00]
The brightness value goes through the same conversion the col03 intensity path applies.
Subcommand 0x01 doesn't act on its own — byte[4] selects the operation:
[ReportID, 0xF8, 0x09, 0x01, operation, data...]
Controls the 3-digit display found on many Fanatec wheels, hubs, and button modules. Typically used to show gear, speed, or short text strings.
The same command is used regardless of the underlying display hardware — physical LED 7-segment displays and small OLED displays are both addressed identically. On devices with a larger ITM-capable OLED (e.g., PBME), this drives the legacy mode (the last ITM page), which renders 7-segment-style content. See Display Capabilities for per-device display types.
[ReportID, 0xF8, 0x09, 0x01, 0x02, <seg1>, <seg2>, <seg3>]
| Byte | Field | Description |
|---|---|---|
| 5 | seg1 | Left digit — segment bitmask |
| 6 | seg2 | Center digit — segment bitmask |
| 7 | seg3 | Right digit — segment bitmask |
Each segment byte is a bitmask controlling 7 segments plus a decimal point. See 7-Segment Encoding Tables for the full digit, letter, and symbol lookup tables.
seg 0
───────
│ │
5 1
│ │
─────── seg 6
│ │
4 2
│ │
─────── • seg 7 (dot/decimal point)
seg 3
| Bit | Segment | Position |
|---|---|---|
| 0 | Top | Horizontal top bar |
| 1 | Upper-right | Vertical right upper |
| 2 | Lower-right | Vertical right lower |
| 3 | Bottom | Horizontal bottom bar |
| 4 | Lower-left | Vertical left lower |
| 5 | Upper-left | Vertical left upper |
| 6 | Middle | Horizontal middle bar |
| 7 | Dot | Decimal point |
The decimal point (bit 7, 0x80) can be combined with any character via bitwise OR:
Digit 3 with dot: 0x4F | 0x80 = 0xCF
Letter A with dot: 0x77 | 0x80 = 0xF7
Examples:
Display Gear "5": [RID, F8, 09, 01, 02, 00, 6D, 00] (center = 0x6D)
Display Speed "142": [RID, F8, 09, 01, 02, 06, 66, 5B] (1, 4, 2)
Display Text "Hi": [RID, F8, 09, 01, 02, 76, 06, 00] (H, I, blank)
Clear Display: [RID, F8, 09, 01, 02, 00, 00, 00]
A general, SubId-parameterized report trigger — not a button-module command. A single firmware command requests that the device emit a class of report selected by the SubId byte; button-module refresh is only one of its uses (distinct, non-module purposes such as clutch-bite-point and legacy identity ride the identical F8 09 01 06 frame). Sent as an ON/OFF pair:
Trigger ON: [ReportID, 0xF8, 0x09, 0x01, 0x06, 0xFF, <SubId>, 0x00]
Trigger OFF: [ReportID, 0xF8, 0x09, 0x01, 0x06, 0x00, 0x00, 0x00]
| Byte | ON value | OFF value | Description |
|---|---|---|---|
| 4 | 0x06 |
0x06 |
Report trigger subcmd |
| 5 | 0xFF |
0x00 |
Enable flag (ON/OFF) |
| 6 | SubId | 0x00 |
Identifies the trigger purpose |
| 7 | 0x00 |
0x00 |
Padding |
SubId values:
| SubId | Purpose | Used By |
|---|---|---|
| 0 | Plain "emit USB input report" / the OFF (deassert) half of every pair | input-report trigger; trailing OFF of every ON/OFF pulse |
| 1 | Button module detection refresh | BME refresh — re-scan the attached button module |
| 2 | CBP data request / notification | Set CBP / Get CBP |
| 3 | Hardware-identity report trigger | legacy stack only; superseded by the FF 08 system report on modern col03 bases |
SubId=2 tells the firmware either "I just set CBP" (after Set) or "please report the current CBP" (before Get). The firmware responds with a HID input report that the input handler processes.
SubId selects which report the device returns; the same
0x06frame carries all of them, so the mechanism is general rather than module-specific. From what we've observed, the SubId is understood to select:1= re-detect an attached button module,2= clutch bite point (paired with Set CBP),3= a legacy hardware-identity report (superseded by the System Report on modern col03 bases). ASubId = 4is also emitted during a modern base's startup sequence; its effect is currently unknown.
The correct sequence is one ON/OFF pair followed by a 100ms sleep. The sleep gives the firmware time to process the trigger and send its response.
Sets the clutch bite point (CBP) engagement threshold for analog clutch paddles.
[ReportID, 0xF8, 0x09, 0x01, 0x17, 0x01, <CBP>, 0x00]
| Byte | Field | Description |
|---|---|---|
| 5 | 0x01 |
Enable flag (always 0x01) |
| 6 | CBP | CBP value (0–100, clamped) |
CBP uses a completely separate command path from the tuning menu — it is not part of the tuning payload structure.
Prerequisites:
- The connected steering wheel must have clutch paddles.
- CBP operations should not be performed during active display updates to avoid flickering.
Complete Set CBP sequence:
1. Send: [RID, F8, 09, 01, 17, 01, <CBP>, 00] ← Set CBP value
2. Send: [RID, F8, 09, 01, 06, FF, 02, 00] ← Trigger ON (SubId=2)
3. Send: [RID, F8, 09, 01, 06, 00, 00, 00] ← Trigger OFF
4. Wait: 100ms ← Firmware processing time
Complete Get CBP sequence:
1. Send: [RID, F8, 09, 01, 06, FF, 02, 00] ← Trigger ON (SubId=2)
2. Send: [RID, F8, 09, 01, 06, 00, 00, 00] ← Trigger OFF
3. Wait: 100ms ← Wait for firmware response
4. Read: CBP value from device input report
The 100ms delay is necessary because the firmware sends a response via the input handler. The official software retrieves the resulting value from a Windows registry key where the filter driver stores it.
Display interaction: The SubId=2 trigger can cause the firmware to momentarily display the CBP value on the 7-segment display. This is by design — the firmware uses the trigger as a signal to show CBP feedback to the user. The 100ms delay helps, but the firmware may hold the CBP display for longer. During this period, sending 7-segment display commands will conflict with the firmware's CBP display, causing visible flickering. For OLED-equipped devices (e.g., PBME), use display ownership to yield to the firmware during CBP operations. For non-OLED devices, display suspension must be managed by the host software.
On certain OLED-equipped devices (currently only the PBME), the host can explicitly take or release control of the display. The official software checks whether the device has an OLED before sending this command.
Host takes control: [RID, F8, 09, 01, 18, 02, 00, 00]
Release to firmware: [RID, F8, 09, 01, 18, 01, 00, 00]
| Byte[5] | Meaning |
|---|---|
0x02 |
Host control — firmware stops updating the display |
0x01 |
Firmware control — firmware resumes display ownership |
This is important during operations where the firmware needs to show its own content (e.g., CBP adjustment, tuning menu navigation). The host should release control, wait for the operation to complete, then reclaim control.
For non-OLED devices (including LED 7-segment displays and the PBMR's small OLED), this command is a no-op. Display conflict management on these devices must be handled by the host software.
Some older wheelbases (e.g., CSL Elite Wheel Base) have a resident 9-LED rev strip on the wheelbase itself — separate from any rev indicator on the connected wheel. This can be driven by its own command class at byte[2] = 0x13.
The base and wheel share the rev-LED channel, then split. At power-on both the base strip and the wheel's rev LEDs follow the legacy 0x08 writes. The first 0x13 write moves the base onto its own channel; afterward 0x13 drives the base and 0x08 drives only the wheel — independently, until power-cycle. That's how an RPM display can run on the base strip and a RevStripe wheel at once: drive the wheel via 0x08 and the base via 0x13.
Sets which of the base strip's LEDs are lit:
[ReportID, 0xF8, 0x13, bits_0_7, bit_8, 0x00, 0x00, 0x00]
9-LED bitmask in natural bit order — byte[3] = LEDs 0–7 (bit N → LED N), byte[4] bit 0 = LED 8.
Different packing from
0x08.0x13is natural order (bit 0 = LED 0); the wheel's0x08bitmask is reversed (data_hibits 7..0 = LEDs 1..8). Encode each target with its own layout.
Seen in captures alongside 0x13; appears to enable/disable the base strip:
Enable: [ReportID, 0xF8, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00]
Disable: [ReportID, 0xF8, 0x14, 0xFF, 0x00, 0x00, 0x00, 0x00]
Captures of official software driving a RevStripe wheel bracket the color writes with 0x14 — 0xFF first, 0x00 afterwards.
All col03 commands use the [0xFF, cmd_class, subcmd, ...] framing described in Report Framing.
Modern LED protocol using per-LED RGB565 color values. Used by newer rims with col03 support (PBME, CSSWFORMV3, GTSWX, etc.).
Byte: [0] [1] [2] [3..4] [5..6] ...
0xFF 0x01 subcmd LED0_RGB LED1_RGB ...
Each LED color is a 16-bit BGR565 value stored in big-endian byte order. A color value of 0x0000 means the LED is off.
RPM/shift indicator colors. Up to 30 LEDs, one RGB565 value per LED.
FF 01 00 [R0hi R0lo] [R1hi R1lo] ... [R8hi R8lo] 00...
Status/warning indicator colors. Up to 30 LEDs, one RGB565 value per LED.
FF 01 01 [F0hi F0lo] [F1hi F1lo] ... [F5hi F5lo] 00...
Button backlight RGB colors. Up to 12 RGB565 values. Only applies to devices with RGB-capable button LEDs (currently PSWBMW, GTSWX, and the PBMR module).
Note — PBMR color inconsistency: The PBMR is the only known device that interprets these 16-bit values as BGR555 (5 bits per channel, green MSB ignored). The 6th green bit — bit 10 — is silently discarded. For example,
0x0400(G=32, a dim green) renders as black on the PBMR because bit 10 is the only bit set and it's the ignored one. Construct colors for the PBMR by quantizing green to 5 bits and shifting it left by 5, which leaves bit 10 clear. See PBMR for details.
Byte: [0] [1] [2] [3..4] ... [25..26] [27]
0xFF 0x01 0x02 LED0_RGB ... LED11_RGB commit
| Field | Bytes | Description |
|---|---|---|
| LED colors | 3–26 | Up to 12 RGB565 values (big-endian) |
| Commit | 27 | 0x01 = apply, 0x00 = stage only |
Per-button intensity values. 3-bit values (range 0–7).
Byte: [0] [1] [2] [3] [4] ... [17] [18]
0xFF 0x01 0x03 int_0 int_1 ... int_14 commit
| Field | Bytes | Description |
|---|---|---|
| Intensities | 3–17 | 15 intensity bytes (3-bit, range 0–7) |
| Commit | 18 | 0x01 = apply, 0x00 = stage only |
The intensity payload has 15 slots while the color payload has 12 — the extra slots exist because intensity values are 1 byte each vs 2 bytes for RGB565 colors. On some devices, the higher-indexed intensity slots control additional lighting elements (e.g., encoder backlighting) that are intensity-only and have no corresponding color slot.
Staging behavior: When both color and intensity need to change:
- Send the color report with commit =
0x00(stage) - Send the intensity report with commit =
0x01(commit both)
When only one has changed, send that report alone with commit = 0x01.
Conventionally the ITM session enable (command class 0x02, not 0x05). It is separate from the persistent ITM Mode gate (FF 05 02); Fanatec's bring-up sequences include both, though only the gate has been observed to have a measured effect to date — see Control Model.
FF 02 02 00 [00 x60]
| Byte | Value | Description |
|---|---|---|
| 0 | 0xFF |
Report ID |
| 1 | 0x02 |
Command class |
| 2 | 0x02 |
Sub-command |
| 3 | 0x00/0x01 |
Observed as 0x00 (before page changes) and 0x01 (at software startup) in official-software captures. Not a page selector — paging is PageSet. Semantics unresolved. |
Important:
- In hardware testing to date, "Enable" produced no observable effect in any state tried — mid-session, after gate-off, after a wheel hot-swap, after a base power cycle — and cold bring-up succeeds without it (gate + PageSet alone). Until this command is better understood, it is recommended to retain it in bring-up sequences purely to match official-software behavior.
- On its own, Enable shows nothing. What the display shows is governed by the ITM Mode gate and PageSet — see Control Model for the bring-up.
Controls all wheelbase settings (SEN, FF, damper, spring, etc.) via command class 0x03.
Byte: [0] [1] [2] [3] [4..63]
0xFF 0x03 subcmd devId payload (zero-padded to 64 bytes)
Sets tuning parameters.
[FF 03 00 devId data[0] data[1] ... data[59]] (64 bytes)
The tuning data starts at byte[4] in WRITE commands. See Tuning Payload Structure for field offsets and WRITE Report Byte Map for the complete byte-level layout.
The device rejects WRITE commands that don't reflect the current state. Always use the Read-Modify-Write Pattern. The WRITE itself produces no response — once sent, no trigger or acknowledgment is needed.
Subcmd
0x06disambiguation: Two unrelated commands share the number0x06. Neither is part of the WRITE flow:
- TOGGLE (
FF 03 06) — col03 tuning subcmd that switches between standard and simplified tuning mode.- Report Trigger (
RID F8 09 01 06) — col01 group 0x01 subcmd used for CBP operations.
Switches the active setup index (0–4).
[FF 03 01 devId setupIndex 00 ... 00]
Requests the current tuning state. Triggers a response on the col03 IN endpoint.
Request:
[FF 03 02 00 00 00 ... 00] (64 bytes)
Response:
[FF 03 devId data[0] data[1] ... data[60]] (64 bytes)
In the READ response, tuning data starts at byte[3] — shifted by 1 byte relative to the WRITE command. See READ vs WRITE Report Layout for details.
Persists current tuning state to device flash.
[FF 03 03 devId 00 ... 00]
Restores factory defaults for tuning parameters.
[FF 03 04 devId 00 ... 00]
Toggles between standard and simplified tuning mode.
[FF 03 06 devId 00 ... 00]
The tuning payload is embedded in both READ responses and WRITE commands at different byte offsets (see READ vs WRITE Report Layout). The offsets below are relative to the start of the payload, not the HID report:
| Offset | Field | Type | Range | Description |
|---|---|---|---|---|
| 0 | UserSetupIndex | byte | 0–4 | Active setup slot |
| 1 | SEN | byte | 0–255 | Steering Sensitivity |
| 2 | FF | byte | 0–255 | Force Feedback strength |
| 3 | SHO | byte | 0–255 | Shock / vibration intensity |
| 4 | BLI | byte | 0–255 | Brake Linearity |
| 5 | LIN | byte | 0–255 | Linearity (aliased as FFS in some variants) |
| 6 | DEA | byte | 0–255 | Dead Zone |
| 7 | DRI | sbyte | -128–127 | Drift Mode (signed — the only signed field) |
| 8 | FOR | byte | 0–255 | Force |
| 9 | SPR | byte | 0–255 | Spring |
| 10 | DPR | byte | 0–255 | Damper |
| 11 | NDP | byte | 0–255 | Natural Damper |
| 12 | NFR | byte | 0–255 | Natural Friction |
| 13 | BRF | byte | 0–255 | Brake Force |
| 14 | BRG | byte | 0–255 | Brake Gain |
| 15 | FEI | byte | 0–255 | Force Effect Intensity |
| 16 | MPS | byte | 0–255 | Max Power Supply / Motor Protection |
| 17 | APM | byte | 0–255 | Advanced Paddle Mode (rotary wheels only) |
| 18 | INT | byte | 0–255 | Interactivity |
| 19 | NIN | byte | 0–255 | Natural Inertia |
| 20 | FUL | byte | 0–255 | Full Lock (steering angle) |
| 21 | BIL | byte | 0–255 | Bilateral / Balance |
| 22 | ROT | byte | 0–255 | Rotation |
Notes:
- DRI (offset 7) is the only signed byte in the structure.
- LIN vs FFS: Two variants of this data structure exist in the official software — one names offset 5
LIN, the other calls itFFS. Same byte position, same semantics. - APM (offset 17): Only populated on APM-capable wheels. Zero on all other wheels.
- Payload offsets 23–59 are reserved and always zero in WRITE commands (60 payload bytes at HID bytes 4–63). READ responses carry one additional trailing byte (offset 60 at HID byte 63). The SDK's internal managed struct is 65 bytes (offsets 0–64), but only offsets 0–59 appear on the wire in a WRITE.
Critical: READ responses and WRITE commands place the tuning data at different byte offsets within the 64-byte HID report.
| Direction | Header | Data Start | Reason |
|---|---|---|---|
| READ response | [FF 03 devId ...] |
byte[3] | No subcmd byte in response |
| WRITE command | [FF 03 00 devId ...] |
byte[4] | Subcmd 0x00 occupies byte[2] |
Conversion from READ → WRITE buffer:
writeBuf[0] = 0xFF
writeBuf[1] = 0x03
writeBuf[2] = 0x00 // subcmd = WRITE
writeBuf[3] = readBuf[2] // device ID
writeBuf[4..63] = readBuf[3..62] // tuning data (shifted by 1)
Step 1: Send READ
→ [FF 03 02 00 ... 00]
Step 2: Receive response
← [FF 03 02 XX YY ZZ ...] (devId=0x02, data follows)
Step 3: Build WRITE buffer
writeBuf = [FF 03 00 02 XX YY ZZ ...]
(copy readBuf[2..62] → writeBuf[3..63])
Step 4: Modify desired fields
e.g., writeBuf[4 + field_offset] = newValue
Step 5: Send WRITE
→ [FF 03 00 02 XX YY' ZZ ...]
Example — Changing Force Feedback Strength:
FF is at struct offset 2. In the WRITE buffer: writeBuf[4 + 2] = writeBuf[6]:
writeBuf[6] = 80; // Set FF to 80
Example — Switching Setup Slot:
UserSetupIndex is at struct offset 0. In the WRITE buffer: writeBuf[4]:
writeBuf[4] = 2; // Switch to setup slot 2
The dedicated SELECT SETUP subcmd can also be used for this.
Quick reference mapping HID byte positions to tuning fields:
| HID Byte | Content | Struct Offset | Field |
|---|---|---|---|
| 0 | 0xFF (report ID) |
— | — |
| 1 | 0x03 (cmd class) |
— | — |
| 2 | 0x00 (WRITE subcmd) |
— | — |
| 3 | device ID | — | — |
| 4 | UserSetupIndex | 0 | Setup slot |
| 5 | SEN | 1 | Sensitivity |
| 6 | FF | 2 | Force Feedback |
| 7 | SHO | 3 | Shock |
| 8 | BLI | 4 | Brake Linearity |
| 9 | LIN/FFS | 5 | Linearity |
| 10 | DEA | 6 | Dead Zone |
| 11 | DRI (signed) | 7 | Drift |
| 12 | FOR | 8 | Force |
| 13 | SPR | 9 | Spring |
| 14 | DPR | 10 | Damper |
| 15 | NDP | 11 | Natural Damper |
| 16 | NFR | 12 | Natural Friction |
| 17 | BRF | 13 | Brake Force |
| 18 | BRG | 14 | Brake Gain |
| 19 | FEI | 15 | Force Effect Int. |
| 20 | MPS | 16 | Max Power Supply |
| 21 | APM | 17 | Adv Paddle Mode |
| 22 | INT | 18 | Interactivity |
| 23 | NIN | 19 | Natural Inertia |
| 24 | FUL | 20 | Full Lock |
| 25 | BIL | 21 | Bilateral |
| 26 | ROT | 22 | Rotation |
| 27–63 | 0x00 |
23–59 | Reserved |
The firmware supports event-driven tuning change notification — no polling required:
| Event | Description |
|---|---|
| TuningMenuDataChanged | Tuning parameters changed (user adjusted via wheel controls) |
| ITMPageChanged | ITM page subscription changed |
| AnalysisPageChanged | Analysis page updated |
| DeviceSettingsChanged | CBP / TorqueMode / MaxTorque / FFS changed |
CBP is not part of the tuning data structure. It uses a completely separate command path via col01. See Set Clutch Bite Point.
When setting multiple tuning parameters at once, CBP should be sent after the col03 tuning data write. This matches the ordering observed in the official software — tuning data first, then CBP:
1. Send tuning data via col03 (FF 03 00 ...) ← WRITE command
2. Send CBP via col01 (RID F8 09 01 17 ...)
3. Send trigger pair (SubId=2)
4. Wait 100ms
Controls OLED and LCD telemetry displays. Provides multi-page telemetry dashboards with parameters like speed, gear, lap times, tyre temperatures, and more. See ITM Supported Devices for the device compatibility matrix.
All ITM display commands share the frame:
Byte: [0] [1] [2] [3..63]
0xFF 0x05 subcmd payload
Sends telemetry values for display. Each entry contains a handle, parameter ID, and value:
FF 05 01 <entries...> [00-padded to 64 bytes]
Each entry:
| Offset | Size | Field | Description |
|---|---|---|---|
| 0 | 1 | Device ID | Which display this entry is for (values as in PageSet) |
| 1 | 1 | Handle | Parameter handle (dictated by the firmware — see Firmware Subscription Pushes) |
| 2–3 | 2 | Param ID | Parameter ID (little-endian). See ITM Parameter IDs. |
| 4 | 1 | Size | Value size in bytes — 1, 2, or 4 for numeric types; text-typed parameters use the character count (e.g. ENGINE_MAPPING sends 1–2 bytes) |
| 5+ | N | Value | Parameter value (little-endian, size from above) |
The leading byte is the display-device id — which display the entry is for, using the same values as PageSet. Entries addressed to a display that isn't attached are silently ignored.
Multiple entries may be packed into one report; the firmware accepts batched updates.
Turns ITM on or off at the firmware level — the same persistent state the Fanatec software's "ITM" switch sets; the small "ITM" text in the lower-right of the display reflects it. This is the persistent FF 05 02 gate, not the session Enable (FF 02 02); bring-up sequences conventionally include both, though only the gate has a measured effect — see Control Model.
FF 05 02 00(off): the display drops to the true legacy 7-segment view and the "ITM" indicator disappears (any cached 7-seg value already on screen persists).FF 05 02 01(on): ITM turns on and shows the page from the most recent PageSet since the gate-off — or the legacy ITM page if none (page 6 on BME/GTSWX; the number varies by display — see ITM Page Layouts). If that lands on a different page than before, the firmware emits the page's subscription push: an unsubscribe-all clearing the old handles, then the new page's handles. Landing on the legacy ITM page — which carries no telemetry parameters — pushes only the unsubscribe-all. A gate-on that lands where the display already was — e.g. a bare gate cycle from legacy — emits nothing.
A PageSet works even while gated off: it's recorded (no visual, ITM is off) and applied on the next gate-on. So gate off → PageSet(N) → gate on lands directly on page N, whereas a bare gate off → gate on lands on legacy. There is always a current page — the legacy ITM page is the fallback, never "no page." (Hardware-confirmed.)
Don't conflate the two legacy-looking states. The legacy ITM page (gate on, session live) and true legacy (gate off) are easy to mistake for each other: both render 7-segment-style col01 content, and the visible difference is the small "ITM" text in the lower-right of the screen — shown on the legacy ITM page, absent in true legacy. On the wire they are worse than similar: entering either state produces at most an unsubscribe-all, so an unsubscribe-all with no subscriptions following does not identify which state the display entered, and this protocol has no state query. To disambiguate actively, send a
PageSettargeting a telemetry page: a subscription push back means the session was live (and the display is now on that page); silence means gated off (the PageSet was recorded, not applied) — or a lost command. Thegate off → PageSet → gate onsequence converges from either state, which is why recovery procedures prefer it over diagnosis.
Enable: FF 05 02 01 [00 x60]
Disable: FF 05 02 00 [00 x60]
| Byte | Value | Description |
|---|---|---|
| 3 | 0x01 / 0x00 |
ITM on / off |
The setting is persistent (survives power cycles — the same state the official software's ITM switch reflects), but the runtime session is not: after a base power cycle or a wheel re-seat the device behaves cold regardless of the persisted setting — PageSets are ignored until a fresh gate off → PageSet → gate on re-establishes the session. Persistence tells you what the display will be set to; it does not save the host from re-running bring-up.
Functionally a unit/suffix report: it attaches a short ASCII decoration to a parameter the firmware has already subscribed — a unit like "C" on a temperature, or a total like "/24" that renders "Position 5 /24". That is all it does. "ParamDefs" is the historical name used throughout these docs, dating from an early misreading that this command defined which parameters a page displays — it does not: page composition and placement are fixed by the firmware and announced via subscription pushes; this command only decorates those slots.
FF 05 03 <entries...> [00-padded to 64 bytes]
Each entry:
| Offset | Size | Field | Description |
|---|---|---|---|
| 0 | 1 | Device ID | Which display this entry is for (values as in PageSet) |
| 1 | 1 | Slot ID | The target parameter's handle with the high bit set (0x80 | handle — e.g., 0x82 decorates handle 2) |
| 2 | 1 | Position Lo | Low byte of position. Always 0x00 in captures; purpose unknown. |
| 3 | 1 | Position Hi | High byte of position. Always 0x00 in captures; purpose unknown. |
| 4 | 1 | Suffix Length | Number of suffix bytes following (0 = no suffix) |
| 5+ | N | Suffix Bytes | ASCII text appended after values (e.g., 2F 30 = "/0") |
Example — two slots with suffix "/0":
03 82 00 00 02 2F 30 ← slot 0x82, suffix "/0" (2 bytes: '/', '0')
03 83 00 00 02 2F 30 ← slot 0x83, suffix "/0"
The suffix system attaches a unit/total to a slot (e.g. 2F 30 = "/0" total denominator → "Lap 5 / 20"; 43 = "C" for °C on temperature params). The slot ID is 0x80 + handle — i.e. ParamDefs decorates the same handles used by ValueUpdate, but with the high bit set. ParamDefs is cosmetic (units only): a param's value renders from its ValueUpdate alone; only params that carry a unit/total get a ParamDefs entry.
Suffixes are the entire wire-level unit mechanism. Unit conversion happens host-side — the value is converted before sending (°C vs °F, km/h vs mph) and the matching suffix text chosen; the firmware renders whatever number it is given. A total suffix like "/24" is literal text, so the entry must be re-sent whenever the total changes. Suffix frames appear at bring-up, on page changes, and on total changes — not per value tick.
Maximum subscribed parameters per device: 16.
Selects which ITM page is active on a specific display device. Byte[3] is always the display-device id; byte[4] is the page:
FF 05 04 <deviceId> <page> [00 x59]
| Byte | Field | Description |
|---|---|---|
| 3 | Device ID | Target display: 1=Base, 3=BME/GTSWX, 4=Bentley — see ITM Supported Devices. Device 2 ("SmallOLED") exists in the protocol's device numbering but no known hardware implements it; frames addressed to it are seemingly accepted, but have no effect. |
| 4 | Page number | Page to display (1–6, device-dependent). See ITM Page Layouts. |
Space page-change commands at least ~100 ms apart — an established convention that matches official-software pacing; tighter spacing has not been probed.
Dropped PageSets: concurrent traffic is an identified cause. PageSet reliability depends on what else is on the wire. In hardware testing, switches issued on an otherwise-quiet col03 channel were consistently confirmed by a push, while switches issued with ValueUpdates streaming alongside failed a substantial fraction of the time (no push, no page change) — and in one instance, streaming that continued through the retries left the display unresponsive until a quiet gate-off → PageSet → gate-on cycle recovered it. The failure rate likely varies with wheelbase, firmware, and traffic volume; the mechanism — a switch can silently fail to take, and concurrent traffic makes that more likely — should be assumed everywhere. Two rules for reliable host-driven paging follow: (1) suspend ValueUpdate/ParamDefs traffic from shortly before a PageSet until its push confirms; (2) treat the push as the only acknowledgment that a switch happened — never assume a PageSet took, and be prepared to retry.
A second, expected case looks identical to a dropped command: the firmware only pushes on an actual page change. A PageSet for the page the display is already on correctly produces no push (consistent in testing; a preceding Enable makes no difference). The absence of a push is therefore ambiguous — the host cannot tell "already on this page" from "command lost." To convert silence into signal, force a genuine change: switch to a different page and back (each real change must push).
Practical guidance for host-driven paging: suspend value traffic around the switch, treat a missing push as a possible failure only when targeting a different page, retry with ≥100 ms spacing, and escalate to a flip-away-and-back — then to a gate cycle with the PageSet sent while gated off, the most reliable recovery observed in testing (see the gate).
The dev-2 PageSet stream is not a keepalive. Current official software emits PageSet(2, X) at ~10 Hz from its first ITM activity until it exits (X varies by rig/version — pinned to 11, device 2's legacy page, or mirroring the active device-3 page). Since no current hardware implements device 2, the stream has no effect on anything; it runs even while gated off and stops only when the software exits. It serves no protocol function: the display has no idle timeout (see Control Model) and no periodic frame is needed — our recommendation is to not send it.
Resets every rendered ITM field to its per-field placeholder — laps show --- / -, times show --:--.-, and so on. The frame is fixed; it takes no device id and no page (interface-global — it resets the ITM display state as a whole, not one display device):
FF 05 05 01 [00 x60]
What it does and doesn't touch (hardware-verified):
- Cleared: every field value previously written by ValueUpdate, on every ITM page.
- Untouched: the ITM Mode gate, the session enable, the active page, and the firmware's current subscriptions — no subscription push is emitted, and value flow resumes at the existing handles. (The reset elicits nothing on the input endpoint; it is a command, not a query.)
- Untouched: the legacy ITM page — its 7-segment-style content is rendered from col01 display writes and is cleared through that path instead.
This is the only known way to clear already-written field values: gating ITM off and back on does not clear them — the firmware retains every field's value across the off→on cycle and shows it again on re-enable (hardware-verified). The official software issues a reset during its own ITM initialization. Because subscriptions survive the reset, subsequent ValueUpdates repaint the same handles with no re-bring-up.
Control of the ITM display is split between the host and the firmware — neither drives it alone:
- The host selects the page. A host PageSet chooses which page is shown; the wheel's display button does the same thing. The two are equivalent triggers within a live session — neither works before a session exists (a cold display ignores both until brought up).
- The firmware assigns the handles. On every page change it pushes the new page's parameter subscriptions (see Firmware Subscription Pushes); the host echoes ValueUpdates only at the handles the firmware pushed. Values sent at guessed handles — for a page the firmware hasn't announced — are ignored.
So the host chooses what page; the firmware chooses which handles.
Bring-up sequence (hardware-verified on a PBME):
1. FF 05 02 01 ... ← ITM Mode gate ON (persistent — 0x02 above)
2. FF 02 02 00 ... ← session Enable (optional, kept for official parity; see 0x02 — ITM Enable)
3. FF 05 04 <dev> <page> ← PageSet to establish a page
4. FF 05 01 <entries> ← ValueUpdates at the handles the PageSet pushes back
Step 3 is what actually brings a page up: in testing, a bare FF 02 02 enable with no PageSet produced no push and no page. Within a live session, a PageSet — or a wheel-button press — is all that's needed to make the firmware push a page's subscription. But note the gate's setting-vs-session distinction (see ITM Mode): after a power cycle or wheel re-seat the device is cold no matter what setting persisted, and the full sequence — with the gate command actually re-sent — is required again.
No keepalive. The display has no idle timeout: it holds its content indefinitely, and the value stream isn't even required to keep it lit. (Hardware-confirmed.) There is no liveness ping in this protocol — the periodic frame current official software emits is a page-set to an absent display device, not a keepalive (see PageSet) — and none is needed.
This is the mechanism behind the Control Model above. When the page changes — from a host PageSet or the wheel button — the firmware pushes one or more FF 05 01 reports on the col03 input endpoint, telling the host exactly which parameters to display at which handles. A host PageSet makes the firmware push the new page's subscription within tens of milliseconds (20–70 ms observed in testing; hosts should budget a comfortably larger deadline before treating a push as missed); the host then echoes ValueUpdates for those subscriptions.
Fragmentation varies by setup. One tested setup (Podium DD+ with a PBME) consolidates a push into 1–2 multi-entry reports; another (ClubSport DD with a GTSWX) sends one entry per report — 6–7 reports arriving ~2 ms apart for a single page change. Both wheelbase and wheel differ between those observations, so whether the framing is determined by the base or by the wheel/module is unknown. Accumulate across reports in all cases.
Each pushed report uses the same FF 05 01 framing, with 5-byte entries:
| Offset | Size | Field | Description |
|---|---|---|---|
| 0 | 1 | Device ID | Which display this entry is for (values as in PageSet) |
| 1 | 1 | Handle | Firmware handle. The 0x80 bit marks a "slot" param (one with a ParamDefs unit). Host ValueUpdate handle = handle & 0x7F. |
| 2–3 | 2 | Param ID | Subscribed parameter (little-endian), or 0xFFFF = unsubscribe that handle. |
| 4 | 1 | Type | Value data-type/size selector — low nibble is the wire type (1=text, 2/3=1-byte, 4/5=2-byte, 6/7/9=int32, 8/10=float32); high-nibble meaning unknown (varies per display for the same parameter). Not the display unit. The same parameter can declare different types on different displays — GEAR is a 1-byte int on a PBME (0x12) but text on a GTSWX (0x11) — and the host's value encoding must follow the declared type, or the field renders nothing (capture- and hardware-confirmed). |
A page change arrives across several reports — an unsubscribe of the old handles (0xFFFF), plus one or more (often overlapping / partial) reports listing the new page's handles. The host must accumulate them — apply each entry as it arrives (subscribe, or 0xFFFF = unsubscribe) — never treating a single report as the complete set. (One observed instance: page 3's 7 handles arrived as two overlapping reports sharing h1–h5, one adding h6=BRAKE_BIAS, the other h0=SPEED — the specific handles are illustrative only, per the allocation rules below.) Example — switching to the Tyre Temps page:
FF 05 01 03 00 0001 34 03 01 0004 12 03 82 002A 32 03 83 0030 32 ...
└ h0=SPEED ┘ └ h1=GEAR ┘ └ h2=TYRE_FL ┘ └ h3=TYRE_RL ┘
(0x82 & 0x7F = 2, so the host sends TYRE_FL at handle 2.)
Never assume a fixed page→handle map — handle allocation varies by setup, and handles are not page-owned. The universal rules, consistent across every observation:
- The firmware announces every assignment via push; assignments are not a property of the page. Whichever page is established first takes the base handles (a session brought up on page 2 lands it at the handles page 1 would otherwise get).
- A handle number can be re-bound to a different parameter on any page change — observed within a single session:
h9= ERS_LEVEL while Fuel/ERS/DRS was up, then BEST_LAP_TIME after switching to Lap Times. This is the deeper reason to stop sending values the moment a page change begins: entries at the old page's handles may land on re-bound parameters, not just be ignored. - Handle numbers live in a small bounded space (≈
h0–h13; no growth observed over 60 consecutive switches), every change unsubscribes what it replaces, and a gate cycle resets the table.
On top of those invariants, two different allocation strategies have been observed: one setup (Podium DD+ with a PBME) double-buffers between two regions (≈h0–h5 / h6–h13), subscribing each new page into the currently-free region — the same page lands at different handles on successive visits (6, 0, 6, 0 … across a full cycle), and a 7th parameter spills into the far region. Another setup (ClubSport DD with a GTSWX) reuses one region in place for every page, rebinding the same handle numbers each change. Neither strategy should be assumed: follow the pushed handles after every change and never cache a page→handle map across visits.
Getting the first page. The firmware pushes a subscription only when a page is established or changed. So after enabling, send a PageSet (or wait for a wheel-button press) and populate the handles it pushes back; values sent before that push, at guessed handles, are ignored. See Control Model for the full bring-up.
Not all parameters need to be sent at full rate. Recommended minimum intervals:
| Category | Delay (ms) | Parameters |
|---|---|---|
| Real-time | 0 | SPEED, GEAR, POSITION, LAP, LAST_LAP_TIME, BEST_LAP_TIME, BRAKE_BIAS, FUEL, DRS, ABS, TC |
| Near real-time | 30 | LAP_TIME |
| Moderate | 40–50 | RPM_MAX, TYRE temps |
| Low-frequency | 100 | ENGINE_MAPPING, ERS, others |
The official software supports automatic page switching based on telemetry events (page contents are in ITM Page Layouts):
Value-Change Triggers:
| Trigger | Target Page |
|---|---|
| Lap number or Position changed | Page 1 (Lap Info) |
| DRS zone or DRS active changed | Page 2 (Fuel/ERS/DRS) |
| TC, ABS, EngineMap, or BrakeBias changed | Page 3 (Car Settings) |
| Best lap time changed | Page 4 (Lap Times) |
Threshold Triggers:
| Trigger | Target Page |
|---|---|
| Fuel below threshold | Page 2 |
| ERS below threshold | Page 2 |
| Oil temp above threshold | Page 3 |
| Any tyre temp out of range | Page 5 |
Favorite Page:
Each device has a configurable favorite page with a display duration (default 10 seconds, range 3–60 seconds). After a trigger-caused page change, the display reverts to the favorite page after the duration expires.
The system report is how a modern col03-capable wheelbase reports its identity and configuration: the connected base, the attached wheel or hub, and any button module on a module-capable hub. It is the primary way to identify connected hardware over pure HID.
Enable push-on-change: FF 08 01 FF (disable: FF 08 01 00)
Trigger one report: FF 08 02
After a single enable, the base pushes a fresh system report on every attachment change (wheel, hub, or module connect/disconnect) and is otherwise silent. A trigger requests one immediate report — used to seed the initial identity on connect. A directly-connected modern base needs no prior handshake to respond.
As far as we've observed, the system report is specific to modern col03-capable wheelbases; older col01-only wheels self-identify through their regular input report instead, and some wheel-side adapters expose col03 without implementing it. Behavior on the oldest bases is inferred, not confirmed.
The base replies on col03 with a 64-byte report beginning FF 08. Identity-bearing fields (offsets relative to the leading 0xFF):
| Offset | Field | Description |
|---|---|---|
0x02–0x03 |
SystemConfig | 16-bit little-endian (byte[3] << 8 | byte[2]). Its only effect is selecting the firmware-version field widths: legacy (< 6) uses 1-byte version fields, extended (≥ 6) uses 4-byte blocks. It does not affect the identity offsets below. Every base observed reports the extended layout with byte[3] = 0x00; the legacy layout is inferred from the field structure, not yet observed. byte[0x02] is the low byte of this word and doubles as a BaseType (wheelbase model) hint — but only while byte[0x03] is 0x00; if a base ever set byte[0x03] nonzero, byte[0x02] would be a version low-byte, not a base code. |
0x18 |
WheelCode | Attached wheel or hub, as a single-byte code. 0x00 = nothing attached; 0xFF = EXT_INFO, a reserved escape for devices that report identity through extended fields instead. Code → device: see devices.md. |
0x1F |
Module | Button-module presence — meaningful only when a module-capable hub is attached (not all hubs accept modules): 0x00 = none, 0x01 = PBME, 0x02 = PBMR. |
The WheelCode (0x18) and Module (0x1F) bytes sit at fixed offsets, outside the firmware-version fields and read independently of SystemConfig, so they decode identically on either layout. Remaining offsets carry firmware-version fields (widths set by SystemConfig) and transient status bytes that are not part of identity and may differ between consecutive reports.
The numeric codes above map to specific hardware. The wheel/hub code (0x18) is a single byte, with 0xFF reserved as the EXT_INFO escape, so treat the published list as the currently-known set, not a closed enumeration. A code that isn't in the known set is not a protocol error: a consumer should fall back to a generic identity and continue rather than reject the report. The wheel/hub and module codes are the reliable identity signals here; the BaseType hint (byte[0x02]) is best treated as advisory, since it is only valid while byte[0x03] is 0x00 (see the row above). The complete code → device tables (BaseType, wheel/hub codes, modules, and which hubs accept a module) live in devices.md.
Worked example.
FF 08 0C 00 00 … 0C … 02 …→ BaseType0x0C, WheelCode0x0C, Module0x02= a ClubSport DD+ base with a Podium Hub and a Podium Button Module Rally attached.
To bring a modern base online for identity, the host:
- Enable the system report:
FF 08 01 FF - Trigger one report to seed initial state:
FF 08 02 - Read col03 and scan the first bytes for the
FF 08signature; decode BaseType / WheelCode / Module.
Thereafter, attachment changes arrive as pushed FF 08 reports — no repeated triggering is needed (re-enable periodically to keep the subscription alive). To force a re-detect of a button module hot-attached to an already-connected hub, send the 0x06 Report Trigger with SubId = 1. A modern wheelbase needs no acquire or host-presence handshake before responding.
These sections group related commands across collections for quick navigation.
Fanatec steering wheels support several types of LEDs controlled through two distinct protocol generations:
| Type | Purpose | Typical Count | Color |
|---|---|---|---|
| Rev LEDs | RPM / shift indicator strip | 9 | Per-LED RGB (modern) or on/off (legacy) |
| Flag LEDs | Status / warning indicators | 6 | Per-LED RGB (modern) or single color (legacy) |
| Button LEDs | Button backlighting (RGB devices only) | Up to 12 | Per-LED RGB + intensity |
| RevStripe | Single-color LED strip | 1 (entire strip) | RGB333, 7 colors plus off |
Protocol selection by wheel type:
| Capability | Protocol | Collection | Color Depth |
|---|---|---|---|
| RGB LED + col03 support | Modern | col03 0x01 | RGB565 (65K colors) |
| RGB LED + no col03 | Legacy RGB | col01 0x0A | 3-bit per LED via col01 |
| Non-RGB LED | Legacy bitmask | col01 0x08 | On/off only + global RGB333 |
| RevStripe | Legacy color | col01 0x08 | RGB333, 7 colors plus off |
See the devices reference for the per-wheel capability matrix.
Batching behavior: The official software tracks per-LED dirty state internally and batches changes — only sending reports for LEDs whose on/off state or color has changed since the last update.
Fanatec devices have two display protocols:
| Display Type | Protocol | Collection | Devices |
|---|---|---|---|
| 3-digit (7-seg / small OLED) | col01 group 0x01, subcmd 0x02 | col01 | Most wheels, hubs, button modules |
| Multi-page telemetry (ITM) | col03 0x05 + 0x02 enable | col03 | PDD1/PDD2 base, PBME, Bentley, GTSWX |
Display preemption: The 7-segment display can be preempted by tuning menu navigation, CBP adjustment, and firmware boot/init. During these periods, host display writes will conflict with firmware output. See Display Ownership for OLED mitigation.
Tuning spans two collections:
| Function | Collection | Command |
|---|---|---|
| Read/write tuning parameters | col03 | 0x03 subcmds |
| Clutch bite point | col01 | 0x17 Set CBP + 0x06 Trigger |
| Display ownership during tuning | col01 | 0x18 Display Ownership |
When setting both tuning parameters and CBP, always send col03 tuning data first, then col01 CBP. See CBP in Tuning Context.
The legacy col01 protocol uses a 9-bit RGB333 color encoding packed into 2 bytes:
byte[5] (data_hi): [ G1 G0 | R2 R1 R0 | B2 B1 B0 ] (GG_RRR_BBB)
byte[4] (data_lo): [ 0 0 0 0 0 0 0 G2 ] (.......G)
Each channel has 3 bits (0–7), so 512 values are representable, but no hardware tested to date renders more than the eight fully saturated combinations — seven colors plus off. Those eight are also the only values official software emits, and at least one of the remainder (01 00) puts the rim into pattern mode, see 0x07.
| Color | R | G | B | data_lo (byte[4]) | data_hi (byte[5]) |
|---|---|---|---|---|---|
| Off | 0 | 0 | 0 | 0x00 |
0x00 |
| Red | 7 | 0 | 0 | 0x00 |
0x38 |
| Green | 0 | 7 | 0 | 0x01 |
0xC0 |
| Blue | 0 | 0 | 7 | 0x00 |
0x07 |
| Yellow | 7 | 7 | 0 | 0x01 |
0xF8 |
| Magenta | 7 | 0 | 7 | 0x00 |
0x3F |
| Cyan | 0 | 7 | 7 | 0x01 |
0xC7 |
| White | 7 | 7 | 7 | 0x01 |
0xFF |
The modern col03 protocol uses 16-bit colors in big-endian byte order, packed blue-high, red-low:
Bits: BBBBB GGGGGG RRRRR
15-11 10-5 4-0
- Blue: 5 bits (0–31)
- Green: 6 bits (0–63)
- Red: 5 bits (0–31)
- 65,536 possible colors
0x0000= LED off
Sanity check: pure red is 0x001F, pure blue is 0xF800.
Channel order is per-interface, and no format name carries it:
| Path | Channel order (high → low) |
|---|---|
col03 color (FF 01 00/01/02) |
B, G, R |
col01 0x08 RevStripe color |
G, R, B |
col01 0x0A per-LED 3-bit |
R, G, B (bits 3n, 3n+1, 3n+2) |
Green sits in bits 10-5 under either 565 layout, so a green-only value cannot distinguish them. Verify ordering with red or blue.
Some hardware reads only 5 green bits and ignores the sixth. For those devices, quantize green to 5 bits and place it in the low 5 bits of the green field, leaving bit 10 clear:
Bits: BBBBB _GGGGG RRRRR
15-11 10 9-5 4-0
(bit 10 always zero)
| Character | Hex | Binary | Segments Active |
|---|---|---|---|
| 0 | 0x3F |
0111111 |
0,1,2,3,4,5 |
| 1 | 0x06 |
0000110 |
1,2 |
| 2 | 0x5B |
1011011 |
0,1,3,4,6 |
| 3 | 0x4F |
1001111 |
0,1,2,3,6 |
| 4 | 0x66 |
1100110 |
1,2,5,6 |
| 5 | 0x6D |
1101101 |
0,2,3,5,6 |
| 6 | 0x7D |
1111101 |
0,2,3,4,5,6 |
| 7 | 0x07 |
0000111 |
0,1,2 |
| 8 | 0x7F |
1111111 |
0,1,2,3,4,5,6 |
| 9 | 0x6F |
1101111 |
0,1,2,3,5,6 |
| Char | Hex | Char | Hex | Char | Hex |
|---|---|---|---|---|---|
| A | 0x77 |
J | 0x0E |
S | 0x6D |
| B | 0x7C |
K | 0x75 |
T | 0x78 |
| C | 0x58 |
L | 0x38 |
U | 0x3E |
| D | 0x5E |
M | 0x37 |
V | 0x18 |
| E | 0x79 |
N | 0x54 |
W | 0x7E |
| F | 0x71 |
O | 0x5C |
X | 0x76 |
| G | 0x3D |
P | 0x73 |
Y | 0x6E |
| H | 0x76 |
Q | 0x67 |
Z | 0x5B |
| I | 0x06 |
R | 0x50 |
| Symbol | Hex | Description |
|---|---|---|
| Blank | 0x00 |
All segments off |
- (Dash) |
0x40 |
Middle segment only |
_ (Underscore) |
0x08 |
Bottom segment only |
. (Dot) |
0x80 |
Decimal point only |
The firmware recognizes a vocabulary of parameter IDs. Only a subset is confirmed to render correctly on current firmware — the rest may display no label, show unexpected formatting, or be silently ignored.
| ID | Name | Size | Type | Notes |
|---|---|---|---|---|
| 1 | SPEED | 2 | Int16 LE | Present on all pages as header |
| 2 | RPM | 4 | Int32 | |
| 3 | RPM_MAX | 4 | Int32 | |
| 4 | GEAR | 1 | Per-display | Present on all pages as header. The wire form differs by display and is dictated by the Type byte in the subscription push: displays declaring a Uint8 type (e.g. PBME) take numeric — 0x00 renders "n", 1–9 literal digits, 0xFF renders "r" (reverse) — and ignore ASCII bytes; displays declaring a text type (e.g. GTSWX, Formula V3) take a single ASCII char — 'n', '1'–'9', 'r' (lowercase) — as the official software sends. Both capture-confirmed. Sending the wrong form renders nothing. |
| 5 | FUEL | 4 | Float32 LE | Supports total (FUEL_MAX) |
| 6 | FUEL_MAX | 4 | Float32 LE | |
| 7 | FUEL_PER_LAP | 4 | Float32 LE | |
| 9 | ERS_LEVEL | 4 | Int32 LE | Percentage |
| 14 | DRS_ZONE | 1 | Uint8 | 0 or 1 |
| 15 | DRS_ACTIVE | 1 | Uint8 | 0 or 1 |
| 18 | ABS_SETTING | 1 | Uint8 | |
| 20 | TC_SETTING | 1 | Uint8 | |
| 25 | BRAKE_BIAS | 4 | Int32 LE | Tenths of a percent — 51.2% = 512. Int32, not Float32 (confirmed by capture; sending a float displays a capped 99.9%). |
| 26 | ENGINE_MAPPING | 1–2 | ASCII text | The map rendered as text — map 10 travels as bytes '1' '0' (31 30), size = digit count. Not a Uint8 (confirmed by capture; sending the numeric byte hangs the PBME Car Settings page). |
| 33 | OIL_TEMP | 1 | Uint8 | Unit-converted (C/F) |
| 42 | TYRE_FL_C_TEMP | 1 | Uint8 | Unit-converted (C/F) |
| 45 | TYRE_FR_C_TEMP | 1 | Uint8 | Unit-converted (C/F) |
| 48 | TYRE_RL_C_TEMP | 1 | Uint8 | Unit-converted (C/F) |
| 51 | TYRE_RR_C_TEMP | 1 | Uint8 | Unit-converted (C/F) |
| ID | Name | Size | Type | Notes |
|---|---|---|---|---|
| 501 | POSITION | 1 | Uint8 | Supports total (POSITION_TOTAL) |
| 505 | LAP | 1 | Uint8 | Supports total (LAP_TOTAL) |
| 509 | LAP_TIME | 4 | Float32 LE | Current lap, seconds |
| 510 | LAST_LAP_TIME | 4 | Float32 LE | |
| 511 | BEST_LAP_TIME | 4 | Float32 LE | |
| 516 | DELTA_OWN_BEST | 4 | Float32 LE | Seconds, +/- |
| 519 | CAR_AHEAD | 4 | Float32 LE | Gap in seconds |
| 520 | CAR_BEHIND | 4 | Float32 LE | Gap in seconds |
The full parameter vocabulary includes 120+ IDs covering tyre pressures, brake temps, G-forces, pedal positions, flags, system metrics (CPU/GPU), and more. These are defined in the firmware's shared vocabulary but are not confirmed to render on all display types. The complete parameter ID ranges:
- 0–84: Vehicle telemetry
- 501–536: Race/timing data and flags
- 1001–1008: System metrics (CPU load, GPU temp, FPS, etc.)
- 65535 (
0xFFFF): UNSUBSCRIBE sentinel
SPEED and GEAR appear on every page as persistent header fields.
Base and BME use identical page layouts. Page 6 is the legacy/default fallback.
Page 1 — Lap Info:
| Param ID | Name | Size | Type |
|---|---|---|---|
| 1 | SPEED | 2 | Int16 LE |
| 4 | GEAR | 1 | Uint8 |
| 505 | LAP | 1 | Uint8 |
| 501 | POSITION | 1 | Uint8 |
| 509 | LAP_TIME | 4 | Float32 LE |
| 510 | LAST_LAP_TIME | 4 | Float32 LE |
Detection signature: LAP (505) present in subscription.
Page 2 — Fuel / ERS / DRS:
| Param ID | Name | Size | Type |
|---|---|---|---|
| 1 | SPEED | 2 | Int16 LE |
| 4 | GEAR | 1 | Uint8 |
| 5 | FUEL | 4 | Float32 LE |
| 9 | ERS_LEVEL | 4 | Int32 LE |
| 14 | DRS_ZONE | 1 | Uint8 |
| 15 | DRS_ACTIVE | 1 | Uint8 |
| 516 | DELTA_OWN_BEST | 4 | Float32 LE |
Detection signature: ERS_LEVEL (9).
Page 3 — Car Settings:
| Param ID | Name | Size | Type |
|---|---|---|---|
| 1 | SPEED | 2 | Int16 LE |
| 4 | GEAR | 1 | Uint8 |
| 20 | TC_SETTING | 1 | Uint8 |
| 18 | ABS_SETTING | 1 | Uint8 |
| 25 | BRAKE_BIAS | 4 | Int32 LE (tenths of a percent) |
| 26 | ENGINE_MAPPING | 1–2 | ASCII text |
| 33 | OIL_TEMP | 1 | Uint8 |
Detection signature: OIL_TEMP (33).
Warning: This page is format-sensitive. BRAKE_BIAS is an Int32 in tenths of a percent (a Float32 shows a capped 99.9%), and ENGINE_MAPPING is ASCII text (map 10 =
31 30) — sending it as a numeric Uint8 hangs the PBME firmware. Rapid value cycling on this page can also cause firmware lockups.
Page 4 — Lap Times:
| Param ID | Name | Size | Type |
|---|---|---|---|
| 1 | SPEED | 2 | Int16 LE |
| 4 | GEAR | 1 | Uint8 |
| 510 | LAST_LAP_TIME | 4 | Float32 LE |
| 511 | BEST_LAP_TIME | 4 | Float32 LE |
| 519 | CAR_AHEAD | 4 | Float32 LE |
| 520 | CAR_BEHIND | 4 | Float32 LE |
Detection signature: CAR_AHEAD (519).
Page 5 — Tyre Temps:
| Param ID | Name | Size | Type |
|---|---|---|---|
| 1 | SPEED | 2 | Int16 LE |
| 4 | GEAR | 1 | Uint8 |
| 42 | TYRE_FL_C_TEMP | 1 | Uint8 |
| 45 | TYRE_FR_C_TEMP | 1 | Uint8 |
| 48 | TYRE_RL_C_TEMP | 1 | Uint8 |
| 51 | TYRE_RR_C_TEMP | 1 | Uint8 |
Detection signature: TYRE_FL_C_TEMP (42).
Page 6 — Legacy / Default:
No telemetry parameters. Fallback page when ITM is inactive.
Bentley uses the same parameters but with no Car Settings page and only 5 pages total:
| Page | Content | Detection |
|---|---|---|
| 1 | Lap Info | LAP (505) |
| 2 | Fuel / ERS / DRS | ERS_LEVEL (9) |
| 3 | Lap Times | CAR_AHEAD (519) |
| 4 | Tyre Temps | TYRE_FL_C_TEMP (42) |
| 5 | Legacy / Default | — |
GTSWX uses the same page content as Base/BME but with multi-parameter detection signatures — all params in the group must be present before the page is matched:
| Page | Content | Detection (ALL required) |
|---|---|---|
| 1 | Lap Info | LAP + POSITION + LAP_TIME + LAST_LAP_TIME |
| 2 | Fuel / ERS / DRS | FUEL + ERS_LEVEL + DRS_ZONE + DRS_ACTIVE + DELTA_OWN_BEST |
| 3 | Car Settings | TC_SETTING + ABS_SETTING + ENGINE_MAPPING + OIL_TEMP + BRAKE_BIAS |
| 4 | Lap Times (compact) | LAST_LAP_TIME + BEST_LAP_TIME + CAR_AHEAD (no CAR_BEHIND) |
| 5 | Tyre Temps | Any one of: TYRE_FL/FR/RL_C_TEMP |
| 6 | Legacy / Default | — |
ITM display support depends on the wheelbase, steering wheel, and button module combination:
| ITM Device | Detection | Device ID | Notes |
|---|---|---|---|
| Base | Wheelbase is PDD1, PDD1 (PS4), or PDD2 | 1 | Wheelbase's own display |
| BME | Button Module Endurance connected | 3 | PBME's large OLED |
| Bentley | Bentley GT3 steering wheel | 4 | Bentley wheel's built-in display |
| GTSWX | GT Steering Wheel Extreme | 3 | GTSWX's built-in display |
Note: BME and GTSWX share Device ID 3 on the wire. They are mutually exclusive — a setup will have one or the other, never both.
Only these steering wheels report APM (Advanced Paddle Mode) via the tuning menu:
| Code |
|---|
| CSLESWMCL |
| CSSWFORMV2 |
| CSLESWMCLV2 |
| CSLSWGT3 |
| CSSWFORMV3 |
For all other wheels, APM is ignored (always zero).