HID: logitech: add Bolt receiver support for Logitech HID++ devices

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

Handle Bolt receiver notifications in hid-logitech-dj and add a
Bolt-specific initialization path in hid-logitech-hidpp, separate from
the existing Unifying receiver path.

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.

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>
Signed-off-by: Jiri Kosina <jkosina@suse.com>
This commit is contained in:
Erik Håkansson
2026-08-10 22:56:34 +02:00
committed by Jiri Kosina
parent 81a2f7801c
commit 022eb347ff
2 changed files with 91 additions and 8 deletions

View File

@@ -121,6 +121,7 @@ enum recvr_type {
recvr_type_27mhz,
recvr_type_bluetooth,
recvr_type_dinovo,
recvr_type_bolt,
};
struct dj_report {
@@ -1156,6 +1157,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) */
@@ -1745,6 +1750,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.
@@ -1884,6 +1907,9 @@ static int logi_dj_probe(struct hid_device *hdev,
* treat these as logitech-dj interfaces then this causes input events
* reported through this extra interface to not be reported correctly.
* To avoid this, we treat these as generic-hid devices.
*
* Bolt receivers only use LOGITECH_DJ_INTERFACE_NUMBER for receiver
* reporting. Treat all other Bolt interfaces as generic-hid devices.
*/
switch (id->driver_data) {
case recvr_type_dj: no_dj_interfaces = 3; break;
@@ -1897,10 +1923,20 @@ static int logi_dj_probe(struct hid_device *hdev,
}
if (hid_is_usb(hdev)) {
intf = to_usb_interface(hdev->dev.parent);
if (intf && intf->altsetting->desc.bInterfaceNumber >=
no_dj_interfaces) {
hdev->quirks |= HID_QUIRK_INPUT_PER_APP;
return hid_hw_start(hdev, HID_CONNECT_DEFAULT);
if (intf) {
bool generic_hid_interface;
if (id->driver_data == recvr_type_bolt)
generic_hid_interface =
intf->altsetting->desc.bInterfaceNumber !=
LOGITECH_DJ_INTERFACE_NUMBER;
else
generic_hid_interface =
intf->altsetting->desc.bInterfaceNumber >= no_dj_interfaces;
if (generic_hid_interface) {
hdev->quirks |= HID_QUIRK_INPUT_PER_APP;
return hid_hw_start(hdev, HID_CONNECT_DEFAULT);
}
}
}
@@ -2103,6 +2139,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),

View File

@@ -988,7 +988,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;
@@ -4378,8 +4379,50 @@ static int hidpp_initialize_battery(struct hidpp_device *hidpp)
return ret;
}
static bool hidpp_is_bolt_child(struct hid_device *hdev)
{
struct device *parent = hdev->dev.parent;
struct hid_device *receiver_hdev;
if (!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 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;
@@ -4731,9 +4774,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;