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

105 lines
5.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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:
```bash
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
```bash
~/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:
```bash
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 `#define`s at the top of the
`.bpf.c` and re-run the script.