mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
* [PATCH RFC 0/4] HID: logitech: add Bolt support with HID++ input handling
@ 2026-10-01 22:23 Erik Håkansson
  2026-10-01 22:23 ` [PATCH RFC 1/4] HID: logitech: add Bolt receiver support Erik Håkansson
                   ` (3 more replies)
  0 siblings, 4 replies; 5+ messages in thread
From: Erik Håkansson @ 2026-10-01 22:23 UTC (permalink / raw)
  To: Filipe Laíns, Jiri Kosina, Benjamin Tissoires, Bastien Nocera
  Cc: Rafael Passos, Grégoire Stein, Alexey Zagorodnikov,
	Oleksandr Natalenko, Roman Stingler, Lovekesh Solanki,
	Kateřina Medvědová,
	linux-input, linux-kernel, Erik Håkansson

This series is posted as an RFC for review and hardware testing since the
previous patch had some issues and had to be reverted. It is not ready for
merging pending testing and discussion of defaults.
The series adds Bolt receiver support to the Logitech HID++ and DJ drivers,
with revised native input handling and HID++ wheel diversion.

Original patch: https://lore.kernel.org/linux-input/20260712003051.338194-1-erikhakan@gmail.com/

The original approach failed to use the correct descriptor for the virtual
Bolt mouse device, which may have caused mouse reports to be incorrectly
interpreted. In an attempt to fix that, the second approach was to not let
the DJ driver claim interfaces 0 and 1, and thus let the generic HID
handler handle native keyboard and mouse reports. This had the side effect
of incorrect scroll scaling for 0x2121 (hi-res scroll) devices, and
attempts to fix that caused various side-effects, especially when used
together with user-space settings for scrolling.
Attempts to fix everything failed to account for the fact that we were
treating Bolt devices differently than other non-DJ devices.

Further, some attempts of fixing tried to rely on storing user settings
on-device, but these do not seem to survive reconnect or turning off and on
when tested in the Bolt MX Keys or Lightspeed G502.

This series aims to fix all of this, while also preferring HID++ reports
for Bolt mice to allow for device identification, despite the fact that
Bolt receivers do not support DJ mode. Regular mouse and keyboard events
will still be native HID events and can't be identified per device.

- Handle Bolt mice the same way as e.g. Lightspeed mice are handled, i.e.
  let the DJ driver claim interfaces 0-2 and use the existing 16-bit mouse
  descriptor for the virtual mouse.
- Fix diverted 0x2121 HID++ wheel movement, including resolution and
  software-side inversion when scroll direction is inverted. Also, handle
  both low- and high-resolution reports coming over HID++. Bolt devices
  have HID++ diversion enabled by default.
- Add HID++ 0x2150 thumbwheel handling, with device-provided scaling and
  direction. Bolt devices have HID++ diversion enabled by default.
- Extend the M650 Back/Forward diversion quirk to devices connected through
  the Bolt receiver.
- Add HID++ 0x2130 ratchet-wheel handling. Bolt devices have HID++
  diversion enabled by default.

HID++ diversion supplies a device index, allowing supported wheel events to
be attributed and scaled per device. Ordinary native mouse and keyboard
reports still lack that index. Their source remains ambiguous when several
devices share a receiver; this series does not solve that limitation.

Non-Bolt wheel-reporting defaults remain unchanged, however the 0x2121
general behaviour has been changed. Instead of first setting
high-resolution mode and then attempting to fetch 0x2121 capabilities (and
thus scale) from the device, we now get the capabilities first.
Previously, in case of failure to fetch, high-resolution scroll reports
would be scaled as low-resolution, or whichever scale already existed. Now,
if we fail to fetch capabilities, we request low-resolution native HID and
set the scale to 1.

All settings are applied at device connect (or driver load), and userspace
may override afterwards. Solaar is known to apply its settings a few
seconds after the kernel driver is loaded for a device.
Userspace changes to wheel resolution, diversion and inversion should now
be handled correctly. These combinations still require hardware testing.

A risk with this series is that for testing I only have access to an MX
Keys for Business over Bolt and a G502 over Lightspeed. The series has been
tested with both and works as expected, but it needs testing with as many
devices as possible to ensure none of the previous issues remain.

The following items need to be tested:
1. First and foremost, the 16-bit descriptor finding was based on looking
   at the native descriptor from the Bolt receiver and not something I
   could test myself without a Bolt mouse.
   Hence, just regular mouse movement needs to be verified with a few
   different devices.
2. Further, scrolling with a 0x2121-supporting device (i.e. high-resolution
   scrolling) needs to be tested with Solaar set to all four combinations
   of HID++ diversion + resolution, and all four combinations of HID++
   diversion + scroll direction inversion.
