From be590c154a45d05f713b6c40e6f58115dc7dddad Mon Sep 17 00:00:00 2001 From: "Peter F. Patel-Schneider" Date: Thu, 12 Nov 2020 13:11:30 -0500 Subject: [PATCH] docs: add documentation for rules processing and change name of rules file --- docs/capabilities.md | 8 ++ docs/rules.md | 135 +++++++++++++++++++++++++ lib/logitech_receiver/diversion.py | 83 +++------------ lib/logitech_receiver/notifications.py | 2 +- 4 files changed, 156 insertions(+), 72 deletions(-) create mode 100644 docs/rules.md diff --git a/docs/capabilities.md b/docs/capabilities.md index 31188190..a998df5e 100644 --- a/docs/capabilities.md +++ b/docs/capabilities.md @@ -104,6 +104,14 @@ of the program, this cached information may become incorrect. Currently there is no way to force an update of the cached information besides restarting the program. + +## Rule-based Processing of HID++ Feature Notifications + +Solaar can process HID++ Feature Notifications from devices to, for example, +change the speed of some thumb wheels. For more information on this capability of Solaar see +[the rules page](https://pwr-solaar.github.io/Solaar/rules). + + ## Battery Icons For many devices, Solaar shows the approximate battery level via icons that diff --git a/docs/rules.md b/docs/rules.md new file mode 100644 index 00000000..04f4d653 --- /dev/null +++ b/docs/rules.md @@ -0,0 +1,135 @@ +--- +title: Rule Processing of HID++ Notifications +layout: page +--- + +Logitech devices that use HID++ version 2.0 or greater produce feature-based +notifications that Solaar can process using a simple rule language. For +example, using rules Solaar can emulate an `XF86_MonBrightnessDown` key tap +in response to the pressing of the `Brightness Down` key on Craft keyboards, +which normally does not produce any input at all when the keyboard is in +Windows mode. + +Solaar's rules only activate on HID++ notifications so device actions that +normally produce HID output need rule processing have to be first be set to +this mode. Currently Solaar can set (divert) some mouse scroll wheels, some +mouse thumb wheels, the crown of Craft keyboards, and some keys to produce +HID++ notifications. Look for `HID++` or `Diversion` settings to see what +diversion can be done with your devices. Runing Solaar with the `-dd` +option will show information about notifications, including their feature +name, report number, and data. + +In response to a feature-based HID++ notification Solaar runs a sequence of +rules. A `Rule` is a sequence of components, which are either sub-rules, +conditions, or actions. Conditions and actions are dictionaries with one +entry whose key is the name of the condition or action and whose value is +the argument of the action. + +If the last thing that a rule does is execute an action, no more rules are +processed for the notification. + +Rules are evaluated by evaluating each of their components in order. The +evaluation of a rule is terminated early if a condition component evaluates +to false or the last evaluated sub-component of a component is an action. A +rule is false if its last evaluated component evaluates to a false value. + +`Not` conditions take a single component and are true if their component +evaluates to a false value. +`Or` conditions take a sequence of components and are evaluated by +evaluating each of their components in order. +An Or condition is terminated early if a component evaluates to true or the +last evaluated sub-component of a component is an action. +A Or condition is true if its last evaluated component evaluates to a true +value. `And` conditions take a sequence of components are evaluted the same +as rules. + +`Process` conditions are true if the name of the active process starts with +their string argument. +`Feature` conditions are if true if the name of the feature of the current +notification is their string argument. +`Report` conditions are if true if the report number in the current +notification is their integer argument. +`Modifiers` conditions take either a string or a sequence of strings, which +can only be `Shift`, `Control`, `Alt`, and `Super`. +Modifiers conditions are true if their argument is the current keyboard +modifiers. +`Key` conditions are true if the Logitech name of the last key down is their +string argument. Logitech key names are shown in the `Key/Button Diversion` +setting. +`Test` conditions are true if their test evaluates to true on the feature, +report, and data of the current notification. +Test conditions can return a number instead of a boolean. + +Test conditions consisting of a sequence of three or four integers use the first +two to select bytes of the notification data. +Writing this kind of test condition is not trivial. +Three-element test conditions are true if the selected bytes bit-wise anded +with its third element is non-zero. +The value of these test conditions is the result of the and. +Four-element test conditions are true if the selected bytes form a signed +integer between the third and fourth elements. +The value of these test condition is the signed value of the selected bytes +if that is non-zero otherwise True. + +The other test conditions are mnemonic shorthands for meaningful feature, +report, and data combinations. +A `crown_right` test is the rotation amount of a `CROWN` right rotation. +A `crown_left` test is the rotation amount of a `CROWN` left rotation. +A `crown_right_ratchet` test is the ratchet amount of a `CROWN` right ratchet rotation. +A `crown_left_ratchet` test is the ratchet amount of a `CROWN` left ratchet rotation. +A `crown_tap` test is true for a `CROWN` tap. +A `crown_start_press` test is true for the start of a `CROWN` press. +A `crown_stop_press` test is true for the end of a `CROWN` press. +A `crown_pressed` test is true for a `CROWN` notification with the Crown pressed. +A `thumb_wheel_up` test is the rotation amount of a `THUMB WHEEL` upward rotation. +A `thumb_wheel_down` test is the rotation amount of a `THUMB WHEEL` downward rotation. +`lowres_wheel_up`, `lowres_wheel_down`, `hires_wheel_up`, `hires_wheel_down` are the +same but for `LOWRES WHEEL` and `HIRES WHEEL`. +`True` and `False` tests return True and False, respectively. + +A `KeyPress` action takes a sequence of X11 key symbols and simulates a chorded keypress on the keyboard. +Any key symbols that correspond to modifier keys that are in the current keyboard modifiers are ignored. +A `MouseScroll` action takes a sequence of two numbers and simulates a horizontal and vertical mouse scroll of these amounts. +If the previous condition in the parent rule returns a number the scroll amounts are multiplied by this number. +An `Execute` actions takes a program and arguments and executes it asynchronously. + +Solaar has several built-in rules, which are run after user-created rules and so can be overridden by user-created rules. +One rule turns +`Brightness Down` key press notifications into `XF86_MonBrightnessDown` key taps +and `Brightness Up` key press notifications into `XF86_MonBrightnessUp` key taps. +Another rule makes Craft crown ratchet movements move between tabs when the crown is pressed +and up and down otherwise. +A third rule turns Craft crown ratchet movements into `XF86_AudioNext` or `XF86_AudioPrev` key taps when the crown is pressed and `XF86_AudioRaiseVolume` or `XF86_AudioLowerVolume` otherwise. +A fourth rule doubles the speed of `THUMB WHEEL` movements unless the `Control` modifier is on. +All of these rules are only active if the key or feature is diverted, of course. + +Solaar reads rules from a YAML configuration file (normally `~/.config/solaar/rules.yaml`). +This file contains zero or more documents, each a rule. + +Here is a file with three rules: + +``` +%YAML 1.3 +--- +- Feature: CROWN +- Process: quodlibet +- Rule: [ Test: crown_start_press, KeyPress: XF86_AudioMute ] +- Rule: [ Test: crown_pressed, Test: crown_right_ratchet, KeyPress: XF86_AudioNext ] +- Rule: [ Test: crown_pressed, Test: crown_left_ratchet, KeyPress: XF86_AudioPrev ] +- Rule: [ Test: crown_right_ratchet, KeyPress: XF86_AudioRaiseVolume ] +- Rule: [ Test: crown_left_ratchet, KeyPress: XF86_AudioLowerVolume ] +... +--- +- 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] ] +... +``` diff --git a/lib/logitech_receiver/diversion.py b/lib/logitech_receiver/diversion.py index 8eea2ef6..79ed5a6e 100644 --- a/lib/logitech_receiver/diversion.py +++ b/lib/logitech_receiver/diversion.py @@ -17,8 +17,6 @@ ## with this program; if not, write to the Free Software Foundation, Inc., ## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -# Map diverted feature notifications to keyboard and mouse input - import os as _os import os.path as _path @@ -123,64 +121,7 @@ def key_press_handler(reply): _thread.start_new_thread(display.record_enable_context, (context, key_press_handler)) # display.record_free_context(context) when should this be done?? -# Solaar processes feature notifications from devices by evaluating a rule (usually consisting of a -# sequence of sub-rules) on them. -# -# A rule is a sequence of conditions, actions, and sub-rules. -# -# Rules are evaluated by evaluating each of their components in order. -# A rule evaluation is terminated early if a condition component evaluates to false or -# the last evaluated sub-component of a component is an action. -# A Rule is false if its last evaluated component evaluates to a false value. -# -# Not conditions are true if their component evaluates to a false value. -# Or conditions are evaluated by evaluating each of their components in order. -# An Or condition is terminated early if a component evaluates to true or -# the last evaluated sub-component of a component is an action. -# A Or condition is true if its last evaluated component evaluates to a true value. -# And conditions are evaluted the same as rules. -# -# Process conditions are true if the name of the active process starts with their argument. -# Feature conditions are if true if the name of the feature of the current notification is their argument. -# Report conditions are if true if the report number in the current notification is their argument. -# Modifiers conditions are true if the current keyboard modifiers are their arguments. -# The permissable modifiers are 'Shift', 'Control', 'Alt', and 'Super'. -# Key conditions are true if the Logitech name of the last key down is their argument. -# Test conditions are true if their test evaluates to true on the feature, report, and data of the current notification. -# Test conditions can return a number instead of a boolean. -# -# Test conditions consisting of a list of three or four integers use the first two to select bytes of the data. -# Three-element test conditions are true if the selected bytes bit-wise anded with its third element is non-zero. -# The value of these test conditions is the result of the and. -# Four-element test conditions are true if the selected bytes form a signed integer between the third and fourth elements. -# The value of test test condition is the signed value of the selected bytes if that is non-zero otherwise True. -# -# The other test conditions are mnemonic shorthands for meaningful feature, report, and data combinations. -# A crown_right test is the rotation amount of a CROWN right rotation. -# A crown_left test is the rotation amount of a CROWN left rotation. -# A crown_right_ratchet test is the ratchet amount of a CROWN right ratchet rotation. -# A crown_left_ratchet test is the ratchet amount of a CROWN left ratchet rotation. -# A crown_tap test is true for a CROWN tap. -# A crown_start_press test is true for the start of a CROWN press. -# A crown_stop_press test is true for the end of a CROWN press. -# A crown_pressed test is true for a CROWN notification with the Crown pressed. -# A thumb_wheel_up test is the rotation amount of a THUMB_WHEEL upward rotation. -# A thumb_wheel_down test is the rotation amount of a THUMB_WHEEL downward rotation. -# lowres_wheel_up, lowres_wheel_down, hires_wheel_up, hires_wheel_down are the same but for LOWRES_WHEEL and HIRES_WHEEL. -# True and False tests return True and False, respectively. -# -# A KeyPress action takes X11 key symbols and simulates a chorded keypress on the keyboard. -# Any key symbols that correspond to modifier keys that are in the current keyboard modifiers are ignored. -# A MouseScroll action takes two numbers and simulates a horizontal and vertical mouse scroll of these amounts. -# If the previous condition in the parent rule returns a number the scroll amounts are multiplied by this number. -# An Execute actions takes a program and arguments and executes it asynchronously. -# -# Solaar reads rules from a configuration file (normally ~/.config/solaar/divert.yaml). -# This file contains zero or more documents, each a rule. -# Rule components are one-element dictionaries with key the component name (with initial Captial letter). -# If a component has multiple arguments they are written as a sequence. -# Solaar constructs the rule that it uses to process feature notifications from these rules (if any) -# followed by some built-in rules. +# See docs/rules.md for documentation keyboard = _keyboard.Controller() mouse = _mouse.Controller() @@ -264,7 +205,7 @@ class Not(Condition): self.component = self.compile(op) def __str__(self): - return 'NOT: ' + str(self.component) + return 'Not: ' + str(self.component) def evaluate(self, feature, notification, device, status, last_result): result = self.component.evaluate(feature, notification, device, status, last_result) @@ -294,7 +235,7 @@ class And(Condition): self.components = [self.compile(a) for a in args] def __str__(self): - return 'AND: [' + ', '.join(str(c) for c in self.components) + ']' + return 'And: [' + ', '.join(str(c) for c in self.components) + ']' def evaluate(self, feature, notification, device, status, last_result): result = True @@ -314,7 +255,7 @@ class Process(Condition): _log.warn('rule Process argument not a string: %s', process) def __str__(self): - return 'PROCESS: ' + str(self.process) + return 'Process: ' + str(self.process) def evaluate(self, feature, notification, device, status, last_result): return active_process_name.startswith(self.process) if isinstance(self.process, str) else False @@ -328,7 +269,7 @@ class Feature(Condition): self.feature = _F[feature] def __str__(self): - return 'FEATURE: ' + str(self.feature) + return 'Feature: ' + str(self.feature) def evaluate(self, feature, notification, device, status, last_result): return feature == self.feature @@ -342,7 +283,7 @@ class Report(Condition): self.report = report def __str__(self): - return 'REPORT: ' + str(self.report) + return 'Report: ' + str(self.report) def evaluate(self, report, notification, device, status, last_result): return (notification.address >> 4) == self.report @@ -363,7 +304,7 @@ class Modifiers(Condition): _log.warn('unknown rule Modifier value: %s', k) def __str__(self): - return 'MODIFIERS: ' + str(self.desired) + return 'Modifiers: ' + str(self.desired) def evaluate(self, feature, notification, device, status, last_result): return self.desired == (current_key_modifiers & MODIFIER_MASK) @@ -378,7 +319,7 @@ class Key(Condition): self.key = 0 def __str__(self): - return 'KEY: ' + (str(self.key) if self.key else 'None') + return 'Key: ' + (str(self.key) if self.key else 'None') def evaluate(self, feature, notification, device, status, last_result): return self.key and self.key == key_down @@ -414,7 +355,7 @@ class Test(Condition): _log.warn('rule Test argument not valid %s', test) def __str__(self): - return 'TEST: ' + str(self.test) + return 'Test: ' + str(self.test) def evaluate(self, feature, notification, device, status, last_result): return self.function(feature, notification.address, notification.data) @@ -573,7 +514,7 @@ rules = Rule([ {'Rule': [{'Test': 'crown_left_ratchet'}, {'KeyPress': 'XF86_AudioLowerVolume'}]} ]}, {'Rule': [ # Thumb wheel does horizontal movement, doubled if control key not pressed - {'Feature': 'THUMB_WHEEL'}, # with control modifier on mouse scrolling sometimes does something different! + {'Feature': 'THUMB WHEEL'}, # with control modifier on mouse scrolling sometimes does something different! {'Rule': [{'Modifiers': 'Control'}, {'Test': 'thumb_wheel_up'}, {'MouseScroll': [-1, 0]}]}, {'Rule': [{'Modifiers': 'Control'}, {'Test': 'thumb_wheel_down'}, {'MouseScroll': [-1, 0]}]}, {'Rule': [{'Or': [{'Test': 'thumb_wheel_up'}, {'Test': 'thumb_wheel_down'}]}, {'MouseScroll': [-2, 0]}]} @@ -596,7 +537,7 @@ def process_notification(device, status, notification, feature): _XDG_CONFIG_HOME = _os.environ.get('XDG_CONFIG_HOME') or _path.expanduser(_path.join('~', '.config')) -_file_path = _path.join(_XDG_CONFIG_HOME, 'solaar', 'divert.yaml') +_file_path = _path.join(_XDG_CONFIG_HOME, 'solaar', 'rules.yaml') def _load_config_rule_file(): @@ -611,7 +552,7 @@ def _load_config_rule_file(): _log.info('load rule: %s', rule) loaded_rules.append(rule) if _log.isEnabledFor(_INFO): - _log.info('loaded %d rules from %s', len(loaded_rules), config_file) + _log.info('loaded %d rules from %s', len(loaded_rules), config_file.name) loaded_rules.extend(rules.components) rules = Rule(loaded_rules) except Exception as e: diff --git a/lib/logitech_receiver/notifications.py b/lib/logitech_receiver/notifications.py index 2e74d231..7dec2d20 100644 --- a/lib/logitech_receiver/notifications.py +++ b/lib/logitech_receiver/notifications.py @@ -388,4 +388,4 @@ def _process_feature_notification(device, status, n, feature): _diversion.process_notification(device, status, n, feature) if _log.isEnabledFor(_DEBUG): - _log.debug('%s: unrecognized %s for feature %s (index %02X)', device, n, feature, n.sub_id) + _log.debug('%s: notification for feature %r, report %s, data %s', device, feature, n.sub_id >> 4, _strhex(n.data))