testsAndMisc/linux_configuration/fixes/g29_shifter.bpf.c
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

140 lines
4.8 KiB
C

// SPDX-License-Identifier: GPL-2.0-only
/*
* Logitech G29 + Driving Force Shifter: make 1st and 3rd engage reliably.
*
* The wheel firmware decodes the H-shifter's hall-sensor X/Y into gear
* buttons itself, with thresholds symmetric around Y=125 (top row engages at
* Y>=~195, releases at Y<=~179). This shifter's neutral sits at Y~105, so the
* top gates rest at Y 180-193 and only cross the engage line when shoved.
* Measured 2026-09-11 from raw HID captures; see linux_configuration/docs/DOCS-g29-shifter.md.
*
* The bottom row has the same disease in milder form: it rests at Y 30-60,
* the firmware releases at Y>=~72, and the stick relaxing to Y 58-87 drops
* 2nd/4th for a second (seen in every baseline capture).
*
* This program re-decodes the gears from the raw values with wider
* hysteresis and ORs the bit into the report before hid-logitech parses it.
* It never clears anything, so the firmware's own decode cannot regress.
* 5th (top-right) is left to the firmware: no capture data for that gate.
*
* Input report, 12 bytes, no report id:
* 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 ~50-90, centre ~105-139, right ~166-191)
* 10 shifter Y (bottom row ~30-60, neutral ~105, top row ~180-233)
* 11 flags, 0x40 = stick pushed down (reverse)
*/
#include "vmlinux.h"
#include "hid_bpf.h"
#include "hid_bpf_helpers.h"
#include <bpf/bpf_tracing.h>
#define VID_LOGITECH 0x046D
#define PID_G29_PS3_MODE 0xC24F
HID_BPF_CONFIG(
HID_DEVICE(BUS_USB, HID_GROUP_ANY, VID_LOGITECH, PID_G29_PS3_MODE)
);
#define REPORT_LEN 12
#define GEAR_BYTE 2
#define GEAR1_BIT 0x01
#define GEAR2_BIT 0x02
#define GEAR3_BIT 0x04
#define GEAR4_BIT 0x08
#define GEAR6_BIT 0x20
#define GEARR_BIT 0x40
#define TOP_BITS (GEAR1_BIT | GEAR3_BIT)
#define RIGHT_BOTTOM_BITS (GEAR6_BIT | GEARR_BIT)
#define SHIFTER_X 9
#define SHIFTER_Y 10
#define FLAGS 11
#define FLAG_DOWN 0x40
/* Calibration (raw 0-255 hall values). Recalibrate by editing these and
* re-running fix_g29_shifter.sh; g29_shifter_capture.py prints the numbers. */
#define X_LEFT_MAX 96 /* X <= this: left column (1st/2nd) */
#define X_CENTRE_MAX 151 /* X <= this: centre column (3rd/4th); above: right */
#define Y_TOP_ENGAGE 160 /* enter a top gate; firmware needs ~195 */
#define Y_TOP_RELEASE 140 /* leave it; neutral is ~105 so this stays clear */
#define Y_BOTTOM_ENGAGE 65 /* enter a bottom gate; firmware needs ~56 */
#define Y_BOTTOM_RELEASE 85 /* leave it; firmware lets go at ~72 */
/* Hysteresis state: 0 or one gear bit. Persists across reports. */
static __u8 engaged_bit;
/* Gear bit the stick would be in if it were engaged in the given row.
* Right-bottom is 6th or R depending on whether the stick is pushed down;
* once engaged there the flag is ignored so a flicker cannot toggle gears. */
static __u8 target_bit(__u8 x, bool top, bool down, __u8 held)
{
if (x <= X_LEFT_MAX)
return top ? GEAR1_BIT : GEAR2_BIT;
if (x <= X_CENTRE_MAX)
return top ? GEAR3_BIT : GEAR4_BIT;
if (top)
return 0; /* 5th: no calibration data, firmware only */
if (held & RIGHT_BOTTOM_BITS)
return held;
return down ? GEARR_BIT : GEAR6_BIT;
}
SEC(HID_BPF_DEVICE_EVENT)
int BPF_PROG(g29_shifter_event, struct hid_bpf_ctx *hctx)
{
__u8 *data = hid_bpf_get_data(hctx, 0 /* offset */, REPORT_LEN);
__u8 x, y;
bool down, top;
if (!data)
return 0; /* EPERM check */
x = data[SHIFTER_X];
y = data[SHIFTER_Y];
down = data[FLAGS] & FLAG_DOWN;
/* Leaving the gate (back towards neutral) or sliding to another column
* drops the held gear; a fresh engage is evaluated in the same report. */
if (engaged_bit) {
top = engaged_bit & TOP_BITS;
if ((top ? y < Y_TOP_RELEASE : y > Y_BOTTOM_RELEASE) ||
target_bit(x, top, down, engaged_bit) != engaged_bit)
engaged_bit = 0;
}
if (!engaged_bit) {
if (y >= Y_TOP_ENGAGE)
engaged_bit = target_bit(x, true, down, 0);
else if (y <= Y_BOTTOM_ENGAGE)
engaged_bit = target_bit(x, false, down, 0);
}
/* Never contradict the firmware on 6th-vs-R: if it already reports one
* of the pair, adding the other would show two gears at once. */
if ((engaged_bit & RIGHT_BOTTOM_BITS) &&
(data[GEAR_BYTE] & RIGHT_BOTTOM_BITS & ~engaged_bit))
return 0;
data[GEAR_BYTE] |= engaged_bit;
return 0;
}
HID_BPF_OPS(g29_shifter) = {
.hid_device_event = (void *)g29_shifter_event,
};
/* The G29 exposes a second, empty HID interface (.0013 here); only bind to
* the joystick collection: Usage Page Generic Desktop, Usage Joystick. */
SEC("syscall")
int probe(struct hid_bpf_probe_args *ctx)
{
if (ctx->rdesc_size > 4 &&
ctx->rdesc[0] == 0x05 && ctx->rdesc[1] == 0x01 &&
ctx->rdesc[2] == 0x09 && ctx->rdesc[3] == 0x04)
ctx->retval = 0;
else
ctx->retval = -EINVAL;
return 0;
}
char _license[] SEC("license") = "GPL";