* [PATCH 0/2] HID: logitech-hidpp: fix Signature M650 side button timing
@ 2026-06-13 17:51 Elliot Douglas
2026-06-13 17:51 ` [PATCH 1/2] HID: logitech-hidpp: add HID++ 2.0 reprogrammable button support Elliot Douglas
` (2 more replies)
0 siblings, 3 replies; 11+ messages in thread
From: Elliot Douglas @ 2026-06-13 17:51 UTC (permalink / raw)
To: linux-input; +Cc: lains, hadess, jikos, bentiss, linux-kernel, edouglas7358
The Logitech Signature M650 over Bluetooth exposes its side buttons in the
normal mouse report, but the reported BTN_SIDE/BTN_EXTRA events are short
click-like events emitted around button release rather than physical
press/release events with the real hold duration. The device appears to reserve
the held side-button state for a built-in gesture mode: holding a side button
long enough, or holding it while using the wheel for horizontal scrolling, can
mean the normal mouse report never emits a usable side-button press at all.
That makes the buttons unusable for standard Linux hold actions such as
push-to-talk, drag modifiers, or remapping rules that depend on key-up timing.
When HID++ 2.0 feature 0x1b04, SpecialKeysMseButtons /
REPROG_CONTROLS_V4, temporarily diverts the same controls, the device sends
diverted-control notifications with real press and release timing. This series
adds quirk-gated support for those notifications and enables it for the
Bluetooth Signature M650.
Before enabling diversion, the driver verifies that each mapped control is
present in the device's HID++ control table and is advertised as a divertable
mouse control.
The diverted M650 controls are reported as BTN_BACK and BTN_FORWARD. Logitech's
Signature M650 getting-started page labels these physical controls as
Back/Forward buttons and describes their default page-navigation behavior:
https://support.logi.com/hc/en-nz/articles/4414473810583-Getting-Started-Signature-M650
The reprogrammable-control support is per-product and parses the full HID++
divertedButtonsEvent pressed-control list, so it can support devices with more
buttons without relying on a single last-control release heuristic. Only the
Signature M650 opts in for now. Other Logitech devices should only be enabled
after their HID++ control IDs and divertedButtonsEvent behavior are captured
and verified.
There is evidence that this is not unique to the M650. A prior MX Anywhere 3
patch used the same HID++ feature to fix thumb buttons that only activated on
release, and Logitech documents side-button + wheel horizontal scrolling for
both the MX Anywhere 3/3S and Signature M650. Solaar's device reports and rules
documentation also show HID++ divertable back/forward controls on MX Master 3
and MX Master 3S class devices. This series remains conservative and only
enables the device tested here.
Tested with a Logitech Signature M650 L over Bluetooth, HID ID
0005:046D:B02A. Baseline evtest showed short release-time BTN_SIDE/BTN_EXTRA
events. Earlier local testing of the same HID++ diversion path showed real
hold-duration press/release events, including holds longer than 4 seconds for
both buttons.
Elliot Douglas (2):
HID: logitech-hidpp: add HID++ 2.0 reprogrammable button support
HID: logitech-hidpp: enable reprogrammable buttons on Signature M650
drivers/hid/hid-logitech-hidpp.c | 236 ++++++++++++++++++++++++++++++-
1 file changed, 235 insertions(+), 1 deletion(-)
base-commit: f0866517be9345d8245d32b722574b8aecccb348
--
2.54.0
^ permalink raw reply [flat|nested] 11+ messages in thread
* [PATCH 1/2] HID: logitech-hidpp: add HID++ 2.0 reprogrammable button support
2026-06-13 17:51 [PATCH 0/2] HID: logitech-hidpp: fix Signature M650 side button timing Elliot Douglas
@ 2026-06-13 17:51 ` Elliot Douglas
2026-06-17 10:28 ` Bastien Nocera
2026-06-13 17:51 ` [PATCH 2/2] HID: logitech-hidpp: enable reprogrammable buttons on Signature M650 Elliot Douglas
2026-07-04 23:10 ` [PATCH v2 0/2] HID: logitech-hidpp: fix Signature M650 side button timing Elliot Douglas
2 siblings, 1 reply; 11+ messages in thread
From: Elliot Douglas @ 2026-06-13 17:51 UTC (permalink / raw)
To: linux-input; +Cc: lains, hadess, jikos, bentiss, linux-kernel, edouglas7358
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 profiles 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>
---
drivers/hid/hid-logitech-hidpp.c | 215 +++++++++++++++++++++++++++++++
1 file changed, 215 insertions(+)
diff --git a/drivers/hid/hid-logitech-hidpp.c b/drivers/hid/hid-logitech-hidpp.c
index 70ba1a5e40d8..24c9cfaa4f37 100644
--- a/drivers/hid/hid-logitech-hidpp.c
+++ b/drivers/hid/hid-logitech-hidpp.c
@@ -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
@@ -205,6 +206,7 @@ struct hidpp_device {
struct hidpp_scroll_counter vertical_wheel_counter;
u8 wireless_feature_index;
+ u8 reprog_controls_feature_index;
int hires_wheel_multiplier;
u8 hires_wheel_feature_index;
@@ -3601,6 +3603,209 @@ 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;
+};
+
+struct hidpp_reprog_controls_profile {
+ const struct hidpp_reprog_control_mapping *mappings;
+ unsigned int mapping_count;
+};
+
+static const struct hidpp_reprog_controls_profile *
+hidpp20_reprog_controls_get_profile(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, ¶ms[0]);
+ params[2] = flags;
+ put_unaligned_be16(control, ¶ms[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_controls_profile *profile;
+ u8 flags = HIDPP_REPROG_CONTROLS_TEMPORARY_DIVERTED |
+ HIDPP_REPROG_CONTROLS_CHANGE_TEMPORARY_DIVERT;
+ unsigned int i;
+
+ if (!(hidpp->quirks & HIDPP_QUIRK_HIDPP_REPROG_CONTROLS_BTNS))
+ return;
+
+ profile = hidpp20_reprog_controls_get_profile(hidpp);
+ if (!profile)
+ return;
+
+ if (hidpp_root_get_feature(hidpp, HIDPP_PAGE_REPROG_CONTROLS_V4,
+ &hidpp->reprog_controls_feature_index))
+ return;
+
+ for (i = 0; i < profile->mapping_count; i++) {
+ u16 control = profile->mappings[i].control;
+
+ if (!hidpp20_reprog_controls_find_control(hidpp, control))
+ continue;
+
+ hidpp20_reprog_controls_set_control_reporting(hidpp, control, flags);
+ }
+}
+
+static int hidpp20_reprog_controls_raw_event(struct hidpp_device *hidpp,
+ u8 *data, int size)
+{
+ const struct hidpp_reprog_controls_profile *profile;
+ 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_feature_index == 0xff)
+ return 0;
+
+ profile = hidpp20_reprog_controls_get_profile(hidpp);
+ if (!profile)
+ 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 (i = 0; i < profile->mapping_count; i++) {
+ mapping = &profile->mappings[i];
+ 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_controls_profile *profile;
+ unsigned int i;
+
+ profile = hidpp20_reprog_controls_get_profile(hidpp);
+ if (!profile)
+ return;
+
+ for (i = 0; i < profile->mapping_count; i++)
+ input_set_capability(input_dev, EV_KEY, profile->mappings[i].code);
+}
+
static void hidpp10_extra_mouse_buttons_populate_input(
struct hidpp_device *hidpp, struct input_dev *input_dev)
{
@@ -3859,6 +4064,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 +4179,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 +4476,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)
@@ -4436,6 +4650,7 @@ 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;
hid_set_drvdata(hdev, hidpp);
ret = hid_parse(hdev);
--
2.54.0
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [PATCH 2/2] HID: logitech-hidpp: enable reprogrammable buttons on Signature M650
2026-06-13 17:51 [PATCH 0/2] HID: logitech-hidpp: fix Signature M650 side button timing Elliot Douglas
2026-06-13 17:51 ` [PATCH 1/2] HID: logitech-hidpp: add HID++ 2.0 reprogrammable button support Elliot Douglas
@ 2026-06-13 17:51 ` Elliot Douglas
2026-06-17 10:28 ` Bastien Nocera
2026-07-04 23:10 ` [PATCH v2 0/2] HID: logitech-hidpp: fix Signature M650 side button timing Elliot Douglas
2 siblings, 1 reply; 11+ messages in thread
From: Elliot Douglas @ 2026-06-13 17:51 UTC (permalink / raw)
To: linux-input; +Cc: lains, hadess, jikos, bentiss, linux-kernel, edouglas7358
The Bluetooth Signature M650 exposes its side buttons through the normal
mouse report, but the observed events are short click-like events emitted
around release rather than physical press/release state.
The device appears to use the held side-button state for its built-in
gesture and side-button + wheel horizontal-scroll mode. As a result,
holding a side button long enough can prevent the normal mouse report from
emitting a usable button event at all.
HID++ REPROG_CONTROLS_V4 diversion for control IDs 0x0053 and 0x0056
provides real press and release timing for those same controls. Logitech
documents the Signature M650 side buttons as Back/Forward buttons, so
report the diverted controls as BTN_BACK and BTN_FORWARD.
Link: https://support.logi.com/hc/en-nz/articles/4414473810583-Getting-Started-Signature-M650
Signed-off-by: Elliot Douglas <edouglas7358@gmail.com>
---
drivers/hid/hid-logitech-hidpp.c | 21 ++++++++++++++++++++-
1 file changed, 20 insertions(+), 1 deletion(-)
diff --git a/drivers/hid/hid-logitech-hidpp.c b/drivers/hid/hid-logitech-hidpp.c
index 24c9cfaa4f37..80108778ee80 100644
--- a/drivers/hid/hid-logitech-hidpp.c
+++ b/drivers/hid/hid-logitech-hidpp.c
@@ -3621,6 +3621,9 @@ static int hidpp10_extra_mouse_buttons_raw_event(struct hidpp_device *hidpp,
#define HIDPP_REPROG_CONTROLS_EVENT_DIVERTED 0x00
+#define HIDPP_REPROG_CONTROL_M650_BACK 0x0053
+#define HIDPP_REPROG_CONTROL_M650_FORWARD 0x0056
+
struct hidpp_reprog_control_mapping {
u16 control;
u16 code;
@@ -3631,9 +3634,24 @@ struct hidpp_reprog_controls_profile {
unsigned int mapping_count;
};
+static const struct hidpp_reprog_control_mapping m650_reprog_control_mappings[] = {
+ { HIDPP_REPROG_CONTROL_M650_BACK, BTN_BACK },
+ { HIDPP_REPROG_CONTROL_M650_FORWARD, BTN_FORWARD },
+};
+
+static const struct hidpp_reprog_controls_profile m650_reprog_controls_profile = {
+ .mappings = m650_reprog_control_mappings,
+ .mapping_count = ARRAY_SIZE(m650_reprog_control_mappings),
+};
+
static const struct hidpp_reprog_controls_profile *
hidpp20_reprog_controls_get_profile(struct hidpp_device *hidpp)
{
+ switch (hidpp->hid_dev->product) {
+ case 0xb02a:
+ return &m650_reprog_controls_profile;
+ }
+
return NULL;
}
@@ -4921,7 +4939,8 @@ static const struct hid_device_id hidpp_devices[] = {
{ /* MX Vertical mouse over Bluetooth */
HID_BLUETOOTH_DEVICE(USB_VENDOR_ID_LOGITECH, 0xb020) },
{ /* Signature M650 over Bluetooth */
- HID_BLUETOOTH_DEVICE(USB_VENDOR_ID_LOGITECH, 0xb02a) },
+ HID_BLUETOOTH_DEVICE(USB_VENDOR_ID_LOGITECH, 0xb02a),
+ .driver_data = HIDPP_QUIRK_HIDPP_REPROG_CONTROLS_BTNS },
{ /* MX Master 3 mouse over Bluetooth */
HID_BLUETOOTH_DEVICE(USB_VENDOR_ID_LOGITECH, 0xb023) },
{ /* MX Anywhere 3 mouse over Bluetooth */
--
2.54.0
^ permalink raw reply related [flat|nested] 11+ messages in thread
* Re: [PATCH 1/2] HID: logitech-hidpp: add HID++ 2.0 reprogrammable button support
2026-06-13 17:51 ` [PATCH 1/2] HID: logitech-hidpp: add HID++ 2.0 reprogrammable button support Elliot Douglas
@ 2026-06-17 10:28 ` Bastien Nocera
2026-06-18 1:16 ` Elliot Douglas
0 siblings, 1 reply; 11+ messages in thread
From: Bastien Nocera @ 2026-06-17 10:28 UTC (permalink / raw)
To: Elliot Douglas, linux-input; +Cc: lains, jikos, bentiss, linux-kernel
On Sat, 2026-06-13 at 10:51 -0700, Elliot Douglas wrote:
> 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 profiles so adding support for
> another mouse does not change the evdev capabilities advertised by
> already-supported devices.
How does this forced setting work/clash with the programmable buttons
in Solaar?
I've added some inline comments below.
>
> 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>
> ---
> drivers/hid/hid-logitech-hidpp.c | 215 +++++++++++++++++++++++++++++++
> 1 file changed, 215 insertions(+)
>
> diff --git a/drivers/hid/hid-logitech-hidpp.c b/drivers/hid/hid-logitech-hidpp.c
> index 70ba1a5e40d8..24c9cfaa4f37 100644
> --- a/drivers/hid/hid-logitech-hidpp.c
> +++ b/drivers/hid/hid-logitech-hidpp.c
> @@ -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
> @@ -205,6 +206,7 @@ struct hidpp_device {
> struct hidpp_scroll_counter vertical_wheel_counter;
>
> u8 wireless_feature_index;
> + u8 reprog_controls_feature_index;
>
> int hires_wheel_multiplier;
> u8 hires_wheel_feature_index;
> @@ -3601,6 +3603,209 @@ 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;
> +};
> +
> +struct hidpp_reprog_controls_profile {
> + const struct hidpp_reprog_control_mapping *mappings;
probably needs a __counted_by(), or maybe as it's static, it might be
better to not require an intermediate struct, and return a NULL-
terminated array instead.
> + unsigned int mapping_count;
> +};
> +
> +static const struct hidpp_reprog_controls_profile *
> +hidpp20_reprog_controls_get_profile(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, ¶ms[0]);
> + params[2] = flags;
> + put_unaligned_be16(control, ¶ms[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_controls_profile *profile;
> + u8 flags = HIDPP_REPROG_CONTROLS_TEMPORARY_DIVERTED |
> + HIDPP_REPROG_CONTROLS_CHANGE_TEMPORARY_DIVERT;
> + unsigned int i;
> +
> + if (!(hidpp->quirks & HIDPP_QUIRK_HIDPP_REPROG_CONTROLS_BTNS))
> + return;
> +
> + profile = hidpp20_reprog_controls_get_profile(hidpp);
Could the profile be cached in the hidpp_device struct?
> + if (!profile)
> + return;
> +
> + if (hidpp_root_get_feature(hidpp, HIDPP_PAGE_REPROG_CONTROLS_V4,
> + &hidpp->reprog_controls_feature_index))
> + return;
> +
> + for (i = 0; i < profile->mapping_count; i++) {
> + u16 control = profile->mappings[i].control;
> +
> + if (!hidpp20_reprog_controls_find_control(hidpp, control))
> + continue;
> +
> + hidpp20_reprog_controls_set_control_reporting(hidpp, control, flags);
> + }
> +}
> +
> +static int hidpp20_reprog_controls_raw_event(struct hidpp_device *hidpp,
> + u8 *data, int size)
> +{
> + const struct hidpp_reprog_controls_profile *profile;
> + 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_feature_index == 0xff)
> + return 0;
> +
> + profile = hidpp20_reprog_controls_get_profile(hidpp);
> + if (!profile)
> + 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 (i = 0; i < profile->mapping_count; i++) {
> + mapping = &profile->mappings[i];
> + 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_controls_profile *profile;
> + unsigned int i;
> +
> + profile = hidpp20_reprog_controls_get_profile(hidpp);
> + if (!profile)
> + return;
> +
> + for (i = 0; i < profile->mapping_count; i++)
> + input_set_capability(input_dev, EV_KEY, profile->mappings[i].code);
> +}
> +
> static void hidpp10_extra_mouse_buttons_populate_input(
> struct hidpp_device *hidpp, struct input_dev *input_dev)
> {
> @@ -3859,6 +4064,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 +4179,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 +4476,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)
> @@ -4436,6 +4650,7 @@ 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;
> hid_set_drvdata(hdev, hidpp);
>
> ret = hid_parse(hdev);
^ permalink raw reply [flat|nested] 11+ messages in thread
* Re: [PATCH 2/2] HID: logitech-hidpp: enable reprogrammable buttons on Signature M650
2026-06-13 17:51 ` [PATCH 2/2] HID: logitech-hidpp: enable reprogrammable buttons on Signature M650 Elliot Douglas
@ 2026-06-17 10:28 ` Bastien Nocera
0 siblings, 0 replies; 11+ messages in thread
From: Bastien Nocera @ 2026-06-17 10:28 UTC (permalink / raw)
To: Elliot Douglas, linux-input; +Cc: lains, jikos, bentiss, linux-kernel
Hey Elliot,
I have an m650 on hand on with which I should be able to test your
patch next week, once we've gone through a first round of reviews.
Benjamin, is there something I can capture locally that could make it
into the HID tests for this feature?
Inline comments below
On Sat, 2026-06-13 at 10:51 -0700, Elliot Douglas wrote:
> The Bluetooth Signature M650 exposes its side buttons through the
> normal
> mouse report, but the observed events are short click-like events
> emitted
> around release rather than physical press/release state.
>
> The device appears to use the held side-button state for its built-in
> gesture and side-button + wheel horizontal-scroll mode. As a result,
> holding a side button long enough can prevent the normal mouse report
> from
> emitting a usable button event at all.
>
> HID++ REPROG_CONTROLS_V4 diversion for control IDs 0x0053 and 0x0056
> provides real press and release timing for those same controls.
> Logitech
> documents the Signature M650 side buttons as Back/Forward buttons, so
> report the diverted controls as BTN_BACK and BTN_FORWARD.
>
> Link:
> https://support.logi.com/hc/en-nz/articles/4414473810583-Getting-Started-Signature-M650
>
> Signed-off-by: Elliot Douglas <edouglas7358@gmail.com>
> ---
> drivers/hid/hid-logitech-hidpp.c | 21 ++++++++++++++++++++-
> 1 file changed, 20 insertions(+), 1 deletion(-)
>
> diff --git a/drivers/hid/hid-logitech-hidpp.c b/drivers/hid/hid-
> logitech-hidpp.c
> index 24c9cfaa4f37..80108778ee80 100644
> --- a/drivers/hid/hid-logitech-hidpp.c
> +++ b/drivers/hid/hid-logitech-hidpp.c
> @@ -3621,6 +3621,9 @@ static int
> hidpp10_extra_mouse_buttons_raw_event(struct hidpp_device *hidpp,
>
> #define HIDPP_REPROG_CONTROLS_EVENT_DIVERTED 0x00
>
> +#define HIDPP_REPROG_CONTROL_M650_BACK 0x0053
> +#define HIDPP_REPROG_CONTROL_M650_FORWARD 0x0056
I read through the 0x1b04 docs, and I'm trying to understand whether
those CID numbers change in different hardware, or if they're hardcoded
(a back button will always have the 83/0x53 value).
Seems to me that it's the latter? The back button is listed in the
Example Control ID table in the 0x1b04 docs.
> +
> struct hidpp_reprog_control_mapping {
> u16 control;
> u16 code;
> @@ -3631,9 +3634,24 @@ struct hidpp_reprog_controls_profile {
> unsigned int mapping_count;
> };
>
> +static const struct hidpp_reprog_control_mapping
> m650_reprog_control_mappings[] = {
> + { HIDPP_REPROG_CONTROL_M650_BACK, BTN_BACK },
> + { HIDPP_REPROG_CONTROL_M650_FORWARD, BTN_FORWARD },
> +};
> +
> +static const struct hidpp_reprog_controls_profile
> m650_reprog_controls_profile = {
> + .mappings = m650_reprog_control_mappings,
> + .mapping_count = ARRAY_SIZE(m650_reprog_control_mappings),
> +};
> +
> static const struct hidpp_reprog_controls_profile *
> hidpp20_reprog_controls_get_profile(struct hidpp_device *hidpp)
> {
> + switch (hidpp->hid_dev->product) {
> + case 0xb02a:
You probably want a define for that constant.
> + return &m650_reprog_controls_profile;
> + }
> +
> return NULL;
> }
>
> @@ -4921,7 +4939,8 @@ static const struct hid_device_id
> hidpp_devices[] = {
> { /* MX Vertical mouse over Bluetooth */
> HID_BLUETOOTH_DEVICE(USB_VENDOR_ID_LOGITECH, 0xb020) },
> { /* Signature M650 over Bluetooth */
> - HID_BLUETOOTH_DEVICE(USB_VENDOR_ID_LOGITECH, 0xb02a) },
> + HID_BLUETOOTH_DEVICE(USB_VENDOR_ID_LOGITECH, 0xb02a),
> + .driver_data = HIDPP_QUIRK_HIDPP_REPROG_CONTROLS_BTNS },
> { /* MX Master 3 mouse over Bluetooth */
> HID_BLUETOOTH_DEVICE(USB_VENDOR_ID_LOGITECH, 0xb023) },
> { /* MX Anywhere 3 mouse over Bluetooth */
^ permalink raw reply [flat|nested] 11+ messages in thread
* Re: [PATCH 1/2] HID: logitech-hidpp: add HID++ 2.0 reprogrammable button support
2026-06-17 10:28 ` Bastien Nocera
@ 2026-06-18 1:16 ` Elliot Douglas
2026-07-01 22:32 ` Elliot Douglas
0 siblings, 1 reply; 11+ messages in thread
From: Elliot Douglas @ 2026-06-18 1:16 UTC (permalink / raw)
To: Bastien Nocera; +Cc: linux-input, lains, jikos, bentiss, linux-kernel
Thanks, that makes sense.
For Solaar, this is not continuously forced. The kernel only programs
temporary diversion when the device connects. Solaar can still issue HID++
commands through hidraw, so if Solaar changes reporting for the same controls
afterwards, the last writer wins.
If Solaar takes over those controls for custom actions, the kernel would stop
receiving the diverted button notifications for normal evdev reporting until
the kernel diverts the controls again, for example after reconnect. While the
controls remain diverted, hidraw clients should still receive the raw HID++
reports.
I have addressed the inline comments locally for v2:
- replaced the profile/count wrapper with NULL-terminated mapping arrays
- cached the selected mapping pointer in struct hidpp_device
I'll wait for Benjamin's input to send Patch v2.
On Wed, Jun 17, 2026 at 3:28 AM Bastien Nocera <hadess@hadess.net> wrote:
>
> On Sat, 2026-06-13 at 10:51 -0700, Elliot Douglas wrote:
> > 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 profiles so adding support for
> > another mouse does not change the evdev capabilities advertised by
> > already-supported devices.
>
> How does this forced setting work/clash with the programmable buttons
> in Solaar?
>
> I've added some inline comments below.
>
> >
> > 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>
> > ---
> > drivers/hid/hid-logitech-hidpp.c | 215 +++++++++++++++++++++++++++++++
> > 1 file changed, 215 insertions(+)
> >
> > diff --git a/drivers/hid/hid-logitech-hidpp.c b/drivers/hid/hid-logitech-hidpp.c
> > index 70ba1a5e40d8..24c9cfaa4f37 100644
> > --- a/drivers/hid/hid-logitech-hidpp.c
> > +++ b/drivers/hid/hid-logitech-hidpp.c
> > @@ -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
> > @@ -205,6 +206,7 @@ struct hidpp_device {
> > struct hidpp_scroll_counter vertical_wheel_counter;
> >
> > u8 wireless_feature_index;
> > + u8 reprog_controls_feature_index;
> >
> > int hires_wheel_multiplier;
> > u8 hires_wheel_feature_index;
> > @@ -3601,6 +3603,209 @@ 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;
> > +};
> > +
> > +struct hidpp_reprog_controls_profile {
> > + const struct hidpp_reprog_control_mapping *mappings;
>
> probably needs a __counted_by(), or maybe as it's static, it might be
> better to not require an intermediate struct, and return a NULL-
> terminated array instead.
>
> > + unsigned int mapping_count;
> > +};
> > +
> > +static const struct hidpp_reprog_controls_profile *
> > +hidpp20_reprog_controls_get_profile(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, ¶ms[0]);
> > + params[2] = flags;
> > + put_unaligned_be16(control, ¶ms[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_controls_profile *profile;
> > + u8 flags = HIDPP_REPROG_CONTROLS_TEMPORARY_DIVERTED |
> > + HIDPP_REPROG_CONTROLS_CHANGE_TEMPORARY_DIVERT;
> > + unsigned int i;
> > +
> > + if (!(hidpp->quirks & HIDPP_QUIRK_HIDPP_REPROG_CONTROLS_BTNS))
> > + return;
> > +
> > + profile = hidpp20_reprog_controls_get_profile(hidpp);
>
> Could the profile be cached in the hidpp_device struct?
>
> > + if (!profile)
> > + return;
> > +
> > + if (hidpp_root_get_feature(hidpp, HIDPP_PAGE_REPROG_CONTROLS_V4,
> > + &hidpp->reprog_controls_feature_index))
> > + return;
> > +
> > + for (i = 0; i < profile->mapping_count; i++) {
> > + u16 control = profile->mappings[i].control;
> > +
> > + if (!hidpp20_reprog_controls_find_control(hidpp, control))
> > + continue;
> > +
> > + hidpp20_reprog_controls_set_control_reporting(hidpp, control, flags);
> > + }
> > +}
> > +
> > +static int hidpp20_reprog_controls_raw_event(struct hidpp_device *hidpp,
> > + u8 *data, int size)
> > +{
> > + const struct hidpp_reprog_controls_profile *profile;
> > + 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_feature_index == 0xff)
> > + return 0;
> > +
> > + profile = hidpp20_reprog_controls_get_profile(hidpp);
> > + if (!profile)
> > + 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 (i = 0; i < profile->mapping_count; i++) {
> > + mapping = &profile->mappings[i];
> > + 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_controls_profile *profile;
> > + unsigned int i;
> > +
> > + profile = hidpp20_reprog_controls_get_profile(hidpp);
> > + if (!profile)
> > + return;
> > +
> > + for (i = 0; i < profile->mapping_count; i++)
> > + input_set_capability(input_dev, EV_KEY, profile->mappings[i].code);
> > +}
> > +
> > static void hidpp10_extra_mouse_buttons_populate_input(
> > struct hidpp_device *hidpp, struct input_dev *input_dev)
> > {
> > @@ -3859,6 +4064,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 +4179,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 +4476,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)
> > @@ -4436,6 +4650,7 @@ 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;
> > hid_set_drvdata(hdev, hidpp);
> >
> > ret = hid_parse(hdev);
^ permalink raw reply [flat|nested] 11+ messages in thread
* Re: [PATCH 1/2] HID: logitech-hidpp: add HID++ 2.0 reprogrammable button support
2026-06-18 1:16 ` Elliot Douglas
@ 2026-07-01 22:32 ` Elliot Douglas
2026-07-03 12:53 ` Bastien Nocera
0 siblings, 1 reply; 11+ messages in thread
From: Elliot Douglas @ 2026-07-01 22:32 UTC (permalink / raw)
To: Bastien Nocera; +Cc: linux-input, lains, jikos, bentiss, linux-kernel
Just wanted to poke on this thread again, Benjamin or Bastien, what is needed to
push this forward or should I send the v2 at this point?
Thanks,
Elliot
On Wed, Jun 17, 2026 at 6:16 PM Elliot Douglas <edouglas7358@gmail.com> wrote:
>
> Thanks, that makes sense.
>
> For Solaar, this is not continuously forced. The kernel only programs
> temporary diversion when the device connects. Solaar can still issue HID++
> commands through hidraw, so if Solaar changes reporting for the same controls
> afterwards, the last writer wins.
>
> If Solaar takes over those controls for custom actions, the kernel would stop
> receiving the diverted button notifications for normal evdev reporting until
> the kernel diverts the controls again, for example after reconnect. While the
> controls remain diverted, hidraw clients should still receive the raw HID++
> reports.
>
> I have addressed the inline comments locally for v2:
> - replaced the profile/count wrapper with NULL-terminated mapping arrays
> - cached the selected mapping pointer in struct hidpp_device
>
> I'll wait for Benjamin's input to send Patch v2.
>
>
> On Wed, Jun 17, 2026 at 3:28 AM Bastien Nocera <hadess@hadess.net> wrote:
> >
> > On Sat, 2026-06-13 at 10:51 -0700, Elliot Douglas wrote:
> > > 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 profiles so adding support for
> > > another mouse does not change the evdev capabilities advertised by
> > > already-supported devices.
> >
> > How does this forced setting work/clash with the programmable buttons
> > in Solaar?
> >
> > I've added some inline comments below.
> >
> > >
> > > 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>
> > > ---
> > > drivers/hid/hid-logitech-hidpp.c | 215 +++++++++++++++++++++++++++++++
> > > 1 file changed, 215 insertions(+)
> > >
> > > diff --git a/drivers/hid/hid-logitech-hidpp.c b/drivers/hid/hid-logitech-hidpp.c
> > > index 70ba1a5e40d8..24c9cfaa4f37 100644
> > > --- a/drivers/hid/hid-logitech-hidpp.c
> > > +++ b/drivers/hid/hid-logitech-hidpp.c
> > > @@ -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
> > > @@ -205,6 +206,7 @@ struct hidpp_device {
> > > struct hidpp_scroll_counter vertical_wheel_counter;
> > >
> > > u8 wireless_feature_index;
> > > + u8 reprog_controls_feature_index;
> > >
> > > int hires_wheel_multiplier;
> > > u8 hires_wheel_feature_index;
> > > @@ -3601,6 +3603,209 @@ 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;
> > > +};
> > > +
> > > +struct hidpp_reprog_controls_profile {
> > > + const struct hidpp_reprog_control_mapping *mappings;
> >
> > probably needs a __counted_by(), or maybe as it's static, it might be
> > better to not require an intermediate struct, and return a NULL-
> > terminated array instead.
> >
> > > + unsigned int mapping_count;
> > > +};
> > > +
> > > +static const struct hidpp_reprog_controls_profile *
> > > +hidpp20_reprog_controls_get_profile(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, ¶ms[0]);
> > > + params[2] = flags;
> > > + put_unaligned_be16(control, ¶ms[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_controls_profile *profile;
> > > + u8 flags = HIDPP_REPROG_CONTROLS_TEMPORARY_DIVERTED |
> > > + HIDPP_REPROG_CONTROLS_CHANGE_TEMPORARY_DIVERT;
> > > + unsigned int i;
> > > +
> > > + if (!(hidpp->quirks & HIDPP_QUIRK_HIDPP_REPROG_CONTROLS_BTNS))
> > > + return;
> > > +
> > > + profile = hidpp20_reprog_controls_get_profile(hidpp);
> >
> > Could the profile be cached in the hidpp_device struct?
> >
> > > + if (!profile)
> > > + return;
> > > +
> > > + if (hidpp_root_get_feature(hidpp, HIDPP_PAGE_REPROG_CONTROLS_V4,
> > > + &hidpp->reprog_controls_feature_index))
> > > + return;
> > > +
> > > + for (i = 0; i < profile->mapping_count; i++) {
> > > + u16 control = profile->mappings[i].control;
> > > +
> > > + if (!hidpp20_reprog_controls_find_control(hidpp, control))
> > > + continue;
> > > +
> > > + hidpp20_reprog_controls_set_control_reporting(hidpp, control, flags);
> > > + }
> > > +}
> > > +
> > > +static int hidpp20_reprog_controls_raw_event(struct hidpp_device *hidpp,
> > > + u8 *data, int size)
> > > +{
> > > + const struct hidpp_reprog_controls_profile *profile;
> > > + 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_feature_index == 0xff)
> > > + return 0;
> > > +
> > > + profile = hidpp20_reprog_controls_get_profile(hidpp);
> > > + if (!profile)
> > > + 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 (i = 0; i < profile->mapping_count; i++) {
> > > + mapping = &profile->mappings[i];
> > > + 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_controls_profile *profile;
> > > + unsigned int i;
> > > +
> > > + profile = hidpp20_reprog_controls_get_profile(hidpp);
> > > + if (!profile)
> > > + return;
> > > +
> > > + for (i = 0; i < profile->mapping_count; i++)
> > > + input_set_capability(input_dev, EV_KEY, profile->mappings[i].code);
> > > +}
> > > +
> > > static void hidpp10_extra_mouse_buttons_populate_input(
> > > struct hidpp_device *hidpp, struct input_dev *input_dev)
> > > {
> > > @@ -3859,6 +4064,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 +4179,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 +4476,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)
> > > @@ -4436,6 +4650,7 @@ 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;
> > > hid_set_drvdata(hdev, hidpp);
> > >
> > > ret = hid_parse(hdev);
^ permalink raw reply [flat|nested] 11+ messages in thread
* Re: [PATCH 1/2] HID: logitech-hidpp: add HID++ 2.0 reprogrammable button support
2026-07-01 22:32 ` Elliot Douglas
@ 2026-07-03 12:53 ` Bastien Nocera
0 siblings, 0 replies; 11+ messages in thread
From: Bastien Nocera @ 2026-07-03 12:53 UTC (permalink / raw)
To: Elliot Douglas; +Cc: linux-input, lains, jikos, bentiss, linux-kernel
On Wed, 2026-07-01 at 15:32 -0700, Elliot Douglas wrote:
> Just wanted to poke on this thread again, Benjamin or Bastien, what
> is needed to
> push this forward or should I send the v2 at this point?
You're definitely better off sending a new version when there's no
feedback incoming, it acts as a gentle reminder and avoids another back
and forth if there's no further comments (apart from the ones in the
original review).
>
> Thanks,
> Elliot
>
> On Wed, Jun 17, 2026 at 6:16 PM Elliot Douglas
> <edouglas7358@gmail.com> wrote:
> >
> > Thanks, that makes sense.
> >
> > For Solaar, this is not continuously forced. The kernel only
> > programs
> > temporary diversion when the device connects. Solaar can still
> > issue HID++
> > commands through hidraw, so if Solaar changes reporting for the
> > same controls
> > afterwards, the last writer wins.
> >
> > If Solaar takes over those controls for custom actions, the kernel
> > would stop
> > receiving the diverted button notifications for normal evdev
> > reporting until
> > the kernel diverts the controls again, for example after reconnect.
> > While the
> > controls remain diverted, hidraw clients should still receive the
> > raw HID++
> > reports.
> >
> > I have addressed the inline comments locally for v2:
> > - replaced the profile/count wrapper with NULL-terminated mapping
> > arrays
> > - cached the selected mapping pointer in struct hidpp_device
> >
> > I'll wait for Benjamin's input to send Patch v2.
> >
> >
> > On Wed, Jun 17, 2026 at 3:28 AM Bastien Nocera <hadess@hadess.net>
> > wrote:
> > >
> > > On Sat, 2026-06-13 at 10:51 -0700, Elliot Douglas wrote:
> > > > 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 profiles so adding
> > > > support for
> > > > another mouse does not change the evdev capabilities advertised
> > > > by
> > > > already-supported devices.
> > >
> > > How does this forced setting work/clash with the programmable
> > > buttons
> > > in Solaar?
> > >
> > > I've added some inline comments below.
> > >
> > > >
> > > > 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>
> > > > ---
> > > > drivers/hid/hid-logitech-hidpp.c | 215
> > > > +++++++++++++++++++++++++++++++
> > > > 1 file changed, 215 insertions(+)
> > > >
> > > > diff --git a/drivers/hid/hid-logitech-hidpp.c
> > > > b/drivers/hid/hid-logitech-hidpp.c
> > > > index 70ba1a5e40d8..24c9cfaa4f37 100644
> > > > --- a/drivers/hid/hid-logitech-hidpp.c
> > > > +++ b/drivers/hid/hid-logitech-hidpp.c
> > > > @@ -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
> > > > @@ -205,6 +206,7 @@ struct hidpp_device {
> > > > struct hidpp_scroll_counter vertical_wheel_counter;
> > > >
> > > > u8 wireless_feature_index;
> > > > + u8 reprog_controls_feature_index;
> > > >
> > > > int hires_wheel_multiplier;
> > > > u8 hires_wheel_feature_index;
> > > > @@ -3601,6 +3603,209 @@ 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;
> > > > +};
> > > > +
> > > > +struct hidpp_reprog_controls_profile {
> > > > + const struct hidpp_reprog_control_mapping *mappings;
> > >
> > > probably needs a __counted_by(), or maybe as it's static, it
> > > might be
> > > better to not require an intermediate struct, and return a NULL-
> > > terminated array instead.
> > >
> > > > + unsigned int mapping_count;
> > > > +};
> > > > +
> > > > +static const struct hidpp_reprog_controls_profile *
> > > > +hidpp20_reprog_controls_get_profile(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, ¶ms[0]);
> > > > + params[2] = flags;
> > > > + put_unaligned_be16(control, ¶ms[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_controls_profile *profile;
> > > > + u8 flags = HIDPP_REPROG_CONTROLS_TEMPORARY_DIVERTED |
> > > > + HIDPP_REPROG_CONTROLS_CHANGE_TEMPORARY_DIVERT;
> > > > + unsigned int i;
> > > > +
> > > > + if (!(hidpp->quirks &
> > > > HIDPP_QUIRK_HIDPP_REPROG_CONTROLS_BTNS))
> > > > + return;
> > > > +
> > > > + profile = hidpp20_reprog_controls_get_profile(hidpp);
> > >
> > > Could the profile be cached in the hidpp_device struct?
> > >
> > > > + if (!profile)
> > > > + return;
> > > > +
> > > > + if (hidpp_root_get_feature(hidpp,
> > > > HIDPP_PAGE_REPROG_CONTROLS_V4,
> > > > + &hidpp-
> > > > >reprog_controls_feature_index))
> > > > + return;
> > > > +
> > > > + for (i = 0; i < profile->mapping_count; i++) {
> > > > + u16 control = profile->mappings[i].control;
> > > > +
> > > > + if (!hidpp20_reprog_controls_find_control(hidpp,
> > > > control))
> > > > + continue;
> > > > +
> > > > +
> > > > hidpp20_reprog_controls_set_control_reporting(hidpp, control,
> > > > flags);
> > > > + }
> > > > +}
> > > > +
> > > > +static int hidpp20_reprog_controls_raw_event(struct
> > > > hidpp_device *hidpp,
> > > > + u8 *data, int size)
> > > > +{
> > > > + const struct hidpp_reprog_controls_profile *profile;
> > > > + 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_feature_index == 0xff)
> > > > + return 0;
> > > > +
> > > > + profile = hidpp20_reprog_controls_get_profile(hidpp);
> > > > + if (!profile)
> > > > + 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 (i = 0; i < profile->mapping_count; i++) {
> > > > + mapping = &profile->mappings[i];
> > > > + 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_controls_profile *profile;
> > > > + unsigned int i;
> > > > +
> > > > + profile = hidpp20_reprog_controls_get_profile(hidpp);
> > > > + if (!profile)
> > > > + return;
> > > > +
> > > > + for (i = 0; i < profile->mapping_count; i++)
> > > > + input_set_capability(input_dev, EV_KEY, profile-
> > > > >mappings[i].code);
> > > > +}
> > > > +
> > > > static void hidpp10_extra_mouse_buttons_populate_input(
> > > > struct hidpp_device *hidpp, struct
> > > > input_dev *input_dev)
> > > > {
> > > > @@ -3859,6 +4064,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 +4179,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 +4476,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)
> > > > @@ -4436,6 +4650,7 @@ 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;
> > > > hid_set_drvdata(hdev, hidpp);
> > > >
> > > > ret = hid_parse(hdev);
^ permalink raw reply [flat|nested] 11+ messages in thread
* [PATCH v2 0/2] HID: logitech-hidpp: fix Signature M650 side button timing
2026-06-13 17:51 [PATCH 0/2] HID: logitech-hidpp: fix Signature M650 side button timing Elliot Douglas
2026-06-13 17:51 ` [PATCH 1/2] HID: logitech-hidpp: add HID++ 2.0 reprogrammable button support Elliot Douglas
2026-06-13 17:51 ` [PATCH 2/2] HID: logitech-hidpp: enable reprogrammable buttons on Signature M650 Elliot Douglas
@ 2026-07-04 23:10 ` Elliot Douglas
2026-07-04 23:10 ` [PATCH v2 1/2] HID: logitech-hidpp: add HID++ 2.0 reprogrammable button support Elliot Douglas
2026-07-04 23:10 ` [PATCH v2 2/2] HID: logitech-hidpp: enable reprogrammable buttons on Signature M650 Elliot Douglas
2 siblings, 2 replies; 11+ messages in thread
From: Elliot Douglas @ 2026-07-04 23:10 UTC (permalink / raw)
To: linux-input; +Cc: lains, hadess, jikos, bentiss, linux-kernel, edouglas7358
The Logitech Signature M650 over Bluetooth exposes its side buttons in the
normal mouse report, but the reported BTN_SIDE/BTN_EXTRA events are short
click-like events emitted around button release rather than physical
press/release events with the real hold duration. The device appears to reserve
the held side-button state for a built-in gesture mode: holding a side button
long enough, or holding it while using the wheel for horizontal scrolling, can
mean the normal mouse report never emits a usable side-button press at all.
That makes the buttons unusable for standard Linux hold actions such as
push-to-talk, drag modifiers, or remapping rules that depend on key-up timing.
When HID++ 2.0 feature 0x1b04, SpecialKeysMseButtons /
REPROG_CONTROLS_V4, temporarily diverts the same controls, the device sends
diverted-control notifications with real press and release timing. This series
adds quirk-gated support for those notifications and enables it for the
Bluetooth Signature M650.
Before enabling diversion, the driver verifies that each mapped control is
present in the device's HID++ control table and is advertised as a divertable
mouse control.
The kernel only programs temporary diversion when the device connects. It
does not continuously force the setting. A userspace HID++ tool such as
Solaar can still issue HID++ commands through hidraw; if Solaar changes the
reporting mode for the same controls afterwards, the last writer wins. That
means Solaar can still take over those controls for custom actions, but the
kernel will no longer receive the diverted button notifications for normal
evdev reporting until the kernel diverts the controls again, for example after
reconnect. While the controls remain diverted, hidraw clients should still
receive the raw reports and the kernel reports the matching evdev state.
The diverted M650 controls are reported as BTN_BACK and BTN_FORWARD. Logitech's
Signature M650 getting-started page labels these physical controls as
Back/Forward buttons and describes their default page-navigation behavior:
https://support.logi.com/hc/en-nz/articles/4414473810583-Getting-Started-Signature-M650
The reprogrammable-control support is per-product and parses the full HID++
divertedButtonsEvent pressed-control list, so it can support devices with more
buttons without relying on a single last-control release heuristic. Only the
Signature M650 opts in for now. Other Logitech devices should only be enabled
after their HID++ control IDs and divertedButtonsEvent behavior are captured
and verified.
There is evidence that this is not unique to the M650. A prior MX Anywhere 3
patch used the same HID++ feature to fix thumb buttons that only activated on
release, and Logitech documents side-button + wheel horizontal scrolling for
both the MX Anywhere 3/3S and Signature M650. Solaar's device reports and rules
documentation also show HID++ divertable back/forward controls on MX Master 3
and MX Master 3S class devices. This series remains conservative and only
enables the device tested here.
Tested with a Logitech Signature M650 L over Bluetooth, HID ID
0005:046D:B02A. Baseline evtest showed short release-time BTN_SIDE/BTN_EXTRA
events. Earlier local testing of the same HID++ diversion path showed real
hold-duration press/release events, including holds longer than 4 seconds for
both buttons.
Changes in v2:
- Replace the profile/count wrapper with NULL-terminated mapping arrays.
- Cache the selected reprogrammable-control mapping in struct hidpp_device.
- Add a named constant for the M650 Bluetooth product ID.
- Use common Back/Forward names for the 0x0053/0x0056 HID++ control IDs.
Elliot Douglas (2):
HID: logitech-hidpp: add HID++ 2.0 reprogrammable button support
HID: logitech-hidpp: enable reprogrammable buttons on Signature M650
drivers/hid/hid-logitech-hidpp.c | 225 ++++++++++++++++++++++++++++++-
1 file changed, 224 insertions(+), 1 deletion(-)
base-commit: f0866517be9345d8245d32b722574b8aecccb348
--
2.54.0
^ permalink raw reply [flat|nested] 11+ messages in thread
* [PATCH v2 1/2] HID: logitech-hidpp: add HID++ 2.0 reprogrammable button support
2026-07-04 23:10 ` [PATCH v2 0/2] HID: logitech-hidpp: fix Signature M650 side button timing Elliot Douglas
@ 2026-07-04 23:10 ` Elliot Douglas
2026-07-04 23:10 ` [PATCH v2 2/2] HID: logitech-hidpp: enable reprogrammable buttons on Signature M650 Elliot Douglas
1 sibling, 0 replies; 11+ messages in thread
From: Elliot Douglas @ 2026-07-04 23:10 UTC (permalink / raw)
To: linux-input; +Cc: lains, hadess, jikos, bentiss, linux-kernel, edouglas7358
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>
---
drivers/hid/hid-logitech-hidpp.c | 205 +++++++++++++++++++++++++++++++
1 file changed, 205 insertions(+)
diff --git a/drivers/hid/hid-logitech-hidpp.c b/drivers/hid/hid-logitech-hidpp.c
index 70ba1a5e40d8..f9189e14fb78 100644
--- a/drivers/hid/hid-logitech-hidpp.c
+++ b/drivers/hid/hid-logitech-hidpp.c
@@ -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, ¶ms[0]);
+ params[2] = flags;
+ put_unaligned_be16(control, ¶ms[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)
@@ -4436,6 +4639,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);
--
2.54.0
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [PATCH v2 2/2] HID: logitech-hidpp: enable reprogrammable buttons on Signature M650
2026-07-04 23:10 ` [PATCH v2 0/2] HID: logitech-hidpp: fix Signature M650 side button timing Elliot Douglas
2026-07-04 23:10 ` [PATCH v2 1/2] HID: logitech-hidpp: add HID++ 2.0 reprogrammable button support Elliot Douglas
@ 2026-07-04 23:10 ` Elliot Douglas
1 sibling, 0 replies; 11+ messages in thread
From: Elliot Douglas @ 2026-07-04 23:10 UTC (permalink / raw)
To: linux-input; +Cc: lains, hadess, jikos, bentiss, linux-kernel, edouglas7358
The Bluetooth Signature M650 exposes its side buttons through the normal
mouse report, but the observed events are short click-like events emitted
around release rather than physical press/release state.
The device appears to use the held side-button state for its built-in
gesture and side-button + wheel horizontal-scroll mode. As a result,
holding a side button long enough can prevent the normal mouse report from
emitting a usable button event at all.
HID++ REPROG_CONTROLS_V4 diversion for control IDs 0x0053 and 0x0056
provides real press and release timing for those same controls. Logitech
documents the Signature M650 side buttons as Back/Forward buttons, so
report the diverted controls as BTN_BACK and BTN_FORWARD.
The HID++ 0x1b04 documentation lists those control IDs as Back and
Forward. The driver still verifies that the controls are present in the
device control table and advertised as divertable before changing their
reporting mode.
Link: https://support.logi.com/hc/en-nz/articles/4414473810583-Getting-Started-Signature-M650
Signed-off-by: Elliot Douglas <edouglas7358@gmail.com>
---
drivers/hid/hid-logitech-hidpp.c | 20 +++++++++++++++++++-
1 file changed, 19 insertions(+), 1 deletion(-)
diff --git a/drivers/hid/hid-logitech-hidpp.c b/drivers/hid/hid-logitech-hidpp.c
index f9189e14fb78..e71dd2457e80 100644
--- a/drivers/hid/hid-logitech-hidpp.c
+++ b/drivers/hid/hid-logitech-hidpp.c
@@ -3624,14 +3624,30 @@ static int hidpp10_extra_mouse_buttons_raw_event(struct hidpp_device *hidpp,
#define HIDPP_REPROG_CONTROLS_EVENT_DIVERTED 0x00
+#define HIDPP_REPROG_CONTROL_BACK 0x0053
+#define HIDPP_REPROG_CONTROL_FORWARD 0x0056
+
+#define HIDPP_PRODUCT_SIGNATURE_M650 0xb02a
+
struct hidpp_reprog_control_mapping {
u16 control;
u16 code;
};
+static const struct hidpp_reprog_control_mapping m650_reprog_control_mappings[] = {
+ { HIDPP_REPROG_CONTROL_BACK, BTN_BACK },
+ { HIDPP_REPROG_CONTROL_FORWARD, BTN_FORWARD },
+ { }
+};
+
static const struct hidpp_reprog_control_mapping *
hidpp20_reprog_controls_get_mappings(struct hidpp_device *hidpp)
{
+ switch (hidpp->hid_dev->product) {
+ case HIDPP_PRODUCT_SIGNATURE_M650:
+ return m650_reprog_control_mappings;
+ }
+
return NULL;
}
@@ -4911,7 +4927,9 @@ static const struct hid_device_id hidpp_devices[] = {
{ /* MX Vertical mouse over Bluetooth */
HID_BLUETOOTH_DEVICE(USB_VENDOR_ID_LOGITECH, 0xb020) },
{ /* Signature M650 over Bluetooth */
- HID_BLUETOOTH_DEVICE(USB_VENDOR_ID_LOGITECH, 0xb02a) },
+ HID_BLUETOOTH_DEVICE(USB_VENDOR_ID_LOGITECH,
+ HIDPP_PRODUCT_SIGNATURE_M650),
+ .driver_data = HIDPP_QUIRK_HIDPP_REPROG_CONTROLS_BTNS },
{ /* MX Master 3 mouse over Bluetooth */
HID_BLUETOOTH_DEVICE(USB_VENDOR_ID_LOGITECH, 0xb023) },
{ /* MX Anywhere 3 mouse over Bluetooth */
--
2.54.0
^ permalink raw reply related [flat|nested] 11+ messages in thread
end of thread, other threads:[~2026-07-04 23:11 UTC | newest]
Thread overview: 11+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-06-13 17:51 [PATCH 0/2] HID: logitech-hidpp: fix Signature M650 side button timing Elliot Douglas
2026-06-13 17:51 ` [PATCH 1/2] HID: logitech-hidpp: add HID++ 2.0 reprogrammable button support Elliot Douglas
2026-06-17 10:28 ` Bastien Nocera
2026-06-18 1:16 ` Elliot Douglas
2026-07-01 22:32 ` Elliot Douglas
2026-07-03 12:53 ` Bastien Nocera
2026-06-13 17:51 ` [PATCH 2/2] HID: logitech-hidpp: enable reprogrammable buttons on Signature M650 Elliot Douglas
2026-06-17 10:28 ` Bastien Nocera
2026-07-04 23:10 ` [PATCH v2 0/2] HID: logitech-hidpp: fix Signature M650 side button timing Elliot Douglas
2026-07-04 23:10 ` [PATCH v2 1/2] HID: logitech-hidpp: add HID++ 2.0 reprogrammable button support Elliot Douglas
2026-07-04 23:10 ` [PATCH v2 2/2] HID: logitech-hidpp: enable reprogrammable buttons on Signature M650 Elliot Douglas
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox