docs: add documentation for rules processing and change name of rules file

This commit is contained in:
Peter F. Patel-Schneider 2020-11-12 13:11:30 -05:00
parent 30e4324906
commit be590c154a
4 changed files with 156 additions and 72 deletions

View File

@ -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

135
docs/rules.md Normal file
View File

@ -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] ]
...
```

View File

@ -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:

View File

@ -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))