From 3f03b833b97de811980c8f9bb9faa9430bbc15c3 Mon Sep 17 00:00:00 2001 From: Rongrong Date: Thu, 18 Jun 2026 00:25:18 +0800 Subject: [PATCH 1/7] rules: MouseScroll: Do not consider bool as number While the current code path probably won't run into a mistaken scroll amount as True == 1, let's fix this as my radar beeped when I first saw this. Signed-off-by: Rongrong --- lib/logitech_receiver/diversion.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/logitech_receiver/diversion.py b/lib/logitech_receiver/diversion.py index e933f5ad..dc76df2a 100644 --- a/lib/logitech_receiver/diversion.py +++ b/lib/logitech_receiver/diversion.py @@ -289,6 +289,10 @@ def signed(bytes_: bytes) -> int: return int.from_bytes(bytes_, "big", signed=True) +def is_number(n) -> bool: + return not isinstance(n, bool) and isinstance(n, numbers.Number) + + def xy_direction(_x, _y): # normalize x and y m = math.sqrt((_x * _x) + (_y * _y)) @@ -1216,7 +1220,7 @@ class MouseScroll(Action): def __init__(self, amounts, warn=True): if len(amounts) == 1 and isinstance(amounts[0], list): amounts = amounts[0] - if not (len(amounts) == 2 and all([isinstance(a, numbers.Number) for a in amounts])): + if not (len(amounts) == 2 and all([is_number(a) for a in amounts])): if warn: logger.warning("rule MouseScroll argument not two numbers %s", amounts) amounts = [0, 0] @@ -1227,7 +1231,7 @@ class MouseScroll(Action): def evaluate(self, feature, notification: HIDPPNotification, device, last_result): amounts = self.amounts - if isinstance(last_result, numbers.Number): + if is_number(last_result): amounts = [math.floor(last_result * a) for a in self.amounts] if logger.isEnabledFor(logging.INFO): logger.info("MouseScroll action: %s %s %s", self.amounts, last_result, amounts) From f7cb2f9a408de61d3c522abd584853363f512e0e Mon Sep 17 00:00:00 2001 From: Rongrong Date: Thu, 18 Jun 2026 00:25:18 +0800 Subject: [PATCH 2/7] rules: MouseScroll: Round to zero for signed values Rounding to negative infinity makes no sense for the purpose here. While the current code path probably won't run into a mistaken scroll amount as no rule component returns a floating point number, let's fix this as my radar beeped too. Signed-off-by: Rongrong --- lib/logitech_receiver/diversion.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/logitech_receiver/diversion.py b/lib/logitech_receiver/diversion.py index dc76df2a..87dd727c 100644 --- a/lib/logitech_receiver/diversion.py +++ b/lib/logitech_receiver/diversion.py @@ -1232,7 +1232,7 @@ class MouseScroll(Action): def evaluate(self, feature, notification: HIDPPNotification, device, last_result): amounts = self.amounts if is_number(last_result): - amounts = [math.floor(last_result * a) for a in self.amounts] + amounts = [math.trunc(last_result * a) for a in self.amounts] if logger.isEnabledFor(logging.INFO): logger.info("MouseScroll action: %s %s %s", self.amounts, last_result, amounts) dx, dy = amounts From 1e6272a15fe4af8d42f58260cd97c600e7129603 Mon Sep 17 00:00:00 2001 From: Rongrong Date: Thu, 18 Jun 2026 00:25:18 +0800 Subject: [PATCH 3/7] rules: Test: Fix divergence between documentation and code Return True when the documentation says so to ensure that the result is not accidentally taken into MouseScroll's scroll amount multiplication. Signed-off-by: Rongrong --- lib/logitech_receiver/diversion.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/logitech_receiver/diversion.py b/lib/logitech_receiver/diversion.py index 87dd727c..537b9755 100644 --- a/lib/logitech_receiver/diversion.py +++ b/lib/logitech_receiver/diversion.py @@ -418,10 +418,10 @@ TESTS = { "crown_left": [lambda f, r, d, a: f == SupportedFeature.CROWN and r == 0 and d[1] >= 128 and 256 - d[1], False], "crown_right_ratchet": [lambda f, r, d, a: f == SupportedFeature.CROWN and r == 0 and d[2] < 128 and d[2], False], "crown_left_ratchet": [lambda f, r, d, a: f == SupportedFeature.CROWN and r == 0 and d[2] >= 128 and 256 - d[2], False], - "crown_tap": [lambda f, r, d, a: f == SupportedFeature.CROWN and r == 0 and d[5] == 0x01 and d[5], False], - "crown_start_press": [lambda f, r, d, a: f == SupportedFeature.CROWN and r == 0 and d[6] == 0x01 and d[6], False], - "crown_end_press": [lambda f, r, d, a: f == SupportedFeature.CROWN and r == 0 and d[6] == 0x05 and d[6], False], - "crown_pressed": [lambda f, r, d, a: f == SupportedFeature.CROWN and r == 0 and 0x01 <= d[6] <= 0x04 and d[6], False], + "crown_tap": [lambda f, r, d, a: f == SupportedFeature.CROWN and r == 0 and d[5] == 0x01 and True, False], + "crown_start_press": [lambda f, r, d, a: f == SupportedFeature.CROWN and r == 0 and d[6] == 0x01 and True, False], + "crown_end_press": [lambda f, r, d, a: f == SupportedFeature.CROWN and r == 0 and d[6] == 0x05 and True, False], + "crown_pressed": [lambda f, r, d, a: f == SupportedFeature.CROWN and r == 0 and 0x01 <= d[6] <= 0x04 and True, False], "thumb_wheel_up": [thumb_wheel_up, True], "thumb_wheel_down": [thumb_wheel_down, True], "lowres_wheel_up": [ From b56f30d24df314662ae77c42ad4a012aa9736aea Mon Sep 17 00:00:00 2001 From: Rongrong Date: Thu, 18 Jun 2026 00:25:19 +0800 Subject: [PATCH 4/7] rules: Test: Fix divergence between crown and wheel All wheel tests return signed rotation amounts so that they can share the same rule to multiply the rotation, as per the example rules in the documentation. However, this doesn't apply to crown tests and thumb_wheel rules with an argument, which makes no sense as they also return rotation amount. Hence, fix them by returning signed rotation amounts. Signed-off-by: Rongrong --- lib/logitech_receiver/diversion.py | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/lib/logitech_receiver/diversion.py b/lib/logitech_receiver/diversion.py index 537b9755..7c14e3fb 100644 --- a/lib/logitech_receiver/diversion.py +++ b/lib/logitech_receiver/diversion.py @@ -384,7 +384,7 @@ def thumb_wheel_up(f, r, d, a): return signed(d[0:2]) < 0 and signed(d[0:2]) elif thumb_wheel_displacement <= -a: thumb_wheel_displacement += a - return 1 + return -1 else: return False @@ -414,10 +414,22 @@ def charging(f, r, d, _a): TESTS = { - "crown_right": [lambda f, r, d, a: f == SupportedFeature.CROWN and r == 0 and d[1] < 128 and d[1], False], - "crown_left": [lambda f, r, d, a: f == SupportedFeature.CROWN and r == 0 and d[1] >= 128 and 256 - d[1], False], - "crown_right_ratchet": [lambda f, r, d, a: f == SupportedFeature.CROWN and r == 0 and d[2] < 128 and d[2], False], - "crown_left_ratchet": [lambda f, r, d, a: f == SupportedFeature.CROWN and r == 0 and d[2] >= 128 and 256 - d[2], False], + "crown_right": [ + lambda f, r, d, a: f == SupportedFeature.CROWN and r == 0 and signed(d[1:2]) > 0 and signed(d[1:2]), + False, + ], + "crown_left": [ + lambda f, r, d, a: f == SupportedFeature.CROWN and r == 0 and signed(d[1:2]) < 0 and signed(d[1:2]), + False, + ], + "crown_right_ratchet": [ + lambda f, r, d, a: f == SupportedFeature.CROWN and r == 0 and signed(d[2:3]) > 0 and signed(d[2:3]), + False, + ], + "crown_left_ratchet": [ + lambda f, r, d, a: f == SupportedFeature.CROWN and r == 0 and signed(d[2:3]) < 0 and signed(d[2:3]), + False, + ], "crown_tap": [lambda f, r, d, a: f == SupportedFeature.CROWN and r == 0 and d[5] == 0x01 and True, False], "crown_start_press": [lambda f, r, d, a: f == SupportedFeature.CROWN and r == 0 and d[6] == 0x01 and True, False], "crown_end_press": [lambda f, r, d, a: f == SupportedFeature.CROWN and r == 0 and d[6] == 0x05 and True, False], From 94cb282c773f0d57ce595fb9e6a60c4dfcd3f226 Mon Sep 17 00:00:00 2001 From: Rongrong Date: Thu, 18 Jun 2026 00:25:19 +0800 Subject: [PATCH 5/7] rules: Fix last_result not passed among chained rules A previous refactor accidentally broke chained rules that rely on the result chaining, so fix it. With it fixed, some rule examples from docs/rules.md work properly again, yay! --- - Feature: THUMB WHEEL - Rule: [ Modifiers: Control, Test: thumb_wheel_up, MouseScroll: [-2, 0] ] - Rule: - Modifiers: Control - Test: thumb_wheel_down - MouseScroll: [-2, 0] - Rule: [ Or: [ Test: thumb_wheel_up, Test: thumb_wheel_down ], MouseScroll: [-1, 0] ] ... --- - Feature: LOWRES WHEEL - Rule: [ Or: [ Test: lowres_wheel_up, Test: lowres_wheel_down ], MouseScroll: [0, 2] ] ... Fixes: 46366b243012 ("Fix warnings from automatic code inspections") Signed-off-by: Rongrong --- lib/logitech_receiver/diversion.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/lib/logitech_receiver/diversion.py b/lib/logitech_receiver/diversion.py index 7c14e3fb..670fd739 100644 --- a/lib/logitech_receiver/diversion.py +++ b/lib/logitech_receiver/diversion.py @@ -481,14 +481,13 @@ class RuleComponent: def _evaluate(components, feature, notification: HIDPPNotification, device, result) -> Any: - res = True for component in components: - res = component.evaluate(feature, notification, device, result) - if not isinstance(component, Action) and res is None: + result = component.evaluate(feature, notification, device, result) + if not isinstance(component, Action) and result is None: return None - if isinstance(component, Condition) and not res: - return res - return res + if isinstance(component, Condition) and not result: + return result + return result class Rule(RuleComponent): From 25b14b568a33da29d45b089f98d6604615c2216d Mon Sep 17 00:00:00 2001 From: Rongrong Date: Thu, 18 Jun 2026 00:25:19 +0800 Subject: [PATCH 6/7] rules: uinput: Fix device leakage While randomly messing around with rule components (I plan to add some new ones), I found this bug by screwing things up (shh...) Signed-off-by: Rongrong --- lib/logitech_receiver/diversion.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/logitech_receiver/diversion.py b/lib/logitech_receiver/diversion.py index 670fd739..4732512e 100644 --- a/lib/logitech_receiver/diversion.py +++ b/lib/logitech_receiver/diversion.py @@ -330,6 +330,11 @@ def simulate_uinput(what, code, arg): logger.debug("uinput simulated input %s %s %s", what, code, arg) return True except Exception as e: + try: + udevice.close() + except Exception: + pass + udevice = None logger.warning("uinput write failed: %s", e) From 8296eea51c031cb15dc6d91558aeaf616c1c80a6 Mon Sep 17 00:00:00 2001 From: Rongrong Date: Thu, 18 Jun 2026 00:25:19 +0800 Subject: [PATCH 7/7] rules: uinput: Do not register KEY_MAX As doing so makes no sense. Signed-off-by: Rongrong --- lib/logitech_receiver/diversion.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/logitech_receiver/diversion.py b/lib/logitech_receiver/diversion.py index 4732512e..e9f0f039 100644 --- a/lib/logitech_receiver/diversion.py +++ b/lib/logitech_receiver/diversion.py @@ -240,7 +240,7 @@ if evdev: } # uinput capability for keyboard keys, mouse buttons, and scrolling - key_events = [c for n, c in evdev.ecodes.ecodes.items() if n.startswith("KEY") and n != "KEY_CNT"] + key_events = [c for n, c in evdev.ecodes.ecodes.items() if n.startswith("KEY") and n not in {"KEY_CNT", "KEY_MAX"}] for _, evcode in buttons.values(): if evcode: key_events.append(evcode)