Skip to content

Simulated TrueForce Internals

mescon edited this page Jul 20, 2026 · 1 revision

Simulated TrueForce Internals

How logi-tf-sim works inside. The user-facing guide is Simulated TrueForce; the crate is userspace/logi-dd/crates/tf-sim (binary logi-tf-sim).

Layering

The crate is deliberately layered so each piece is testable pure:

Module Role
codemasters, pcars pure &[u8] -> Option<(game id, Telemetry)> packet parsers, fixture-tested
telemetry the normalized sample all parsers decode into
synth pure engine-note generator (1 kHz sample stream)
config the ~/.config/logi-dd/tf-sim.conf key=value store
tf safe wrapper over the libtrueforce FFI (stream lifecycle)
leds the rev-display feeder (wheel_rev_level via sysfs)
daemon the UDP listen / synthesize / watchdog loop
sweep the --sweep hardware-test mode (synthetic RPM sweep)

Telemetry parsers

Two wire formats, each on its own UDP port, both parsed permissively (a malformed packet is skipped, never fatal):

  • Codemasters / EA float array (default port 20777): the classic format spoken by DiRT Rally 2.0 and the older Codemasters titles.
  • Project CARS 2 (default port 5606): also spoken by Automobilista 2.

Both normalize into one Telemetry sample:

pub struct Telemetry {
    pub rpm: f32,        // engine speed
    pub max_rpm: f32,    // redline (> 0 for a valid sample)
    pub throttle: f32,   // 0.0 to 1.0
    pub speed: f32,      // m/s
}

The daemon auto-detects the game from the packet format and applies that game's per-game config (enable, intensity).

Synthesis

EngineSynth generates the 1 kHz sample stream the wheel's DSP consumes:

  • A fundamental at rpm / 60 * pitch_scale Hz plus 2x and 3x harmonics at falling gains (1, 1/2, 1/4). The gains factor so the summed waveform crosses zero exactly twice per fundamental cycle, keeping the felt pitch equal to the engine rate (and making the spectral unit test exact).
  • Amplitude is idle_floor + throttle * gain (0.15 + 0.85·throttle), everything scaled by the effective intensity (master × per-game).
  • The generator is stateful only in its oscillator phase, so frequency changes are click-free. Frequency is capped below Nyquist.
  • pitch_scale (0.1-2.0, config pitch as percent) scales the felt rev rate. The default is 50%: at 100% (crank-rate fundamental) the hardware feel test read as an alarmingly fast engine; half rate feels like an engine.

The libtrueforce stream thread does the packetizing (4 samples per 250 Hz packet, see TrueForce Protocol); the synth only produces samples.

The rev-LED consumer

leds::RevLeds mirrors telemetry RPM onto the wheel's rev display through the driver's wheel_rev_level sysfs attribute (0-10 LEDs lit; on the RS50 the fill uses the active LIGHTSYNC slot's colors and direction, on a real G PRO rim the onboard profile owns the colors).

  • Level = round(10 * rpm / max_rpm), clamped 0-10; a missing or zero redline reads as 0 so a car that never reported its limiter shows a dark strip instead of a division artifact.
  • Pacing: writes are rate-limited to one per ~160 ms and skipped while the level is unchanged. Faster bursts would starve the wheel's shared HID++ command processor and can cut FFB out (see Protocol LIGHTSYNC); the driver additionally coalesces on its side.
  • Discovery honors LOGI_DD_SYSFS_DIR (the same development override the logi-dd frontends use), falling back to a scan of /sys/bus/hid/devices/*/wheel_rev_level.
  • Everything is best-effort: a wheel without the attribute, a failed write or a missing driver never disturbs the TrueForce stream riding alongside.

The daemon loop

  • Listens on both UDP ports simultaneously; idle until telemetry arrives.
  • On the first valid sample from an enabled game, ensures a libtrueforce session (which runs the 68-packet two-pass init, see TrueForce Protocol) and starts streaming synthesized samples.
  • A watchdog stops the stream when telemetry goes quiet, releasing the wheel; the rev display is restored to the idle LED pattern.
  • Config changes from the Setup page (master switch, intensities, pitch, per-game enables) are picked up without restarting the daemon.

The test sweep

logi-tf-sim --sweep (also reachable from the Setup page) runs a synthetic RPM run-up through the exact same synth, stream and LED paths as real telemetry, letting a user feel and calibrate the effect without a game. It is consent-gated in the UIs because it moves a strong direct-drive wheel.

Testing without hardware

The parsers and synth are pure and covered by fixture tests (crates/tf-sim/tests); the LED feeder takes an injected clock and an explicit attribute path so tests drive it against a temp file. Run the lot with cargo test -p tf-sim in userspace/logi-dd.

Clone this wiki locally