linux-input.vger.kernel.org archive mirror
 help / color / mirror / Atom feed
From: Vicki Pfau <vi@endrift.com>
To: Dmitry Torokhov <dmitry.torokhov@gmail.com>, linux-input@vger.kernel.org
Cc: Vicki Pfau <vi@endrift.com>
Subject: [PATCH v2 5/5] Input: xbox_gip - Add wheel support
Date: Tue, 16 Sep 2025 18:19:34 -0700	[thread overview]
Message-ID: <20250917011937.1649481-6-vi@endrift.com> (raw)
In-Reply-To: <20250917011937.1649481-1-vi@endrift.com>

This adds preliminary support for racing wheel support in xbox_gip,
exposing them mapped to the newly added axes.

Signed-off-by: Vicki Pfau <vi@endrift.com>
---
 drivers/input/joystick/xbox_gip.c | 209 ++++++++++++++++++++++++++++--
 1 file changed, 201 insertions(+), 8 deletions(-)

diff --git a/drivers/input/joystick/xbox_gip.c b/drivers/input/joystick/xbox_gip.c
index 94e20ab4b672c..1be6ad0d6eb66 100644
--- a/drivers/input/joystick/xbox_gip.c
+++ b/drivers/input/joystick/xbox_gip.c
@@ -10,7 +10,7 @@
  * - Event logging
  * - Sending fragmented messages
  * - Raw character device
- * - Wheel support
+ * - Wheel force feedback
  * - Flight stick support
  * - More arcade stick testing
  * - Arcade stick extra buttons
@@ -220,6 +220,22 @@
 #define GIP_EXTENDED_STATUS_ACCESS_DENIED	3
 #define GIP_EXTENDED_STATUS_FAILED		4
 
+/* Wheel-specific flags */
+#define GIP_WHEEL_HAS_POWER		BIT(3)
+#define GIP_WHEEL_HANDBRAKE_CONN	BIT(4)
+#define GIP_WHEEL_CLUTCH_CONN		BIT(5)
+#define GIP_WHEEL_BRAKE_CONN		BIT(6)
+#define GIP_WHEEL_THROTTLE_CONN		BIT(7)
+
+#define GIP_HSHIFTER_NONE	0
+#define GIP_HSHIFTER_2POS	1 /* 2 position, no neutral */
+#define GIP_HSHIFTER_2POS_N	2 /* 2 position, neutral */
+#define GIP_HSHIFTER_RTL_1TL	3 /* Reverse top left, first top left */
+#define GIP_HSHIFTER_RTL_1BL	4 /* Reverse top left, first bottom left */
+#define GIP_HSHIFTER_RBL	5 /* Reverse bottom left */
+#define GIP_HSHIFTER_RTR	6 /* Reverse top right */
+#define GIP_HSHIFTER_RBR	7 /* Reverse bottom right */
+
 /* Internal constants, not part of protocol */
 #define GIP_DEFAULT_IN_SYSTEM_MESSAGES 0x5e
 #define GIP_DEFAULT_OUT_SYSTEM_MESSAGES 0x472
@@ -235,6 +251,8 @@
 
 #define GIP_QUIRK_NO_HELLO		BIT(0)
 #define GIP_QUIRK_NO_IMPULSE_VIBRATION	BIT(1)
+#define GIP_QUIRK_FORCE_GAMEPAD_SB	BIT(2)
+#define GIP_QUIRK_WHEEL_FORCE_HANDBRAKE	BIT(31)
 
 #define GIP_LED_GUIDE_MAX_BRIGHTNESS	100 /* Spec says 47, but larger values work */
 #define GIP_LED_GUIDE_INIT_BRIGHTNESS	20
@@ -277,6 +295,11 @@ enum gip_elite_button_format {
 	GIP_BTN_FMT_XBE2_5,
 };
 
+enum gip_vendor_type {
+	GIP_VENDOR_NONE = 0,
+	GIP_VENDOR_LOGI_TRUE_FORCE_WHEEL = 1,
+};
+
 static const guid_t guid_arcade_stick =
 	GUID_INIT(0x332054cc, 0xa34b, 0x41d5, 0xa3, 0x4a, 0xa6, 0xa6, 0x71, 0x1e, 0xc4, 0xb3);
 static const guid_t guid_console_function_map =
@@ -355,6 +378,18 @@ static const struct gip_audio_format gip_audio_format_table[MAX_GIP_AUDIO_FORMAT
 	[GIP_AUDIO_FORMAT_48000HZ_8CH] = { .rate = 48000, .channels = 8 },
 };
 
