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

213 lines
6.1 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 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)
# shellcheck source=g29_wheel_mode.sh
source "$SCRIPT_DIR/g29_wheel_mode.sh"
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"
}
# Native-mode device only; any other mode is named, not just "not found".
find_device() {
local dev
if dev="$(g29_sysfs_device "$G29_PID_NATIVE")"; then
echo "$dev"
return
fi
echo "Error: $(g29_mode_explanation "$(g29_wheel_mode)")" >&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 udevadm control --reload
sudo systemctl daemon-reload
sudo systemctl enable "$UNIT_NAME"
log "safety net installed: $unit + $(basename "$ENSURE_RULE")"
}
show_status() {
local mode
mode="$(g29_wheel_mode)"
log "wheel mode: $mode ($(g29_mode_explanation "$mode"))"
if [[ "$mode" == native ]]; then
log "device: $(find_device)"
fi
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
case "$(g29_wheel_mode)" in
native) ;;
absent)
# Not a failure: the udev rule re-runs this unit when it appears.
log "$(g29_mode_explanation absent); nothing to attach to"
exit 0
;;
*)
# The wheel still "works" here -- with the bad 1st/3rd decode this
# fix exists to remove -- so fail visibly (journal, systemctl
# --failed) and name the mode instead of "not found".
log "$(g29_mode_explanation "$(g29_wheel_mode)")" >&2
exit 1
;;
esac
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"
install_safety_net
dev="$(find_device)"
# Re-trigger so the new rules attach without a re-plug.
sudo udevadm trigger --action=add "$dev"
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