3. MX Master 4 thumbwheel (0x2150, or any other mouse with this), diversion
   on and off in Solaar.
4. M650 Back/Forward clicks when connected over Bolt.
5. Ratchet wheel (0x2130, I think Signature M650 and M750 support this,
   from issue reports online) with diversion on and off in Solaar.
6. For all devices, testing so everything works after turning off and on,
   after reconnecting, after reboot, after switching to Bluetooth and back.
7. There was previously one report of scrambled keyboard input for a Logi
   POP Icon Keys when letting the keyboard interface be claimed by the DJ
   driver. This issue was never traced to a root cause and may have been
   unrelated. It has not been replicated on MX Keys. It should be retested.

Besides testing, a few decisions need to be made:

A. Currently, no HID++ diversion is enabled for keyboards, but at least MX
   Keys for Business supports diverting some keys over HID++. These include
   e.g. media keys, brightness control, etc. Some proprietary keys ONLY go
   over HID++ and do not support native HID at all.
   However, the use case for diverting only some keys seems small. Without
   diverting the entire keyboard, it can't be used for e.g. separate
   layout, or remapping all keys on only one keyboard, etc. The only
   possible use case is for special treatment of only those few divertable
   keys.
   However, the downside is that not all keycodes map to native HID
   keycodes so a list of all diverted key mappings needs to be maintained.
   Further, it might interfere with keyboards that support reprogramming
   keys.
   I could add it anyway though, either in this series or in a future
   patch?
   On the MX Keys S, a few keys seem to only work when diverted, or are
   diverted always, such as FN lock, mute microphone, backlight up/down,
   etc. These HID++ events currently have no handler in this driver.

B. Bolt devices now default to diverting the various scroll events to
   HID++. This is to be able to separate those events by originating
   device, in case their settings or support differ, so e.g. scroll
   resolution scaling will be applied correctly per device, instead of
   scaling different-resolution devices according to one device's scaling.
   But the use case of having multiple mice on the same Bolt receiver is
   rare, and perhaps it's better to let Bolt devices default to native HID
   and let the user override with Solaar if necessary?

   tshark captures of a Windows guest using MX Keys for Business suggest
   that the Windows driver enables HID++ diversion for keys, but this is
   not verified for mice.

Finally, thanks to Rafael Passos for the original Bolt HID++ wheel-event
work. The related Magnetar-OS patches were also used as inspiration:
https://github.com/Magnetar-OS/logitech-bolt-hidpp-dkms/tree/main/patches

Signed-off-by: Erik Håkansson <erikhakan@gmail.com>
---
Erik Håkansson (4):
      HID: logitech: add Bolt receiver support
      HID: logitech: handle HID++ thumbwheel reports
      HID: logitech: divert M650 side buttons over Bolt
      HID: logitech-hidpp: support ratchet wheel

 drivers/hid/hid-logitech-dj.c    |  33 ++-
 drivers/hid/hid-logitech-hidpp.c | 471 ++++++++++++++++++++++++++++++++++++---
 2 files changed, 464 insertions(+), 40 deletions(-)
---
base-commit: d72f75f1d9c038ead2588b42de6c2531e3b80bea
change-id: 20261001-bolt-input-rfc-9748607cc6b2

Best regards,
-- 
Erik Håkansson <erikhakan@gmail.com>


^ permalink raw reply	[flat|nested] 5+ messages in thread

* [PATCH RFC 1/4] HID: logitech: add Bolt receiver support
  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 ` Erik Håkansson
  2026-10-01 22:23 ` [PATCH RFC 2/4] HID: logitech: handle HID++ thumbwheel reports Erik Håkansson
                   ` (2 subsequent siblings)
  3 siblings, 0 replies; 5+ messages in thread
From: Erik Håkansson @ 2026-10-01 22:23 UTC (permalink / raw)
  To: Filipe Laíns, Jiri Kosina, Benjamin Tissoires, Bastien Nocera
  Cc: Rafael Passos, Grégoire Stein, Alexey Zagorodnikov,
	Oleksandr Natalenko, Roman Stingler, Lovekesh Solanki,
	Kateřina Medvědová,
	linux-input, linux-kernel, Erik Håkansson

Add Logitech Bolt receiver support to the Logitech HID receiver and HID++
drivers.

This allows Bolt-connected HID++ devices to expose battery information
through the kernel power_supply path, so userspace tools can report their
battery status with the correct device model.

Have hid-logitech-dj claim Bolt interfaces 0-2 and use the
mse_high_res_descriptor for virtual mice, instead of the default
mse_descriptor, to properly handle reports as 16-bit instead of 12-bit.

Since Bolt receivers do not support DJ mode, reroute supported reports for
Bolt devices over HID++, so device identification is possible through the
HID++ device index. Ordinary mouse and keyboard reports still use native
HID, and are not possible to divert to HID++, and since native HID reports
lack device index, it will still not be possible to identify which device
sent them.
Extend 0x2121 high resolution scroll to work over HID++, including software
inversion for inverted scrolling, and make Bolt devices default to HID++
diversion for 0x2121 reports.

Also, treat HIDPP_ERROR_CONNECT_FAIL like other disconnected-device errors
when retrieving protocol version to avoid protocol error messages when a
Bolt device powers off.

Tested with:
- Logitech MX Keys for Business via Bolt receiver

Signed-off-by: Erik Håkansson <erikhakan@gmail.com>
---
 drivers/hid/hid-logitech-dj.c    |  33 +++++++-
 drivers/hid/hid-logitech-hidpp.c | 160 ++++++++++++++++++++++++++++++++++-----
 2 files changed, 173 insertions(+), 20 deletions(-)

diff --git a/drivers/hid/hid-logitech-dj.c b/drivers/hid/hid-logitech-dj.c
index 390d106d5dc1..8a53e631f476 100644
--- a/drivers/hid/hid-logitech-dj.c
+++ b/drivers/hid/hid-logitech-dj.c
@@ -121,6 +121,7 @@ enum recvr_type {
 	recvr_type_27mhz,
 	recvr_type_bluetooth,
 	recvr_type_dinovo,
+	recvr_type_bolt,
 };
 
 struct dj_report {
@@ -637,8 +638,6 @@ static const u8 hid_reportid_size_map[NUMBER_OF_HID_REPORTS] = {
 };
 
 
-#define LOGITECH_DJ_INTERFACE_NUMBER 0x02
-
 static const struct hid_ll_driver logi_dj_ll_driver;
 
 static int logi_dj_recv_query_paired_devices(struct dj_receiver_dev *djrcv_dev);
@@ -1156,6 +1155,10 @@ static void logi_hidpp_recv_queue_notif(struct hid_device *hdev,
 		logi_hidpp_dev_conn_notif_equad(hdev, hidpp_report, &workitem);
 		workitem.reports_supported |= STD_KEYBOARD;
 		break;
+	case 0x10:
+		device_type = "Bolt";
+		logi_hidpp_dev_conn_notif_equad(hdev, hidpp_report, &workitem);
+		break;
 	}
 
 	/* custom receiver device (eg. powerplay) */
@@ -1540,7 +1543,8 @@ static int logi_dj_ll_parse(struct hid_device *hid)
 	if (djdev->reports_supported & STD_MOUSE) {
 		dbg_hid("%s: sending a mouse descriptor, reports_supported: %llx\n",
 			__func__, djdev->reports_supported);
-		if (djdev->dj_receiver_dev->type == recvr_type_gaming_hidpp ||
+		if (djdev->dj_receiver_dev->type == recvr_type_bolt ||
+		    djdev->dj_receiver_dev->type == recvr_type_gaming_hidpp ||
 		    djdev->dj_receiver_dev->type == recvr_type_mouse_only)
 			rdcat(rdesc, &rsize, mse_high_res_descriptor,
 			      sizeof(mse_high_res_descriptor));
@@ -1745,6 +1749,24 @@ static int logi_dj_hidpp_event(struct hid_device *hdev,
 
 	dj_dev = djrcv_dev->paired_dj_devices[device_index];
 
+	/*
+	 * Bolt receivers send explicit unpair notifications as HID++ events;
+	 * queue device removal when we receive one.
+	 */
+	if (djrcv_dev->type == recvr_type_bolt &&
+	    hidpp_report->report_id == REPORT_ID_HIDPP_SHORT &&
+	    hidpp_report->sub_id == REPORT_TYPE_NOTIF_DEVICE_UNPAIRED) {
+		struct dj_workitem workitem = {
+			.device_index = device_index,
+			.type = WORKITEM_TYPE_UNPAIRED,
+		};
+
+		kfifo_in(&djrcv_dev->notif_fifo, &workitem, sizeof(workitem));
+		schedule_work(&djrcv_dev->work);
+		spin_unlock_irqrestore(&djrcv_dev->lock, flags);
+		return false;
+	}
+
 	/*
 	 * With 27 MHz receivers, we do not get an explicit unpair event,
 	 * remove the old device if the user has paired a *different* device.
@@ -1894,6 +1916,7 @@ static int logi_dj_probe(struct hid_device *hdev,
 	case recvr_type_27mhz:		no_dj_interfaces = 2; break;
 	case recvr_type_bluetooth:	no_dj_interfaces = 2; break;
 	case recvr_type_dinovo:		no_dj_interfaces = 2; break;
+	case recvr_type_bolt:		no_dj_interfaces = 3; break;
 	}
 	if (hid_is_usb(hdev)) {
 		intf = to_usb_interface(hdev->dev.parent);
@@ -2103,6 +2126,10 @@ static const struct hid_device_id logi_dj_receivers[] = {
 	  HID_USB_DEVICE(USB_VENDOR_ID_LOGITECH,
 		USB_DEVICE_ID_LOGITECH_NANO_RECEIVER_LIGHTSPEED_1_3),
 	 .driver_data = recvr_type_gaming_hidpp_ls_1_3},
+	{ /* Logitech Bolt receiver (0xc548) */
+	  HID_USB_DEVICE(USB_VENDOR_ID_LOGITECH,
+			 USB_DEVICE_ID_LOGITECH_BOLT_RECEIVER),
+	 .driver_data = recvr_type_bolt},
 	{ /* Logitech lightspeed receiver (0xc54d) */
 	  HID_USB_DEVICE(USB_VENDOR_ID_LOGITECH,
 		USB_DEVICE_ID_LOGITECH_NANO_RECEIVER_LIGHTSPEED_1_4),
diff --git a/drivers/hid/hid-logitech-hidpp.c b/drivers/hid/hid-logitech-hidpp.c
index 9646380febac..ff6a1095e7ba 100644
--- a/drivers/hid/hid-logitech-hidpp.c
+++ b/drivers/hid/hid-logitech-hidpp.c
@@ -213,6 +213,8 @@ struct hidpp_device {
 
 	int hires_wheel_multiplier;
 	u8 hires_wheel_feature_index;
+	bool hires_wheel_has_invert;
+	bool hires_wheel_invert;
 
 	bool connected_once;
 };
@@ -988,7 +990,8 @@ static int hidpp_root_get_protocol_version(struct hidpp_device *hidpp)
 	}
 
 	/* the device might not be connected */
-	if (ret == HIDPP_ERROR_RESOURCE_ERROR ||
+	if (ret == HIDPP_ERROR_CONNECT_FAIL ||
+	    ret == HIDPP_ERROR_RESOURCE_ERROR ||
 	    ret == HIDPP_ERROR_UNKNOWN_DEVICE)
 		return -EIO;
 
@@ -2044,9 +2047,15 @@ static int hidpp_hrs_set_highres_scrolling_mode(struct hidpp_device *hidpp,
 
 #define CMD_HIRES_WHEEL_GET_WHEEL_CAPABILITY	0x00
 #define CMD_HIRES_WHEEL_SET_WHEEL_MODE		0x20
+#define EVENT_HIRES_WHEEL_MOVEMENT		0x00
+#define HIRES_WHEEL_CAPABILITY_HAS_INVERT	BIT(3)
+#define HIRES_WHEEL_MODE_USE_HIDPP		BIT(0)
+#define HIRES_WHEEL_MODE_HIGH_RESOLUTION	BIT(1)
+#define HIRES_WHEEL_MODE_INVERT			BIT(2)
+#define HIRES_WHEEL_EVENT_HIGH_RESOLUTION	BIT(4)
 
 static int hidpp_hrw_get_wheel_capability(struct hidpp_device *hidpp,
-	u8 *multiplier)
+	u8 *multiplier, bool *has_invert)
 {
 	u8 feature_index;
 	int ret;
@@ -2064,6 +2073,8 @@ static int hidpp_hrw_get_wheel_capability(struct hidpp_device *hidpp,
 		goto return_default;
 
 	*multiplier = response.fap.params[0];
+	*has_invert = response.fap.params[1] &
+		HIRES_WHEEL_CAPABILITY_HAS_INVERT;
 	return 0;
 return_default:
 	hid_warn(hidpp->hid_dev,
@@ -2084,9 +2095,9 @@ static int hidpp_hrw_set_wheel_mode(struct hidpp_device *hidpp, bool invert,
 	if (ret)
 		return ret;
 
-	params[0] = (invert          ? BIT(2) : 0) |
-		    (high_resolution ? BIT(1) : 0) |
-		    (use_hidpp       ? BIT(0) : 0);
+	params[0] = (invert          ? HIRES_WHEEL_MODE_INVERT : 0) |
+		    (high_resolution ? HIRES_WHEEL_MODE_HIGH_RESOLUTION : 0) |
+		    (use_hidpp       ? HIRES_WHEEL_MODE_USE_HIDPP : 0);
 
 	return hidpp_send_fap_command_sync(hidpp, feature_index,
 					   CMD_HIRES_WHEEL_SET_WHEEL_MODE,
@@ -3903,15 +3914,58 @@ static int hidpp10_consumer_keys_raw_event(struct hidpp_device *hidpp,
 /* High-resolution scroll wheels                                              */
 /* -------------------------------------------------------------------------- */
 
+static bool hidpp_is_bolt_child(struct hid_device *hdev)
+{
+	struct device *parent = hdev->dev.parent;
+	struct hid_device *receiver_hdev;
+
+	if (hdev->group != HID_GROUP_LOGITECH_DJ_DEVICE || !parent)
+		return false;
+
+	receiver_hdev = to_hid_device(parent);
+	return receiver_hdev->vendor == USB_VENDOR_ID_LOGITECH &&
+	       receiver_hdev->product == USB_DEVICE_ID_LOGITECH_BOLT_RECEIVER;
+}
+
+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 =
+		(high_resolution && hidpp->hires_wheel_multiplier > 0) ?
+		hidpp->hires_wheel_multiplier : 1;
+}
+
 static int hi_res_scroll_enable(struct hidpp_device *hidpp)
 {
-	int ret;
+	int ret, fallback_ret;
 	u8 multiplier = 1;
+	bool has_invert = false;
 
 	if (hidpp->capabilities & HIDPP_CAPABILITY_HIDPP20_HI_RES_WHEEL) {
-		ret = hidpp_hrw_set_wheel_mode(hidpp, false, true, false);
-		if (ret == 0)
-			ret = hidpp_hrw_get_wheel_capability(hidpp, &multiplier);
+		/* Check capabilities before setting mode, to guard against
+		 * enabling high res and then failing to read multiplier.
+		 */
+		ret = hidpp_hrw_get_wheel_capability(hidpp, &multiplier,
+						     &has_invert);
+		if (ret) {
+			/* Avoid diverting wheel reports without a known
+			 * multiplier.
+			 */
+			fallback_ret = hidpp_hrw_set_wheel_mode(hidpp, false,
+							 false, false);
+			if (fallback_ret)
+				hid_dbg(hidpp->hid_dev,
+					"Could not restore native wheel reporting: %d\n",
+					fallback_ret);
+			hidpp->hires_wheel_multiplier = 1;
+			hidpp->hires_wheel_has_invert = false;
+			hidpp_hrw_update_wheel_mode(hidpp, false, false);
+			return ret;
+		}
+		ret = hidpp_hrw_set_wheel_mode(hidpp, false, true,
+					      hidpp_is_bolt_child(
+					       hidpp->hid_dev));
 	} else if (hidpp->capabilities & HIDPP_CAPABILITY_HIDPP20_HI_RES_SCROLL) {
 		ret = hidpp_hrs_set_highres_scrolling_mode(hidpp, true,
 							   &multiplier);
@@ -3933,6 +3987,10 @@ 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);
+	}
 	hid_dbg(hidpp->hid_dev, "wheel multiplier = %d\n", multiplier);
 	return 0;
 }
@@ -3975,6 +4033,10 @@ static int hidpp_initialize_hires_scroll(struct hidpp_device *hidpp)
 	return 0;
 }
 
+/*
+ * The low nibble of data[3] is the software ID: 0 for wheel
+ * notifications, nonzero and echoed from the request for replies.
+ */
 static int hidpp20_hires_wheel_raw_event(struct hidpp_device *hidpp,
 						u8 *data, int size)
 {
@@ -3988,12 +4050,47 @@ static int hidpp20_hires_wheel_raw_event(struct hidpp_device *hidpp,
 	    data[2] != hidpp->hires_wheel_feature_index)
 		return 0;
 
+	/* This checks only the function nibble, not software ID, so in theory
+	 * an event with ID 0x20 would also apply here, but there is none in
+	 * the 0x2121 specification.
+	 */
 	if ((data[3] & 0xf0) == CMD_HIRES_WHEEL_SET_WHEEL_MODE) {
 		u8 mode = data[4];
-		bool hires = (mode & 0x02) != 0;
-		int new_multiplier = (hires && hidpp->hires_wheel_multiplier > 0)
-			? hidpp->hires_wheel_multiplier : 1;
-		hidpp->vertical_wheel_counter.wheel_multiplier = new_multiplier;
+
+		hidpp_hrw_update_wheel_mode(hidpp,
+			mode & HIRES_WHEEL_MODE_INVERT,
+			mode & HIRES_WHEEL_MODE_HIGH_RESOLUTION);
+		return 1;
+	}
+
+	/* Match wheelMovement (function 0, software ID 0). */
+	if (data[3] == EVENT_HIRES_WHEEL_MOVEMENT) {
+		int delta;
+
+		if (size < 7 || !hidpp->input ||
+		    hidpp->hires_wheel_multiplier <= 0)
+			return 1;
+
+		/* The event flag describes whether the event is high resolution
+		 * or not.
+		 */
+		hidpp->vertical_wheel_counter.wheel_multiplier =
+			(data[4] & HIRES_WHEEL_EVENT_HIGH_RESOLUTION) ?
+			hidpp->hires_wheel_multiplier : 1;
+
+		delta = (s16)get_unaligned_be16(&data[5]);
+		if (!delta)
+			return 1;
+
+		/* HID++ 2.0 does not invert scroll regardless of setting,
+		 * hence do so in driver
+		 */
+		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);
 		return 1;
 	}
 
@@ -4295,7 +4392,7 @@ static int hidpp_event(struct hid_device *hdev, struct hid_field *field,
 	/* A scroll event may occur before the multiplier has been retrieved or
 	 * the input device set, or high-res scroll enabling may fail. In such
 	 * cases we must return early (falling back to default behaviour) to
-	 * avoid a crash in hidpp_scroll_counter_handle_scroll.
+	 * avoid consuming an event that we cannot scale.
 	 */
 	if (!(hidpp->capabilities & HIDPP_CAPABILITY_HI_RES_SCROLL)
 	    || value == 0 || hidpp->input == NULL
@@ -4404,8 +4501,37 @@ static int hidpp_initialize_battery(struct hidpp_device *hidpp)
 	return ret;
 }
 
+static int hidpp_bolt_init(struct hidpp_device *hidpp)
+{
+	struct hid_device *hdev = hidpp->hid_dev;
+	char *name;
+	int ret;
+
+	ret = hidpp_serial_init(hidpp);
+	if (ret)
+		return ret;
+
+	name = hidpp_get_device_name(hidpp);
+	if (!name)
+		return -EIO;
+
+	snprintf(hdev->name, sizeof(hdev->name), "%s", name);
+	dbg_hid("HID++ Bolt: Got name: %s\n", name);
+
+	kfree(name);
+	return 0;
+}
+
+static int hidpp_receiver_init(struct hidpp_device *hidpp)
+{
+	if (hidpp_is_bolt_child(hidpp->hid_dev))
+		return hidpp_bolt_init(hidpp);
+
+	return hidpp_unifying_init(hidpp);
+}
+
 /* Get name + serial for USB and Bluetooth HID++ devices */
-static void hidpp_non_unifying_init(struct hidpp_device *hidpp)
+static void hidpp_non_receiver_init(struct hidpp_device *hidpp)
 {
 	struct hid_device *hdev = hidpp->hid_dev;
 	char *name;
@@ -4757,9 +4883,9 @@ static int hidpp_probe(struct hid_device *hdev, const struct hid_device_id *id)
 
 	/* Get name + serial, store in hdev->name + hdev->uniq */
 	if (id->group == HID_GROUP_LOGITECH_DJ_DEVICE)
-		hidpp_unifying_init(hidpp);
+		hidpp_receiver_init(hidpp);
 	else
-		hidpp_non_unifying_init(hidpp);
+		hidpp_non_receiver_init(hidpp);
 
 	if (hidpp->quirks & HIDPP_QUIRK_DELAYED_INIT)
 		connect_mask &= ~HID_CONNECT_HIDINPUT;

-- 
2.55.0


^ permalink raw reply	[flat|nested] 5+ messages in thread

* [PATCH RFC 2/4] HID: logitech: handle HID++ thumbwheel reports
  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
  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
  3 siblings, 0 replies; 5+ messages in thread
From: Erik Håkansson @ 2026-10-01 22:23 UTC (permalink / raw)
  To: Filipe Laíns, Jiri Kosina, Benjamin Tissoires, Bastien Nocera
  Cc: Rafael Passos, Grégoire Stein, Alexey Zagorodnikov,
	Oleksandr Natalenko, Roman Stingler, Lovekesh Solanki,
	Kateřina Medvědová,
	linux-input, linux-kernel, Erik Håkansson

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


^ permalink raw reply	[flat|nested] 5+ messages in thread

* [PATCH RFC 3/4] HID: logitech: divert M650 side buttons over Bolt
  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 ` [PATCH RFC 2/4] HID: logitech: handle HID++ thumbwheel reports Erik Håkansson
