* [PATCH 0/2] HID: flydigi: add support for vader 5 pro
@ 2026-08-07 1:23 Denis Benato
2026-08-07 1:23 ` [PATCH 1/2] " Denis Benato
2026-08-07 1:23 ` [PATCH 2/2] Input: xpad - introduce a static table to ignore devices Denis Benato
0 siblings, 2 replies; 3+ messages in thread
From: Denis Benato @ 2026-08-07 1:23 UTC (permalink / raw)
To: 'Jiri Kosina'
Cc: 'Benjamin Tissoires', 'Dmitry Torokho v',
linux-input, linux-kernel, Denis Benato
Hi all,
A few days ago I was contacted by a friend of mine who was having his
share of issues with his Flydigi Vader 5 Pro controller: specifically
the rumble didn't work correctly, alongside other issues that were
partially solved by using the linked project.
However at the current time his main interest is in Steam support
and the driver was working in everything but Steam, which insists
on using the controller in hid mode rather than evdev mode
by default, and when in evdev mode it is disregarding additional
buttons and the gyroscope.
So we ended up with a perfectly working driver for the controller,
that at the cost of gyroscope support and back buttons has
rumble working correctly in Steam.
Since the driver works in basically everything that is not Steam,
including the browser, I decided to submit it anyway as someone
else will surely be interested in using some other way that is not
the aforementioned program.
To make optimal use of this device the xpad driver should not
bind to the controller, and this is the case for more than just
this controller as Derek told me, so I added some basic infrastructure
to use the best available driver for the device.
I also want to thank the original authors that did the work
of implementing the controller the first time (in SDL).
Best regards,
Denis Benato
Link: https://github.com/DuncanTPerkins/vader5-pro-linux-fix
Denis Benato (2):
HID: flydigi: add support for vader 5 pro
Input: xpad - introduce a static table to ignore devices
drivers/hid/Kconfig | 8 +
drivers/hid/Makefile | 1 +
drivers/hid/hid-flydigi.c | 887 ++++++++++++++++++++++++++++++++++
drivers/hid/hid-ids.h | 3 +
drivers/input/joystick/xpad.c | 24 +
5 files changed, 923 insertions(+)
create mode 100644 drivers/hid/hid-flydigi.c
--
2.47.3
^ permalink raw reply [flat|nested] 3+ messages in thread
* [PATCH 1/2] HID: flydigi: add support for vader 5 pro
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
2026-08-07 1:23 ` [PATCH 2/2] Input: xpad - introduce a static table to ignore devices Denis Benato
1 sibling, 0 replies; 3+ messages in thread
From: Denis Benato @ 2026-08-07 1:23 UTC (permalink / raw)
To: 'Jiri Kosina'
Cc: 'Benjamin Tissoires', 'Dmitry Torokho v',
linux-input, linux-kernel, Denis Benato, Adolfo Rodrigues
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
^ permalink raw reply related [flat|nested] 3+ messages in thread
* [PATCH 2/2] Input: xpad - introduce a static table to ignore devices
2026-08-07 1:23 [PATCH 0/2] HID: flydigi: add support for vader 5 pro Denis Benato
2026-08-07 1:23 ` [PATCH 1/2] " Denis Benato
@ 2026-08-07 1:23 ` Denis Benato
1 sibling, 0 replies; 3+ messages in thread
From: Denis Benato @ 2026-08-07 1:23 UTC (permalink / raw)
To: 'Jiri Kosina'
Cc: 'Benjamin Tissoires', 'Dmitry Torokho v',
linux-input, linux-kernel, Denis Benato, Derek J. Clark
Certain devices can work both as HID and as an xpad device: binding both
would duplicate inputs causing userspace to receive double inputs; at the
same time if a HID device is available it is to be preferred as it can
support additional features such as integrated IMU and additional buttons
and more, depending on the specific driver, therefore allow only the
best driver available to bind the device.
Suggested-by: Derek J. Clark <derekjohn.clark@gmail.com>
Signed-off-by: Denis Benato <denis.benato@linux.dev>
---
drivers/input/joystick/xpad.c | 24 ++++++++++++++++++++++++
1 file changed, 24 insertions(+)
diff --git a/drivers/input/joystick/xpad.c b/drivers/input/joystick/xpad.c
index feb8f368f834..545119a4fb36 100644
--- a/drivers/input/joystick/xpad.c
+++ b/drivers/input/joystick/xpad.c
@@ -406,6 +406,18 @@ static const struct xpad_device {
{ 0x0000, 0x0000, "Generic X-Box pad", 0, XTYPE_UNKNOWN }
};
+#define XPAD_SUBSTITUTE(_vid, _pid, _enabled) \
+ { .vid = (_vid), .pid = (_pid), .enabled = (_enabled) }
+
+static const struct xpad_excluded_device {
+ u16 vid;
+ u16 pid;
+ bool enabled;
+} xpad_excluded_devices[] = {
+ XPAD_SUBSTITUTE(0x37d7, 0x2401, IS_ENABLED(CONFIG_HID_FLYDIGI)),
+ { }
+};
+
/* buttons shared with xbox and xbox360 */
static const signed short xpad_common_btn[] = {
BTN_A, BTN_B, BTN_X, BTN_Y, /* "analog" buttons */
@@ -2044,11 +2056,23 @@ static int xpad_probe(struct usb_interface *intf, const struct usb_device_id *id
struct usb_device *udev = interface_to_usbdev(intf);
struct usb_xpad *xpad;
struct usb_endpoint_descriptor *ep_irq_in, *ep_irq_out;
+ const struct xpad_excluded_device *excluded;
int i, error;
if (intf->cur_altsetting->desc.bNumEndpoints != 2)
return -ENODEV;
+ for (excluded = xpad_excluded_devices;
+ excluded->vid || excluded->pid;
+ excluded++) {
+ if (!excluded->enabled)
+ continue;
+
+ if (le16_to_cpu(udev->descriptor.idVendor) == excluded->vid &&
+ le16_to_cpu(udev->descriptor.idProduct) == excluded->pid)
+ return -ENODEV;
+ }
+
for (i = 0; xpad_device[i].idVendor; i++) {
if ((le16_to_cpu(udev->descriptor.idVendor) == xpad_device[i].idVendor) &&
(le16_to_cpu(udev->descriptor.idProduct) == xpad_device[i].idProduct))
--
2.47.3
^ permalink raw reply related [flat|nested] 3+ messages in thread
end of thread, other threads:[~2026-08-07 1:24 UTC | newest]
Thread overview: 3+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-07 1:23 [PATCH 0/2] HID: flydigi: add support for vader 5 pro Denis Benato
2026-08-07 1:23 ` [PATCH 1/2] " Denis Benato
2026-08-07 1:23 ` [PATCH 2/2] Input: xpad - introduce a static table to ignore devices Denis Benato
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox