-
Notifications
You must be signed in to change notification settings - Fork 5
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.
Usage Page: 0xFFFD (vendor-defined)
Usage: 0xFD01
Report ID: 0x01
Size: 63 bytes IN + 63 bytes OUT (64 with report ID)
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
| 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 |
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.
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 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.
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.
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.
- 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.