testsAndMisc/linux_configuration/fixes/fix_g29_shifter.sh
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

195 lines
5.6 KiB
Bash
Executable File

#!/bin/bash
# ============================================================================
# Logitech G29 Driving Force Shifter: make 1st/3rd engage reliably (HID-BPF)
#
# Builds g29_shifter.bpf.c (same dir) against the udev-hid-bpf headers and
# attaches it to the wheel. Why and what the numbers mean: ../docs/DOCS-g29-shifter.md.
#
# fix_g29_shifter.sh build + persistent install (udev rule)
# fix_g29_shifter.sh --test build + attach until unplug/--remove only
# fix_g29_shifter.sh --remove detach from the running device
# fix_g29_shifter.sh --ensure attach only if not attached (used by systemd)
# fix_g29_shifter.sh --status show whether a program is attached
# ============================================================================
set -euo pipefail
SCRIPT_NAME="$(basename "$0")"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly SCRIPT_NAME SCRIPT_DIR
readonly SRC="$SCRIPT_DIR/g29_shifter.bpf.c"
readonly HEADERS_REPO="https://gitlab.freedesktop.org/libevdev/udev-hid-bpf.git"
readonly HEADERS_TAG="2.3.0-20260703" # must match the installed udev-hid-bpf
# HOME is unset under systemd, and `set -u` would abort before --ensure runs.
readonly CACHE_DIR="${XDG_CACHE_HOME:-${HOME:-/var/cache}/.cache}/g29_shifter"
readonly HEADERS_DIR="$CACHE_DIR/udev-hid-bpf"
readonly BUILD_DIR="$CACHE_DIR/build"
readonly OBJ="$BUILD_DIR/g29_shifter.bpf.o"
readonly VID_PID="0003:046D:C24F"
readonly INSTALLED_OBJ="/etc/udev-hid-bpf/g29_shifter.bpf.o"
readonly INSTALLED_RULE="/etc/udev/rules.d/99-hid-bpf-g29_shifter.rules"
readonly UNIT_NAME="g29-shifter-bpf.service"
readonly UNIT_SRC_DIR="$SCRIPT_DIR/systemd"
readonly ENSURE_RULE="/etc/udev/rules.d/99-g29-shifter-ensure.rules"
readonly PACKAGES=(udev-hid-bpf bpf clang libbpf git)
MODE="install"
usage() {
sed -n '4,13p' "$0" | sed 's/^# \{0,3\}//'
exit 0
}
log() { echo "[$SCRIPT_NAME] $*"; }
install_packages() {
local -a missing=()
local pkg
for pkg in "${PACKAGES[@]}"; do
pacman -Q "$pkg" &>/dev/null || missing+=("$pkg")
done
if ((${#missing[@]})); then
log "installing: ${missing[*]}"
sudo pacman -S --needed --noconfirm "${missing[@]}"
fi
}
# The Arch package ships no headers; fetch the matching upstream tag once.
fetch_headers() {
if [[ -f "$HEADERS_DIR/src/bpf/hid_bpf_helpers.h" ]]; then
return
fi
log "fetching udev-hid-bpf headers ($HEADERS_TAG)"
rm -rf "$HEADERS_DIR"
git clone -q --depth 1 --branch "$HEADERS_TAG" "$HEADERS_REPO" "$HEADERS_DIR"
}
# Mirrors upstream src/bpf/meson.build: clang -target bpf, then a bpftool strip.
build() {
mkdir -p "$BUILD_DIR"
log "compiling $(basename "$SRC")"
clang -std=gnu11 -fno-stack-protector -O2 -target bpf -g -c \
-fms-extensions -Wno-microsoft-anon-tag "-D__$(uname -m)__" \
-I"$HEADERS_DIR/src/bpf" -idirafter /usr/include \
"$SRC" -o "$OBJ.unstripped"
bpftool gen object "$OBJ" "$OBJ.unstripped"
log "built $OBJ"
}
# The wheel exposes two HID interfaces; only the joystick one has a descriptor.
find_device() {
local dev
for dev in /sys/bus/hid/devices/"$VID_PID".*; do
if [[ -s "$dev/report_descriptor" ]]; then
echo "$dev"
return
fi
done
echo "Error: no G29 ($VID_PID) with a report descriptor found in sysfs" >&2
exit 1
}
is_attached() {
sudo udev-hid-bpf list-loaded 2>/dev/null | grep -q g29_shifter
}
# The udev rule is the primary attach path; this unit exists because a missed
# attach is silent — the wheel still works, just with the old bad decode.
install_safety_net() {
local unit="/etc/systemd/system/$UNIT_NAME"
sed "s#__SCRIPT__#$SCRIPT_DIR/$SCRIPT_NAME#" "$UNIT_SRC_DIR/$UNIT_NAME" |
sudo install -m 644 /dev/stdin "$unit"
sudo install -m 644 "$UNIT_SRC_DIR/$(basename "$ENSURE_RULE")" "$ENSURE_RULE"
sudo systemctl daemon-reload
sudo systemctl enable "$UNIT_NAME"
log "safety net installed: $unit + $(basename "$ENSURE_RULE")"
}
show_status() {
local dev
dev="$(find_device)"
log "device: $dev"
if [[ -f "$INSTALLED_OBJ" && -f "$INSTALLED_RULE" ]]; then
log "persistent: $INSTALLED_OBJ + $(basename "$INSTALLED_RULE")"
else
log "persistent: not installed (run without --test)"
fi
if is_attached; then
log "attached now: yes"
else
log "attached now: NO"
fi
if systemctl is-enabled --quiet "$UNIT_NAME" 2>/dev/null; then
log "safety net: enabled"
else
log "safety net: NOT enabled"
fi
}
main() {
local dev
case "$MODE" in
status)
show_status
;;
ensure)
if is_attached; then
log "already attached; nothing to do"
exit 0
fi
dev="$(find_device)"
if [[ -f "$INSTALLED_OBJ" ]]; then
sudo udev-hid-bpf add "$dev" "$INSTALLED_OBJ"
log "re-attached $INSTALLED_OBJ to $dev (udev rule had missed it)"
else
log "not installed; run $SCRIPT_NAME without --test first" >&2
exit 1
fi
;;
remove)
dev="$(find_device)"
sudo udev-hid-bpf --verbose remove "$dev"
log "detached from $dev"
;;
test)
install_packages
fetch_headers
build
dev="$(find_device)"
sudo udev-hid-bpf --verbose add "$dev" "$OBJ"
log "attached to $dev (non-persistent; --remove or re-plug undoes it)"
;;
install)
install_packages
fetch_headers
build
sudo udev-hid-bpf install --force "$OBJ"
sudo udevadm control --reload
dev="$(find_device)"
# Re-trigger so the new rule attaches without a re-plug.
sudo udevadm trigger --action=add "$dev"
install_safety_net
log "installed to /etc/udev-hid-bpf/ and attached to $dev"
show_status
;;
esac
}
while [[ $# -gt 0 ]]; do
case $1 in
--test) MODE="test" ;;
--ensure) MODE="ensure" ;;
--remove) MODE="remove" ;;
--status) MODE="status" ;;
-h | --help) usage ;;
*)
echo "Unknown option: $1" >&2
exit 1
;;
esac
shift
done
main