[PATCH RESEND 4/4] HID: winwing: drive the URSA MINOR rumble motor

From: René Onier

Date: Wed Sep 30 2026 - 17:04:57 EST


An URSA MINOR stick has a single rumble motor, carried by the grip rather
than by the base and addressed by its own device number, derived from the
product id. The two-motor throttle path sends to device 0x01 and 0x03,
which a stick ignores, so give it its own path.

Both magnitudes feed the one motor and the stronger one wins, which is
what the memoryless force-feedback core expects of a device that cannot
drive them separately.

The Orion 2 keeps its two motors.

Signed-off-by: René Onier <f3nr1l@xxxxxx>
---
drivers/hid/hid-winwing.c | 50 ++++++++++++++++++++++++++++++++++++++-
1 file changed, 49 insertions(+), 1 deletion(-)

diff --git a/drivers/hid/hid-winwing.c b/drivers/hid/hid-winwing.c
index a52e7efece..86caff9183 100644
--- a/drivers/hid/hid-winwing.c
+++ b/drivers/hid/hid-winwing.c
@@ -4,6 +4,7 @@
* HID driver for WinWing Orion 2 throttle and URSA MINOR sticks
*
* Copyright (c) 2023 Ivan Gorinov
+ * Copyright (c) 2026 René Onier
*/

#include <linux/device.h>
@@ -45,8 +46,10 @@
#define URSA_LIGHT_DEVICE 0x20
#define URSA_LIGHT_FAMILY 0xbb

-/* Rumble motors of the TGRIP-15 grips, one report each */
+/* Rumble motors answer on the grip, not on the base */
#define WINWING_STICK_FAMILY 0xbf
+
+/* The TGRIP-15 grips have two motors, each taking its own report */
#define WINWING_RUMBLE_LEFT_DEVICE 0x01
#define WINWING_RUMBLE_RIGHT_DEVICE 0x03

@@ -95,6 +98,10 @@ struct winwing_drv_data {
int rumble_right;
unsigned long quirks;

+ /* URSA MINOR: single motor, on the grip */
+ __u8 rumble_device;
+ int rumble_sent;
+
/* Lighting controller and LED set of this model */
unsigned int num_leds;
__u8 led_device;
@@ -296,6 +303,40 @@ static inline int convert_magnitude(int x)
return ((x * 255) >> 16) + 1;
}

+/* Device number of the grip, 0x09 for the left stick and 0x0a for the right */
+static inline __u8 winwing_ursa_device(__u32 product)
+{
+ return (product & 0xff) - 0x20;
+}
+
+/*
+ * An URSA MINOR stick has a single motor, driven by the same opcode as the
+ * lights but addressed to the grip instead of the base. Both magnitudes feed
+ * it and the stronger one wins.
+ */
+static int winwing_ursa_rumble(struct winwing_drv_data *data, __u8 *buf)
+{
+ int ret;
+ __u8 m;
+
+ m = convert_magnitude(max(data->rumble.strong_magnitude,
+ data->rumble.weak_magnitude));
+
+ if (m == data->rumble_sent)
+ return 0;
+
+ ret = winwing_send_set_ledx(data->hdev, buf, data->rumble_device,
+ WINWING_STICK_FAMILY, 0, m);
+ if (ret < 0) {
+ hid_err(data->hdev, "error %d (%*ph)\n", ret,
+ WINWING_REPORT_LEN, buf);
+ return ret;
+ }
+ data->rumble_sent = m;
+
+ return 0;
+}
+
static int winwing_haptic_rumble(struct winwing_drv_data *data)
{
__u8 *buf;
@@ -312,6 +353,9 @@ static int winwing_haptic_rumble(struct winwing_drv_data *data)
if (!buf)
return -EINVAL;

+ if (data->quirks & WINWING_URSA_MINOR)
+ return winwing_ursa_rumble(data, buf);
+
m = convert_magnitude(data->rumble.strong_magnitude);
if (m != data->rumble_left) {
int ret;
@@ -382,6 +426,10 @@ static int winwing_init_ff(struct hid_device *hdev, struct hid_input *hidinput)
data->report_rumble = devm_kzalloc(&hdev->dev, MAX_REPORT, GFP_KERNEL);
data->rumble_left = -1;
data->rumble_right = -1;
+ data->rumble_sent = -1;
+
+ if (data->quirks & WINWING_URSA_MINOR)
+ data->rumble_device = winwing_ursa_device(hdev->product);

input_set_capability(hidinput->input, EV_FF, FF_RUMBLE);

--
2.55.0