mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
From: "Erik Håkansson" <erikhakan@gmail.com>
To: "Filipe Laíns" <lains@riseup.net>,
	"Jiri Kosina" <jikos@kernel.org>,
	"Benjamin Tissoires" <bentiss@kernel.org>,
	"Bastien Nocera" <hadess@hadess.net>
Cc: "Rafael Passos" <rafael@rcpassos.me>,
	"Grégoire Stein" <greyxor@protonmail.com>,
	"Alexey Zagorodnikov" <xglooom@gmail.com>,
	"Oleksandr Natalenko" <oleksandr@natalenko.name>,
	"Roman Stingler" <roman.stingler@gmail.com>,
	"Lovekesh Solanki" <lovekeshsolanki00@gmail.com>,
	"Kateřina Medvědová" <k8ie@mcld.eu>,
	linux-input@vger.kernel.org, linux-kernel@vger.kernel.org,
	"Erik Håkansson" <erikhakan@gmail.com>
Subject: [PATCH RFC 2/4] HID: logitech: handle HID++ thumbwheel reports
Date: Fri, 02 Oct 2026 00:23:36 +0200	[thread overview]
Message-ID: <20261002-bolt-input-rfc-v1-2-333e1f350586@gmail.com> (raw)
In-Reply-To: <20261002-bolt-input-rfc-v1-0-333e1f350586@gmail.com>

Add support for HID++ thumbwheel reports on devices that support the 0x2150
feature. Read resolution and default direction. For Bolt devices, default
to diverting thumbwheel reports to HID++ to get device index support, so
source device is identifiable.

Generalize the scroll handling to support the thumbwheel events in addition
to the already supported vertical scroll events.

Signed-off-by: Erik Håkansson <erikhakan@gmail.com>
---
 drivers/hid/hid-logitech-hidpp.c | 217 ++++++++++++++++++++++++++++++++++-----
 1 file changed, 192 insertions(+), 25 deletions(-)

diff --git a/drivers/hid/hid-logitech-hidpp.c b/drivers/hid/hid-logitech-hidpp.c
index ff6a1095e7ba..464685ad21bb 100644
--- a/drivers/hid/hid-logitech-hidpp.c
+++ b/drivers/hid/hid-logitech-hidpp.c
@@ -23,6 +23,8 @@
 #include <linux/workqueue.h>
 #include <linux/atomic.h>
 #include <linux/fixp-arith.h>
+#include <linux/limits.h>
+#include <linux/math64.h>
 #include <linux/unaligned.h>
 #include "usbhid/usbhid.h"
 #include "hid-ids.h"
