hidpp20: support version 4 of REPROG_CONTROLS_V4
This commit is contained in:
parent
1361af5501
commit
1973693cc8
|
@ -361,12 +361,12 @@ class FeaturesArray(object):
|
||||||
|
|
||||||
class ReprogrammableKey(object):
|
class ReprogrammableKey(object):
|
||||||
"""Information about a control present on a device with the `REPROG_CONTROLS` feature.
|
"""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:
|
Read-only properties:
|
||||||
- index {int} -- index in the control ID table
|
- index {int} -- index in the control ID table
|
||||||
- key {_NamedInt} -- the name of this control
|
- key {_NamedInt} -- the name of this control
|
||||||
- default_task {_NamedInt} -- the native function 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):
|
def __init__(self, device, index, cid, tid, flags):
|
||||||
self._device = device
|
self._device = device
|
||||||
|
@ -395,7 +395,8 @@ class ReprogrammableKey(object):
|
||||||
|
|
||||||
class ReprogrammableKeyV4(ReprogrammableKey):
|
class ReprogrammableKeyV4(ReprogrammableKey):
|
||||||
"""Information about a control present on a device with the `REPROG_CONTROLS_V4` feature.
|
"""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
|
Contains all the functionality of `ReprogrammableKey` plus remapping keys and /diverting/ them
|
||||||
in order to handle keypresses in a custom way.
|
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 {int} -- the group this control belongs to; other controls with this group in their
|
||||||
`group_mask` can be remapped to this control
|
`group_mask` can be remapped to this control
|
||||||
- group_mask {List[str]} -- this control can be remapped to any control ID in these groups
|
- 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
|
- 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
|
- remappable_to {List[_NamedInt]} -- list of actions which this control can be remapped to
|
||||||
- mapping_flags {List[str]} -- mapping flags set on the control
|
- 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)
|
ReprogrammableKey.__init__(self, device, index, cid, tid, flags)
|
||||||
self.pos = pos
|
self.pos = pos
|
||||||
self.group = group
|
self.group = group
|
||||||
self._gmask = gmask
|
self._gmask = gmask
|
||||||
self.rawXY_reportable = rawxy
|
|
||||||
self._mapping_flags = None
|
self._mapping_flags = None
|
||||||
self._mapped_to = None
|
self._mapped_to = None
|
||||||
|
|
||||||
|
@ -459,16 +458,19 @@ class ReprogrammableKeyV4(ReprogrammableKey):
|
||||||
def set_diverted(self, value: bool):
|
def set_diverted(self, value: bool):
|
||||||
"""If set, the control is diverted temporarily and reports presses as HID++ events
|
"""If set, the control is diverted temporarily and reports presses as HID++ events
|
||||||
until a HID++ configuration reset occurs."""
|
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):
|
def set_persistently_diverted(self, value: bool):
|
||||||
"""If set, the control is diverted permanently and reports presses as HID++ events."""
|
"""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):
|
def set_rawXY_reporting(self, value: bool):
|
||||||
"""If set, the mouse reports all its raw XY events while this control is pressed
|
"""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."""
|
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):
|
def remap(self, to: _NamedInt):
|
||||||
"""Remaps this control to another action."""
|
"""Remaps this control to another action."""
|
||||||
|
@ -483,16 +485,20 @@ class ReprogrammableKeyV4(ReprogrammableKey):
|
||||||
*tuple(_pack('!H', self._cid)),
|
*tuple(_pack('!H', self._cid)),
|
||||||
)
|
)
|
||||||
if mapped_data:
|
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):
|
if cid != self._cid and _log.isEnabledFor(_WARNING):
|
||||||
_log.warn(
|
_log.warn(
|
||||||
f'REPROG_CONTROLS_V4 endpoint getCidReporting on device {self._device} replied ' +
|
f'REPROG_CONTROLS_V4 endpoint getCidReporting on device {self._device} replied ' +
|
||||||
f'with a different control ID ({cid}) than requested ({self._cid}).'
|
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
|
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:
|
else:
|
||||||
raise FeatureCallError('No reply from device.')
|
raise FeatureCallError(msg='No reply from device.')
|
||||||
except Exception:
|
except Exception:
|
||||||
if _log.isEnabledFor(_ERROR):
|
if _log.isEnabledFor(_ERROR):
|
||||||
_log.error(f'Exception in _getCidReporting on device {self._device}: ', exc_info=1)
|
_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._mapping_flags = 0
|
||||||
self._mapped_to = self._cid
|
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
|
"""Sends a `setCidReporting` request with the given parameters to the control. Raises
|
||||||
an exception if the parameters are invalid.
|
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
|
# We need diversion to report raw XY, so divert temporarily
|
||||||
# (since XY reporting is also temporary)
|
# (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:
|
if remap != 0 and remap not in self.remappable_to:
|
||||||
raise FeatureNotSupported(
|
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}.'
|
f'on device {self._device}.'
|
||||||
)
|
)
|
||||||
|
|
||||||
mkbit = lambda v: 1 if v else 0
|
|
||||||
isset = lambda v: mkbit(v is not None)
|
|
||||||
|
|
||||||
pkt = tuple(
|
pkt = tuple(
|
||||||
_pack(
|
_pack(
|
||||||
'!HBH',
|
'!HBH',
|
||||||
self._cid,
|
self._cid,
|
||||||
(isset(rawXY) << 5)
|
bfield & 0xff,
|
||||||
| (mkbit(rawXY) << 4)
|
|
||||||
| (isset(persist) << 3)
|
|
||||||
| (mkbit(persist) << 2)
|
|
||||||
| (isset(divert) << 1)
|
|
||||||
| mkbit(divert),
|
|
||||||
remap,
|
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)
|
ret = feature_request(self._device, FEATURE.REPROG_CONTROLS_V4, 0x30, *pkt)
|
||||||
|
@ -595,18 +615,9 @@ class KeysArray(object):
|
||||||
elif self.keyversion == 4:
|
elif self.keyversion == 4:
|
||||||
keydata = feature_request(self.device, FEATURE.REPROG_CONTROLS_V4, 0x10, index)
|
keydata = feature_request(self.device, FEATURE.REPROG_CONTROLS_V4, 0x10, index)
|
||||||
if keydata:
|
if keydata:
|
||||||
cid, tid, flags, pos, group, gmask, rawxy = _unpack('!HHBBBBB', keydata[:9])
|
cid, tid, flags1, pos, group, gmask, flags2 = _unpack('!HHBBBBB', keydata[:9])
|
||||||
self.keys[index] = ReprogrammableKeyV4(
|
flags = flags1 | (flags2 << 8)
|
||||||
self.device,
|
self.keys[index] = ReprogrammableKeyV4(self.device, index, cid, tid, flags, pos, group, gmask)
|
||||||
index,
|
|
||||||
cid,
|
|
||||||
tid,
|
|
||||||
flags,
|
|
||||||
pos,
|
|
||||||
group,
|
|
||||||
gmask,
|
|
||||||
(rawxy & 0x1) == 0x1,
|
|
||||||
)
|
|
||||||
self.cid_to_tid[cid] = tid
|
self.cid_to_tid[cid] = tid
|
||||||
if group != 0: # 0 = does not belong to a group
|
if group != 0: # 0 = does not belong to a group
|
||||||
self.group_cids[special_keys.CID_GROUP[group]].append(cid)
|
self.group_cids[special_keys.CID_GROUP[group]].append(cid)
|
||||||
|
|
|
@ -285,6 +285,10 @@ def _process_feature_notification(device, status, n, feature):
|
||||||
dx, dy = _unpack('!hh', n.data[:4])
|
dx, dy = _unpack('!hh', n.data[:4])
|
||||||
_log.debug('%s: rawXY dx=%i dy=%i', device, dx, dy)
|
_log.debug('%s: rawXY dx=%i dy=%i', device, dx, dy)
|
||||||
return True
|
return True
|
||||||
|
elif n.address == 0x20:
|
||||||
|
if _log.isEnabledFor(_DEBUG):
|
||||||
|
_log.debug('%s: received analyticsKeyEvents', device)
|
||||||
|
return True
|
||||||
elif _log.isEnabledFor(_WARNING):
|
elif _log.isEnabledFor(_WARNING):
|
||||||
_log.warn('%s: unknown REPROG_CONTROLS_V4 %s', device, n)
|
_log.warn('%s: unknown REPROG_CONTROLS_V4 %s', device, n)
|
||||||
|
|
||||||
|
|
|
@ -488,8 +488,13 @@ TASK = _NamedInts(
|
||||||
LedToggle=0x00DD, #
|
LedToggle=0x00DD, #
|
||||||
)
|
)
|
||||||
TASK._fallback = lambda x: 'unknown:%04X' % x
|
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(
|
KEY_FLAG = _NamedInts(
|
||||||
|
analytics_key_events=0x400,
|
||||||
|
force_raw_XY=0x200,
|
||||||
|
raw_XY=0x100,
|
||||||
virtual=0x80,
|
virtual=0x80,
|
||||||
persistently_divertable=0x40,
|
persistently_divertable=0x40,
|
||||||
divertable=0x20,
|
divertable=0x20,
|
||||||
|
@ -499,7 +504,15 @@ KEY_FLAG = _NamedInts(
|
||||||
is_FN=0x02,
|
is_FN=0x02,
|
||||||
mse=0x01
|
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_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)
|
CID_GROUP = _NamedInts(g8=8, g7=7, g6=6, g5=5, g4=4, g3=3, g2=2, g1=1)
|
||||||
DISABLE = _NamedInts(
|
DISABLE = _NamedInts(
|
||||||
|
|
|
@ -198,6 +198,9 @@ def _print_device(dev):
|
||||||
gmask_fmt = ','.join(k.group_mask)
|
gmask_fmt = ','.join(k.group_mask)
|
||||||
gmask_fmt = gmask_fmt if gmask_fmt else 'empty'
|
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))
|
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:
|
if dev.online:
|
||||||
battery = _hidpp20.get_battery(dev)
|
battery = _hidpp20.get_battery(dev)
|
||||||
if battery is None:
|
if battery is None:
|
||||||
|
|
Loading…
Reference in New Issue