testsAndMisc/linux_configuration/docs/DOCS-g29-shifter.md
Krzysztof kuhy Rudnicki 7e2d78c7db Add the G29 shifter HID-BPF fix (1st/3rd engage reliably)
Live on this host since 2026-09-11 but never committed. The wheel firmware
decodes the shifter's hall sensor itself and this unit's top row rests at
Y 180-193, below the ~195 engage threshold, so 1st/3rd often did not
register. g29_shifter.bpf.c re-decodes those two gates from the raw X/Y
bytes with hysteresis (engage >= 160, release <= 140) and ORs the bit in
before hid-logitech parses the report; it never clears a bit, so the
firmware's working gates cannot regress. fix_g29_shifter.sh builds,
attaches and installs it persistently (udev-hid-bpf rule + a systemd
safety-net unit pulled in on every re-enumeration); g29_shifter_capture.py
records and grades raw captures per gate.

Two files were over the 250-line cap: the test file is split at the
record/CLI boundary with the report builders moved into a conftest fixture
(the naming hook allows only test_*.py in tests/), and the grader's
docstrings are tightened. No behaviour change to the installed fix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SS8M5wk88nJTXxDWz61KKk
2026-09-14 22:12:31 +02:00

5.1 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_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.

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.
  3. /etc/udev/rules.d/99-g29-shifter-ensure.rules pulls that unit in on every re-enumeration, so hotplug is covered as well as boot.

--status reports all of it. 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.

If the symptom returns after a boot

Check --status first. If it says 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.