@ 2026-10-01 22:23 ` Erik Håkansson
  2026-10-01 22:23 ` [PATCH RFC 4/4] HID: logitech-hidpp: support ratchet wheel Erik Håkansson
  3 siblings, 0 replies; 5+ messages in thread
From: Erik Håkansson @ 2026-10-01 22:23 UTC (permalink / raw)
  To: Filipe Laíns, Jiri Kosina, Benjamin Tissoires, Bastien Nocera
  Cc: Rafael Passos, Grégoire Stein, Alexey Zagorodnikov,
	Oleksandr Natalenko, Roman Stingler, Lovekesh Solanki,
	Kateřina Medvědová,
	linux-input, linux-kernel, Erik Håkansson

The M650 uses the 0x1B04 reprogrammable-controls handler for Back and
Forward over Bluetooth by assigning a quirk. Make sure M650 gets the same
quirk when connected through a Bolt receiver too.

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

diff --git a/drivers/hid/hid-logitech-hidpp.c b/drivers/hid/hid-logitech-hidpp.c
index 464685ad21bb..9217ad4a97f4 100644
--- a/drivers/hid/hid-logitech-hidpp.c
+++ b/drivers/hid/hid-logitech-hidpp.c
@@ -5150,6 +5150,9 @@ static const struct hid_device_id hidpp_devices[] = {
 	{ /* Logitech G502 Lightspeed Wireless Gaming Mouse */
 	  LDJ_DEVICE(0x407f),
 	  .driver_data = HIDPP_QUIRK_RESET_HI_RES_SCROLL },
+	{ /* Signature M650 over Bolt */
+	  LDJ_DEVICE(HIDPP_PRODUCT_SIGNATURE_M650),
+	  .driver_data = HIDPP_QUIRK_HIDPP_REPROG_CONTROLS_BTNS },
 
 	{ LDJ_DEVICE(HID_ANY_ID) },
 

-- 
2.55.0


^ permalink raw reply	[flat|nested] 5+ messages in thread

* [PATCH RFC 4/4] HID: logitech-hidpp: support ratchet wheel
  2026-10-01 22:23 [PATCH RFC 0/4] HID: logitech: add Bolt support with HID++ input handling Erik Håkansson
                   ` (2 preceding siblings ...)
  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 ` Erik Håkansson
  3 siblings, 0 replies; 5+ messages in thread
From: Erik Håkansson @ 2026-10-01 22:23 UTC (permalink / raw)
  To: Filipe Laíns, Jiri Kosina, Benjamin Tissoires, Bastien Nocera
  Cc: Rafael Passos, Grégoire Stein, Alexey Zagorodnikov,
	Oleksandr Natalenko, Roman Stingler, Lovekesh Solanki,
	Kateřina Medvědová,
	linux-input, linux-kernel, Erik Håkansson

Detect the 0x2130 Ratchet wheel feature and handle its HID++ scroll events.
Enable HID++ wheel diversion by default for Bolt-connected devices, but
leave other devices unchanged.

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

diff --git a/drivers/hid/hid-logitech-hidpp.c b/drivers/hid/hid-logitech-hidpp.c
index 9217ad4a97f4..6195080505cb 100644
--- a/drivers/hid/hid-logitech-hidpp.c
+++ b/drivers/hid/hid-logitech-hidpp.c
@@ -101,6 +101,7 @@ MODULE_PARM_DESC(disable_tap_to_click,
 #define HIDPP_CAPABILITY_HIDPP10_FAST_SCROLL	BIT(9)
 #define HIDPP_CAPABILITY_ADC_MEASUREMENT	BIT(10)
 #define HIDPP_CAPABILITY_HIDPP20_THUMBWHEEL	BIT(11)
+#define HIDPP_CAPABILITY_HIDPP20_RATCHET_WHEEL	BIT(12)
 
 #define lg_map_key_clear(c)  hid_map_usage_clear(hi, usage, bit, max, EV_KEY, (c))
 
@@ -225,6 +226,9 @@ struct hidpp_device {
 	u8 hires_wheel_feature_index;
 	bool hires_wheel_has_invert;
 	bool hires_wheel_invert;
+
+	u8 ratchet_wheel_feature_index;
+
 	u8 thumbwheel_feature_index;
 	bool thumbwheel_positive_right;
 
@@ -2131,6 +2135,27 @@ static int hidpp_hrw_set_wheel_mode(struct hidpp_device *hidpp, bool invert,
 					   params, sizeof(params), &response);
 }
 
+/* -------------------------------------------------------------------------- */
+/* 0x2130: Ratchet Wheel                                                      */
+/* -------------------------------------------------------------------------- */
+
+#define HIDPP_PAGE_RATCHET_WHEEL		0x2130
+#define CMD_RATCHET_WHEEL_SET_WHEEL_MODE	0x10
+#define EVENT_RATCHET_WHEEL_MOVEMENT		0x00
+#define RATCHET_WHEEL_MODE_USE_HIDPP		BIT(0)
+
+static int hidpp_ratchet_wheel_set_mode(struct hidpp_device *hidpp,
+					bool use_hidpp)
+{
+	struct hidpp_report response;
+	u8 mode = use_hidpp ? RATCHET_WHEEL_MODE_USE_HIDPP : 0;
+
+	return hidpp_send_fap_command_sync(hidpp,
+					   hidpp->ratchet_wheel_feature_index,
+					   CMD_RATCHET_WHEEL_SET_WHEEL_MODE,
+					   &mode, sizeof(mode), &response);
+}
+
 /* -------------------------------------------------------------------------- */
 /* 0x2150: Thumbwheel                                                         */
 /* -------------------------------------------------------------------------- */
@@ -4105,6 +4130,35 @@ static int hidpp_initialize_hires_scroll(struct hidpp_device *hidpp)
 	return 0;
 }
 
+static void ratchet_wheel_scroll_enable(struct hidpp_device *hidpp)
+{
+	int ret;
+
+	if (!hidpp_is_bolt_child(hidpp->hid_dev))
+		return;
+
+	ret = hidpp_ratchet_wheel_set_mode(hidpp, true);
+	if (ret)
+		hid_dbg(hidpp->hid_dev,
+			"Could not divert ratchet wheel reports: %d\n", ret);
+}
+
+static void hidpp_initialize_ratchet_wheel(struct hidpp_device *hidpp)
+{
+	int ret;
+
+	hidpp->capabilities &= ~HIDPP_CAPABILITY_HIDPP20_RATCHET_WHEEL;
+	hidpp->ratchet_wheel_feature_index = 0xff;
+	if (hidpp->protocol_major < 2)
+		return;
+
+	ret = hidpp_root_get_feature(hidpp, HIDPP_PAGE_RATCHET_WHEEL,
+				     &hidpp->ratchet_wheel_feature_index);
+	if (ret)
+		return;
+	hidpp->capabilities |= HIDPP_CAPABILITY_HIDPP20_RATCHET_WHEEL;
+}
+
 static void thumbwheel_scroll_enable(struct hidpp_device *hidpp)
 {
 	bool divert;
@@ -4213,6 +4267,44 @@ static int hidpp20_hires_wheel_raw_event(struct hidpp_device *hidpp,
 	return 0;
 }
 
+static int hidpp20_ratchet_wheel_raw_event(struct hidpp_device *hidpp,
+					   u8 *data, int size)
+{
+	int delta_v, delta_h;
+
+	if (hidpp->ratchet_wheel_feature_index == 0xff)
+		return 0;
+
+	if (size < 6)
+		return 0;
+
+	if (data[0] != REPORT_ID_HIDPP_LONG ||
+	    data[2] != hidpp->ratchet_wheel_feature_index)
+		return 0;
+
+	if (data[3] != EVENT_RATCHET_WHEEL_MOVEMENT)
+		return 0;
+
+	if (!hidpp->input)
+		return 1;
+
+	delta_v = (s8)data[4];
+	delta_h = (s8)data[5];
+	if (delta_v) {
+		input_report_rel(hidpp->input, REL_WHEEL_HI_RES, delta_v * 120);
+		input_report_rel(hidpp->input, REL_WHEEL, delta_v);
+	}
+	if (delta_h) {
+		input_report_rel(hidpp->input, REL_HWHEEL_HI_RES,
+				 delta_h * 120);
+		input_report_rel(hidpp->input, REL_HWHEEL, delta_h);
+	}
+	if (delta_v || delta_h)
+		input_sync(hidpp->input);
+
+	return 1;
+}
+
 static int hidpp20_thumbwheel_raw_event(struct hidpp_device *hidpp,
 					       u8 *data, int size)
 {
@@ -4480,6 +4572,12 @@ static int hidpp_raw_hidpp_event(struct hidpp_device *hidpp, u8 *data,
 			return ret;
 	}
 
+	if (hidpp->capabilities & HIDPP_CAPABILITY_HIDPP20_RATCHET_WHEEL) {
+		ret = hidpp20_ratchet_wheel_raw_event(hidpp, data, size);
+		if (ret != 0)
+			return ret;
+	}
+
 	if (hidpp->capabilities & HIDPP_CAPABILITY_HIDPP20_THUMBWHEEL) {
 		ret = hidpp20_thumbwheel_raw_event(hidpp, data, size);
 		if (ret != 0)
@@ -4828,6 +4926,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_ratchet_wheel(hidpp);
 	hidpp_initialize_thumbwheel_scroll(hidpp);
 
 	/* forward current battery state */
@@ -4852,6 +4951,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_RATCHET_WHEEL)
+		ratchet_wheel_scroll_enable(hidpp);
 	if (hidpp->capabilities & HIDPP_CAPABILITY_HIDPP20_THUMBWHEEL)
 		thumbwheel_scroll_enable(hidpp);
 

-- 
2.55.0


^ permalink raw reply	[flat|nested] 5+ messages in thread

end of thread, other threads:[~2026-10-01 22:23 UTC | newest]

Thread overview: 5+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
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 ` [PATCH RFC 2/4] HID: logitech: handle HID++ thumbwheel reports Erik Håkansson
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

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®