Skip to content

TrueForce Protocol

mescon edited this page Jul 20, 2026 · 1 revision

TrueForce Protocol

TrueForce is a high-frequency audio-haptic stream that supplements traditional force feedback: rather than low-rate force updates, the host sends a ~1000 Hz audio waveform to the wheel's DSP, which drives the motor with much higher fidelity. This page documents the wire protocol; the user recipe is in Force Feedback in Games and the native implementation in libtrueforce.

The protocol runs entirely on USB interface 2 (endpoints 0x03 OUT / 0x83 IN), which the kernel driver delegates to hidraw. No HID++ feature activation is required: userspace opens the hidraw node and starts writing. Two userspace paths consume it:

  • Proton sims (verified): Logitech's own signed SDK DLLs run unmodified inside Wine and reach the wheel through Wine's HID stack. Verified end to end on ACC and AC EVO, in both the RS50's native and compat modes (the SDK drives either PID identically).
  • Native Linux apps: libtrueforce reimplements the same protocol in C.

The RS50 and G Pro use byte-for-byte identical init and streaming packets; treat TrueForce as a single protocol across the direct-drive family.

HID descriptor (interface 2)

Usage Page: 0xFFFD (vendor-defined)
Usage:      0xFD01
Report ID:  0x01
Size:       63 bytes IN + 63 bytes OUT (64 with report ID)

Common packet header

byte[0]:    0x01              Report ID
byte[1-3]:  0x00 0x00 0x00    Padding
byte[4]:    COMMAND_TYPE
byte[5]:    SEQUENCE          Rolling u8 counter (wraps), shared across all types
byte[6..]:  PAYLOAD           Type-specific

Command types (host to device, endpoint 0x03)

Type Purpose
0x01 Audio data stream (dominant during gameplay)
0x03 Start / play
0x04 Stop / clear
0x05 Parameter upload (48 floats, one per packet)
0x06 Effect slot configuration (6 slots)
0x07 Query / handshake
0x09 Runtime parameter update
0x0b Unknown (seen in AC EVO's init with float 1.0)
0x0e Operating range, IEEE 754 LE float degrees

Initialisation sequence (sent twice)

G Hub sends a 68-packet init sequence, then the same 68 packets a second time (the sequence counter restarts at 1 each pass) before the per-sample stream begins; a single pass is unreliable on cold boot. The packets are stored verbatim in userspace/libtrueforce/src/tf_init_data.h:

Packets Type Purpose
1-48 0x05 48 parameters as IEEE 754 LE floats
49 0x01 neutral sample (primes the stream)
50 0x0e operating range = float 2700.0 (the wheel's max)
51, 53, 55... 0x01 neutral samples between the others
52 0x07 handshake / query
54-66 (even) 0x06 effect slot configurations (slots 1-6)
60 0x09 runtime parameter update
67 0x04 stop / clear
68 0x03 start / play

Notably, the committed init replays every type-0x05 packet with a zero value payload (only the index byte varies) and TrueForce still works end to end, so the specific float values G Hub sends (channel counts, filter coefficients, crossover frequencies) are not required for basic operation.

Audio data stream (type 0x01)

byte[6-7]:   u16 LE      Most-recent sample ("cur", the motor torque target)
byte[8-9]:   u16 LE      Duplicate of bytes 6-7
byte[10]:    0x04        Number of new samples in this packet
byte[11]:    0x0d        Constant (rarely varies)
byte[12-15]: window[0] L, window[0] R (u16 LE each, mono duplicated)
...
byte[60-63]: window[12]

Layout invariants observed across captures:

  • A 13-slot rolling window holds the most recent samples, oldest at window[0]. Each packet advances the window by 4 new samples.
  • Every sample is duplicated (L and R); the wheel is single-motor, the stereo is ceremonial.
  • Samples are u16 LE offset binary (centre 0x8000, 0x0000 = full left, 0xFFFF = full right).
  • The preamble ("cur") is the motor torque target: while a TrueForce session is active the wheel steers by cur and the window plays additively on top as audio; cur OVERRIDES the HID++ 0x8123 force path. AC EVO carries its game force in cur and independent audio in the window of the same packet.

Packet cadence: libtrueforce streams 250 Hz (1 kHz effective sample rate); the kernel driver's unified texture stream runs 500 Hz; games send anywhere from 250 to ~1000 packets/s (AC EVO at the top) and the wheel accepts the whole range. Under input starvation the sender repeats the previous window (Windows does the same) and the wheel gradually unwinds.

Type 0x0e: operating range

Type-0x0e carries the wheel's operating range as an IEEE 754 LE float in degrees at bytes 6-9. This is logiWheelSetOperatingRange*() on the wire, and it is the root cause of the "wheel resets to 90° on game launch" bug: some games' SDK sessions append a 0x0e with 90.0 at session init, flipping the physical range with zero HID++ traffic. The kernel driver's range poll detects this and auto-restores (wheel_range_restore; see the Sysfs API Reference).

Firmware behaviors discovered live:

  • Session-scoped: a bare 0x0e on an idle interface is ignored; the range write only takes effect inside an initialised TF session.
  • Idle revert: if a session goes quiet (~a minute) the firmware reverts the session's range change on its own and broadcasts the restored value over HID++. A running game keeps its session alive, which is why real launch-time resets persist.

Device response (type 0x02, endpoint 0x83)

The wheel answers every outgoing packet with real-time feedback:

byte[6-7]:   u16 LE      Motor current or temperature (undecoded)
byte[8]:     status byte (undecoded)
byte[9-10]:  wheel position (LE16, matches the joystick axis)
byte[11-12]: wheel position, ~1 sample older
byte[13-16]: 32-bit device-side sample counter

libtrueforce consumes these while a stream is active and exposes the latest snapshot via its Linux-native logitf_get_stream_feedback() API; the kernel driver ignores them.

Coexistence with classic FFB

Classic force packets share this endpoint but are distinguished by the new-sample count (byte 10 = 0); see Protocol Force Feedback. Playing a sine on TrueForce while holding a constant-torque effect is verified to work: the firmware demultiplexes by packet content and the two coexist.

Open items

  • The type-0x02 motor field, status byte and checksum-like byte 17 are undecoded; correlating the motor field against commanded torque on a live wheel would pin it down.
  • Byte 11 (0x0d) is passed through verbatim; some captures show byte 10 = 0x05 (5 new samples), which libtrueforce does not use.
  • Whether the 48 init floats are game-specific or universal is unconfirmed; the same data produces working TrueForce across BeamNG and ACC.

Clone this wiki locally