@@ -98,6 +100,7 @@ MODULE_PARM_DESC(disable_tap_to_click,
 #define HIDPP_CAPABILITY_HIDPP20_HI_RES_SCROLL	BIT(8)
 #define HIDPP_CAPABILITY_HIDPP10_FAST_SCROLL	BIT(9)
 #define HIDPP_CAPABILITY_ADC_MEASUREMENT	BIT(10)
+#define HIDPP_CAPABILITY_HIDPP20_THUMBWHEEL	BIT(11)
 
 #define lg_map_key_clear(c)  hid_map_usage_clear(hi, usage, bit, max, EV_KEY, (c))
 
@@ -165,15 +168,21 @@ struct hidpp_battery {
 /**
  * struct hidpp_scroll_counter - Utility class for processing high-resolution
  *                             scroll events.
- * @wheel_multiplier: the scalar multiplier to be applied to each wheel event
+ * @scale_numerator: numerator of normal scroll steps per device count
+ * @scale_denominator: denominator of normal scroll steps per device count
  * @remainder: counts the number of high-resolution units moved since the last
  *             low-resolution event (REL_WHEEL or REL_HWHEEL) was sent. Should
  *             only be used by class methods.
  * @direction: direction of last movement (1 or -1)
  * @last_time: last event time, used to reset remainder after inactivity
+ *
+ * One device count represents scale_numerator / scale_denominator normal
+ * scroll steps: 1 / wheel_multiplier for a vertical wheel, or native_res /
+ * diverted_res for a diverted thumbwheel.
  */
 struct hidpp_scroll_counter {
-	int wheel_multiplier;
+	u16 scale_numerator;
+	u16 scale_denominator;
 	int remainder;
 	int direction;
 	unsigned long long last_time;
@@ -206,6 +215,7 @@ struct hidpp_device {
 
 	struct hidpp_battery battery;
 	struct hidpp_scroll_counter vertical_wheel_counter;
+	struct hidpp_scroll_counter thumbwheel_counter;
 
 	u8 wireless_feature_index;
 	u8 reprog_controls_feature_index;
@@ -215,6 +225,8 @@ struct hidpp_device {
 	u8 hires_wheel_feature_index;
 	bool hires_wheel_has_invert;
 	bool hires_wheel_invert;
+	u8 thumbwheel_feature_index;
+	bool thumbwheel_positive_right;
 
 	bool connected_once;
 };
@@ -544,28 +556,42 @@ static void hidpp_update_usb_wireless_status(struct hidpp_device *hidpp)
 
 /**
  * hidpp_scroll_counter_handle_scroll() - Send high- and low-resolution scroll
- *                                        events given a high-resolution wheel
- *                                        movement.
+ *                                        events given a wheel movement.
  * @input_dev: Pointer to the input device
- * @counter: a hid_scroll_counter struct describing the wheel.
- * @hi_res_value: the movement of the wheel, in the mouse's high-resolution
- *                units.
+ * @counter: a hidpp_scroll_counter struct describing the wheel.
+ * @value: the movement of the wheel, in the device's reporting units.
+ * @hi_res_code: high-resolution event code for this wheel axis.
+ * @low_res_code: low-resolution event code for this wheel axis.
  *
- * Given a high-resolution movement, this function converts the movement into
+ * Given a wheel movement, this function converts the movement into
  * fractions of 120 and emits high-resolution scroll events for the input
- * device. It also uses the multiplier from &struct hid_scroll_counter to
- * emit low-resolution scroll events when appropriate for
- * backwards-compatibility with userspace input libraries.
+ * device. It also uses the counter to emit low-resolution scroll events
+ * when appropriate for backwards-compatibility with userspace input
+ * libraries.
+ *
+ * Return: true if a nonzero scroll event was reported
  */
-static void hidpp_scroll_counter_handle_scroll(struct input_dev *input_dev,
+static bool hidpp_scroll_counter_handle_scroll(struct input_dev *input_dev,
 					       struct hidpp_scroll_counter *counter,
-					       int hi_res_value)
+					       int value,
+					       unsigned int hi_res_code,
+					       unsigned int low_res_code)
 {
+	s64 scaled;
+	int hi_res_value;
 	int low_res_value, remainder, direction;
 	unsigned long long now, previous;
 
-	hi_res_value = hi_res_value * 120/counter->wheel_multiplier;
-	input_report_rel(input_dev, REL_WHEEL_HI_RES, hi_res_value);
+	if (!counter->scale_denominator)
+		return false;
+
+	scaled = div_s64((s64)value * 120 * counter->scale_numerator,
+			 counter->scale_denominator);
+	if (!scaled || scaled > INT_MAX - 120 || scaled < INT_MIN + 120)
+		return false;
+	hi_res_value = scaled;
+
+	input_report_rel(input_dev, hi_res_code, hi_res_value);
 
 	remainder = counter->remainder;
 	direction = hi_res_value > 0 ? 1 : -1;
@@ -599,10 +625,11 @@ static void hidpp_scroll_counter_handle_scroll(struct input_dev *input_dev,
 		low_res_value = remainder / 120;
 		if (low_res_value == 0)
 			low_res_value = (hi_res_value > 0 ? 1 : -1);
-		input_report_rel(input_dev, REL_WHEEL, low_res_value);
+		input_report_rel(input_dev, low_res_code, low_res_value);
 		remainder -= low_res_value * 120;
 	}
 	counter->remainder = remainder;
+	return true;
 }
 
 /* -------------------------------------------------------------------------- */
@@ -2104,6 +2131,47 @@ static int hidpp_hrw_set_wheel_mode(struct hidpp_device *hidpp, bool invert,
 					   params, sizeof(params), &response);
 }
 
+/* -------------------------------------------------------------------------- */
+/* 0x2150: Thumbwheel                                                         */
+/* -------------------------------------------------------------------------- */
+
+#define HIDPP_PAGE_THUMBWHEEL			0x2150
+#define CMD_THUMBWHEEL_GET_INFO			0x00
+#define CMD_THUMBWHEEL_SET_REPORTING		0x20
+#define EVENT_THUMBWHEEL			0x00
+
+static int hidpp_thumbwheel_get_info(struct hidpp_device *hidpp,
+				    u16 *native_res, u16 *diverted_res,
+				    bool *positive_right)
+{
+	struct hidpp_report response;
+	int ret;
+
+	ret = hidpp_send_fap_command_sync(hidpp,
+					  hidpp->thumbwheel_feature_index,
+					  CMD_THUMBWHEEL_GET_INFO,
+					  NULL, 0, &response);
+	if (ret)
+		return ret;
+
+	*native_res = get_unaligned_be16(&response.fap.params[0]);
+	*diverted_res = get_unaligned_be16(&response.fap.params[2]);
+	*positive_right = response.fap.params[4] & BIT(0);
+	return 0;
+}
+
+static int hidpp_thumbwheel_set_reporting(struct hidpp_device *hidpp,
+					 bool diverted)
+{
+	struct hidpp_report response;
+	u8 params[2] = { diverted, 0 }; /* No inversion. */
+
+	return hidpp_send_fap_command_sync(hidpp,
+					   hidpp->thumbwheel_feature_index,
+					   CMD_THUMBWHEEL_SET_REPORTING,
+					   params, sizeof(params), &response);
+}
+
 /* -------------------------------------------------------------------------- */
 /* 0x4301: Solar Keyboard                                                     */
 /* -------------------------------------------------------------------------- */
@@ -3323,7 +3391,8 @@ static int m560_raw_event(struct hid_device *hdev, u8 *data, int size)
 		v = sign_extend32(data[6], 7);
 		if (v != 0)
 			hidpp_scroll_counter_handle_scroll(hidpp->input,
-					&hidpp->vertical_wheel_counter, v);
+					&hidpp->vertical_wheel_counter, v,
+					REL_WHEEL_HI_RES, REL_WHEEL);
 
 		input_sync(hidpp->input);
 	}
@@ -3931,7 +4000,8 @@ static void hidpp_hrw_update_wheel_mode(struct hidpp_device *hidpp,
 	bool invert, bool high_resolution)
 {
 	hidpp->hires_wheel_invert = invert;
-	hidpp->vertical_wheel_counter.wheel_multiplier =
+	hidpp->vertical_wheel_counter.scale_numerator = 1;
+	hidpp->vertical_wheel_counter.scale_denominator =
 		(high_resolution && hidpp->hires_wheel_multiplier > 0) ?
 		hidpp->hires_wheel_multiplier : 1;
 }
@@ -3986,10 +4056,12 @@ static int hi_res_scroll_enable(struct hidpp_device *hidpp)
 	}
 
 	hidpp->hires_wheel_multiplier = multiplier;