+struct gip_wheel_info {
+	uint8_t connections;
+	uint8_t shifter_type: 3;
+	uint8_t max_gear: 5;
+	uint16_t angle_setting;
+	uint16_t max_angle;
+	uint16_t max_throttle;
+	uint16_t max_brake;
+	uint16_t max_clutch;
+	uint8_t max_handbrake;
+};
+
 struct gip_quirks {
 	uint16_t vendor_id;
 	uint16_t product_id;
@@ -371,6 +406,11 @@ struct gip_quirks {
 };
 
 static const struct gip_quirks quirks[] = {
+	/* Thrustmaster T128X GIP Racing Wheel */
+	{ 0x044f, 0xb69c, 0,
+		.quirks = GIP_QUIRK_FORCE_GAMEPAD_SB | GIP_QUIRK_WHEEL_FORCE_HANDBRAKE,
+		.device_type = GIP_TYPE_WHEEL },
+
 	/* Xbox One Controller (model 1573) */
 	{ 0x045e, 0x02d1, 0, .override_name = "Xbox One Controller" },
 
@@ -498,11 +538,14 @@ struct gip_attachment {
 	enum gip_elite_button_format xbe_format;
 	uint32_t features;
 	uint32_t quirks;
+	enum gip_vendor_type vendor_type;
 
 	int extra_buttons;
 	int extra_axes;
 
 	bool dpad_as_buttons;
+	struct gip_wheel_info wheel;
+	int8_t logi_dial_state;
 	struct hid_device *hdev;
 };
 
@@ -1574,10 +1617,44 @@ static int gip_setup_input_device(struct gip_attachment *attachment)
 		input_set_capability(input, EV_KEY, BTN_THUMBR);
 		input_set_capability(input, EV_KEY, BTN_THUMBL);
 		break;
-	case GIP_TYPE_FLIGHT_STICK:
 	case GIP_TYPE_WHEEL:
+		input_set_abs_params(input, ABS_WHEEL,
+			-attachment->wheel.max_angle - 1,
+			attachment->wheel.max_angle, 0, 0);
+		input_abs_set_res(input, ABS_WHEEL, attachment->wheel.angle_setting);
+		if (attachment->wheel.max_throttle)
+			input_set_abs_params(input, ABS_GAS, 0,
+				attachment->wheel.max_throttle, 0, 0);
+
+		if (attachment->wheel.max_brake)
+			input_set_abs_params(input, ABS_BRAKE, 0,
+				attachment->wheel.max_brake, 0, 0);
+
+		if (attachment->wheel.max_clutch)
+			input_set_abs_params(input, ABS_CLUTCH, 0,
+				attachment->wheel.max_clutch, 0, 0);
+
+		if (attachment->wheel.max_handbrake)
+			input_set_abs_params(input, ABS_HANDBRAKE, 0,
+				attachment->wheel.max_handbrake, 0, 0);
+
+		if (attachment->wheel.shifter_type)
+			input_set_abs_params(input, ABS_SHIFTER, -1,
+				attachment->wheel.max_gear, 0, 0);
+
+
+		if (attachment->vendor_type == GIP_VENDOR_LOGI_TRUE_FORCE_WHEEL) {
+			input_set_capability(input, EV_KEY, BTN_THUMBL);
+			input_set_capability(input, EV_KEY, BTN_THUMBR);
+			input_set_capability(input, EV_KEY, KEY_KPPLUS);
+			input_set_capability(input, EV_KEY, KEY_KPMINUS);
+			input_set_capability(input, EV_KEY, KEY_KPENTER);
+			input_set_capability(input, EV_REL, REL_DIAL);
+		}
+		break;
 	case GIP_TYPE_UNKNOWN:
 	case GIP_TYPE_NAVIGATION_CONTROLLER:
+	case GIP_TYPE_FLIGHT_STICK:
 		break;
 	case GIP_TYPE_CHATPAD:
 	case GIP_TYPE_HEADSET:
@@ -1602,6 +1679,11 @@ static int gip_setup_input_device(struct gip_attachment *attachment)
 	if (attachment->vendor_id == 0x045e && attachment->product_id == 0x0b0a)
 		input_set_abs_params(input, ABS_PROFILE, 0, 3, 0, 0);
 
+	if (attachment->quirks & GIP_QUIRK_WHEEL_FORCE_HANDBRAKE) {
+		input_set_capability(input, EV_KEY, BTN_THUMBR);
+		input_set_capability(input, EV_KEY, BTN_THUMBL);
+	}
+
 #ifdef CONFIG_JOYSTICK_XBOX_GIP_FF
 	if (attachment->features & GIP_FEATURE_MOTOR_CONTROL) {
 		input_set_capability(input, EV_FF, FF_RUMBLE);
@@ -1681,6 +1763,14 @@ static int gip_send_init_sequence(struct gip_attachment *attachment)
 			return rc;
 	}
 
+	if (attachment->attachment_type == GIP_TYPE_WHEEL) {
+		struct gip_initial_reports_request request = { 0 };
+
+		gip_send_vendor_message(attachment,
+			GIP_CMD_INITIAL_REPORTS_REQUEST, 0, &request,
+			sizeof(request));
+	}
+
 	usb_make_path(attachment->device->udev, attachment->phys,
 		sizeof(attachment->phys));
 	len = strlen(attachment->phys);
@@ -1689,7 +1779,8 @@ static int gip_send_init_sequence(struct gip_attachment *attachment)
 			sizeof(attachment->phys) - len, "/input%d",
 			attachment->attachment_index);
 
-	if (gip_attachment_is_controller(attachment) && !attachment->input) {
+	if (gip_attachment_is_controller(attachment) && !attachment->input
+		&& attachment->attachment_type != GIP_TYPE_WHEEL) {
 		rc = gip_setup_input_device(attachment);
 		if (rc == -ENODEV)
 			return 0;
@@ -2006,6 +2097,13 @@ static int gip_handle_command_metadata_respose(struct gip_attachment *attachment
 			expected_guid = &guid_headset;
 			break;
 		}
+
+		if (strcmp(type, "Logi.Xbox.Input.TrueForceWheel") == 0) {
+			attachment->attachment_type = GIP_TYPE_WHEEL;
+			attachment->vendor_type = GIP_VENDOR_LOGI_TRUE_FORCE_WHEEL;
+			expected_guid = &guid_logi_true_force_wheel;
+			break;
+		}
 	}
 
 	found_expected_guid = !expected_guid;
@@ -2308,13 +2406,87 @@ static void gip_handle_arcade_stick_report(struct gip_attachment *attachment,
 	}
 }
 
+static void gip_handle_wheel_report(struct gip_attachment *attachment,
+	struct input_dev *dev, const uint8_t *bytes, int num_bytes)
+{
+	int32_t axis;
+
+	if (num_bytes < 16)
+		return;
+
+	axis = bytes[2];
+	axis |= bytes[3] << 8;
+	input_report_abs(dev, ABS_WHEEL, axis - 0x8000);
+
+	if (attachment->wheel.connections & GIP_WHEEL_THROTTLE_CONN) {
+		axis = bytes[4];
+		axis |= bytes[5] << 8;
+		input_report_abs(dev, ABS_GAS, axis);
+	}
+
+	if (attachment->wheel.connections & GIP_WHEEL_BRAKE_CONN) {
+		axis = bytes[6];
+		axis |= bytes[7] << 8;
+		input_report_abs(dev, ABS_BRAKE, axis);
+	}
+
+	if (attachment->wheel.connections & GIP_WHEEL_CLUTCH_CONN) {
+		axis = bytes[8];
+		axis |= bytes[9] << 8;
+		input_report_abs(dev, ABS_CLUTCH, axis);
+	}
+
+	if (attachment->wheel.connections & GIP_WHEEL_HANDBRAKE_CONN)
+		input_report_abs(dev, ABS_HANDBRAKE, bytes[10]);
+
+	if (attachment->wheel.shifter_type)
+		input_report_abs(dev, ABS_SHIFTER, (int8_t)bytes[12]);
+
+	if (attachment->vendor_type == GIP_VENDOR_LOGI_TRUE_FORCE_WHEEL && num_bytes >= 18) {
+		int dial = bytes[17] >> 5;
+
+		input_report_key(dev, BTN_THUMBL, bytes[17] & BIT(0));
+		input_report_key(dev, BTN_THUMBR, bytes[17] & BIT(1));
+		input_report_key(dev, KEY_KPPLUS, bytes[17] & BIT(2));
+		input_report_key(dev, KEY_KPMINUS, bytes[17] & BIT(3));
+		input_report_key(dev, KEY_KPENTER, bytes[17] & BIT(4));
+		if (dial == 0 && attachment->logi_dial_state == 7)
+			input_report_rel(dev, REL_DIAL, -1);
+		else if (dial == 7 && attachment->logi_dial_state == 0)
+			input_report_rel(dev, REL_DIAL, 1);
+		else
+			input_report_rel(dev, REL_DIAL,
+				attachment->logi_dial_state - dial);
+		attachment->logi_dial_state = dial;
+	}
+}
+
 static int gip_handle_ll_input_report(struct gip_attachment *attachment,
 	const struct gip_header *header, const uint8_t *bytes, int num_bytes)
 {
 	struct input_dev *dev = attachment->input;
 
-	if (!dev)
-		return -ENODEV;
+	if (!dev) {
+		if (attachment->attachment_type == GIP_TYPE_WHEEL) {
+			if (num_bytes < 17)
+				return -EINVAL;
+			attachment->wheel.max_gear = bytes[11] & 0x1F;
+			attachment->wheel.shifter_type = bytes[11] >> 5;
+			attachment->wheel.angle_setting = bytes[13];
+			attachment->wheel.angle_setting |= bytes[14] << 8;
+			attachment->wheel.connections = bytes[16];
+
+			if (attachment->quirks & GIP_QUIRK_WHEEL_FORCE_HANDBRAKE)
+				attachment->wheel.connections |= GIP_WHEEL_HANDBRAKE_CONN;
+
+			if (attachment->wheel.angle_setting && attachment->wheel.max_angle)
+				return gip_setup_input_device(attachment);
+			else
+				return 0;
+		} else {
+			return -ENODEV;
+		}
+	}
 
 	if (attachment->device_state != GIP_STATE_START) {
 		dev_dbg(GIP_DEV(attachment), "Discarding early input report\n");
@@ -2337,6 +2509,14 @@ static int gip_handle_ll_input_report(struct gip_attachment *attachment,
 	case GIP_TYPE_ARCADE_STICK:
 		gip_handle_arcade_stick_report(attachment, dev, bytes, num_bytes);
 		break;
+	case GIP_TYPE_WHEEL:
+		gip_handle_wheel_report(attachment, dev, bytes, num_bytes);
+		break;
+	}
+
+	if (attachment->quirks & GIP_QUIRK_FORCE_GAMEPAD_SB) {
+		input_report_key(dev, BTN_THUMBL, bytes[1] & BIT(6));
+		input_report_key(dev, BTN_THUMBR, bytes[1] & BIT(7));
 	}
 
 	if (attachment->features & GIP_FEATURE_ELITE_BUTTONS) {
@@ -2414,9 +2594,22 @@ static int gip_handle_ll_input_report(struct gip_attachment *attachment,
 static int gip_handle_ll_static_configuration(struct gip_attachment *attachment,
 	const struct gip_header *header, const uint8_t *bytes, int num_bytes)
 {
-	/* TODO */
-	dev_dbg(GIP_DEV(attachment), "Unimplemented Static Configuration message\n");
-	return -ENOTSUPP;
+	if (attachment->attachment_type == GIP_TYPE_WHEEL) {
+		if (num_bytes < 11)
+			return -EINVAL;
+		attachment->wheel.max_angle = BIT(bytes[0]) - 1;
+		attachment->wheel.max_throttle = BIT(bytes[1]) - 1;
+		attachment->wheel.max_brake = BIT(bytes[2]) - 1;
+		attachment->wheel.max_clutch = BIT(bytes[3]) - 1;
+		attachment->wheel.max_handbrake = BIT(bytes[4]) - 1;
+		if (attachment->wheel.angle_setting && attachment->wheel.max_angle)
+			return gip_setup_input_device(attachment);
+	} else {
+		/* TODO */
+		dev_dbg(GIP_DEV(attachment), "Unimplemented Static Configuration message\n");
+		return -ENOTSUPP;
+	}
+	return 0;
 }
 
 static int gip_handle_ll_button_info_report(struct gip_attachment *attachment,
-- 
2.51.0


  parent reply	other threads:[~2025-09-17  1:20 UTC|newest]

Thread overview: 7+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2025-09-17  1:19 [PATCH v2 0/5] Input: xbox_gip - Add new driver for Xbox GIP Vicki Pfau
2025-09-17  1:19 ` [PATCH v2 1/5] " Vicki Pfau
2025-09-17  1:19 ` [PATCH v2 2/5] Input: xpad - Remove Xbox One support Vicki Pfau
2025-09-17  1:19 ` [PATCH v2 3/5] Input: Add ABS_CLUTCH, HANDBRAKE, and SHIFTER Vicki Pfau
2025-09-17  1:19 ` [PATCH v2 4/5] HID: Map more automobile simulation inputs Vicki Pfau
2025-09-17  1:19 ` Vicki Pfau [this message]
2025-11-01 17:45 ` [PATCH v2 0/5] Input: xbox_gip - Add new driver for Xbox GIP Shengyu Qu

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20250917011937.1649481-6-vi@endrift.com \
    --to=vi@endrift.com \
    --cc=dmitry.torokhov@gmail.com \
    --cc=linux-input@vger.kernel.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).