Driver: hid-logitech-dd (module), logitech-dd (hid_driver)
Devices:
- Logitech RS50 Direct Drive Wheel Base (USB
046d:c276) - Logitech G Pro Racing Wheel (USB
046d:c272Xbox/PC,046d:c268PS/PC)
Applies to: the hid-logitech-dd driver in this repository.
Most of the attributes documented here are shared between the RS50 and G Pro (the two wheels share the settings code path). Attributes that are currently G Pro-only or RS50-only are called out inline.
The driver exposes wheel configuration through sysfs attributes located at:
/sys/bus/hid/devices/<DEVICE_ID>/
Where <DEVICE_ID> is typically 0003:046D:C276.XXXX (RS50), 0003:046D:C272.XXXX (G Pro Xbox/PC), or 0003:046D:C268.XXXX (G Pro PS/PC).
To find your device path:
find /sys/bus/hid/devices -name "*046D*C276*" 2>/dev/null # RS50
find /sys/bus/hid/devices -name "*046D*C27[28]*" 2>/dev/null # G Pro (c272 or c268)Access: Read/Write
Values: desktop, onboard
Controls the operating mode of the wheel base.
- Desktop mode (profile 0): Settings controlled by host software. Sensitivity available.
- Onboard mode (profiles 1-5): Settings stored in wheel profiles. Brake force available.
# Read current mode
cat wheel_mode
# Output: "desktop" or "onboard"
# Switch to onboard mode
echo "onboard" > wheel_mode
# Switch to desktop mode
echo "desktop" > wheel_modeAccess: Read/Write
Values: 0 (desktop), 1-5 (onboard profiles)
Controls the active profile. Profile 0 is desktop mode; profiles 1-5 are onboard profiles.
# Read current profile
cat wheel_profile
# Output: 0-5
# Switch to onboard profile 3
echo 3 > wheel_profile
# Switch to desktop mode (profile 0)
echo 0 > wheel_profileAccess: Read/Write
The five onboard slots' names, queried live from the wheel. Use this to
know which slot number wheel_profile should get.
cat wheel_profile_names
# 1: RACE
# 2: DRIFT
# 3: PROFILE 3
# 4: PROFILE 4
# 5: PROFILE 5Writing renames one slot, as <slot>:<name> (feature 0x8137 fn4). The
wheel persists the name to its own NVM on the write; there is no separate
save step.
# Rename slot 3
echo "3:RACE" > wheel_profile_namesSlot is 1-5. On an RS50 the name may be up to 9 characters (the same
length as its stock PROFILE 3), may contain spaces, and is stored
uppercased - echo "3:Race" reads back as 3: RACE. A longer name is
refused by the wheel itself and surfaces as -EIO; a bad slot, an empty
name, or one over the 14-byte HID++ payload gives -EINVAL.
Access: Read/Write
Values: 90 to 2700 (degrees)
Sets the steering wheel rotation range.
# Read current range
cat wheel_range
# Output: degrees (e.g., "900")
# Set to 540 degrees
echo 540 > wheel_rangeOn the G PRO PID (046d:c272 / 046d:c268) - both real G PRO and
RS50-in-compat - the standard HID++ range feature is not advertised
at the index the native code expects; the driver falls back to
feature 0x8138 (index 0x18, fn 2) captured from G Hub. See
docs/PROTOCOL_SPECIFICATION.md section 5.1. The fallback
write only takes effect while the wheel is in desktop mode; the
wheel boots in onboard mode, so write 0 to wheel_profile first
to enter desktop mode and have subsequent range writes take effect
on the motor.
External-change detection: some game launches (observed with
Assetto Corsa EVO under Proton) reset the wheel's physical range to
90 degrees without any HID++ notification. The driver re-reads the
true range from the wheel every 20 seconds (paused while force
feedback is actively playing, so the synchronous query can never
stall the force stream; it catches up within one interval of the
effects stopping); if it changed externally,
the reported wheel_range value is updated to the real one, the
change is logged in dmesg (rotation range changed externally), and
poll()ers on the attribute are notified via sysfs_notify().
When the external value is exactly 90 (the known SDK session-init
pathology - see wheel_range_restore below), the driver also
restores the previous range automatically.
Access: Read/Write
Values: 0 or 1
Default: 1
Automatic recovery from the launch-time 90-degree reset. Root cause (usbmon-verified): some games' SDK sessions push an operating range of 90 degrees once at session start via a TrueForce interface-2 packet, invisible to HID++. With this enabled, the driver restores the pre-reset range automatically. Verified end-to-end against a faithful reproduction of the game traffic: detection to restore in under 100 ms once the poll samples the change.
Safety gates, each earned from a real incident:
- fires only for an EXTERNAL silent change landing exactly on 90; any other externally-set value (a game applying its configured steering lock: 540, 850, ...) is respected as legitimate intent;
- desktop mode only, and never an automatic mode switch;
- the wheel must be stationary (two encoder reads 50 ms apart); restores only ever widen the range, which cannot snap the wheel;
- at most 3 restores per session, then the driver logs and yields
(
an external writer keeps changing the rotation range); - an explicit
wheel_rangewrite supersedes any pending restore and resets the strike counter.
0 = detect-and-report only: the
change is still logged and wheel_range stays honest, but recovery
is manual (wheel_profile=0 then wheel_range=<degrees> once FFB
is idle).
Access: Read/Write
Values: 0 to 100 (percentage)
Sets the force feedback strength.
Internal encoding: The driver converts percentage to a 16-bit value where:
- 0% = 0x0000
- 100% = 0xFFFF (corresponds to 8.0 Nm max torque)
# Read current strength
cat wheel_strength
# Output: percentage (e.g., "75")
# Set to 50%
echo 50 > wheel_strengthAccess: Read/Write
Values: 0 to 100 (percentage)
Sets the wheel damping (resistance when turning).
Internal encoding: The driver scales the 0-100 percentage to a 16-bit big-endian value (value = percentage * 65535 / 100) and writes it to page 0x8133 with SET fn=1. See docs/PROTOCOL_SPECIFICATION.md section 5.
# Read current damping
cat wheel_damping
# Set to 25%
echo 25 > wheel_dampingAccess: Read/Write
Values: 0 to 100 (percentage)
Sets the TRUEFORCE bass shaker intensity.
# Read current TRUEFORCE level
cat wheel_trueforce
# Enable at 80%
echo 80 > wheel_trueforce
# Disable
echo 0 > wheel_trueforceAccess: Read/Write
Values: 0 to 100 (percentage)
Mode Restriction: Onboard mode only
Sets the brake pedal force threshold (load cell sensitivity).
Note: Returns -EPERM (Permission denied) in desktop mode.
# Set brake force to 75% (must be in onboard mode)
echo "onboard" > wheel_mode
echo 75 > wheel_brake_forceAccess: Read/Write
Values: 0 to 100 (percentage)
Mode Restriction: Writes only accepted in desktop mode; reads always succeed.
Shapes the steering response curve via feature 0x80A4
(AxisResponseCurve): a 64-point cubic Bezier from (0,0) to (1,1) with
control points P1=(1-s, s), P2=(s, 1-s) for s = value/100. Values below
50 soften the response near centre; values above 50 sharpen it. 50
is the identity curve, so the driver reverts to the wheel's built-in
curve (as G Hub does) rather than uploading a flat line.
This is unrelated to LED brightness. Feature 0x8040 (behind
wheel_led_brightness) is brightness only; sensitivity and brightness are
fully independent.
Reads return the last value written, defaulting to 50. The wheel has no
read-back for the slider, so this is a write-through cache. Writes in
onboard mode fail with -EPERM; if the wheel does not expose the
response-curve feature, writes return -EOPNOTSUPP.
# Sharpen the centre response (must be in desktop mode)
echo "desktop" > wheel_mode
echo 65 > wheel_sensitivityAccess: Read/Write
Values: 1 to 15 (filter level)
Sets the force feedback smoothing/filtering level. G Hub's labels are roughly Minimum (1), Low (7), Medium (11), Maximum (15).
Values outside 1..15 are clamped to that range.
# Read current filter level
cat wheel_ffb_filter
# Set to level 11 (G Hub "Medium")
echo 11 > wheel_ffb_filterAccess: Read/Write
Values: 0 (manual), 1 (auto)
Enables automatic FFB filter adjustment based on game output.
The driver splits the two on-wire meanings of the filter flag byte across two sysfs writes: writing to wheel_ffb_filter stamps the "user set this level right now" bit, writing here toggles only the "auto mode" bit. See docs/PROTOCOL_SPECIFICATION.md section 5 (FFB Filter) for the bitfield decode.
# Enable auto filter
echo 1 > wheel_ffb_filter_auto
# Disable (use manual filter setting)
echo 0 > wheel_ffb_filter_autoAccess: Read/Write
Values: 0 or 1
Default: 1
Controls whether the driver inverts the sign of every FF_CONSTANT
level before sending it to the wheel. This single toggle is what makes
the driver's FFB feel right under both Wine/Proton games and native
Linux apps - the two paths disagree on sign by one flip somewhere in
Wine's DirectInput-to-evdev translation layer, and the driver can't
tell them apart at runtime.
1(default) - invert. Correct for Wine/Proton running Windows games (Assetto Corsa Competizione, etc.). IfFF_CONSTANTfeels like centring forces push the wheel away from centre instead of back toward it, the toggle is at the wrong setting.0- pass-through. Correct for native Linux evdev apps that upload effects with the convention documented inDocumentation/input/ff.rst(direction=0x4000 east, positive level = rightward force).fftest,ffcfstress, and direct EVIOCSFF uploads from custom tools are in this category.
Only affects FF_CONSTANT. All other effect types (FF_SPRING,
FF_DAMPER, FF_FRICTION, FF_INERTIA, FF_RAMP, FF_PERIODIC,
FF_RUMBLE) feel identical at either toggle value.
Caveat for reverse driving: the inversion is unconditional - it
does not look at the wheel's velocity or the car's gear. In sims that
correctly model the self-aligning torque flipping sign at negative
longitudinal velocity (most modern racing sims do), the chain is
"sim physics-inverts for reverse → Wine inverts again → driver inverts
again", which lands as physics-correct destabilising FFB the user
feels as the wheel actively pushing away from centre when reversing.
That is the real-car behaviour and not a driver bug, but it can feel
violent compared to a wheel without TF / direct-drive force. Lowering
wheel_strength is the only knob from the driver side; switching the
sign toggle off would make forward driving feel wrong without fixing
the reverse case.
A contributor cross-checked this on Windows (issue #8, AC EVO and AC): the FFB "gets pretty violent in reverse" there too, with the same wheel and game settings. So the strong reverse force is the sim's physics surfacing through the wheel, identical to the Windows G Hub path, not a sign error or double-inversion on our side.
# Playing Wine/Proton racing games: leave default
cat wheel_ffb_constant_sign # -> 1
# Running native-evdev tools like fftest:
echo 0 | sudo tee wheel_ffb_constant_signThe inversion is confirmed empirically on Assetto Corsa Competizione with this
wheel; a test harness in tests/ff_matrix_test.c cross-checks each toggle value
against native evdev expectations.
Access: Read/Write
Values: 0 to 100 (percentage)
Default: 25
Availability: all direct-drive wheels (RS50 native/compat and real G PRO - every family PID runs the same hidpp_dd_ff_* FFB path).
Synthetic damping applied to emulated FF_SPRING effects, as a
percentage of a FF_DAMPER running the spring's own coefficient.
The driver emulates condition effects host-side: it samples the wheel
position and pushes the computed force back over USB. That loop
latency on a low-friction direct-drive motor makes a stiff, undamped
game-uploaded spring ring - a growing back-and-forth oscillation that
ends with the wheel's over-torque failsafe cutting power (observed
live with Assetto Corsa EVO's map-load centring spring). Real wheels
damp the spring inside the firmware servo loop; this knob restores
that behaviour. 0 disables it. The damping
scales with the spring's own coefficient, so stiff springs get
proportionally stronger damping.
cat wheel_spring_damping # -> 25
echo 40 > wheel_spring_dampingAccess: Read/Write
Values: tf or kf (also accepts 1 / 0)
Default: tf
Availability: all direct-drive wheels (RS50 native/compat and real G PRO - every family PID runs the same hidpp_dd_ff_* FFB path).
Selects where vibration-class effects - FF_RUMBLE and periodic
effects at 20 Hz or faster (period <= 50 ms) - are actuated:
tf(default) - the driver streams them on the wheel's TrueForce audio-haptic channel (interface 2, ~1 kHz sample rate), the same physical path the Windows SDK uses for texture. Steering-shaping effects (FF_CONSTANT, conditions, slow periodics) stay on the force channel, so rumble no longer modulates the steering axis. This matches the Windows KF/TF split; the "gritty/notchy steering under rumble" reported in issue #8 is thekfbehaviour.kf- legacy: everything is summed into the single steering force. Kept as a fallback and for A/B comparison.
The TrueForce session is brought up lazily: the first time a
texture-class effect actually plays, the driver replays the captured
68-packet init sequence twice (G Hub behaviour) and then streams
unified packets at 500 Hz while texture effects are active - each
packet carries the steering-force sum in its preamble and four
texture-audio window slots (2 kHz slot rate), the same shape AC EVO
streams (dmesg: TrueForce texture channel ready). Wheels that never see texture
effects never see TF traffic. If the init fails, texture effects
fall back to the steering channel - degraded feel, never lost - and
the driver retries on a later texture playback (up to 3 attempts per
session, logged in dmesg).
An effect's channel is decided when its playback starts and held for the whole play cycle, so re-parametrising a playing effect across the 20 Hz crossover (or the session init completing mid-play) never yanks a live effect between channels. Playbacks started before the session is ready ride the steering channel for their duration; the next playback moves to the TF stream.
Texture amplitude respects FF_GAIN and wheel_strength (the wheel
firmware scales steering forces by the strength setting itself but
plays TF samples at face value, so the driver applies strength to
texture in software for consistency), and is additionally capped at
half of full scale: above roughly 0.5-0.7 FS the wheel's DSP crosses
from vibration into pulling the steering axis, so the cap keeps a
synthetic full-scale rumble from hijacking steering torque. Real
games stream texture far below the cap.
Note for SDK games (ACC, AC EVO with the TrueForce shim): those
stream TrueForce themselves via hidraw and normally do not send
evdev rumble at the same time, so the two streams do not meet. If a
game somehow does both, set kf to keep the wheel's TF input to a
single writer.
cat wheel_texture_route # -> tf
echo kf > wheel_texture_route # A/B back to the legacy mixingAccess: Write-only (mode 0220)
Values: 0 to 65535 (raw encoder position)
Availability: RS50 and G Pro. Returns -EOPNOTSUPP if the wheel does not expose page 0x812C on sub-device 0x05 (no known variant lacks it, but the driver does not assume).
Low-level primitive: writes the given raw 16-bit encoder value to adopt
as the new centre. The driver sends 10 05 <idx> 3D <hi> <lo> 00 to
HID++ sub-device 0x05, feature page 0x812C (see
docs/PROTOCOL_SPECIFICATION.md section 5 for the wire format).
Verified on RS50 from 2026-04-22_re_calibrate.pcapng.
Use this only if you already have the raw encoder value you want to
make the centre (e.g., you read it via a HID++ GET yourself, or you
want to seed a specific reference value). For the common case ("make
the wheel's current physical position the new centre") use
wheel_calibrate_here below, which does the GET+SET internally.
# Low-level: you already have the raw encoder number you want.
echo 32768 > wheel_calibrateAccess: Write-only (mode 0220)
Values: any non-empty write triggers the operation
Availability: same as wheel_calibrate
One-shot "use the wheel's current physical position as the new centre". The driver issues fn=1 GET to read the current raw encoder value, then fn=3 SET with that value. Mirrors what G Hub does when the user clicks Calibrate on Windows. Hold the wheel at the desired centre (typically true centre), then write to this attribute.
# Hold the wheel at true physical centre, then:
echo 1 > wheel_calibrate_hereNo state is stored in the driver; the wheel's firmware persists the new centre across power cycles (same as G Hub on Windows).
The RS50 has 10 RGB LEDs in a horizontal strip across the upper faceplate (an engine-RPM / shift indicator). The driver provides per-slot configuration with 5 custom slots (0-4).
Per-model availability: the
wheel_led_*LIGHTSYNC attributes exist on the RS50 in both native and G-PRO-compat enumeration (the RS50's faceplate LED-strip hardware doesn't change with the PID; verified live 2026-04-29). On a real G PRO Racing Wheel they are hidden: that rim has level-based rev lights with no per-LED RGB, exposed aswheel_rev_levelinstead (see its entry below).G PRO PID (
046d:c272/046d:c268): covers both real G PRO Racing Wheel and RS50-in-G-PRO-compat-mode. Both run through the samehidpp_dd_ff_*code path and expose the same wheel-config surface; the LED attributes differ per rim (see the per-model note above - the driver tells the two apart by USB product string). On the RS50 in compat mode, LIGHTSYNC works the same way as native - feature0x807Ais advertised at the same index discovery picks up in native, andwheel_led_*writes drive the LED strip end-to-end (verified against the live wheel 2026-04-29). Wheel-config attributes that work via fallback feature paths (seedocs/PROTOCOL_SPECIFICATION.mdsection 5.1):wheel_range,wheel_strength,wheel_trueforce,wheel_damping,wheel_ffb_filter,wheel_profile(write0to enter desktop mode), andwheel_calibrate. The remaining attributes (wheel_brake_force,wheel_ffb_filter_auto,wheel_sensitivity) are unsupported by this firmware: once their mode gating is satisfied the store returns-EOPNOTSUPP(notewheel_brake_forcestill returns-EPERMin desktop mode andwheel_sensitivityin onboard mode before that check). For those, configure via the wheel's OLED menu or via Windows G Hub on a Windows host.
- Select a slot:
echo 2 > wheel_led_slot - Set direction (optional):
echo 1 > wheel_led_direction - Set colors:
echo "FF0000 FF0000 ... (10 colors)" > wheel_led_colors - Apply changes:
echo 1 > wheel_led_apply
Alternatively, use built-in effects via wheel_led_effect.
Access: Read/Write
Values: 0 to 4 (custom slot index)
Selects the active custom LED slot and renders it on the strip. On the
wire a custom slot IS effect value 5 + slot (see wheel_led_effect),
so writing this attribute is equivalent to writing effect 5 + slot:
the driver applies the slot's stored config and switches the strip to it
(hardware-verified working, both directions, 2026-07-20). The slot-scoped
attributes (wheel_led_slot_name, wheel_led_colors,
wheel_led_direction, wheel_led_slot_brightness) target the slot
selected here.
# Select slot 2 (CUSTOM 3) and render it
echo 2 > wheel_led_slotAccess: Read/Write Values: String (max 8 characters)
Gets or sets the name of the currently selected LED slot. Names are stored on the device.
# Read current slot name
cat wheel_led_slot_name
# Output: "CUSTOM 1" (or user-defined name)
# Set a custom name for the slot
echo "Racing" > wheel_led_slot_nameAccess: Read/Write
Values: 0 to 100 (percentage)
Gets or sets the brightness for the currently selected slot. Each slot can have its own brightness level, which is applied when the slot is activated via wheel_led_apply.
# Read current slot brightness
cat wheel_led_slot_brightness
# Output: brightness percentage (e.g., "75")
# Set slot brightness to 50%
echo 50 > wheel_led_slot_brightnessNote: This is per-slot brightness. Use wheel_led_brightness to set global brightness without changing slot settings.
Access: Read/Write
Values: 0 to 3
Sets the LED animation direction for the current slot:
0= Left to Right1= Right to Left2= Inside Out (center outward)3= Outside In (edges inward)
On the wire the 0x807B config carries a 1-4 value; the driver translates
between this 0-3 enum and the device's 1-4 encoding internally (see
PROTOCOL_SPECIFICATION.md section 9.4.1).
# Set direction to Right-to-Left
echo 1 > wheel_led_directionAccess: Read/Write
Format: 10 space-separated hex RGB values (RRGGBB)
Sets all 10 LED colors for the current slot. LED1 is leftmost.
# Set all LEDs to red
echo "FF0000 FF0000 FF0000 FF0000 FF0000 FF0000 FF0000 FF0000 FF0000 FF0000" > wheel_led_colors
# Rainbow effect (example)
echo "FF0000 FF7F00 FFFF00 7FFF00 00FF00 00FF7F 00FFFF 007FFF 0000FF 7F00FF" > wheel_led_colors
# Read current colors
cat wheel_led_colors
# Output: "RRGGBB RRGGBB RRGGBB RRGGBB RRGGBB RRGGBB RRGGBB RRGGBB RRGGBB RRGGBB"Access: Read/Write
Values: 0 to 100 (percentage)
Sets the global LED brightness.
# Set brightness to 50%
echo 50 > wheel_led_brightnessBrightness changed from the wheel itself (OLED menu) is tracked: the
driver consumes the official x8040 brightnessChangeEvent broadcast,
updates this attribute's value (and wheel_sensitivity when in
desktop mode, per the aliasing described there), and notifies
poll()ers on wheel_led_brightness.
Access: Read/Write
Values: 1 to 9
Selects the LED animation mode. Values match feature 0x807A fn3 on the wire:
| Value | Effect |
|---|---|
1 |
Inside to out |
2 |
Outside to in |
3 |
Right to left |
4 |
Left to right |
5-9 |
The five custom slots: 5 = CUSTOM 1 .. 9 = CUSTOM 5 |
Values 5-9 ARE the custom slots (decoded 2026-07-20): there is no
separate slot-activate command, selecting a slot is selecting its
effect number. Writing 5 renders the ACTIVE slot - the driver
re-applies wheel_led_slot's stored RGB and sends 5 + slot on the
wire, so through this attribute 5 always means "custom mode with the
currently selected slot". Writing 6-9 selects that specific slot's
image directly without moving wheel_led_slot; prefer wheel_led_slot
(or 5) so the slot-scoped attributes keep targeting the rendered
slot. Writes for the animated modes (1-4) just switch the effect and
leave the cached colors untouched. Writes outside 1..9 are clamped
to the nearest end of the range.
Effect changed from outside the driver (G Hub-style tools, or the
wheel itself) is tracked: the driver consumes the LIGHTSYNC
effect-change broadcast, updates this attribute, and notifies
poll()ers.
# Render the active custom slot
echo 5 > wheel_led_effect
# Render CUSTOM 3 (slot 2) directly
echo 7 > wheel_led_effect
# Animate right to left
echo 3 > wheel_led_effectAccess: Read/Write
Availability: all direct-drive wheels (feature 0x80A4)
The steering axis's 64-point response curve - the store behind G Hub's
Sensitivity slider. Write reset to revert to the built-in
(linear) curve, or 2-64 whitespace-separated in:out pairs (decimal
0-65535, strictly increasing in, non-decreasing out, starting at
0:0 and ending at 65535:65535). Fewer than 64 pairs are resampled
by linear interpolation to the 64 points the wheel stores.
# Soften the centre (slower response near straight-ahead)
echo "0:0 32768:16384 65535:65535" > wheel_response_curve
# Back to the built-in curve
echo reset > wheel_response_curve
cat wheel_response_curve
# 64/64 points loaded (0 = built-in curve)The wheel applies this curve to the steering axis it reports to the PC (base
0x80A4 axis 0, the same wheel_sensitivity uploads its slider curve to).
wheel_sensitivity and wheel_response_curve both write that one axis-0 curve,
so the last one written wins. The axis does not change until the wheel next
moves: it sends no HID reports while held still, so an upload appears to do
nothing until you nudge the wheel; cat wheel_response_curve reads the point
count back from the wheel and is the honest check. Use reset if steering feels
wrong after an upload. Whether curves persist across power cycles is untested.
Access: Read/Write
Values: 0-10 (number of rev LEDs lit)
Availability: real G PRO rim AND the RS50's LIGHTSYNC strip
(hardware-verified 2026-07-20). On the RS50 the level fills the strip
with the SELECTED custom slot's stored config: during testing the fill
tracked slot 0's stored direction (all four directions watched live)
while slot 0 was selected. An earlier draft of this note said the fill
was "not a rendering of the active custom slot"; that observation came
from the then-broken slot switch (the driver pinned selection to
CUSTOM 1, fixed 2026-07-20), so the fill's source is simply the
selected slot - a fill taken after selecting a different slot is
expected to render that slot's colours and direction, but this has not
been directly re-observed post-fix (expected, re-verify). A written
level holds until the next write; write wheel_led_effect to restore
the normal idle pattern.
Rev-light level for the G PRO rim. The G PRO's rim lights are
level-based: the host commands how many LEDs are lit (0-10) and the
wheel's onboard profile owns colours, direction and scaling. Protocol
decoded from a G HUB capture by the TF4ALL project (see
docs/PROTOCOL_SPECIFICATION.md section 9). The first write arms the
feature. Writes return immediately: the driver coalesces them and
flushes only the newest level at G HUB's ~160 ms cadence (faster
bursts would starve the wheel's shared HID++ command processor), so a
fast telemetry feeder always shows the latest value with no queueing
lag. The
wheel holds a level for a while but reverts eventually - a telemetry
feeder should refresh at ~1 Hz or faster (natural for rev-light use).
Status: hardware-validated on the RS50 (2026-07-20, live 0-10-0 sweep demos; fill semantics per the note above); not yet validated on a real G PRO - reports welcome (issue tracker).
# Light 7 of 10 rev LEDs
echo 7 > wheel_rev_levelAccess: Read-only Values: 12-character device serial
The wheel's real serial number, read from HID++ DeviceInfo (feature
0x0003 fn2) at init. Matches the USB iSerial descriptor.
Access: Read-only
Firmware versions read from DeviceInfo at init: the wheel base's active main firmware and the motor unit's servo firmware (from sub-device 0x05's own DeviceInfo).
cat wheel_firmware
# base: U1 65.03.B0038
# motor: SC 02.01.B0042Include this output in bug reports - firmware-dependent behaviour (feature index drift, settings quirks) is tracked against it.
Access: Write-only
Values: 1 (apply)
Applies the current slot configuration to the device.
# Apply current slot settings
echo 1 > wheel_led_applyThe pedal unit is a separate MCU (HID++ sub-device 0x02) that applies a
64-point 0x80A4 response curve to each axis it reports to the PC. This was
verified live on an RS50 (2026-07-16) with a two-plateau throttle curve: the
reported axis dwelt at exactly the two programmed output values with an empty
gap between them, which a linear axis cannot do. So these are real shaping
controls, the same mechanism as the steering wheel_response_curve.
Each pedal <p> in {throttle, brake, clutch} exposes three attributes.
They all write the single curve the axis holds, so the last write wins. The
_curve attribute always reads back the wheel's true loaded-point count.
Access: Read/Write
The raw 64-point curve, identical in format to wheel_response_curve: write
reset for the built-in linear curve, or 2-64 whitespace-separated in:out
pairs (0-65535, strictly increasing in, non-decreasing out, starting 0:0
and ending 65535:65535; fewer than 64 pairs are resampled). Reads back
"<loaded>/<max> points loaded".
# Dead-until-30%, then linear, on the throttle
echo "0:0 19660:0 65535:65535" > wheel_throttle_curve
echo reset > wheel_throttle_curve
cat wheel_throttle_curve # e.g. "64/64 points loaded (0 = built-in curve)"Access: Read/Write, Values: 0-100 (50 = linear)
G HUB's simple sensitivity slider. Values above 50 make the pedal more
responsive early in its travel, below 50 less; 50 reverts to the built-in
linear curve. Generates the same symmetric-Bezier curve G HUB uploads. The
wheel stores only the resulting curve, so this reads back the last written
slider value, not a device query.
echo 70 > wheel_brake_sensitivityAccess: Read/Write, Values: "<lower> <upper>" percent
Dead travel at each end: output holds 0 until the pedal passes lower%, and
reaches full by (100 - upper)%. lower + upper must be at most 99. "0 0"
reverts to the built-in curve. Reads back the last written pair.
# 8% dead at the bottom, 5% saturation at the top of the clutch
echo "8 5" > wheel_clutch_deadzoneBecause sensitivity, deadzone and the raw curve all write the one hardware curve per axis, use one of them at a time per pedal. To combine a deadzone with a custom shape, author the whole thing as a single
_curveupload (this is what the logi-dd editor does).
Access: Read/Write, Values: 0 (separate) / 1 (combined)
Mode: desktop only
G HUB's "combined pedals" toggle (feature 0x80D0). When on, the wheel merges
the throttle and brake into a single centred axis for legacy games that expect
one pedal axis: released = centre, one pedal drives it up, the other down. The
brake's own axis goes silent. Off for any modern sim. Verified on an RS50: with
it on, ABS_RX re-centres to ~32768 and ABS_RY (the separate brake axis)
stops reporting.
echo 1 > wheel_combined_pedals # merge (legacy games)
echo 0 > wheel_combined_pedals # separate (default)
cat wheel_combined_pedals # 0 or 1Response-curve shaping for the RS Shifter & Handbrake in analog handbrake
mode. The handbrake drives a base axis (0x80A4 axis 4, evdev ABS_Z), and the
driver shapes it with the same mechanism as the pedals, verified on an RS50.
wheel_handbrake_curve- rawin:outpoints orreset, likewheel_response_curve.wheel_handbrake_sensitivity- the 0-100 G HUB slider (50 = linear).
Both write the one curve the axis holds; last write wins. The handbrake input
itself needs no configuration: connected to the wheel base, it works out of the
box as ABS_Z. These attributes only shape it.
echo 70 > wheel_handbrake_sensitivity
echo "0:0 26000:0 65535:65535" > wheel_handbrake_curve # 40% dead travel
echo reset > wheel_handbrake_curveThe accessory rides the wheel's existing report, so its inputs reach evdev with no driver change. By its mode switch:
| Mode | Action | evdev |
|---|---|---|
| Sequential shifter | shift up | BTN_TOP2 |
| Sequential shifter | shift down | BTN_PINKIE |
| Digital handbrake | pull past point | BTN_THUMB2 (face button) |
| Analog handbrake | pull | ABS_Z axis |
These attributes provide compatibility with existing wheel management tools (e.g., Oversteer).
The sysfs filenames use standard Oversteer names (without the wheel_ prefix).
Note: These aliases are created for every wheel this driver binds (RS50 and
G PRO). They exist so Oversteer, which looks for the new-lg4ff attribute names,
can drive the wheel; the same settings are also available under their wheel_*
names documented above.
These attributes follow the de-facto Linux wheel convention (the
new-lg4ff attribute names and scales) that Oversteer and similar tools
speak. Conformance was verified 2026-07-03 by driving every getter and
setter through Oversteer's own code against the live wheel. Note the
scales differ from the wheel_* attributes: tools expect raw device
units here, not percent.
Access: Read/Write
Values: 90 to 2700 (degrees)
Same functionality as wheel_range (degrees on both).
Access: Read/Write
Values: 0 to 65535 (raw; the FF_GAIN scale)
Drives the same wheel strength setting as wheel_strength, but the
file speaks the raw 0-65535 scale tools expect (Oversteer shows it as
percent in its UI). wheel_strength keeps its human-friendly 0-100
percent scale; the two stay in sync.
Access: Read/Write
Values: 0 to 65535 (raw; the FF_AUTOCENTER scale)
A real, driver-emulated centring spring: while
nonzero, the wheel pulls itself toward centre with a damped spring
computed in the 500 Hz effect loop - firm within roughly the central
eighth of the axis, like hardware autocenter on other wheels. Also
reachable through the standard evdev FF_AUTOCENTER control, which
means games that write autocenter 0 before taking over force feedback
correctly disable it for their session. Useful for desk-driving
without a game, or as idle centring.
Access: Read/Write
Values: 0 to 100 (percent), default 100
Global output scales for the emulated FF_SPRING / FF_DAMPER /
FF_FRICTION effect classes, matching the new-lg4ff semantics: 100 =
effects play as the game commanded, lower values tame that effect
class across all games, 0 mutes it. damper_level scales DAMPER
effects from games; the wheel's own firmware damping is wheel_damping.
Access: Read/Write, mode 0600 (root only)
Availability: Only present when the module is built with CONFIG_HID_LOGITECH_HIDPP_DEBUG (e.g. make DEBUG=1). Absent from default builds.
Raw HID++ command shell for protocol bring-up. Write feature fn [params...] (hex), read the last command's response.
# Send fn 0x5c to feature 0x0b with three zero params
echo "0b 5c 00 00 00" > wheel_hidpp_debug
# Read the response
cat wheel_hidpp_debug| Error | Meaning |
|---|---|
-ENODEV |
Device not found or driver not ready |
-EPERM |
Operation not permitted in current mode |
-EINVAL |
Invalid value provided |
-ERANGE |
Value out of range (e.g. wheel_calibrate > 65535, or an active LED slot index out of range) |
-EOPNOTSUPP |
Feature not supported by device |
-EIO |
Communication error with device |
#!/bin/bash
# Set up RS50 for racing
DEVICE=$(find /sys/bus/hid/devices -name "*046D*C276*" | head -1)
cd "$DEVICE" || exit 1
# Force feedback settings
echo 900 > wheel_range # 900 degrees
echo 75 > wheel_strength # 75% force
echo 20 > wheel_damping # 20% damping
echo 50 > wheel_trueforce # 50% TRUEFORCE
# LED: Red theme
echo 0 > wheel_led_slot
echo "FF0000 FF0000 FF0000 FF0000 FF0000 FF0000 FF0000 FF0000 FF0000 FF0000" > wheel_led_colors
echo 1 > wheel_led_apply
echo "RS50 configured!"#!/bin/bash
# Toggle between desktop and onboard mode
DEVICE=$(find /sys/bus/hid/devices -name "*046D*C276*" | head -1)
MODE=$(cat "$DEVICE/wheel_mode")
if [ "$MODE" = "desktop" ]; then
echo "onboard" > "$DEVICE/wheel_mode"
echo "Switched to onboard mode"
else
echo "desktop" > "$DEVICE/wheel_mode"
echo "Switched to desktop mode"
fiFor developers interested in the HID++ protocol details, see:
PROTOCOL_SPECIFICATION.md- Full protocol documentationdev/docs/CAPTURE_ANALYSIS_*.md- USB capture analysis
| Page | Index Var | Description |
|---|---|---|
| 0x8040 | idx_brightness | LED Brightness / Sensitivity |
| 0x807A | idx_lightsync | LIGHTSYNC Effects |
| 0x807B | idx_rgb_config | RGB Zone Configuration |
| 0x812C | idx_calibrate | Centre Calibration (RS50 + G Pro, sub-device 0x05) |
| 0x8133 | idx_damping | Wheel Damping |
| 0x8134 | idx_brakeforce | Brake Force |
| 0x8136 | idx_strength | FFB Strength |
| 0x8137 | idx_profile | Profile/Mode Switching |
| 0x8138 | idx_range | Rotation Range |
| 0x8139 | idx_trueforce | TRUEFORCE |
| 0x8140 | idx_filter | FFB Filter |