-	hidpp->vertical_wheel_counter.wheel_multiplier = multiplier;
 	if (hidpp->capabilities & HIDPP_CAPABILITY_HIDPP20_HI_RES_WHEEL) {
 		hidpp->hires_wheel_has_invert = has_invert;
 		hidpp_hrw_update_wheel_mode(hidpp, false, true);
+	} else {
+		hidpp->vertical_wheel_counter.scale_numerator = 1;
+		hidpp->vertical_wheel_counter.scale_denominator = multiplier;
 	}
 	hid_dbg(hidpp->hid_dev, "wheel multiplier = %d\n", multiplier);
 	return 0;
@@ -4033,6 +4105,49 @@ static int hidpp_initialize_hires_scroll(struct hidpp_device *hidpp)
 	return 0;
 }
 
+static void thumbwheel_scroll_enable(struct hidpp_device *hidpp)
+{
+	bool divert;
+	int ret;
+
+	memset(&hidpp->thumbwheel_counter, 0,
+	       sizeof(hidpp->thumbwheel_counter));
+	ret = hidpp_thumbwheel_get_info(hidpp,
+			&hidpp->thumbwheel_counter.scale_numerator,
+			&hidpp->thumbwheel_counter.scale_denominator,
+			&hidpp->thumbwheel_positive_right);
+
+	/* Non-Bolt devices should default to native HID. Don't enable HID++ */
+	if (!hidpp_is_bolt_child(hidpp->hid_dev))
+		return;
+
+	/* Divert only when both resolutions are available; otherwise request
+	 * native HID reporting.
+	 */
+	divert = !ret && hidpp->thumbwheel_counter.scale_numerator &&
+		 hidpp->thumbwheel_counter.scale_denominator;
+	ret = hidpp_thumbwheel_set_reporting(hidpp, divert);
+	if (ret)
+		hid_dbg(hidpp->hid_dev,
+			"Could not set thumbwheel reporting mode: %d\n", ret);
+}
+
+static void hidpp_initialize_thumbwheel_scroll(struct hidpp_device *hidpp)
+{
+	int ret;
+
+	hidpp->capabilities &= ~HIDPP_CAPABILITY_HIDPP20_THUMBWHEEL;
+	hidpp->thumbwheel_feature_index = 0xff;
+	if (hidpp->protocol_major < 2)
+		return;
+
+	ret = hidpp_root_get_feature(hidpp, HIDPP_PAGE_THUMBWHEEL,
+				     &hidpp->thumbwheel_feature_index);
+	if (ret)
+		return;
+	hidpp->capabilities |= HIDPP_CAPABILITY_HIDPP20_THUMBWHEEL;
+}
+
 /*
  * The low nibble of data[3] is the software ID: 0 for wheel
  * notifications, nonzero and echoed from the request for replies.
@@ -4074,7 +4189,7 @@ static int hidpp20_hires_wheel_raw_event(struct hidpp_device *hidpp,
 		/* The event flag describes whether the event is high resolution
 		 * or not.
 		 */
