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 1/4] HID: logitech: add Bolt receiver support
Date: Fri, 02 Oct 2026 00:23:35 +0200	[thread overview]
Message-ID: <20261002-bolt-input-rfc-v1-1-333e1f350586@gmail.com> (raw)
In-Reply-To: <20261002-bolt-input-rfc-v1-0-333e1f350586@gmail.com>

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


  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 ` Erik Håkansson [this message]
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

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-1-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®