testsAndMisc/linux_configuration/docs/DOCS-g29-shifter.md
Krzysztof kuhy Rudnicki cce6e2c048
Some checks failed
dependency freshness / dependencies on newest stable (push) Has been cancelled
md-naming / markdown naming convention (push) Has been cancelled
Pre-commit checks / pre-commit (push) Has been cancelled
Python tests / test (push) Has been cancelled
Shell tests / shell-tests (push) Has been cancelled
Shell tests / arch-tests (push) Has been cancelled
g29 shifter: no notification, no retry loop -- fail visibly and stop
kuhy: "I do not want it to work like this, I want it to just work
normally." PS4 mode (046D:C260) cannot be supported on this PC (no kernel
driver, SDL lg4ff has no c260 either, so FFB is dead there too) and the
selector is physical. Keep the mode detection so the journal and --status
name PS4 mode instead of "not found"; drop the desktop notification and
the 5-minute Restart loop.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NVfC2T6DyvCFqse9A99zef
2026-09-18 10:56:52 +02:00

6.7 KiB
Raw Blame History

Logitech G29 Driving Force Shifter: 1st/3rd not engaging (HID-BPF fix)

Files: fixes/g29_shifter.bpf.c (the fix), fixes/fix_g29_shifter.sh (build/attach/install), fixes/g29_wheel_mode.sh (PS3/PS4 mode detection), fixes/g29_shifter_capture.py (grades a raw capture). Force feedback (a separate problem, SDL-side): DOCS-g29-ffb.md.

Symptom

The stick physically locks into every gate, but 1st and 3rd often do not register in any game unless the stick is shoved hard forward. 2/4/6/R are fine.

Cause (measured 2026-09-11)

The wheel firmware decodes the shifter's hall sensor into gear buttons itself. The G29 input report (12 bytes, no report id) carries the raw values too:

byte content
0–3 hat (4 bits) + 25 buttons; gears 1–6, R = byte 2 bits 0–6
4–5 wheel, 6–8 pedals
9 shifter X: left ≈47–90 (1/2), centre ≈104–139 (3/4), right ≈166–191 (5/6/R)
10 shifter Y: bottom row ≈30–60, neutral ≈105, top row ≈180–233
11 flags; 0x40 = stick pushed down (reverse)

Firmware thresholds, read off the on/off edges of a raw capture:

row engages releases where this stick actually rests
top (1/3/5) Y ≥ ~195 Y ≤ ~179 180–233, often 180–193 → no gear
bottom (2/4/6/R) Y ≤ ~56 Y ≥ ~72 30–60

The thresholds are symmetric around Y=125 but this unit's neutral is ≈105 (sensor/magnet offset ~20 rearward), so the top row has no margin. The bottom row keeps ~15–25 and occasionally drops for a second when the stick relaxes to Y 58–71 (seen once on 4th, once on 2nd in the baseline captures).

Fix

g29_shifter.bpf.c is a HID-BPF hid_device_event program attached by udev-hid-bpf. It re-decodes 1st/3rd from bytes 9/10 with engage Y ≥ 160, release Y ≤ 140 (hysteresis kept in a BPF global) and ORs the bit into byte 2 before hid-logitech parses the report. It never clears a bit, so the firmware's own decode is untouched — the working gates cannot regress. It is transparent to every game, Proton and Steam Input, and FFB is unaffected. 5th is left to the firmware: no capture data for the top-right gate.

The Arch udev-hid-bpf package ships no headers; the script shallow-clones the matching upstream tag into ~/.cache/g29_shifter/ and compiles with the same clang flags as upstream's meson.build.

The PS3/PS4 selector (found 2026-09-18)

The wheel has a physical PS3/PS4 switch and the USB identity follows it:

selector enumerates as bound by shifter fix
PS3 046D:C294 → 046D:C24F hid-logitech (new-lg4ff) attaches
PS4 046D:C260 hid-generic cannot

C260 has a different report layout, no kernel driver knows it (new-lg4ff 0.5.0's table stops at c24f), and there is no software way out — only the switch. On the 2026-09-15 and 2026-09-17 boots the wheel came up as C260; every layer below looked for C24F only, so the unit failed once with "no G29 found", nothing was said, and 1st/3rd were dead until the selector was flipped ~1 day later (the journal shows C260 → disconnect → C294 → C24F).

Surviving reboots

Three layers, because a missed attach is silent — the wheel still works, just with the bad decode, which is indistinguishable from "the fix stopped working":

  1. udev-hid-bpf install → /etc/udev-hid-bpf/g29_shifter.bpf.o plus /etc/udev/rules.d/99-hid-bpf-g29_shifter.rules, which attaches on add.
  2. g29-shifter-bpf.service (systemd/), enabled at multi-user.target, runs fix_g29_shifter.sh --ensure: a no-op when attached, re-attaches when not. No wheel → exit 0 (layer 3 re-runs it on plug-in). Wheel present in PS4 or compatibility mode → exit 1 with the mode named, so systemctl --failed and the journal say why instead of "not found". No notification and no retry loop: kuhy ruled that out (2026-09-18, "I want it to just work") — flipping the selector re-enumerates the wheel, and layer 3 re-runs the unit.
  3. /etc/udev/rules.d/99-g29-shifter-ensure.rules pulls that unit in on every re-enumeration of C24F or C260, so a wheel powered on in the wrong mode is reported at once, and hotplug is covered as well as boot.

--status reports all of it, mode first. Verified 2026-09-12 by detaching and starting the unit: it re-attached and exited cleanly. That test also caught the unit failing with HOME: unbound variable — systemd has no $HOME, so the cache path now falls back. The mode/notification logic is covered by tests/test_fix_g29_shifter.bats against a fake sysfs tree.

If the symptom returns after a boot

Check --status first. wheel mode: ps4 → flip the selector. If it says native + attached and gears still misbehave, the sensor's zero has drifted and the thresholds need recalibrating — the fixed Y_TOP_RELEASE (140) assumes the stick rests near Y≈105. A drift upward of ~+40 would park the rest position above the release line, so a gear would engage and never let go (the car stays in gear). Measure the rest value:

sudo ~/src/testsAndMisc/linux_configuration/fixes/g29_shifter_capture.py \
    --record 20 ~/data/g29-captures/neutral.txt --runs

with hands off the stick, and compare the reported Y against 105.

Usage

~/src/testsAndMisc/linux_configuration/fixes/fix_g29_shifter.sh --test   # attach until unplug
~/src/testsAndMisc/linux_configuration/fixes/fix_g29_shifter.sh          # persistent (udev rule)
~/src/testsAndMisc/linux_configuration/fixes/fix_g29_shifter.sh --status
~/src/testsAndMisc/linux_configuration/fixes/fix_g29_shifter.sh --remove

Verifying / recalibrating

hidraw sees the report after the BPF program, so a raw capture shows the fix:

sudo timeout 60 xxd -c 12 /dev/hidraw$(basename "$(readlink -f /dev/input/by-id/usb-Logitech_G29_Driving_Force_Racing_Wheel-hidraw)" | tr -dc 0-9) > /tmp/g29.txt
~/src/testsAndMisc/linux_configuration/fixes/g29_shifter_capture.py /tmp/g29.txt --edges

Exit code 1 means some gate had a dwell ≥0.25 s with the bit clear. --edges prints X/Y at every on/off transition — that is where the thresholds come from. If the sensor drifts further, edit the #defines at the top of the .bpf.c and re-run the script.