-		hidpp->vertical_wheel_counter.wheel_multiplier =
+		hidpp->vertical_wheel_counter.scale_denominator =
 			(data[4] & HIRES_WHEEL_EVENT_HIGH_RESOLUTION) ?
 			hidpp->hires_wheel_multiplier : 1;
 
@@ -4088,15 +4203,57 @@ static int hidpp20_hires_wheel_raw_event(struct hidpp_device *hidpp,
 		if (hidpp->hires_wheel_has_invert && hidpp->hires_wheel_invert)
 			delta = -delta;
 
-		hidpp_scroll_counter_handle_scroll(hidpp->input,
-			&hidpp->vertical_wheel_counter, delta);
-		input_sync(hidpp->input);
+		if (hidpp_scroll_counter_handle_scroll(hidpp->input,
+			&hidpp->vertical_wheel_counter, delta,
+			REL_WHEEL_HI_RES, REL_WHEEL))
+			input_sync(hidpp->input);
 		return 1;
 	}
 
 	return 0;
 }
 
+static int hidpp20_thumbwheel_raw_event(struct hidpp_device *hidpp,
+					       u8 *data, int size)
+{
+	int delta;
+
+	if (hidpp->thumbwheel_feature_index == 0xff)
+		return 0;
+
+	if (size < 6)
+		return 0;
+
+	if (data[0] != REPORT_ID_HIDPP_LONG ||
+	    data[2] != hidpp->thumbwheel_feature_index)
+		return 0;
+
+	if (data[3] != EVENT_THUMBWHEEL)
+		return 0;
+
+	if (!hidpp->input || !hidpp->thumbwheel_counter.scale_numerator ||
+	    !hidpp->thumbwheel_counter.scale_denominator)
+		return 1;
+
+	/* The specification calls this scroll delta "rotation". The event also
+	 * carries rotation_status, touch, proximity and single_tap data which
+	 * may be exposed in the future.
+	 */
+	delta = (s16)get_unaligned_be16(&data[4]);
+	if (!delta)
+		return 1;
+
+	/* Linux reports left as negative and right as positive. */
+	if (!hidpp->thumbwheel_positive_right)
+		delta = -delta;
+
+	if (hidpp_scroll_counter_handle_scroll(hidpp->input,
+			&hidpp->thumbwheel_counter, delta,
+			REL_HWHEEL_HI_RES, REL_HWHEEL))
+		input_sync(hidpp->input);
+	return 1;
+}
+
 /* -------------------------------------------------------------------------- */
 /* Generic HID++ devices                                                      */
 /* -------------------------------------------------------------------------- */
