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

129 lines
6.7 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_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:
```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.