The Linux Kernel Mailing List
 help / color / mirror / Atom feed
From: Denis Benato <denis.benato@linux.dev>
To: 'Jiri Kosina' <jikos@kernel.org>
Cc: 'Benjamin Tissoires' <bentiss@kernel.org>,
	'Dmitry Torokho v' <dmitry.torokhov@gmail.com>,
	linux-input@vger.kernel.org, linux-kernel@vger.kernel.org,
	Denis Benato <denis.benato@linux.dev>,
	Adolfo Rodrigues <adolfotregosa@gmail.com>
Subject: [PATCH 1/2] HID: flydigi: add support for vader 5 pro
Date: Fri,  7 Aug 2026 01:23:33 +0000	[thread overview]
Message-ID: <20260807012334.58737-2-denis.benato@linux.dev> (raw)
In-Reply-To: <20260807012334.58737-1-denis.benato@linux.dev>

The Flydigi Vader Pro 5 is a gamepad with additional buttons
and a gyroscope that is connected wireless to a PC via its
own dongle so it's always a USB device: add support for it
by following the enstablished pattern of spawning two evdevs
that share the same uniqid so that SDL can match the two and
expose them as a single controller.

Assisted-by: VSCode:gpt-5.3-codex
Tested-by: Adolfo Rodrigues <adolfotregosa@gmail.com>
Signed-off-by: Denis Benato <denis.benato@linux.dev>
---
 drivers/hid/Kconfig       |   8 +
 drivers/hid/Makefile      |   1 +
 drivers/hid/hid-flydigi.c | 887 ++++++++++++++++++++++++++++++++++++++
 drivers/hid/hid-ids.h     |   3 +
 4 files changed, 899 insertions(+)
 create mode 100644 drivers/hid/hid-flydigi.c

diff --git a/drivers/hid/Kconfig b/drivers/hid/Kconfig
index aa7fa11a0197..59afcdb7ac18 100644
--- a/drivers/hid/Kconfig
+++ b/drivers/hid/Kconfig
@@ -1205,6 +1205,14 @@ config STEAM_FF
 	Say Y here if you want to enable force feedback support for the Steam
 	Deck.
 
+config HID_FLYDIGI
+	tristate "Flydigi game controller support"
+	depends on USB_HID
+	select INPUT_FF_MEMLESS
+	help
+	Support for Flydigi game controllers that use the vendor-specific
+	HID protocol, including Vader 5 Pro.
+
 config HID_STEELSERIES
 	tristate "Steelseries devices support"
 	depends on USB_HID
diff --git a/drivers/hid/Makefile b/drivers/hid/Makefile
index 48a863b245ee..356c19b31585 100644
--- a/drivers/hid/Makefile
+++ b/drivers/hid/Makefile
@@ -56,6 +56,7 @@ obj-$(CONFIG_HID_FT260)		+= hid-ft260.o
 obj-$(CONFIG_HID_GEMBIRD)	+= hid-gembird.o
 obj-$(CONFIG_HID_GFRM)		+= hid-gfrm.o
 obj-$(CONFIG_HID_GLORIOUS)  += hid-glorious.o
+obj-$(CONFIG_HID_FLYDIGI)	+= hid-flydigi.o
 obj-$(CONFIG_HID_VIVALDI_COMMON) += hid-vivaldi-common.o
 obj-$(CONFIG_HID_GOODIX_SPI)	+= hid-goodix-spi.o
 obj-$(CONFIG_HID_GOOGLE_HAMMER)	+= hid-google-hammer.o
diff --git a/drivers/hid/hid-flydigi.c b/drivers/hid/hid-flydigi.c
new file mode 100644
index 000000000000..48129f429f3c
--- /dev/null
+++ b/drivers/hid/hid-flydigi.c
@@ -0,0 +1,887 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Flydigi Vader 5 Pro HID driver
+ *
+ * Exposes two evdev nodes with matching uniq values:
+ * - Gamepad with force feedback
+ * - Motion sensors (accelerometer + gyroscope)
+ *
+ * The protocol details and report layout are based on upstream SDL's
+ * Flydigi HID backend.
+ */
+
+#include <linux/cleanup.h>
+#include <linux/hid.h>
+#include <linux/input.h>
+#include <linux/jiffies.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/spinlock.h>
+#include <linux/unaligned.h>
+#include <linux/usb.h>
+#include <linux/workqueue.h>
+
+#include "hid-ids.h"
+
+#define FLYDIGI_REPORT_SIZE		32
+
+#define FLYDIGI_MAGIC1			0x5a
+#define FLYDIGI_MAGIC2			0xa5
+#define FLYDIGI_CMD_INFO		0x01
+#define FLYDIGI_CMD_STATUS		0x10
+#define FLYDIGI_CMD_STATUS_UPDATE	0x11
+#define FLYDIGI_CMD_HAPTIC		0x12
+#define FLYDIGI_CMD_ACQUIRE		0x1c
+#define FLYDIGI_CMD_INPUT		0xef
+
+#define FLYDIGI_ACQUIRE_PERIOD_MS	30000
+
+/* SDL identifies this class as 4096 counts per g and 2000 dps full-scale. */
+#define FLYDIGI_ACCEL_RES_PER_G	4096
+#define FLYDIGI_GYRO_RES_PER_DPS	16
+
+/* Protocol packet offsets (payload starts at magic byte 0x5a). */
+#define FLYDIGI_OFF_LX			3
+#define FLYDIGI_OFF_LY			5
+#define FLYDIGI_OFF_RX			7
+#define FLYDIGI_OFF_RY			9
+#define FLYDIGI_OFF_DPAD_ABXY		11
+#define FLYDIGI_OFF_MISC_BTNS		12
+#define FLYDIGI_OFF_EXTRA_BTNS		13
+#define FLYDIGI_OFF_SYSTEM_BTNS	14
+#define FLYDIGI_OFF_LT			15
+#define FLYDIGI_OFF_RT			16
+#define FLYDIGI_OFF_GYRO_X		17
+#define FLYDIGI_OFF_GYRO_Z		19
+#define FLYDIGI_OFF_GYRO_Y		21
+#define FLYDIGI_OFF_ACCEL_X		23
+#define FLYDIGI_OFF_ACCEL_Z		25
+#define FLYDIGI_OFF_ACCEL_Y		27
+
+struct flydigi_device {
+	struct hid_device *hdev;
+	struct input_dev *gamepad;
+	struct input_dev *sensors;
+
+	struct mutex output_mutex;
+	spinlock_t lock;
+	u8 output_buf[FLYDIGI_REPORT_SIZE];
+	u8 output_report_id;
+
+	struct work_struct rumble_work;
+	struct delayed_work acquire_work;
+
+	u16 strong;
+	u16 weak;
+	bool gamepad_open;
+	bool removed;
+};
+
+/**
+ * flydigi_send_output - send one vendor packet through the selected report
+ * @fd: controller state
+ * @payload: protocol bytes without the HID report ID prefix
+ * @payload_len: number of bytes in @payload
+ *
+ * Vader firmware variants expose either numbered or unnumbered output reports.
+ * Descriptor probing selects one report ID at probe time, and all later writes
+ * use that framing so the same packet builders work for both variants.
+ *
+ * Return: number of bytes transferred, or a negative error code.
+ */
+static int flydigi_send_output(struct flydigi_device *fd, const u8 *payload,
+			       size_t payload_len)
+{
+	u8 *buf = fd->output_buf;
+	size_t copy_len;
+	bool no_output_report;
+	int ret;
+
+	copy_len = min(payload_len, (size_t)FLYDIGI_REPORT_SIZE - 1);
+
+	no_output_report = !fd->hdev->ll_driver ||
+			   !fd->hdev->ll_driver->output_report;
+
+	{
+		guard(mutex)(&fd->output_mutex);
+
+		memset(buf, 0, FLYDIGI_REPORT_SIZE);
+		buf[0] = fd->output_report_id;
+		memcpy(&buf[1], payload, copy_len);
+		ret = hid_hw_output_report(fd->hdev, buf, FLYDIGI_REPORT_SIZE);
+		if (ret == -EOPNOTSUPP || (ret < 0 && no_output_report)) {
+			ret = hid_hw_raw_request(fd->hdev, fd->output_report_id, buf,
+						 FLYDIGI_REPORT_SIZE, HID_OUTPUT_REPORT,
+						 HID_REQ_SET_REPORT);
+		}
+	}
+
+	if (ret < 0 && ret != -ENODEV)
+		hid_dbg(fd->hdev, "output command failed: %d\n", ret);
+
+	return ret;
+}
+
+static bool flydigi_desc_has_seq(const u8 *rdesc, unsigned int rsize,
+				 const u8 *seq, size_t seq_size)
+{
+	size_t i;
+
+	if (!rdesc || !rsize || !seq || !seq_size || rsize < seq_size)
+		return false;
+
+	for (i = 0; i <= rsize - seq_size; i++) {
+		if (!memcmp(&rdesc[i], seq, seq_size))
+			return true;
+	}
+
+	return false;
+}
+
+/**
+ * flydigi_detect_protocol_descriptor - identify protocol interface layout
+ * @hdev: HID device being probed
+ * @output_report_id: resolved output report ID for protocol writes
+ *
+ * The controller exports multiple interfaces and not all of them transport the
+ * extended Flydigi protocol. We key off descriptor signatures instead of fixed
+ * interface numbers so the driver keeps working across firmware revisions.
+ *
+ * Return: true when the interface looks like the Flydigi protocol endpoint.
+ */
+static bool flydigi_detect_protocol_descriptor(struct hid_device *hdev,
+					      u8 *output_report_id)
+{
+	const u8 *rdesc = hdev->dev_rdesc;
+	unsigned int rsize = hdev->dev_rsize;
+	static const u8 usage_ffa0[] = { 0x06, 0xa0, 0xff };
+	static const u8 report_id_21[] = { 0x85, 0x21, 0x95, 0x1f };
+	static const u8 report_32_in[] = { 0x95, 0x20, 0x81 };
+	static const u8 report_32_out[] = { 0x95, 0x20, 0x91 };
+
+	if (!flydigi_desc_has_seq(rdesc, rsize, usage_ffa0, sizeof(usage_ffa0)))
+		return false;
+
+	if (flydigi_desc_has_seq(rdesc, rsize, report_id_21,
+				 sizeof(report_id_21))) {
+		*output_report_id = 0x21;
+		return true;
+	}
+
+	if (flydigi_desc_has_seq(rdesc, rsize, report_32_in,
+				 sizeof(report_32_in)) &&
+	    flydigi_desc_has_seq(rdesc, rsize, report_32_out,
+				 sizeof(report_32_out))) {
+		*output_report_id = 0x00;
+		return true;
+	}
+
+	return false;
+}
+
+static int flydigi_get_interface_number(struct hid_device *hdev)
+{
+	struct usb_interface *intf;
+	struct usb_host_interface *alt;
+
+	if (!hid_is_usb(hdev))
+		return -ENODEV;
+
+	intf = to_usb_interface(hdev->dev.parent);
+	if (!intf)
+		return -ENODEV;
+
+	alt = intf->cur_altsetting;
+	if (!alt)
+		return -ENODEV;
+
+	return alt->desc.bInterfaceNumber;
+}
+
+/**
+ * flydigi_send_acquire - request ownership of the controller protocol stream
+ * @fd: controller state
+ *
+ * The firmware requires an explicit acquire token and periodic refreshes before
+ * delivering full input and motion data.
+ *
+ * Return: number of bytes transferred, or a negative error code.
+ */
+static int flydigi_send_acquire(struct flydigi_device *fd)
+{
+	const u8 payload[31] = {
+		FLYDIGI_MAGIC1,
+		FLYDIGI_MAGIC2,
+		FLYDIGI_CMD_ACQUIRE,
+		23,
+		1,
+		'S', 'D', 'L'
+	};
+
+	return flydigi_send_output(fd, payload, sizeof(payload));
+}
+
+static int flydigi_send_info_request(struct flydigi_device *fd)
+{
+	const u8 payload[] = {
+		FLYDIGI_MAGIC1,
+		FLYDIGI_MAGIC2,
+		FLYDIGI_CMD_INFO,
+		2,
+		0,
+	};
+
+	return flydigi_send_output(fd, payload, sizeof(payload));
+}
+
+static int flydigi_send_status_request(struct flydigi_device *fd)
+{
+	const u8 payload[] = {
+		FLYDIGI_MAGIC1,
+		FLYDIGI_MAGIC2,
+		FLYDIGI_CMD_STATUS,
+	};
+
+	return flydigi_send_output(fd, payload, sizeof(payload));
+}
+
+static int flydigi_send_rumble(struct flydigi_device *fd, u8 low, u8 high)
+{
+	const u8 payload[] = {
+		FLYDIGI_MAGIC1,
+		FLYDIGI_MAGIC2,
+		FLYDIGI_CMD_HAPTIC,
+		6,
+		low,
+		high,
+		0,
+		0,
+		0,
+	};
+
+	return flydigi_send_output(fd, payload, sizeof(payload));
+}
+
+static void flydigi_rumble_worker(struct work_struct *work)
+{
+	struct flydigi_device *fd = container_of(work, struct flydigi_device,
+						 rumble_work);
+	u16 strong;
+	u16 weak;
+	bool removed;
+
+	{
+		guard(spinlock_irqsave)(&fd->lock);
+
+		strong = fd->strong;
+		weak = fd->weak;
+		removed = fd->removed;
+	}
+
+	if (removed)
+		return;
+
+	flydigi_send_rumble(fd, strong >> 8, weak >> 8);
+}
+
+/**
+ * flydigi_acquire_worker - keep the protocol session alive while opened
+ * @work: delayed work item
+ *
+ * The device times out its acquired state. Refreshing acquire/info/status keeps
+ * the stream active and prevents stale state after userspace idles briefly.
+ */
+static void flydigi_acquire_worker(struct work_struct *work)
+{
+	struct flydigi_device *fd = container_of(to_delayed_work(work),
+						 struct flydigi_device,
+						 acquire_work);
+	bool open;
+	bool removed;
+
+	{
+		guard(spinlock_irqsave)(&fd->lock);
+
+		open = fd->gamepad_open;
+		removed = fd->removed;
+	}
+
+	if (removed || !open)
+		return;
+
+	flydigi_send_acquire(fd);
+	flydigi_send_info_request(fd);
+	flydigi_send_status_request(fd);
+	schedule_delayed_work(&fd->acquire_work,
+			      msecs_to_jiffies(FLYDIGI_ACQUIRE_PERIOD_MS));
+}
+
+static int flydigi_play_effect(struct input_dev *dev, void *data,
+			      struct ff_effect *effect)
+{
+	struct flydigi_device *fd = input_get_drvdata(dev);
+
+	if (effect->type != FF_RUMBLE)
+		return 0;
+
+	{
+		guard(spinlock_irqsave)(&fd->lock);
+
+		if (!fd->removed) {
+			fd->strong = effect->u.rumble.strong_magnitude;
+			fd->weak = effect->u.rumble.weak_magnitude;
+			schedule_work(&fd->rumble_work);
+		}
+	}
+
+	return 0;
+}
+
+static int flydigi_gamepad_open(struct input_dev *dev)
+{
+	struct flydigi_device *fd = input_get_drvdata(dev);
+
+	{
+		guard(spinlock_irqsave)(&fd->lock);
+
+		fd->gamepad_open = true;
+	}
+
+	schedule_delayed_work(&fd->acquire_work, 0);
+
+	return 0;
+}
+
+static void flydigi_gamepad_close(struct input_dev *dev)
+{
+	struct flydigi_device *fd = input_get_drvdata(dev);
+
+	cancel_delayed_work_sync(&fd->acquire_work);
+
+	{
+		guard(spinlock_irqsave)(&fd->lock);
+
+		fd->gamepad_open = false;
+		fd->strong = 0;
+		fd->weak = 0;
+	}
+
+	schedule_work(&fd->rumble_work);
+}
+
+static void flydigi_map_hat(u8 value, int *x, int *y)
+{
+	switch (value & 0x0f) {
+	case 0x01:
+		*x = 0;
+		*y = -1;
+		break;
+	case 0x03:
+		*x = 1;
+		*y = -1;
+		break;
+	case 0x02:
+		*x = 1;
+		*y = 0;
+		break;
+	case 0x06:
+		*x = 1;
+		*y = 1;
+		break;
+	case 0x04:
+		*x = 0;
+		*y = 1;
+		break;
+	case 0x0c:
+		*x = -1;
+		*y = 1;
+		break;
+	case 0x08:
+		*x = -1;
+		*y = 0;
+		break;
+	case 0x09:
+		*x = -1;
+		*y = -1;
+		break;
+	default:
+		*x = 0;
+		*y = 0;
+		break;
+	}
+}
+
+/**
+ * flydigi_report_gamepad - translate one protocol input packet to evdev keys
+ * @fd: controller state
+ * @data: protocol packet starting at magic byte 0x5a
+ *
+ * Keep this mapping aligned with SDL's Flydigi backend so userspace sees the
+ * same logical layout across hidraw and evdev paths.
+ */
+static void flydigi_report_gamepad(struct flydigi_device *fd, const u8 *data)
+{
+	struct input_dev *gamepad = fd->gamepad;
+	s16 axis;
+	int hat_x;
+	int hat_y;
+
+	flydigi_map_hat(data[FLYDIGI_OFF_DPAD_ABXY], &hat_x, &hat_y);
+	input_report_abs(gamepad, ABS_HAT0X, hat_x);
+	input_report_abs(gamepad, ABS_HAT0Y, hat_y);
+
+	input_report_key(gamepad, BTN_SOUTH,
+			 data[FLYDIGI_OFF_DPAD_ABXY] & BIT(4));
+	input_report_key(gamepad, BTN_EAST,
+			 data[FLYDIGI_OFF_DPAD_ABXY] & BIT(5));
+	/*
+	 * Keep BTN_WEST/BTN_NORTH aligned with printed X/Y legends used by
+	 * applications. On-wire encoding for X/Y is swapped on this model.
+	 */
+	input_report_key(gamepad, BTN_WEST,
+			 data[FLYDIGI_OFF_MISC_BTNS] & BIT(0));
+	input_report_key(gamepad, BTN_NORTH,
+			 data[FLYDIGI_OFF_DPAD_ABXY] & BIT(7));
+	input_report_key(gamepad, BTN_SELECT,
+			 data[FLYDIGI_OFF_DPAD_ABXY] & BIT(6));
+	input_report_key(gamepad, BTN_START,
+			 data[FLYDIGI_OFF_MISC_BTNS] & BIT(1));
+	input_report_key(gamepad, BTN_TL,
+			 data[FLYDIGI_OFF_MISC_BTNS] & BIT(2));
+	input_report_key(gamepad, BTN_TR,
+			 data[FLYDIGI_OFF_MISC_BTNS] & BIT(3));
+	input_report_key(gamepad, BTN_THUMBL,
+			 data[FLYDIGI_OFF_MISC_BTNS] & BIT(6));
+	input_report_key(gamepad, BTN_THUMBR,
+			 data[FLYDIGI_OFF_MISC_BTNS] & BIT(7));
+
+	input_report_key(gamepad, BTN_MODE,
+			 data[FLYDIGI_OFF_SYSTEM_BTNS] & BIT(3));
+
+	/*
+	 * SDL's generic Linux evdev mapper recognizes paddles specifically on
+	 * BTN_TRIGGER_HAPPY5..8, so keep M1..M4 on that range for Steam/SDL.
+	 */
+	input_report_key(gamepad, BTN_TRIGGER_HAPPY1,
+			 data[FLYDIGI_OFF_EXTRA_BTNS] & BIT(0)); /* C */
+	input_report_key(gamepad, BTN_TRIGGER_HAPPY2,
+			 data[FLYDIGI_OFF_EXTRA_BTNS] & BIT(1)); /* Z */
+	input_report_key(gamepad, BTN_TRIGGER_HAPPY5,
+			 data[FLYDIGI_OFF_EXTRA_BTNS] & BIT(2)); /* M1 */
+	input_report_key(gamepad, BTN_TRIGGER_HAPPY6,
+			 data[FLYDIGI_OFF_EXTRA_BTNS] & BIT(3)); /* M2 */
+	input_report_key(gamepad, BTN_TRIGGER_HAPPY7,
+			 data[FLYDIGI_OFF_EXTRA_BTNS] & BIT(4)); /* M3 */
+	input_report_key(gamepad, BTN_TRIGGER_HAPPY8,
+			 data[FLYDIGI_OFF_EXTRA_BTNS] & BIT(5)); /* M4 */
+	input_report_key(gamepad, BTN_TRIGGER_HAPPY9,
+			 data[FLYDIGI_OFF_EXTRA_BTNS] & BIT(6)); /* LM */
+	input_report_key(gamepad, BTN_TRIGGER_HAPPY10,
+			 data[FLYDIGI_OFF_EXTRA_BTNS] & BIT(7)); /* RM */
+	input_report_key(gamepad, BTN_TRIGGER_HAPPY11,
+			 data[FLYDIGI_OFF_SYSTEM_BTNS] & BIT(0)); /* Circle */
+
+	axis = (s16)get_unaligned_le16(&data[FLYDIGI_OFF_LX]);
+	input_report_abs(gamepad, ABS_X, axis);
+
+	axis = -(s16)get_unaligned_le16(&data[FLYDIGI_OFF_LY]);
+	if (axis <= -32768)
+		axis = 32767;
+	input_report_abs(gamepad, ABS_Y, axis);
+
+	axis = (s16)get_unaligned_le16(&data[FLYDIGI_OFF_RX]);
+	input_report_abs(gamepad, ABS_RX, axis);
+
+	axis = -(s16)get_unaligned_le16(&data[FLYDIGI_OFF_RY]);
+	if (axis <= -32768)
+		axis = 32767;
+	input_report_abs(gamepad, ABS_RY, axis);
+
+	input_report_abs(gamepad, ABS_Z,
+			 ((int)data[FLYDIGI_OFF_LT] * 257) - 32768);
+	input_report_abs(gamepad, ABS_RZ,
+			 ((int)data[FLYDIGI_OFF_RT] * 257) - 32768);
+
+	input_sync(gamepad);
+}
+
+/**
+ * flydigi_report_sensors - publish IMU samples from one protocol packet
+ * @fd: controller state
+ * @data: protocol packet starting at magic byte 0x5a
+ *
+ * Motion data is exposed as a dedicated evdev node so userspace can consume
+ * gyro/accelerometer data independently of gamepad button polling.
+ */
+static void flydigi_report_sensors(struct flydigi_device *fd, const u8 *data)
+{
+	struct input_dev *sensors = fd->sensors;
+
+	if (!sensors)
+		return;
+
+	input_event(sensors, EV_MSC, MSC_TIMESTAMP,
+		    ktime_to_us(ktime_get_boottime()));
+
+	/* Accelerometer */
+	input_report_abs(sensors, ABS_X,
+			 (s16)get_unaligned_le16(&data[FLYDIGI_OFF_ACCEL_X]));
+	input_report_abs(sensors, ABS_Y,
+			 (s16)get_unaligned_le16(&data[FLYDIGI_OFF_ACCEL_Y]));
+	input_report_abs(sensors, ABS_Z,
+			 -(s16)get_unaligned_le16(&data[FLYDIGI_OFF_ACCEL_Z]));
+
+	/* Gyroscope */
+	input_report_abs(sensors, ABS_RX,
+			 (s16)get_unaligned_le16(&data[FLYDIGI_OFF_GYRO_X]));
+	input_report_abs(sensors, ABS_RY,
+			 (s16)get_unaligned_le16(&data[FLYDIGI_OFF_GYRO_Y]));
+	input_report_abs(sensors, ABS_RZ,
+			 -(s16)get_unaligned_le16(&data[FLYDIGI_OFF_GYRO_Z]));
+
+	input_sync(sensors);
+}
+
+/**
+ * flydigi_raw_event - parse vendor packets delivered by HID core
+ * @hdev: HID device
+ * @report: HID report metadata
+ * @data: raw bytes from device
+ * @size: number of bytes in @data
+ *
+ * The transport may prepend a synthetic report ID byte. Strip it when present,
+ * then dispatch by vendor command so gamepad and sensor state stay in sync.
+ *
+ * Return: always 0 so hidraw still sees the original traffic.
+ */
+static int flydigi_raw_event(struct hid_device *hdev, struct hid_report *report,
+			     u8 *data, int size)
+{
+	struct flydigi_device *fd = hid_get_drvdata(hdev);
+	const u8 *payload = data;
+	int len = size;
+	(void)report;
+
+	if (!fd || size <= 0)
+		return 0;
+
+	if (payload[0] != FLYDIGI_MAGIC1) {
+		payload++;
+		len--;
+	}
+
+	if (len < 31)
+		return 0;
+	if (payload[0] != FLYDIGI_MAGIC1 || payload[1] != FLYDIGI_MAGIC2) {
+		hid_dbg(hdev,
+			"non-protocol packet ignored: sz=%d b0=%02x b1=%02x b2=%02x\n",
+			len, payload[0], payload[1], payload[2]);
+		return 0;
+	}
+
+	switch (payload[2]) {
+	case FLYDIGI_CMD_INPUT:
+		flydigi_report_gamepad(fd, payload);
+		flydigi_report_sensors(fd, payload);
+		break;
+	case FLYDIGI_CMD_INFO:
+	case FLYDIGI_CMD_STATUS:
+	case FLYDIGI_CMD_ACQUIRE:
+	case FLYDIGI_CMD_HAPTIC:
+		/* Expected replies/acks to commands sent by this driver. */
+		break;
+	case FLYDIGI_CMD_STATUS_UPDATE:
+		flydigi_send_status_request(fd);
+		schedule_delayed_work(&fd->acquire_work, 0);
+		break;
+	default:
+		hid_dbg(hdev, "unhandled command: cmd=%02x sz=%d\n",
+			payload[2], len);
+		break;
+	}
+
+	return 0;
+}
+
+static int flydigi_register_gamepad(struct flydigi_device *fd)
+{
+	struct hid_device *hdev = fd->hdev;
+	struct input_dev *gamepad;
+	int ret;
+
+	gamepad = devm_input_allocate_device(&hdev->dev);
+	if (!gamepad)
+		return -ENOMEM;
+
+	gamepad->name = hdev->name;
+	gamepad->phys = hdev->phys;
+	gamepad->uniq = hdev->uniq;
+	gamepad->id.bustype = hdev->bus;
+	gamepad->id.vendor = hdev->vendor;
+	gamepad->id.product = hdev->product;
+	gamepad->id.version = hdev->version;
+	gamepad->open = flydigi_gamepad_open;
+	gamepad->close = flydigi_gamepad_close;
+	input_set_drvdata(gamepad, fd);
+
+	input_set_capability(gamepad, EV_KEY, BTN_SOUTH);
+	input_set_capability(gamepad, EV_KEY, BTN_EAST);
+	input_set_capability(gamepad, EV_KEY, BTN_WEST);
+	input_set_capability(gamepad, EV_KEY, BTN_NORTH);
+	input_set_capability(gamepad, EV_KEY, BTN_SELECT);
+	input_set_capability(gamepad, EV_KEY, BTN_START);
+	input_set_capability(gamepad, EV_KEY, BTN_MODE);
+	input_set_capability(gamepad, EV_KEY, BTN_TL);
+	input_set_capability(gamepad, EV_KEY, BTN_TR);
+	input_set_capability(gamepad, EV_KEY, BTN_THUMBL);
+	input_set_capability(gamepad, EV_KEY, BTN_THUMBR);
+
+	input_set_capability(gamepad, EV_KEY, BTN_TRIGGER_HAPPY1);
+	input_set_capability(gamepad, EV_KEY, BTN_TRIGGER_HAPPY2);
+	input_set_capability(gamepad, EV_KEY, BTN_TRIGGER_HAPPY5);
+	input_set_capability(gamepad, EV_KEY, BTN_TRIGGER_HAPPY6);
+	input_set_capability(gamepad, EV_KEY, BTN_TRIGGER_HAPPY7);
+	input_set_capability(gamepad, EV_KEY, BTN_TRIGGER_HAPPY8);
+	input_set_capability(gamepad, EV_KEY, BTN_TRIGGER_HAPPY9);
+	input_set_capability(gamepad, EV_KEY, BTN_TRIGGER_HAPPY10);
+	input_set_capability(gamepad, EV_KEY, BTN_TRIGGER_HAPPY11);
+
+	input_set_abs_params(gamepad, ABS_X, -32768, 32767, 0, 0);
+	input_set_abs_params(gamepad, ABS_Y, -32768, 32767, 0, 0);
+	input_set_abs_params(gamepad, ABS_RX, -32768, 32767, 0, 0);
+	input_set_abs_params(gamepad, ABS_RY, -32768, 32767, 0, 0);
+	input_set_abs_params(gamepad, ABS_Z, -32768, 32767, 0, 0);
+	input_set_abs_params(gamepad, ABS_RZ, -32768, 32767, 0, 0);
+	input_set_abs_params(gamepad, ABS_HAT0X, -1, 1, 0, 0);
+	input_set_abs_params(gamepad, ABS_HAT0Y, -1, 1, 0, 0);
+
+	input_set_capability(gamepad, EV_FF, FF_RUMBLE);
+	ret = input_ff_create_memless(gamepad, NULL, flydigi_play_effect);
+	if (ret)
+		return ret;
+
+	ret = input_register_device(gamepad);
+	if (ret)
+		return ret;
+
+	fd->gamepad = gamepad;
+	return 0;
+}
+
+static int flydigi_register_sensors(struct flydigi_device *fd)
+{
+	struct hid_device *hdev = fd->hdev;
+	struct input_dev *sensors;
+	char *name;
+	int ret;
+
+	sensors = devm_input_allocate_device(&hdev->dev);
+	if (!sensors)
+		return -ENOMEM;
+
+	name = devm_kasprintf(&hdev->dev, GFP_KERNEL, "%s Motion Sensors",
+			      hdev->name);
+	if (!name)
+		return -ENOMEM;
+
+	sensors->name = name;
+	/* Matching phys/uniq helps userspace pair gamepad and IMU endpoints. */
+	sensors->phys = hdev->phys;
+	sensors->uniq = hdev->uniq;
+	sensors->id.bustype = hdev->bus;
+	sensors->id.vendor = hdev->vendor;
+	sensors->id.product = hdev->product;
+	sensors->id.version = hdev->version;
+	input_set_drvdata(sensors, fd);
+
+	__set_bit(INPUT_PROP_ACCELEROMETER, sensors->propbit);
+	__set_bit(EV_MSC, sensors->evbit);
+	__set_bit(MSC_TIMESTAMP, sensors->mscbit);
+
+	input_set_abs_params(sensors, ABS_X, -32768, 32767, 16, 0);
+	input_set_abs_params(sensors, ABS_Y, -32768, 32767, 16, 0);
+	input_set_abs_params(sensors, ABS_Z, -32768, 32767, 16, 0);
+	input_abs_set_res(sensors, ABS_X, FLYDIGI_ACCEL_RES_PER_G);
+	input_abs_set_res(sensors, ABS_Y, FLYDIGI_ACCEL_RES_PER_G);
+	input_abs_set_res(sensors, ABS_Z, FLYDIGI_ACCEL_RES_PER_G);
+
+	input_set_abs_params(sensors, ABS_RX, -32768, 32767, 16, 0);
+	input_set_abs_params(sensors, ABS_RY, -32768, 32767, 16, 0);
+	input_set_abs_params(sensors, ABS_RZ, -32768, 32767, 16, 0);
+	input_abs_set_res(sensors, ABS_RX, FLYDIGI_GYRO_RES_PER_DPS);
+	input_abs_set_res(sensors, ABS_RY, FLYDIGI_GYRO_RES_PER_DPS);
+	input_abs_set_res(sensors, ABS_RZ, FLYDIGI_GYRO_RES_PER_DPS);
+
+	ret = input_register_device(sensors);
+	if (ret)
+		return ret;
+
+	fd->sensors = sensors;
+	return 0;
+}
+
+/**
+ * flydigi_init_uniq - provide a stable unique string for endpoint pairing
+ * @hdev: HID device
+ *
+ * Userspace often merges controller and sensor nodes only when uniq matches.
+ * Populate uniq early when firmware does not provide one.
+ */
+static void flydigi_init_uniq(struct hid_device *hdev)
+{
+	struct usb_interface *intf;
+	struct usb_device *udev;
+	char path[64];
+	int ret;
+
+	/*
+	 * Some Flydigi units expose a static USB serial string, which is not
+	 * unique across controllers. Use the USB topology path so userspace can
+	 * pair gamepad and sensor nodes per physical device.
+	 */
+	if (hid_is_usb(hdev)) {
+		intf = to_usb_interface(hdev->dev.parent);
+		if (intf) {
+			udev = interface_to_usbdev(intf);
+			if (udev) {
+				ret = usb_make_path(udev, path, sizeof(path));
+				if (ret > 0) {
+					strscpy(hdev->uniq, path, sizeof(hdev->uniq));
+					return;
+				}
+			}
+		}
+	}
+
+	if (hdev->uniq[0])
+		return;
+
+	if (hdev->phys[0]) {
+		strscpy(hdev->uniq, hdev->phys, sizeof(hdev->uniq));
+		return;
+	}
+
+	snprintf(hdev->uniq, sizeof(hdev->uniq), "%04x:%04x",
+		 hdev->vendor, hdev->product);
+}
+
+static int flydigi_probe(struct hid_device *hdev, const struct hid_device_id *id)
+{
+	struct flydigi_device *fd;
+	u8 output_report_id = 0;
+	int ifnum;
+	int ret;
+
+	ifnum = flydigi_get_interface_number(hdev);
+	if (ifnum < 0)
+		return -ENODEV;
+
+	if (!flydigi_detect_protocol_descriptor(hdev, &output_report_id))
+		return -ENODEV;
+
+	hid_dbg(hdev, "binding protocol interface %d (report id 0x%02x)\n",
+		ifnum, output_report_id);
+
+	ret = hid_parse(hdev);
+	if (ret) {
+		hid_err(hdev, "parse failed: %d\n", ret);
+		return ret;
+	}
+
+	fd = devm_kzalloc(&hdev->dev, sizeof(*fd), GFP_KERNEL);
+	if (!fd)
+		return -ENOMEM;
+
+	fd->hdev = hdev;
+	fd->output_report_id = output_report_id;
+	hid_set_drvdata(hdev, fd);
+
+	spin_lock_init(&fd->lock);
+	mutex_init(&fd->output_mutex);
+	INIT_WORK(&fd->rumble_work, flydigi_rumble_worker);
+	INIT_DELAYED_WORK(&fd->acquire_work, flydigi_acquire_worker);
+
+	flydigi_init_uniq(hdev);
+
+	ret = hid_hw_start(hdev, HID_CONNECT_HIDRAW);
+	if (ret) {
+		hid_err(hdev, "hw start failed: %d\n", ret);
+		return ret;
+	}
+
+	ret = hid_hw_open(hdev);
+	if (ret) {
+		hid_err(hdev, "hw open failed: %d\n", ret);
+		hid_hw_stop(hdev);
+		return ret;
+	}
+
+	ret = flydigi_register_gamepad(fd);
+	if (ret)
+		goto err_close;
+
+	ret = flydigi_register_sensors(fd);
+	if (ret)
+		goto err_close;
+
+	/*
+	 * Prime protocol state so input/IMU packets start flowing immediately.
+	 * Best-effort: some devices transiently NAK these while initialization
+	 * settles, and the periodic acquire worker will retry shortly.
+	 */
+	(void)flydigi_send_info_request(fd);
+	(void)flydigi_send_status_request(fd);
+	(void)flydigi_send_acquire(fd);
+
+	return 0;
+
+err_close:
+	hid_hw_close(hdev);
+	hid_hw_stop(hdev);
+	cancel_delayed_work_sync(&fd->acquire_work);
+	cancel_work_sync(&fd->rumble_work);
+	mutex_destroy(&fd->output_mutex);
+	return ret;
+}
+
+static void flydigi_remove(struct hid_device *hdev)
+{
+	struct flydigi_device *fd = hid_get_drvdata(hdev);
+
+	{
+		guard(spinlock_irqsave)(&fd->lock);
+
+		fd->removed = true;
+		fd->strong = 0;
+		fd->weak = 0;
+	}
+
+	cancel_delayed_work_sync(&fd->acquire_work);
+	cancel_work_sync(&fd->rumble_work);
+
+	/* Stop motors on teardown to avoid controllers that keep last FF state. */
+	flydigi_send_rumble(fd, 0, 0);
+
+	hid_hw_close(hdev);
+	hid_hw_stop(hdev);
+	mutex_destroy(&fd->output_mutex);
+}
+
+static const struct hid_device_id flydigi_devices[] = {
+	{ HID_USB_DEVICE(USB_VENDOR_ID_FLYDIGI, USB_DEVICE_ID_FLYDIGI_VADER5) },
+	{ }
+};
+MODULE_DEVICE_TABLE(hid, flydigi_devices);
+
+static struct hid_driver flydigi_driver = {
+	.name = "flydigi",
+	.id_table = flydigi_devices,
+	.probe = flydigi_probe,
+	.remove = flydigi_remove,
+	.raw_event = flydigi_raw_event,
+};
+module_hid_driver(flydigi_driver);
+
+MODULE_AUTHOR("Denis Benato <denis.benato@linux.dev>");
+MODULE_DESCRIPTION("Flydigi HID driver");
+MODULE_LICENSE("GPL");
diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h
index 19ab36489a60..9f151074814a 100644
--- a/drivers/hid/hid-ids.h
+++ b/drivers/hid/hid-ids.h
@@ -515,6 +515,9 @@
 #define USB_VENDOR_ID_FLATFROG		0x25b5
 #define USB_DEVICE_ID_MULTITOUCH_3200	0x0002
 
+#define USB_VENDOR_ID_FLYDIGI		0x37d7
+#define USB_DEVICE_ID_FLYDIGI_VADER5	0x2401
+
 #define USB_VENDOR_ID_FUTABA            0x0547
 #define USB_DEVICE_ID_LED_DISPLAY       0x7000
 
-- 
2.47.3


  reply	other threads:[~2026-08-07  1:23 UTC|newest]

Thread overview: 3+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-08-07  1:23 [PATCH 0/2] HID: flydigi: add support for vader 5 pro Denis Benato
2026-08-07  1:23 ` Denis Benato [this message]
2026-08-07  1:23 ` [PATCH 2/2] Input: xpad - introduce a static table to ignore devices Denis Benato

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=20260807012334.58737-2-denis.benato@linux.dev \
    --to=denis.benato@linux.dev \
    --cc=adolfotregosa@gmail.com \
    --cc=bentiss@kernel.org \
    --cc=dmitry.torokhov@gmail.com \
    --cc=jikos@kernel.org \
    --cc=linux-input@vger.kernel.org \
    --cc=linux-kernel@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