@@ -4323,6 +4480,12 @@ static int hidpp_raw_hidpp_event(struct hidpp_device *hidpp, u8 *data,
 			return ret;
 	}
 
+	if (hidpp->capabilities & HIDPP_CAPABILITY_HIDPP20_THUMBWHEEL) {
+		ret = hidpp20_thumbwheel_raw_event(hidpp, data, size);
+		if (ret != 0)
+			return ret;
+	}
+
 	return 0;
 }
 
@@ -4396,10 +4559,11 @@ static int hidpp_event(struct hid_device *hdev, struct hid_field *field,
 	 */
 	if (!(hidpp->capabilities & HIDPP_CAPABILITY_HI_RES_SCROLL)
 	    || value == 0 || hidpp->input == NULL
-	    || counter->wheel_multiplier == 0)
+	    || counter->scale_denominator == 0)
 		return 0;
 
-	hidpp_scroll_counter_handle_scroll(hidpp->input, counter, value);
+	hidpp_scroll_counter_handle_scroll(hidpp->input, counter, value,
+					   REL_WHEEL_HI_RES, REL_WHEEL);
 	return 1;
 }
 
@@ -4664,6 +4828,7 @@ static void hidpp_connect_event(struct work_struct *work)
 	hidpp_initialize_battery(hidpp);
 	if (!hid_is_usb(hidpp->hid_dev))
 		hidpp_initialize_hires_scroll(hidpp);
+	hidpp_initialize_thumbwheel_scroll(hidpp);
 
 	/* forward current battery state */
 	if (hidpp->capabilities & HIDPP_CAPABILITY_HIDPP10_BATTERY) {
@@ -4687,6 +4852,8 @@ static void hidpp_connect_event(struct work_struct *work)
 
 	if (hidpp->capabilities & HIDPP_CAPABILITY_HI_RES_SCROLL)
 		hi_res_scroll_enable(hidpp);
+	if (hidpp->capabilities & HIDPP_CAPABILITY_HIDPP20_THUMBWHEEL)
+		thumbwheel_scroll_enable(hidpp);
 
 	if (!(hidpp->quirks & HIDPP_QUIRK_DELAYED_INIT) || hidpp->delayed_input)
 		/* if the input nodes are already created, we can stop now */

-- 
2.55.0


  parent reply	other threads:[~2026-10-01 22:23 UTC|newest]

Thread overview: 5+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-10-01 22:23 [PATCH RFC 0/4] HID: logitech: add Bolt support with HID++ input handling Erik Håkansson
2026-10-01 22:23 ` [PATCH RFC 1/4] HID: logitech: add Bolt receiver support Erik Håkansson
2026-10-01 22:23 ` Erik Håkansson [this message]
2026-10-01 22:23 ` [PATCH RFC 3/4] HID: logitech: divert M650 side buttons over Bolt Erik Håkansson
2026-10-01 22:23 ` [PATCH RFC 4/4] HID: logitech-hidpp: support ratchet wheel Erik Håkansson

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=20261002-bolt-input-rfc-v1-2-333e1f350586@gmail.com \
    --to=erikhakan@gmail.com \
    --cc=bentiss@kernel.org \
    --cc=greyxor@protonmail.com \
    --cc=hadess@hadess.net \
    --cc=jikos@kernel.org \
    --cc=k8ie@mcld.eu \
    --cc=lains@riseup.net \
    --cc=linux-input@vger.kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=lovekeshsolanki00@gmail.com \
    --cc=oleksandr@natalenko.name \
    --cc=rafael@rcpassos.me \
    --cc=roman.stingler@gmail.com \
    --cc=xglooom@gmail.com \
    /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

all inboxes | Powered by JetHome®