From nobody Fri Sep 25 13:18:47 2026 Received: from mta0.migadu.com (out-155.mta0.migadu.com [91.218.175.155]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 711F828E0 for ; Sat, 12 Sep 2026 01:59:33 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=91.218.175.155 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789178377; cv=none; b=Iz286vkpbtLSPq4/00md5jkZlkPS9mePIWoHZTJcIKp0Xr6AFWMdNIGM/0/hRPP/rCFzJdt6rCZSXiQU9MGoZBIYVDznAPWS67pyFn1Ua/OiphAN2jGV8Zo50DO1K/7gw0V8/FxqARKIc5InMnRmZncwfqG5en5nmbfi2RDmPkM= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789178377; c=relaxed/simple; bh=WOVgOc4HKJ0rRBfeRQLPO2ygS3tyfQA55wJpNw5fWgI=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=TAUmuEvppHn6btJ8WpVPozMtfCQbolJGrO83p4LGGk/mIgNb/BUBCI267CSWUrPxxHEMkDseqTkbwHQfqeo8/4X61QtHemDYn42J+Aw5sC9StvUvL2/H/1EvJLsmHHK07BobM4lfKoDQEQmoToSkjPfAwSDLZHVeV7jmrV7N/s0= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev; spf=pass smtp.mailfrom=linux.dev; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b=SPp/YmNR; arc=none smtp.client-ip=91.218.175.155 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=linux.dev Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b="SPp/YmNR" X-Envelope-To: linux-kernel@vger.kernel.org DKIM-Signature: a=rsa-sha256; bh=WOVgOc4HKJ0rRBfeRQLPO2ygS3tyfQA55wJpNw5fWgI=; c=simple/simple; d=linux.dev; h=from:to:subject:date:message-id:mime-version:content-type; s=key1; t=1789178371; v=1; x=1789783171; b=SPp/YmNRKA5nIZIxCztEo7Kd99zo/169uyXQh/84zQEnTVcwenjpxgTbez/Z5EXEyvar+i2H lTtWQTRIWpM58da0ri8zQdSJ4AgFHBlwuXEI3GJklq0fXXspWL+HO7pO/gRlfmvn0iYgvOxTkRI AhyPmJWk0m7TtyOR6TiYeIlE= X-Envelope-To: linux-kernel@vger.kernel.org Received: by smtp.migadu.com with ESMTPS id 321ab95c658f9678; Sat, 12 Sep 2026 01:59:31 +0000 X-Mizu-Trace-ID: 321ab95c658f9678 X-Migadu-Flow: FLOW_OUT From: Denis Benato To: 'Jiri Kosina' Cc: 'Benjamin Tissoires' , 'Dmitry Torokhov' , linux-input@vger.kernel.org, linux-kernel@vger.kernel.org, "Derek J. Clark" , Denis Benato , Adolfo Rodrigues Subject: [PATCH v4 1/2] HID: flydigi: add support for vader 5 pro Date: Sat, 12 Sep 2026 01:59:25 +0000 Message-ID: <20260912015926.230874-2-denis.benato@linux.dev> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260912015926.230874-1-denis.benato@linux.dev> References: <20260912015926.230874-1-denis.benato@linux.dev> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" 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 Signed-off-by: Adolfo Rodrigues Signed-off-by: Denis Benato --- drivers/hid/Kconfig | 8 + drivers/hid/Makefile | 1 + drivers/hid/hid-flydigi.c | 1129 +++++++++++++++++++++++++++++++++++++ drivers/hid/hid-ids.h | 3 + 4 files changed, 1141 insertions(+) create mode 100644 drivers/hid/hid-flydigi.c diff --git a/drivers/hid/Kconfig b/drivers/hid/Kconfig index 0e3a0ccd6901..d189c8fa297f 100644 --- a/drivers/hid/Kconfig +++ b/drivers/hid/Kconfig @@ -1196,6 +1196,14 @@ config STEAM_FF Say Y here if you want to enable force feedback support for the Steam Deck. =20 +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 79384d905e00..9d12ea55f7d3 100644 --- a/drivers/hid/Makefile +++ b/drivers/hid/Makefile @@ -56,6 +56,7 @@ obj-$(CONFIG_HID_FT260) +=3D hid-ft260.o obj-$(CONFIG_HID_GEMBIRD) +=3D hid-gembird.o obj-$(CONFIG_HID_GFRM) +=3D hid-gfrm.o obj-$(CONFIG_HID_GLORIOUS) +=3D hid-glorious.o +obj-$(CONFIG_HID_FLYDIGI) +=3D hid-flydigi.o obj-$(CONFIG_HID_VIVALDI_COMMON) +=3D hid-vivaldi-common.o obj-$(CONFIG_HID_GOODIX_SPI) +=3D hid-goodix-spi.o obj-$(CONFIG_HID_GOOGLE_HAMMER) +=3D hid-google-hammer.o diff --git a/drivers/hid/hid-flydigi.c b/drivers/hid/hid-flydigi.c new file mode 100644 index 000000000000..148c53564200 --- /dev/null +++ b/drivers/hid/hid-flydigi.c @@ -0,0 +1,1129 @@ +// 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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 + +/* + * Minimum spacing between consecutive haptic commands written to the + * device. FF core and userspace can request new rumble magnitudes + * far faster than the dongle/firmware can drain them, thus + * overwhelming the device. + */ +#define FLYDIGI_RUMBLE_MIN_INTERVAL_MS 30 + +/* SDL identifies this class as 4096 counts per g and 2000 dps full-scale.= */ +#define FLYDIGI_ACCEL_RES_PER_G 4096 + +/* + * The gyro's raw s16 range (-32768..32767) maps to +/-2000 dps, i.e. + * 32768/2000 =3D 16.384 counts per dps -- not an integer, so it can't be + * expressed exactly through input_abs_set_res() while reporting raw + * counts. Convert to milli-dps instead: at that scale the resolution + * is exactly 1000 units per dps, with no rounding error. + */ +#define FLYDIGI_GYRO_RAW_FS 32768 +#define FLYDIGI_GYRO_MDPS_FS 2000000 +#define FLYDIGI_GYRO_RES_PER_DPS 1000 +#define FLYDIGI_GYRO_FUZZ_MDPS 1000 + +/* 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 + +/* Byte layout of a GET_INFO (FLYDIGI_CMD_INFO) reply. */ +#define FLYDIGI_OFF_BATTERY 11 + +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; + u8 output_report_id; + + struct workqueue_struct *wq; + struct delayed_work rumble_work; + struct delayed_work acquire_work; + unsigned long rumble_last_send; + + struct power_supply *battery; + struct power_supply_desc battery_desc; + u8 battery_capacity; + int battery_status; + + u16 strong; + u16 weak; + bool gamepad_open; + bool removed; +}; + +static s16 flydigi_negate_axis(s16 axis) +{ + if (axis =3D=3D -32768) + return 32767; + + return -axis; +} + +/** + * flydigi_gyro_raw_to_mdps - convert a raw gyro sample to milli-dps + * @raw: signed 16-bit gyro count read from the protocol packet + * + * Return: the sample in milli-degrees/second, matching the resolution + * advertised via input_abs_set_res() on the sensors device. + */ +static s32 flydigi_gyro_raw_to_mdps(s16 raw) +{ + return div_s64((s64)raw * FLYDIGI_GYRO_MDPS_FS, FLYDIGI_GYRO_RAW_FS); +} + +/** + * 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 rep= orts. + * Descriptor probing selects one report ID at probe time, and all later w= rites + * 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 *payloa= d, + size_t payload_len) +{ + u8 *buf =3D fd->output_buf; + size_t copy_len; + bool no_output_report; + int ret; + + copy_len =3D min(payload_len, (size_t)FLYDIGI_REPORT_SIZE - 1); + + no_output_report =3D !fd->hdev->ll_driver || + !fd->hdev->ll_driver->output_report; + + scoped_guard(mutex, &fd->output_mutex) { + memset(buf, 0, FLYDIGI_REPORT_SIZE); + buf[0] =3D fd->output_report_id; + memcpy(&buf[1], payload, copy_len); + ret =3D hid_hw_output_report(fd->hdev, buf, FLYDIGI_REPORT_SIZE); + if (ret =3D=3D -EOPNOTSUPP || (ret < 0 && no_output_report)) { + ret =3D 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 !=3D -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 =3D 0; i <=3D 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 transpor= t 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 endpoin= t. + */ +static bool flydigi_detect_protocol_descriptor(struct hid_device *hdev, + u8 *output_report_id) +{ + const u8 *rdesc =3D hdev->dev_rdesc; + unsigned int rsize =3D hdev->dev_rsize; + static const u8 usage_ffa0[] =3D { 0x06, 0xa0, 0xff }; + static const u8 report_id_21[] =3D { 0x85, 0x21, 0x95, 0x1f }; + static const u8 report_32_in[] =3D { 0x95, 0x20, 0x81 }; + static const u8 report_32_out[] =3D { 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 =3D 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 =3D 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 =3D to_usb_interface(hdev->dev.parent); + if (!intf) + return -ENODEV; + + alt =3D intf->cur_altsetting; + if (!alt) + return -ENODEV; + + return alt->desc.bInterfaceNumber; +} + +/** + * flydigi_send_acquire - request ownership of the controller protocol str= eam + * @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] =3D { + 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[] =3D { + 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[] =3D { + 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[] =3D { + FLYDIGI_MAGIC1, + FLYDIGI_MAGIC2, + FLYDIGI_CMD_HAPTIC, + 6, + low, + high, + 0, + 0, + 0, + }; + + return flydigi_send_output(fd, payload, sizeof(payload)); +} + +/** + * flydigi_queue_rumble - (re)arm the rumble worker respecting the minimum + * spacing between hardware haptic writes + * @fd: controller state + * + * Callers just update fd->strong/fd->weak and call this; the worker always + * picks up the latest values, so back-to-back calls coalesce into a single + * write instead of flooding the device. Must be called with fd->lock held. + */ +static void flydigi_queue_rumble(struct flydigi_device *fd) +{ + unsigned long min_delay =3D msecs_to_jiffies(FLYDIGI_RUMBLE_MIN_INTERVAL_= MS); + unsigned long next_send =3D fd->rumble_last_send + min_delay; + unsigned long delay =3D 0; + + if (time_before(jiffies, next_send)) + delay =3D next_send - jiffies; + + mod_delayed_work(fd->wq, &fd->rumble_work, delay); +} + +static void flydigi_rumble_worker(struct work_struct *work) +{ + struct flydigi_device *fd =3D container_of(to_delayed_work(work), + struct flydigi_device, + rumble_work); + u16 strong; + u16 weak; + bool removed; + + scoped_guard(spinlock_irqsave, &fd->lock) { + strong =3D fd->strong; + weak =3D fd->weak; + removed =3D fd->removed; + } + + if (removed) + return; + + flydigi_send_rumble(fd, strong >> 8, weak >> 8); + + /* Record completion time to prevent flooding the device. */ + scoped_guard(spinlock_irqsave, &fd->lock) + fd->rumble_last_send =3D jiffies; +} + +/** + * 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 briefl= y. + */ +static void flydigi_acquire_worker(struct work_struct *work) +{ + struct flydigi_device *fd =3D container_of(to_delayed_work(work), + struct flydigi_device, + acquire_work); + bool open; + bool removed; + + scoped_guard(spinlock_irqsave, &fd->lock) { + open =3D fd->gamepad_open; + removed =3D fd->removed; + } + + if (removed || !open) + return; + + flydigi_send_acquire(fd); + flydigi_send_info_request(fd); + flydigi_send_status_request(fd); + queue_delayed_work(fd->wq, &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 =3D input_get_drvdata(dev); + + if (effect->type !=3D FF_RUMBLE) + return 0; + + scoped_guard(spinlock_irqsave, &fd->lock) { + if (!fd->removed) { + fd->strong =3D effect->u.rumble.strong_magnitude; + fd->weak =3D effect->u.rumble.weak_magnitude; + flydigi_queue_rumble(fd); + } + } + + return 0; +} + +static int flydigi_gamepad_open(struct input_dev *dev) +{ + struct flydigi_device *fd =3D input_get_drvdata(dev); + + scoped_guard(spinlock_irqsave, &fd->lock) { + fd->gamepad_open =3D true; + } + + queue_delayed_work(fd->wq, &fd->acquire_work, 0); + + return 0; +} + +static void flydigi_gamepad_close(struct input_dev *dev) +{ + struct flydigi_device *fd =3D input_get_drvdata(dev); + bool removed; + + scoped_guard(spinlock_irqsave, &fd->lock) { + fd->gamepad_open =3D false; + fd->strong =3D 0; + fd->weak =3D 0; + } + + cancel_delayed_work_sync(&fd->acquire_work); + + scoped_guard(spinlock_irqsave, &fd->lock) + removed =3D fd->removed; + + /* Stop the motors right away on close. */ + if (!removed) + mod_delayed_work(fd->wq, &fd->rumble_work, 0); +} + +static void flydigi_map_hat(u8 value, int *x, int *y) +{ + switch (value & 0x0f) { + case 0x01: + *x =3D 0; + *y =3D -1; + break; + case 0x03: + *x =3D 1; + *y =3D -1; + break; + case 0x02: + *x =3D 1; + *y =3D 0; + break; + case 0x06: + *x =3D 1; + *y =3D 1; + break; + case 0x04: + *x =3D 0; + *y =3D 1; + break; + case 0x0c: + *x =3D -1; + *y =3D 1; + break; + case 0x08: + *x =3D -1; + *y =3D 0; + break; + case 0x09: + *x =3D -1; + *y =3D -1; + break; + default: + *x =3D 0; + *y =3D 0; + break; + } +} + +/** + * flydigi_report_gamepad - translate one protocol input packet to evdev k= eys + * @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 *da= ta) +{ + struct input_dev *gamepad =3D fd->gamepad; + s16 axis; + int hat_x; + int hat_y; + + if (!gamepad) + return; + + 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 =3D (s16)get_unaligned_le16(&data[FLYDIGI_OFF_LX]); + input_report_abs(gamepad, ABS_X, axis); + + axis =3D flydigi_negate_axis((s16)get_unaligned_le16(&data[FLYDIGI_OFF_LY= ])); + input_report_abs(gamepad, ABS_Y, axis); + + axis =3D (s16)get_unaligned_le16(&data[FLYDIGI_OFF_RX]); + input_report_abs(gamepad, ABS_RX, axis); + + axis =3D flydigi_negate_axis((s16)get_unaligned_le16(&data[FLYDIGI_OFF_RY= ])); + 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 consu= me + * gyro/accelerometer data independently of gamepad button polling. + */ +static void flydigi_report_sensors(struct flydigi_device *fd, const u8 *da= ta) +{ + struct input_dev *sensors =3D 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, + flydigi_negate_axis((s16)get_unaligned_le16(&data[FLYDIGI_OFF_ACCEL_Z]= ))); + + /* Gyroscope, converted to exact milli-dps -- see FLYDIGI_GYRO_RES_PER_DP= S. */ + input_report_abs(sensors, ABS_RX, + flydigi_gyro_raw_to_mdps((s16)get_unaligned_le16(&data[FLYDIGI_OFF_GYR= O_X]))); + input_report_abs(sensors, ABS_RY, + flydigi_gyro_raw_to_mdps((s16)get_unaligned_le16(&data[FLYDIGI_OFF_GYR= O_Y]))); + input_report_abs(sensors, ABS_RZ, + flydigi_gyro_raw_to_mdps(flydigi_negate_axis((s16)get_unaligned_le16(&= data[FLYDIGI_OFF_GYRO_Z])))); + + input_sync(sensors); +} + +/** + * flydigi_report_battery - update battery state from a GET_INFO reply + * @fd: controller state + * @data: protocol packet starting at magic byte 0x5a + * + * Byte 11 packs charge state in the high nibble (0 =3D on battery, + * 1 =3D charging, 2 =3D charged/full) and a coarse 0-5 level in the low + * nibble, matching the layout used by SDL's Flydigi backend. + */ +static void flydigi_report_battery(struct flydigi_device *fd, const u8 *da= ta) +{ + u8 status =3D (data[FLYDIGI_OFF_BATTERY] >> 4) & 0x0f; + u8 level =3D data[FLYDIGI_OFF_BATTERY] & 0x0f; + int capacity; + int psy_status; + + switch (status) { + case 0: + psy_status =3D POWER_SUPPLY_STATUS_DISCHARGING; + capacity =3D level * 20; + break; + case 1: + psy_status =3D POWER_SUPPLY_STATUS_CHARGING; + capacity =3D level * 20; + break; + case 2: + psy_status =3D POWER_SUPPLY_STATUS_FULL; + capacity =3D 100; + break; + default: + psy_status =3D POWER_SUPPLY_STATUS_UNKNOWN; + capacity =3D 0; + break; + } + capacity =3D clamp(capacity, 0, 100); + + scoped_guard(spinlock_irqsave, &fd->lock) { + fd->battery_status =3D psy_status; + fd->battery_capacity =3D capacity; + } + + if (fd->battery) + power_supply_changed(fd->battery); +} + +/** + * 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 pre= sent, + * then dispatch by vendor command so gamepad and sensor state stay in syn= c. + * + * Return: always 0 so hidraw still sees the original traffic. + */ +static int flydigi_raw_event(struct hid_device *hdev, struct hid_report *r= eport, + u8 *data, int size) +{ + struct flydigi_device *fd =3D hid_get_drvdata(hdev); + const u8 *payload =3D data; + bool removed; + int len =3D size; + (void)report; + + if (!fd || size <=3D 0) + return 0; + + if (payload[0] !=3D FLYDIGI_MAGIC1) { + payload++; + len--; + } + + if (len < 31) + return 0; + + scoped_guard(spinlock_irqsave, &fd->lock) + removed =3D fd->removed; + + if (removed) + return 0; + + if (payload[0] !=3D FLYDIGI_MAGIC1 || payload[1] !=3D FLYDIGI_MAGIC2) { + hid_dbg(hdev, + "non-protocol packet ignored: sz=3D%d b0=3D%02x b1=3D%02x b2=3D%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: + flydigi_report_battery(fd, payload); + break; + 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: + { + bool open; + + scoped_guard(spinlock_irqsave, &fd->lock) + open =3D fd->gamepad_open && !fd->removed; + + if (open) + queue_delayed_work(fd->wq, &fd->acquire_work, 0); + } + break; + default: + hid_dbg(hdev, "unhandled command: cmd=3D%02x sz=3D%d\n", + payload[2], len); + break; + } + + return 0; +} + +static int flydigi_register_gamepad(struct flydigi_device *fd) +{ + struct hid_device *hdev =3D fd->hdev; + struct input_dev *gamepad; + int ret; + + gamepad =3D devm_input_allocate_device(&hdev->dev); + if (!gamepad) + return -ENOMEM; + + gamepad->name =3D hdev->name; + gamepad->phys =3D hdev->phys; + gamepad->uniq =3D hdev->uniq; + gamepad->id.bustype =3D hdev->bus; + gamepad->id.vendor =3D hdev->vendor; + gamepad->id.product =3D hdev->product; + gamepad->id.version =3D hdev->version; + gamepad->open =3D flydigi_gamepad_open; + gamepad->close =3D 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 =3D input_ff_create_memless(gamepad, NULL, flydigi_play_effect); + if (ret) + return ret; + + ret =3D input_register_device(gamepad); + if (ret) + return ret; + + fd->gamepad =3D gamepad; + return 0; +} + +static int flydigi_register_sensors(struct flydigi_device *fd) +{ + struct hid_device *hdev =3D fd->hdev; + struct input_dev *sensors; + char *name; + int ret; + + sensors =3D devm_input_allocate_device(&hdev->dev); + if (!sensors) + return -ENOMEM; + + name =3D devm_kasprintf(&hdev->dev, GFP_KERNEL, "%s Motion Sensors", + hdev->name); + if (!name) + return -ENOMEM; + + sensors->name =3D name; + /* Matching phys/uniq helps userspace pair gamepad and IMU endpoints. */ + sensors->phys =3D hdev->phys; + sensors->uniq =3D hdev->uniq; + sensors->id.bustype =3D hdev->bus; + sensors->id.vendor =3D hdev->vendor; + sensors->id.product =3D hdev->product; + sensors->id.version =3D 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, -FLYDIGI_GYRO_MDPS_FS, FLYDIGI_GYRO= _MDPS_FS, + FLYDIGI_GYRO_FUZZ_MDPS, 0); + input_set_abs_params(sensors, ABS_RY, -FLYDIGI_GYRO_MDPS_FS, FLYDIGI_GYRO= _MDPS_FS, + FLYDIGI_GYRO_FUZZ_MDPS, 0); + input_set_abs_params(sensors, ABS_RZ, -FLYDIGI_GYRO_MDPS_FS, FLYDIGI_GYRO= _MDPS_FS, + FLYDIGI_GYRO_FUZZ_MDPS, 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 =3D input_register_device(sensors); + if (ret) + return ret; + + fd->sensors =3D sensors; + return 0; +} + +static int flydigi_battery_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct flydigi_device *fd =3D power_supply_get_drvdata(psy); + unsigned long flags; + int ret =3D 0; + + switch (psp) { + case POWER_SUPPLY_PROP_CAPACITY: + spin_lock_irqsave(&fd->lock, flags); + val->intval =3D fd->battery_capacity; + spin_unlock_irqrestore(&fd->lock, flags); + break; + case POWER_SUPPLY_PROP_STATUS: + spin_lock_irqsave(&fd->lock, flags); + val->intval =3D fd->battery_status; + spin_unlock_irqrestore(&fd->lock, flags); + break; + case POWER_SUPPLY_PROP_SCOPE: + val->intval =3D POWER_SUPPLY_SCOPE_DEVICE; + break; + case POWER_SUPPLY_PROP_MODEL_NAME: + val->strval =3D fd->hdev->name; + break; + default: + ret =3D -EINVAL; + break; + } + + return ret; +} + +static enum power_supply_property flydigi_battery_props[] =3D { + POWER_SUPPLY_PROP_CAPACITY, + POWER_SUPPLY_PROP_STATUS, + POWER_SUPPLY_PROP_SCOPE, + POWER_SUPPLY_PROP_MODEL_NAME, +}; + +/** + * flydigi_register_battery - expose charge state via the power_supply cla= ss + * @fd: controller state + * + * Capacity/status start out unknown and are populated once the first + * GET_INFO reply is parsed by flydigi_report_battery(); the periodic + * acquire worker keeps requesting fresh info so this stays current. + */ +static int flydigi_register_battery(struct flydigi_device *fd) +{ + struct hid_device *hdev =3D fd->hdev; + struct power_supply_config psy_cfg =3D { .drv_data =3D fd }; + char *name =3D devm_kasprintf(&hdev->dev, GFP_KERNEL, "flydigi_%s", + dev_name(&hdev->dev)); + if (!name) + return -ENOMEM; + + fd->battery_desc.name =3D name; + fd->battery_desc.type =3D POWER_SUPPLY_TYPE_BATTERY; + fd->battery_desc.properties =3D flydigi_battery_props; + fd->battery_desc.num_properties =3D ARRAY_SIZE(flydigi_battery_props); + fd->battery_desc.get_property =3D flydigi_battery_get_property; + + fd->battery_status =3D POWER_SUPPLY_STATUS_UNKNOWN; + fd->battery_capacity =3D 0; + + fd->battery =3D devm_power_supply_register(&hdev->dev, &fd->battery_desc, + &psy_cfg); + if (IS_ERR(fd->battery)) + return PTR_ERR(fd->battery); + + 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 match= es. + * 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; + + /* + * Keep any transport-provided unique ID (e.g. Bluetooth address) so + * all evdev nodes created from this HID device share the same uniq. + */ + if (hdev->uniq[0]) + return; + + /* + * Fallback for USB when no stable device ID is available from transport = or + * firmware: use the USB topology path so userspace can pair gamepad and + * sensor nodes per physical device. + */ + if (hid_is_usb(hdev)) { + intf =3D to_usb_interface(hdev->dev.parent); + if (intf) { + udev =3D interface_to_usbdev(intf); + if (udev) { + ret =3D usb_make_path(udev, path, sizeof(path)); + if (ret > 0) { + strscpy(hdev->uniq, path, sizeof(hdev->uniq)); + 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 =3D 0; + int ifnum =3D -1; + int ret; + + if (hid_is_usb(hdev)) { + ifnum =3D flydigi_get_interface_number(hdev); + if (ifnum < 0) + return -ENODEV; + } + + if (!flydigi_detect_protocol_descriptor(hdev, &output_report_id)) + return -ENODEV; + + if (ifnum >=3D 0) + hid_dbg(hdev, + "binding protocol interface %d (report id 0x%02x)\n", + ifnum, output_report_id); + else + hid_dbg(hdev, "binding protocol transport (report id 0x%02x)\n", + output_report_id); + + ret =3D hid_parse(hdev); + if (ret) { + hid_err(hdev, "parse failed: %d\n", ret); + return ret; + } + + fd =3D devm_kzalloc(&hdev->dev, sizeof(*fd), GFP_KERNEL); + if (!fd) + return -ENOMEM; + + fd->hdev =3D hdev; + fd->output_report_id =3D output_report_id; + hid_set_drvdata(hdev, fd); + fd->output_buf =3D devm_kzalloc(&hdev->dev, FLYDIGI_REPORT_SIZE, + GFP_KERNEL); + if (!fd->output_buf) + return -ENOMEM; + + spin_lock_init(&fd->lock); + mutex_init(&fd->output_mutex); + + fd->wq =3D alloc_workqueue("hid-flydigi-%s", WQ_MEM_RECLAIM | WQ_PERCPU, + 0, dev_name(&hdev->dev)); + if (!fd->wq) { + mutex_destroy(&fd->output_mutex); + return -ENOMEM; + } + + INIT_DELAYED_WORK(&fd->rumble_work, flydigi_rumble_worker); + INIT_DELAYED_WORK(&fd->acquire_work, flydigi_acquire_worker); + + flydigi_init_uniq(hdev); + + ret =3D hid_hw_start(hdev, HID_CONNECT_HIDRAW); + if (ret) { + hid_err(hdev, "hw start failed: %d\n", ret); + goto err_wq; + } + + ret =3D flydigi_register_gamepad(fd); + if (ret) + goto err_stop; + + ret =3D flydigi_register_sensors(fd); + if (ret) + goto err_stop; + + ret =3D flydigi_register_battery(fd); + if (ret) + goto err_stop; + + ret =3D hid_hw_open(hdev); + if (ret) { + hid_err(hdev, "hw open failed: %d\n", ret); + goto err_stop; + } + + /* + * 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_wq: + destroy_workqueue(fd->wq); + mutex_destroy(&fd->output_mutex); + return ret; + +err_stop: + hid_hw_stop(hdev); + cancel_delayed_work_sync(&fd->acquire_work); + cancel_delayed_work_sync(&fd->rumble_work); + destroy_workqueue(fd->wq); + mutex_destroy(&fd->output_mutex); + return ret; +} + +static void flydigi_remove(struct hid_device *hdev) +{ + struct flydigi_device *fd =3D hid_get_drvdata(hdev); + + scoped_guard(spinlock_irqsave, &fd->lock) { + fd->removed =3D true; + fd->strong =3D 0; + fd->weak =3D 0; + } + + cancel_delayed_work_sync(&fd->acquire_work); + cancel_delayed_work_sync(&fd->rumble_work); + destroy_workqueue(fd->wq); + + /* 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[] =3D { + { HID_USB_DEVICE(USB_VENDOR_ID_FLYDIGI, + USB_DEVICE_ID_FLYDIGI_VADER5) }, + { } +}; +MODULE_DEVICE_TABLE(hid, flydigi_devices); + +static struct hid_driver flydigi_driver =3D { + .name =3D "flydigi", + .id_table =3D flydigi_devices, + .probe =3D flydigi_probe, + .remove =3D flydigi_remove, + .raw_event =3D flydigi_raw_event, +}; +module_hid_driver(flydigi_driver); + +MODULE_AUTHOR("Denis Benato "); +MODULE_DESCRIPTION("Flydigi HID driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h index c53a78cd58bd..33e8d82b8694 100644 --- a/drivers/hid/hid-ids.h +++ b/drivers/hid/hid-ids.h @@ -523,6 +523,9 @@ #define USB_VENDOR_ID_FLATFROG 0x25b5 #define USB_DEVICE_ID_MULTITOUCH_3200 0x0002 =20 +#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 =20 --=20 2.47.3 From nobody Fri Sep 25 13:18:47 2026 Received: from mta0.migadu.com (out-159.mta0.migadu.com [91.218.175.159]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 1522625B09D for ; Sat, 12 Sep 2026 01:59:33 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=91.218.175.159 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789178376; cv=none; b=rHSnU4q4kziRWIcZYmaPIHNkzszea2SHTJiu5i5BE1czqHxw1N4WpZTNcCHQ/rTCnvHqbLnjUKih8o3LloeDda7BNvcw4YR3RVYR4BRkUiYvPvroH6gOxpNRA5LLEnqdtY631Snahpv95hoSSbxh4Y4kHJ/nOAxgdqVjZkIS1K8= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789178376; c=relaxed/simple; bh=s70WjWUOYhj/JuHH6aUF78zObHqSFRFlD2Dv57grYF4=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=n6jorhwNgmJr6dquQeqdyTf976M0KWwrLj6mmK42HgGHAjy5XlHl0o2AsQ1vzMxMvLlmla6sgjxTDdjmvzW5/sBnCkhJL6O0rhTvteyx2cjeWMrvm9dJsnK0Yy+cMZicx0hdWuJjbbR5JmWbwGoSy0rN22Zzw1UUW/iF/Zdv8WM= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev; spf=pass smtp.mailfrom=linux.dev; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b=TXHJSuBx; arc=none smtp.client-ip=91.218.175.159 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=linux.dev Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b="TXHJSuBx" X-Envelope-To: linux-kernel@vger.kernel.org DKIM-Signature: a=rsa-sha256; bh=s70WjWUOYhj/JuHH6aUF78zObHqSFRFlD2Dv57grYF4=; c=simple/simple; d=linux.dev; h=from:to:subject:date:message-id:mime-version:content-type; s=key1; t=1789178371; v=1; x=1789783171; b=TXHJSuBxnSHQYgD3k9BKzc9tpczyc3lZmBRCz/uPbMbcRXg+Py42SnpTNTKbeWv83BTFoDvc 2zywOTqOPwH1CltyI/CBNYOjn1ielPVGRT2XsY3J/IZ/hWBI4PhbHiqCM+W6beDuHOoX4RCwAoo Zcfd4CLBHuRn9cFtvprqwhZU= X-Envelope-To: linux-kernel@vger.kernel.org Received: by smtp.migadu.com with ESMTPS id 87fb88622ac49665; Sat, 12 Sep 2026 01:59:31 +0000 X-Mizu-Trace-ID: 87fb88622ac49665 X-Migadu-Flow: FLOW_OUT From: Denis Benato To: 'Jiri Kosina' Cc: 'Benjamin Tissoires' , 'Dmitry Torokhov' , linux-input@vger.kernel.org, linux-kernel@vger.kernel.org, "Derek J. Clark" , Denis Benato Subject: [PATCH v4 2/2] Input: xpad - introduce a static table to ignore devices Date: Sat, 12 Sep 2026 01:59:26 +0000 Message-ID: <20260912015926.230874-3-denis.benato@linux.dev> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260912015926.230874-1-denis.benato@linux.dev> References: <20260912015926.230874-1-denis.benato@linux.dev> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" 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 Signed-off-by: Denis Benato --- 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 2da0b7f1722a..bd24beacc788 100644 --- a/drivers/input/joystick/xpad.c +++ b/drivers/input/joystick/xpad.c @@ -407,6 +407,18 @@ static const struct xpad_device { { 0x0000, 0x0000, "Generic X-Box pad", 0, XTYPE_UNKNOWN } }; =20 +#define XPAD_SUBSTITUTE(_vid, _pid, _enabled) \ + { .vid =3D (_vid), .pid =3D (_pid), .enabled =3D (_enabled) } + +static const struct xpad_excluded_device { + u16 vid; + u16 pid; + bool enabled; +} xpad_excluded_devices[] =3D { + XPAD_SUBSTITUTE(0x37d7, 0x2401, IS_ENABLED(CONFIG_HID_FLYDIGI)), + { } +}; + /* buttons shared with xbox and xbox360 */ static const signed short xpad_common_btn[] =3D { BTN_A, BTN_B, BTN_X, BTN_Y, /* "analog" buttons */ @@ -2046,11 +2058,23 @@ static int xpad_probe(struct usb_interface *intf, c= onst struct usb_device_id *id struct usb_device *udev =3D 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; =20 if (intf->cur_altsetting->desc.bNumEndpoints !=3D 2) return -ENODEV; =20 + for (excluded =3D xpad_excluded_devices; + excluded->vid || excluded->pid; + excluded++) { + if (!excluded->enabled) + continue; + + if (le16_to_cpu(udev->descriptor.idVendor) =3D=3D excluded->vid && + le16_to_cpu(udev->descriptor.idProduct) =3D=3D excluded->pid) + return -ENODEV; + } + for (i =3D 0; xpad_device[i].idVendor; i++) { if ((le16_to_cpu(udev->descriptor.idVendor) =3D=3D xpad_device[i].idVend= or) && (le16_to_cpu(udev->descriptor.idProduct) =3D=3D xpad_device[i].idPro= duct)) --=20 2.47.3