From 1973693cc814b1ffa35e0cb0ad0f077a1afe8b96 Mon Sep 17 00:00:00 2001 From: Wojciech Nawrocki Date: Fri, 7 Aug 2020 02:09:37 +0200 Subject: [PATCH] hidpp20: support version 4 of REPROG_CONTROLS_V4 --- lib/logitech_receiver/hidpp20.py | 105 ++++++++++++++----------- lib/logitech_receiver/notifications.py | 4 + lib/logitech_receiver/special_keys.py | 17 +++- lib/solaar/cli/show.py | 3 + 4 files changed, 80 insertions(+), 49 deletions(-) diff --git a/lib/logitech_receiver/hidpp20.py b/lib/logitech_receiver/hidpp20.py index b8011bee..097e87c0 100644 --- a/lib/logitech_receiver/hidpp20.py +++ b/lib/logitech_receiver/hidpp20.py @@ -361,12 +361,12 @@ class FeaturesArray(object): class ReprogrammableKey(object): """Information about a control present on a device with the `REPROG_CONTROLS` feature. - Ref: https://lekensteyn.nl/files/logitech/logitech_hidpp_2.0_specification_draft_2012-06-04.pdf + Ref: https://drive.google.com/file/d/0BxbRzx7vEV7eU3VfMnRuRXktZ3M/view Read-only properties: - index {int} -- index in the control ID table - key {_NamedInt} -- the name of this control - default_task {_NamedInt} -- the native function of this control - - flags {List[str]} -- flags set on the control + - flags {List[str]} -- capabilities and desired software handling of the control """ def __init__(self, device, index, cid, tid, flags): self._device = device @@ -395,7 +395,8 @@ class ReprogrammableKey(object): class ReprogrammableKeyV4(ReprogrammableKey): """Information about a control present on a device with the `REPROG_CONTROLS_V4` feature. - Ref: https://lekensteyn.nl/files/logitech/x1b04_specialkeysmsebuttons.html + Ref (v2): https://lekensteyn.nl/files/logitech/x1b04_specialkeysmsebuttons.html + Ref (v4): https://drive.google.com/file/d/10imcbmoxTJ1N510poGdsviEhoFfB_Ua4/view Contains all the functionality of `ReprogrammableKey` plus remapping keys and /diverting/ them in order to handle keypresses in a custom way. @@ -404,17 +405,15 @@ class ReprogrammableKeyV4(ReprogrammableKey): - group {int} -- the group this control belongs to; other controls with this group in their `group_mask` can be remapped to this control - group_mask {List[str]} -- this control can be remapped to any control ID in these groups - - rawXY_reportable {bool} -- whether the control can be diverted to report raw XY events - mapped_to {_NamedInt} -- which action this control is mapped to; usually itself - remappable_to {List[_NamedInt]} -- list of actions which this control can be remapped to - mapping_flags {List[str]} -- mapping flags set on the control """ - def __init__(self, device, index, cid, tid, flags, pos, group, gmask, rawxy): + def __init__(self, device, index, cid, tid, flags, pos, group, gmask): ReprogrammableKey.__init__(self, device, index, cid, tid, flags) self.pos = pos self.group = group self._gmask = gmask - self.rawXY_reportable = rawxy self._mapping_flags = None self._mapped_to = None @@ -459,16 +458,19 @@ class ReprogrammableKeyV4(ReprogrammableKey): def set_diverted(self, value: bool): """If set, the control is diverted temporarily and reports presses as HID++ events until a HID++ configuration reset occurs.""" - self._setCidReporting(divert=value) + flags = {special_keys.MAPPING_FLAG.diverted: value} + self._setCidReporting(flags=flags) def set_persistently_diverted(self, value: bool): """If set, the control is diverted permanently and reports presses as HID++ events.""" - self._setCidReporting(persist=value) + flags = {special_keys.MAPPING_FLAG.persistently_diverted: value} + self._setCidReporting(flags=flags) def set_rawXY_reporting(self, value: bool): """If set, the mouse reports all its raw XY events while this control is pressed as HID++ events. Gets cleared on a HID++ configuration reset.""" - self._setCidReporting(rawXY=value) + flags = {special_keys.MAPPING_FLAG.raw_XY_diverted: value} + self._setCidReporting(flags=flags) def remap(self, to: _NamedInt): """Remaps this control to another action.""" @@ -483,16 +485,20 @@ class ReprogrammableKeyV4(ReprogrammableKey): *tuple(_pack('!H', self._cid)), ) if mapped_data: - cid, mapping_flags, mapped_to = _unpack('!HBH', mapped_data[:5]) + cid, mapping_flags_1, mapped_to = _unpack('!HBH', mapped_data[:5]) if cid != self._cid and _log.isEnabledFor(_WARNING): _log.warn( f'REPROG_CONTROLS_V4 endpoint getCidReporting on device {self._device} replied ' + f'with a different control ID ({cid}) than requested ({self._cid}).' ) - self._mapping_flags = mapping_flags self._mapped_to = mapped_to if mapped_to != 0 else self._cid + if len(mapped_data) > 5: + mapping_flags_2, = _unpack('!B', mapped_data[5:6]) + else: + mapping_flags_2 = 0 + self._mapping_flags = mapping_flags_1 | (mapping_flags_2 << 8) else: - raise FeatureCallError('No reply from device.') + raise FeatureCallError(msg='No reply from device.') except Exception: if _log.isEnabledFor(_ERROR): _log.error(f'Exception in _getCidReporting on device {self._device}: ', exc_info=1) @@ -500,45 +506,59 @@ class ReprogrammableKeyV4(ReprogrammableKey): self._mapping_flags = 0 self._mapped_to = self._cid - def _setCidReporting(self, divert=None, persist=None, rawXY=None, remap=0): + def _setCidReporting(self, flags=None, remap=0): """Sends a `setCidReporting` request with the given parameters to the control. Raises an exception if the parameters are invalid. + + Parameters: + - flags {Dict[_NamedInt,bool]} -- a dictionary of which mapping flags to set/unset + - remap {int} -- which control ID to remap to; or 0 to keep current mapping """ - if rawXY: + flags = flags if flags else {} # See flake8 B006 + + if special_keys.MAPPING_FLAG.raw_XY_diverted in flags and flags[special_keys.MAPPING_FLAG.raw_XY_diverted]: # We need diversion to report raw XY, so divert temporarily # (since XY reporting is also temporary) - divert = True + flags[special_keys.MAPPING_FLAG.diverted] = True + + if special_keys.MAPPING_FLAG.diverted in flags and not flags[special_keys.MAPPING_FLAG.diverted]: + flags[special_keys.MAPPING_FLAG.raw_XY_diverted] = False + + # The capability required to set a given reporting flag. + FLAG_TO_CAPABILITY = { + special_keys.MAPPING_FLAG.diverted: special_keys.KEY_FLAG.divertable, + special_keys.MAPPING_FLAG.persistently_diverted: special_keys.KEY_FLAG.persistently_divertable, + special_keys.MAPPING_FLAG.analytics_key_events_reporting: special_keys.KEY_FLAG.analytics_key_events, + special_keys.MAPPING_FLAG.force_raw_XY_diverted: special_keys.KEY_FLAG.force_raw_XY, + special_keys.MAPPING_FLAG.raw_XY_diverted: special_keys.KEY_FLAG.raw_XY + } + + bfield = 0 + for f, v in flags.items(): + if v and FLAG_TO_CAPABILITY[f] not in self.flags: + raise FeatureNotSupported( + msg=f'Tried to set mapping flag "{f}" on control "{self.key}" ' + + f'which does not support "{FLAG_TO_CAPABILITY[f]}" on device {self._device}.' + ) + + bfield |= int(f) if v else 0 + bfield |= int(f) << 1 # The 'Xvalid' bit - if divert is not None and special_keys.KEY_FLAG.divertable not in self.flags: - raise FeatureNotSupported(f'Tried to divert non-divertable control {self.key} on device {self._device}.') - if persist is not None and special_keys.KEY_FLAG.persistently_divertable not in self.flags: - raise FeatureNotSupported( - 'Tried to persistently divert non-persistently-divertable control {self.key} on device {self._device}.' - ) - if rawXY is not None and not self.rawXY_reportable: - raise FeatureNotSupported( - f'Tried to request raw XY reports from control {self.key} with no raw XY capability on device {self._device}.' - ) if remap != 0 and remap not in self.remappable_to: raise FeatureNotSupported( - f'Tried to remap control {self.key} to a control ID {remap} which it is not remappable to ' + + msg=f'Tried to remap control "{self.key}" to a control ID {remap} which it is not remappable to ' + f'on device {self._device}.' ) - mkbit = lambda v: 1 if v else 0 - isset = lambda v: mkbit(v is not None) - pkt = tuple( _pack( '!HBH', self._cid, - (isset(rawXY) << 5) - | (mkbit(rawXY) << 4) - | (isset(persist) << 3) - | (mkbit(persist) << 2) - | (isset(divert) << 1) - | mkbit(divert), + bfield & 0xff, remap, + # TODO: to fully support version 4 of REPROG_CONTROLS_V4, append + # another byte `(bfield >> 8) & 0xff` here. But older devices + # might behave oddly given that byte, so we don't send it. ) ) ret = feature_request(self._device, FEATURE.REPROG_CONTROLS_V4, 0x30, *pkt) @@ -595,18 +615,9 @@ class KeysArray(object): elif self.keyversion == 4: keydata = feature_request(self.device, FEATURE.REPROG_CONTROLS_V4, 0x10, index) if keydata: - cid, tid, flags, pos, group, gmask, rawxy = _unpack('!HHBBBBB', keydata[:9]) - self.keys[index] = ReprogrammableKeyV4( - self.device, - index, - cid, - tid, - flags, - pos, - group, - gmask, - (rawxy & 0x1) == 0x1, - ) + cid, tid, flags1, pos, group, gmask, flags2 = _unpack('!HHBBBBB', keydata[:9]) + flags = flags1 | (flags2 << 8) + self.keys[index] = ReprogrammableKeyV4(self.device, index, cid, tid, flags, pos, group, gmask) self.cid_to_tid[cid] = tid if group != 0: # 0 = does not belong to a group self.group_cids[special_keys.CID_GROUP[group]].append(cid) diff --git a/lib/logitech_receiver/notifications.py b/lib/logitech_receiver/notifications.py index 63770985..95b8965c 100644 --- a/lib/logitech_receiver/notifications.py +++ b/lib/logitech_receiver/notifications.py @@ -285,6 +285,10 @@ def _process_feature_notification(device, status, n, feature): dx, dy = _unpack('!hh', n.data[:4]) _log.debug('%s: rawXY dx=%i dy=%i', device, dx, dy) return True + elif n.address == 0x20: + if _log.isEnabledFor(_DEBUG): + _log.debug('%s: received analyticsKeyEvents', device) + return True elif _log.isEnabledFor(_WARNING): _log.warn('%s: unknown REPROG_CONTROLS_V4 %s', device, n) diff --git a/lib/logitech_receiver/special_keys.py b/lib/logitech_receiver/special_keys.py index 7063542f..805fdbf6 100644 --- a/lib/logitech_receiver/special_keys.py +++ b/lib/logitech_receiver/special_keys.py @@ -488,8 +488,13 @@ TASK = _NamedInts( LedToggle=0x00DD, # ) TASK._fallback = lambda x: 'unknown:%04X' % x -# hidpp 4.5 info from https://lekensteyn.nl/files/logitech/x1b04_specialkeysmsebuttons.html +# Capabilities and desired software handling for a control +# Ref: https://drive.google.com/file/d/10imcbmoxTJ1N510poGdsviEhoFfB_Ua4/view +# We treat bytes 4 and 8 of `getCidInfo` as a single bitfield KEY_FLAG = _NamedInts( + analytics_key_events=0x400, + force_raw_XY=0x200, + raw_XY=0x100, virtual=0x80, persistently_divertable=0x40, divertable=0x20, @@ -499,7 +504,15 @@ KEY_FLAG = _NamedInts( is_FN=0x02, mse=0x01 ) -MAPPING_FLAG = _NamedInts(rawXY_diverted=0x10, persistently_diverted=0x04, diverted=0x01) +# Flags describing the reporting method of a control +# We treat bytes 2 and 5 of `get/setCidReporting` as a single bitfield +MAPPING_FLAG = _NamedInts( + analytics_key_events_reporting=0x100, + force_raw_XY_diverted=0x40, + raw_XY_diverted=0x10, + persistently_diverted=0x04, + diverted=0x01 +) CID_GROUP_BIT = _NamedInts(g8=0x80, g7=0x40, g6=0x20, g5=0x10, g4=0x08, g3=0x04, g2=0x02, g1=0x01) CID_GROUP = _NamedInts(g8=8, g7=7, g6=6, g5=5, g4=4, g3=3, g2=2, g1=1) DISABLE = _NamedInts( diff --git a/lib/solaar/cli/show.py b/lib/solaar/cli/show.py index 4e516ce4..f76a19e9 100644 --- a/lib/solaar/cli/show.py +++ b/lib/solaar/cli/show.py @@ -198,6 +198,9 @@ def _print_device(dev): gmask_fmt = ','.join(k.group_mask) gmask_fmt = gmask_fmt if gmask_fmt else 'empty' print(' %s, pos:%d, group:%1d, group mask:%s' % (', '.join(k.flags), k.pos, k.group, gmask_fmt)) + report_fmt = ', '.join(k.mapping_flags) + report_fmt = report_fmt if report_fmt else 'default' + print(' reporting: %s' % (report_fmt)) if dev.online: battery = _hidpp20.get_battery(dev) if battery is None: