HID: logitech-hidpp: add HID++ 2.0 reprogrammable button support

Some Logitech HID++ 2.0 mice can report diverted reprogrammable controls
through HID++ feature 0x1b04, SpecialKeysMseButtons / REPROG_CONTROLS_V4,
instead of the normal HID mouse report.

Add a quirk-gated event path for those controls. The handler temporarily
diverts verified per-product controls, parses divertedButtonsEvent as the
current pressed-control list, and reports the corresponding evdev key state
for every mapped control.

Keep the control mappings in per-product arrays so adding support for
another mouse does not change the evdev capabilities advertised by
already-supported devices.

Documentation for feature 0x1b04 describes divertedButtonsEvent as a list
of currently pressed diverted buttons, which is the event format handled
here.

Link: https://lekensteyn.nl/files/logitech/x1b04_specialkeysmsebuttons.html
Signed-off-by: Elliot Douglas <edouglas7358@gmail.com>
Signed-off-by: Jiri Kosina <jkosina@suse.com>
This commit is contained in:
Elliot Douglas
2026-07-04 16:10:34 -07:00
committed by Jiri Kosina
parent 6bad2e38fe
commit 5e21032ac9

View File

@@ -76,6 +76,7 @@ MODULE_PARM_DESC(disable_tap_to_click,
#define HIDPP_QUIRK_HI_RES_SCROLL_1P0 BIT(28)
#define HIDPP_QUIRK_WIRELESS_STATUS BIT(29)
#define HIDPP_QUIRK_RESET_HI_RES_SCROLL BIT(30)
#define HIDPP_QUIRK_HIDPP_REPROG_CONTROLS_BTNS BIT(31)
/* These are just aliases for now */
#define HIDPP_QUIRK_KBD_SCROLL_WHEEL HIDPP_QUIRK_HIDPP_WHEELS
@@ -178,6 +179,8 @@ struct hidpp_scroll_counter {
unsigned long long last_time;
};
struct hidpp_reprog_control_mapping;
struct hidpp_device {
struct hid_device *hid_dev;
struct input_dev *input;
@@ -205,6 +208,8 @@ struct hidpp_device {
struct hidpp_scroll_counter vertical_wheel_counter;
u8 wireless_feature_index;
u8 reprog_controls_feature_index;
const struct hidpp_reprog_control_mapping *reprog_controls;
int hires_wheel_multiplier;
u8 hires_wheel_feature_index;
@@ -3601,6 +3606,195 @@ static int hidpp10_extra_mouse_buttons_raw_event(struct hidpp_device *hidpp,
return 1;
}
/* -------------------------------------------------------------------------- */
/* HID++2.0 reprogrammable controls */
/* -------------------------------------------------------------------------- */
#define HIDPP_PAGE_REPROG_CONTROLS_V4 0x1b04
#define HIDPP_REPROG_CONTROLS_GET_COUNT 0x00
#define HIDPP_REPROG_CONTROLS_GET_CID_INFO 0x10
#define HIDPP_REPROG_CONTROLS_SET_CONTROL_REPORTING 0x30
#define HIDPP_REPROG_CONTROLS_FLAG_MOUSE BIT(0)
#define HIDPP_REPROG_CONTROLS_FLAG_DIVERT BIT(5)
#define HIDPP_REPROG_CONTROLS_TEMPORARY_DIVERTED BIT(0)
#define HIDPP_REPROG_CONTROLS_CHANGE_TEMPORARY_DIVERT BIT(1)
#define HIDPP_REPROG_CONTROLS_EVENT_DIVERTED 0x00
struct hidpp_reprog_control_mapping {
u16 control;
u16 code;
};
static const struct hidpp_reprog_control_mapping *
hidpp20_reprog_controls_get_mappings(struct hidpp_device *hidpp)
{
return NULL;
}
static int hidpp20_reprog_controls_get_count(struct hidpp_device *hidpp)
{
struct hidpp_report response;
u8 feature_index = hidpp->reprog_controls_feature_index;
u8 cmd = HIDPP_REPROG_CONTROLS_GET_COUNT;
int ret;
ret = hidpp_send_fap_command_sync(hidpp, feature_index, cmd, NULL, 0,
&response);
if (ret > 0)
return -EPROTO;
if (ret)
return ret;
return response.fap.params[0];
}
static int hidpp20_reprog_controls_get_cid_info(struct hidpp_device *hidpp,
u8 index, u16 *control,
u8 *flags)
{
struct hidpp_report response;
u8 feature_index = hidpp->reprog_controls_feature_index;
u8 cmd = HIDPP_REPROG_CONTROLS_GET_CID_INFO;
int ret;
ret = hidpp_send_fap_command_sync(hidpp, feature_index, cmd, &index,
sizeof(index), &response);
if (ret > 0)
return -EPROTO;
if (ret)
return ret;
*control = get_unaligned_be16(&response.fap.params[0]);
*flags = response.fap.params[4];
return 0;
}
static bool hidpp20_reprog_controls_find_control(struct hidpp_device *hidpp,
u16 control)
{
int count, ret;
u16 cid;
u8 flags;
int i;
count = hidpp20_reprog_controls_get_count(hidpp);
if (count < 0)
return false;
for (i = 0; i < count; i++) {
ret = hidpp20_reprog_controls_get_cid_info(hidpp, i, &cid,
&flags);
if (ret)
return false;
if (cid == control)
return (flags & HIDPP_REPROG_CONTROLS_FLAG_MOUSE) &&
(flags & HIDPP_REPROG_CONTROLS_FLAG_DIVERT);
}
return false;
}
static int hidpp20_reprog_controls_set_control_reporting(struct hidpp_device *hidpp,
u16 control, u8 flags)
{
struct hidpp_report response;
u8 params[5];
put_unaligned_be16(control, &params[0]);
params[2] = flags;
put_unaligned_be16(control, &params[3]);
return hidpp_send_fap_command_sync(hidpp,
hidpp->reprog_controls_feature_index,
HIDPP_REPROG_CONTROLS_SET_CONTROL_REPORTING,
params, sizeof(params), &response);
}
static void hidpp20_reprog_controls_connect(struct hidpp_device *hidpp)
{
const struct hidpp_reprog_control_mapping *mapping;
u8 flags = HIDPP_REPROG_CONTROLS_TEMPORARY_DIVERTED |
HIDPP_REPROG_CONTROLS_CHANGE_TEMPORARY_DIVERT;
if (!(hidpp->quirks & HIDPP_QUIRK_HIDPP_REPROG_CONTROLS_BTNS))
return;
if (!hidpp->reprog_controls)
return;
if (hidpp_root_get_feature(hidpp, HIDPP_PAGE_REPROG_CONTROLS_V4,
&hidpp->reprog_controls_feature_index))
return;
for (mapping = hidpp->reprog_controls; mapping->control; mapping++) {
if (!hidpp20_reprog_controls_find_control(hidpp, mapping->control))
continue;
hidpp20_reprog_controls_set_control_reporting(hidpp,
mapping->control,
flags);
}
}
static int hidpp20_reprog_controls_raw_event(struct hidpp_device *hidpp,
u8 *data, int size)
{
const struct hidpp_reprog_control_mapping *mapping;
struct hidpp_report *report = (struct hidpp_report *)data;
u16 controls[4];
bool pressed;
unsigned int i, j;
if (!(hidpp->quirks & HIDPP_QUIRK_HIDPP_REPROG_CONTROLS_BTNS) ||
!hidpp->input ||
!hidpp->reprog_controls ||
hidpp->reprog_controls_feature_index == 0xff)
return 0;
if (size < HIDPP_REPORT_LONG_LENGTH ||
report->fap.feature_index != hidpp->reprog_controls_feature_index ||
report->fap.funcindex_clientid != HIDPP_REPROG_CONTROLS_EVENT_DIVERTED)
return 0;
for (i = 0; i < ARRAY_SIZE(controls); i++)
controls[i] = get_unaligned_be16(&report->fap.params[i * 2]);
for (mapping = hidpp->reprog_controls; mapping->control; mapping++) {
pressed = false;
for (j = 0; j < ARRAY_SIZE(controls); j++) {
if (controls[j] == mapping->control) {
pressed = true;
break;
}
}
input_report_key(hidpp->input, mapping->code, pressed);
}
input_sync(hidpp->input);
return 1;
}
static void hidpp20_reprog_controls_populate_input(struct hidpp_device *hidpp,
struct input_dev *input_dev)
{
const struct hidpp_reprog_control_mapping *mapping;
if (!hidpp->reprog_controls)
return;
for (mapping = hidpp->reprog_controls; mapping->control; mapping++)
input_set_capability(input_dev, EV_KEY, mapping->code);
}
static void hidpp10_extra_mouse_buttons_populate_input(
struct hidpp_device *hidpp, struct input_dev *input_dev)
{
@@ -3859,6 +4053,9 @@ static void hidpp_populate_input(struct hidpp_device *hidpp,
if (hidpp->quirks & HIDPP_QUIRK_HIDPP_EXTRA_MOUSE_BTNS)
hidpp10_extra_mouse_buttons_populate_input(hidpp, input);
if (hidpp->quirks & HIDPP_QUIRK_HIDPP_REPROG_CONTROLS_BTNS)
hidpp20_reprog_controls_populate_input(hidpp, input);
}
static int hidpp_input_configured(struct hid_device *hdev,
@@ -3971,6 +4168,10 @@ static int hidpp_raw_hidpp_event(struct hidpp_device *hidpp, u8 *data,
return ret;
}
ret = hidpp20_reprog_controls_raw_event(hidpp, data, size);
if (ret != 0)
return ret;
if (hidpp->quirks & HIDPP_QUIRK_HIDPP_CONSUMER_VENDOR_KEYS) {
ret = hidpp10_consumer_keys_raw_event(hidpp, data, size);
if (ret != 0)
@@ -4264,6 +4465,8 @@ static void hidpp_connect_event(struct work_struct *work)
return;
}
hidpp20_reprog_controls_connect(hidpp);
if (hidpp->quirks & HIDPP_QUIRK_HIDPP_CONSUMER_VENDOR_KEYS) {
ret = hidpp10_consumer_keys_connect(hidpp);
if (ret)
@@ -4437,6 +4640,8 @@ static int hidpp_probe(struct hid_device *hdev, const struct hid_device_id *id)
hidpp->hid_dev = hdev;
hidpp->name = hdev->name;
hidpp->quirks = id->driver_data;
hidpp->reprog_controls_feature_index = 0xff;
hidpp->reprog_controls = hidpp20_reprog_controls_get_mappings(hidpp);
hid_set_drvdata(hdev, hidpp);
ret = hid_parse(hdev);