# -*- python-mode -*- # -*- coding: UTF-8 -*- ## Copyright (C) 2012-2013 Daniel Pavel ## ## This program is free software; you can redistribute it and/or modify ## it under the terms of the GNU General Public License as published by ## the Free Software Foundation; either version 2 of the License, or ## (at your option) any later version. ## ## This program is distributed in the hope that it will be useful, ## but WITHOUT ANY WARRANTY; without even the implied warranty of ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ## GNU General Public License for more details. ## ## You should have received a copy of the GNU General Public License along ## with this program; if not, write to the Free Software Foundation, Inc., ## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # Logitech Unifying Receiver API. from __future__ import absolute_import, division, print_function, unicode_literals from logging import DEBUG as _DEBUG from logging import ERROR as _ERROR from logging import WARNING as _WARNING from logging import getLogger from typing import List from . import special_keys from .common import FirmwareInfo as _FirmwareInfo from .common import KwException as _KwException from .common import NamedInt as _NamedInt from .common import NamedInts as _NamedInts from .common import pack as _pack from .common import unpack as _unpack _log = getLogger(__name__) del getLogger # # # # /,/p}' | sort -t= -k2 # additional features names taken from https://github.com/cvuchener/hidpp and # https://github.com/Logitech/cpg-docs/tree/master/hidpp20 """Possible features available on a Logitech device. A particular device might not support all these features, and may support other unknown features as well. """ FEATURE = _NamedInts( ROOT=0x0000, FEATURE_SET=0x0001, FEATURE_INFO=0x0002, # Common DEVICE_FW_VERSION=0x0003, DEVICE_UNIT_ID=0x0004, DEVICE_NAME=0x0005, DEVICE_GROUPS=0x0006, DEVICE_FRIENDLY_NAME=0x0007, KEEP_ALIVE=0x0008, RESET=0x0020, # "Config Change" CRYPTO_ID=0x0021, TARGET_SOFTWARE=0x0030, WIRELESS_SIGNAL_STRENGTH=0x0080, DFUCONTROL_LEGACY=0x00C0, DFUCONTROL_UNSIGNED=0x00C1, DFUCONTROL_SIGNED=0x00C2, DFU=0x00D0, BATTERY_STATUS=0x1000, BATTERY_VOLTAGE=0x1001, CHARGING_CONTROL=0x1010, LED_CONTROL=0x1300, GENERIC_TEST=0x1800, DEVICE_RESET=0x1802, OOBSTATE=0x1805, CONFIG_DEVICE_PROPS=0x1806, CHANGE_HOST=0x1814, HOSTS_INFO=0x1815, BACKLIGHT=0x1981, BACKLIGHT2=0x1982, BACKLIGHT3=0x1983, PRESENTER_CONTROL=0x1A00, SENSOR_3D=0x1A01, REPROG_CONTROLS=0x1B00, REPROG_CONTROLS_V2=0x1B01, REPROG_CONTROLS_V2_2=0x1B02, # LogiOptions 2.10.73 features.xml REPROG_CONTROLS_V3=0x1B03, REPROG_CONTROLS_V4=0x1B04, REPORT_HID_USAGE=0x1BC0, PERSISTENT_REMAPPABLE_ACTION=0x1C00, WIRELESS_DEVICE_STATUS=0x1D4B, REMAINING_PAIRING=0x1DF0, FIRMWARE_PROPERTIES=0x1F1F, ADC_MEASUREMENT=0x1F20, # Mouse LEFT_RIGHT_SWAP=0x2001, SWAP_BUTTON_CANCEL=0x2005, POINTER_AXIS_ORIENTATION=0x2006, VERTICAL_SCROLLING=0x2100, SMART_SHIFT=0x2110, HI_RES_SCROLLING=0x2120, HIRES_WHEEL=0x2121, LOWRES_WHEEL=0x2130, THUMB_WHEEL=0x2150, MOUSE_POINTER=0x2200, ADJUSTABLE_DPI=0x2201, POINTER_SPEED=0x2205, ANGLE_SNAPPING=0x2230, SURFACE_TUNING=0x2240, HYBRID_TRACKING=0x2400, # Keyboard FN_INVERSION=0x40A0, NEW_FN_INVERSION=0x40A2, K375S_FN_INVERSION=0x40A3, ENCRYPTION=0x4100, LOCK_KEY_STATE=0x4220, SOLAR_DASHBOARD=0x4301, KEYBOARD_LAYOUT=0x4520, KEYBOARD_DISABLE_KEYS=0x4521, KEYBOARD_DISABLE_BY_USAGE=0x4522, DUALPLATFORM=0x4530, MULTIPLATFORM=0x4531, KEYBOARD_LAYOUT_2=0x4540, CROWN=0x4600, # Touchpad TOUCHPAD_FW_ITEMS=0x6010, TOUCHPAD_SW_ITEMS=0x6011, TOUCHPAD_WIN8_FW_ITEMS=0x6012, TAP_ENABLE=0x6020, TAP_ENABLE_EXTENDED=0x6021, CURSOR_BALLISTIC=0x6030, TOUCHPAD_RESOLUTION=0x6040, TOUCHPAD_RAW_XY=0x6100, TOUCHMOUSE_RAW_POINTS=0x6110, TOUCHMOUSE_6120=0x6120, GESTURE=0x6500, GESTURE_2=0x6501, # Gaming Devices GKEY=0x8010, MKEYS=0x8020, MR=0x8030, BRIGHTNESS_CONTROL=0x8040, REPORT_RATE=0x8060, COLOR_LED_EFFECTS=0x8070, RGB_EFFECTS=0x8071, PER_KEY_LIGHTING=0x8080, PER_KEY_LIGHTING_V2=0x8081, MODE_STATUS=0x8090, ONBOARD_PROFILES=0x8100, MOUSE_BUTTON_SPY=0x8110, LATENCY_MONITORING=0x8111, GAMING_ATTACHMENTS=0x8120, FORCE_FEEDBACK=0x8123, SIDETONE=0x8300, EQUALIZER=0x8310, HEADSET_OUT=0x8320, ) FEATURE._fallback = lambda x: 'unknown:%04X' % x FEATURE_FLAG = _NamedInts(internal=0x20, hidden=0x40, obsolete=0x80) DEVICE_KIND = _NamedInts( keyboard=0x00, remote_control=0x01, numpad=0x02, mouse=0x03, touchpad=0x04, trackball=0x05, presenter=0x06, receiver=0x07 ) FIRMWARE_KIND = _NamedInts(Firmware=0x00, Bootloader=0x01, Hardware=0x02, Other=0x03) BATTERY_OK = lambda status: status not in (BATTERY_STATUS.invalid_battery, BATTERY_STATUS.thermal_error) BATTERY_STATUS = _NamedInts( discharging=0x00, recharging=0x01, almost_full=0x02, full=0x03, slow_recharge=0x04, invalid_battery=0x05, thermal_error=0x06 ) CHARGE_STATUS = _NamedInts(charging=0x00, full=0x01, not_charging=0x02, error=0x07) CHARGE_LEVEL = _NamedInts(average=50, full=90, critical=5) CHARGE_TYPE = _NamedInts(standard=0x00, fast=0x01, slow=0x02) ERROR = _NamedInts( unknown=0x01, invalid_argument=0x02, out_of_range=0x03, hardware_error=0x04, logitech_internal=0x05, invalid_feature_index=0x06, invalid_function=0x07, busy=0x08, unsupported=0x09 ) # # # class FeatureNotSupported(_KwException): """Raised when trying to request a feature not supported by the device.""" pass class FeatureCallError(_KwException): """Raised if the device replied to a feature call with an error.""" pass # # # class FeaturesArray(object): """A sequence of features supported by a HID++ 2.0 device.""" __slots__ = ('supported', 'device', 'features', 'non_features') assert FEATURE.ROOT == 0x0000 def __init__(self, device): assert device is not None self.device = device self.supported = True self.features = None self.non_features = set() def __del__(self): self.supported = False self.device = None self.features = None def _check(self): # print (self.device, "check", self.supported, self.features, self.device.protocol) if self.supported: assert self.device is not None if self.features is not None: return True if not self.device.online: # device is not connected right now, will have to try later return False # I _think_ this is universally true if self.device.protocol and self.device.protocol < 2.0: self.supported = False self.device.features = None self.device = None return False reply = self.device.request(0x0000, _pack('!H', FEATURE.FEATURE_SET)) if reply is None: self.supported = False else: fs_index = ord(reply[0:1]) if fs_index: count = self.device.request(fs_index << 8) if count is None: _log.warn('FEATURE_SET found, but failed to read features count') # most likely the device is unavailable return False else: count = ord(count[:1]) assert count >= fs_index self.features = [None] * (1 + count) self.features[0] = FEATURE.ROOT self.features[fs_index] = FEATURE.FEATURE_SET return True else: self.supported = False return False __bool__ = __nonzero__ = _check def __getitem__(self, index): if self._check(): if isinstance(index, int): if index < 0 or index >= len(self.features): raise IndexError(index) if self.features[index] is None: feature = self.device.feature_request(FEATURE.FEATURE_SET, 0x10, index) if feature: feature, = _unpack('!H', feature[:2]) self.features[index] = FEATURE[feature] return self.features[index] elif isinstance(index, slice): indices = index.indices(len(self.features)) return [self.__getitem__(i) for i in range(*indices)] def __contains__(self, featureId): """Tests whether the list contains given Feature ID""" if self._check(): ivalue = int(featureId) if ivalue in self.non_features: return False may_have = False for f in self.features: if f is None: may_have = True elif ivalue == int(f): return True if may_have: reply = self.device.request(0x0000, _pack('!H', ivalue)) if reply: index = ord(reply[0:1]) if index: self.features[index] = FEATURE[ivalue] return True else: self.non_features.add(ivalue) return False def index(self, featureId): """Gets the Feature Index for a given Feature ID""" if self._check(): may_have = False ivalue = int(featureId) for index, f in enumerate(self.features): if f is None: may_have = True elif ivalue == int(f): return index if may_have: reply = self.device.request(0x0000, _pack('!H', ivalue)) if reply: index = ord(reply[0:1]) self.features[index] = FEATURE[ivalue] return index raise ValueError('%r not in list' % featureId) def __iter__(self): if self._check(): yield FEATURE.ROOT index = 1 last_index = len(self.features) while index < last_index: yield self.__getitem__(index) index += 1 def __len__(self): return len(self.features) if self._check() else 0 # # # class ReprogrammableKey(object): """Information about a control present on a device with the `REPROG_CONTROLS` feature. 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]} -- capabilities and desired software handling of the control """ def __init__(self, device, index, cid, tid, flags): self._device = device self.index = index self._cid = cid self._tid = tid self._flags = flags @property def key(self) -> _NamedInt: return special_keys.CONTROL[self._cid] @property def default_task(self) -> _NamedInt: """NOTE: This NamedInt is a bit mixed up, because its value is the Control ID while the name is the Control ID's native task. But this makes more sense than presenting details of controls vs tasks in the interface. The same convention applies to `mapped_to`, `remappable_to`, `remap` in `ReprogrammableKeyV4`.""" task = str(special_keys.TASK[self._tid]) return _NamedInt(self._cid, task) @property def flags(self) -> List[str]: return special_keys.KEY_FLAG.flag_names(self._flags) class ReprogrammableKeyV4(ReprogrammableKey): """Information about a control present on a device with the `REPROG_CONTROLS_V4` feature. 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. Additional read-only properties: - pos {int} -- position of this control on the device; 1-16 for FN-keys, otherwise 0 - 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 - 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): ReprogrammableKey.__init__(self, device, index, cid, tid, flags) self.pos = pos self.group = group self._gmask = gmask self._mapping_flags = None self._mapped_to = None @property def group_mask(self): return special_keys.CID_GROUP_BIT.flag_names(self._gmask) @property def mapped_to(self) -> _NamedInt: if self._mapped_to is None: self._getCidReporting() self._device.keys._ensure_all_keys_queried() task = str(special_keys.TASK[self._device.keys.cid_to_tid[self._mapped_to]]) return _NamedInt(self._mapped_to, task) @property def remappable_to(self) -> List[_NamedInt]: # this flag is only to show in UI, ignore in Solaar # if special_keys.KEY_FLAG.reprogrammable not in self.flags: # return [] self._device.keys._ensure_all_keys_queried() ret = [] if self.group_mask != []: # only keys with a non-zero gmask are remappable ret = [self.default_task] # it should always be possible to map the key to itself for g in self.group_mask: g = special_keys.CID_GROUP[str(g)] for tgt_cid in self._device.keys.group_cids[g]: tgt_task = str(special_keys.TASK[self._device.keys.cid_to_tid[tgt_cid]]) tgt_task = _NamedInt(tgt_cid, tgt_task) if tgt_task != self.default_task: # don't put itself in twice ret.append(tgt_task) return ret @property def mapping_flags(self) -> List[str]: if self._mapping_flags is None: self._getCidReporting() return special_keys.MAPPING_FLAG.flag_names(self._mapping_flags) 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.""" 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.""" 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.""" flags = {special_keys.MAPPING_FLAG.raw_XY_diverted: value} self._setCidReporting(flags=flags) def remap(self, to: _NamedInt): """Remaps this control to another action.""" self._setCidReporting(remap=int(to)) def _getCidReporting(self): try: mapped_data = feature_request( self._device, FEATURE.REPROG_CONTROLS_V4, 0x20, *tuple(_pack('!H', self._cid)), ) if mapped_data: 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._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(msg='No reply from device.') except Exception: if _log.isEnabledFor(_ERROR): _log.error(f'Exception in _getCidReporting on device {self._device}: ', exc_info=1) # Clear flags and set mapping target to self as fallback self._mapping_flags = 0 self._mapped_to = self._cid 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 """ 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) 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 remap != 0 and remap not in self.remappable_to: raise FeatureNotSupported( msg=f'Tried to remap control "{self.key}" to a control ID {remap} which it is not remappable to ' + f'on device {self._device}.' ) pkt = tuple( _pack( '!HBH', self._cid, 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) if _unpack('!BBBBB', ret[:5]) != pkt and _log.isEnabledFor(_WARNING): _log.warn( f"REPROG_CONTROLS_v4 endpoint setCidReporting on device {self._device} should echo request packet, but didn't." ) # update knowledge of mapping self._getCidReporting() class KeysArray(object): """A sequence of key mappings supported by a HID++ 2.0 device.""" __slots__ = ('device', 'keys', 'keyversion', 'cid_to_tid', 'group_cids') def __init__(self, device, count): assert device is not None self.device = device if FEATURE.REPROG_CONTROLS in self.device.features: self.keyversion = 1 elif FEATURE.REPROG_CONTROLS_V4 in self.device.features: self.keyversion = 4 else: if _log.isEnabledFor(_ERROR): _log.error(f'Trying to read keys on device {device} which has no REPROG_CONTROLS(_VX) support.') self.keyversion = None self.keys = [None] * count """The mapping from Control IDs to their native Task IDs. For example, Control "Left Button" is mapped to Task "Left Click". When remapping controls, we point the control we want to remap at a target Control ID rather than a target Task ID. This has the effect of performing the native task of the target control, even if the target itself is also remapped. So remapping is not recursive.""" self.cid_to_tid = {} """The mapping from Control ID groups to Controls IDs that belong to it. A key k can only be remapped to targets in groups within k.group_mask.""" self.group_cids = {g: [] for g in special_keys.CID_GROUP} def _query_key(self, index: int): """Queries the device for a given key and stores it in self.keys.""" if index < 0 or index >= len(self.keys): raise IndexError(index) # TODO: add here additional variants for other REPROG_CONTROLS if self.keyversion == 1: keydata = feature_request(self.device, FEATURE.REPROG_CONTROLS, 0x10, index) if keydata: cid, tid, flags = _unpack('!HHB', keydata[:5]) self.keys[index] = ReprogrammableKey(self.device, index, cid, tid, flags) self.cid_to_tid[cid] = tid elif self.keyversion == 4: keydata = feature_request(self.device, FEATURE.REPROG_CONTROLS_V4, 0x10, index) if keydata: 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) elif _log.isEnabledFor(_WARNING): _log.warn(f"Key with index {index} was expected to exist but device doesn't report it.") def _ensure_all_keys_queried(self): """The retrieval of key information is lazy, but for certain functionality we need to know all keys. This function makes sure that's the case.""" for (i, k) in enumerate(self.keys): if k is None: self._query_key(i) def __getitem__(self, index): if isinstance(index, int): if index < 0 or index >= len(self.keys): raise IndexError(index) if self.keys[index] is None: self._query_key(index) return self.keys[index] elif isinstance(index, slice): indices = index.indices(len(self.keys)) return [self.__getitem__(i) for i in range(*indices)] def index(self, value): for index, k in enumerate(self.keys): if k is not None and int(value) == int(k.key): return index for index, k in enumerate(self.keys): if k is None: k = self.__getitem__(index) if k is not None: return index def __iter__(self): for k in range(0, len(self.keys)): yield self.__getitem__(k) def __len__(self): return len(self.keys) # # # def feature_request(device, feature, function=0x00, *params, no_reply=False): if device.online and device.features: if feature in device.features: feature_index = device.features.index(int(feature)) return device.request((feature_index << 8) + (function & 0xFF), *params, no_reply=no_reply) def get_firmware(device): """Reads a device's firmware info. :returns: a list of FirmwareInfo tuples, ordered by firmware layer. """ count = feature_request(device, FEATURE.DEVICE_FW_VERSION) if count: count = ord(count[:1]) fw = [] for index in range(0, count): fw_info = feature_request(device, FEATURE.DEVICE_FW_VERSION, 0x10, index) if fw_info: level = ord(fw_info[:1]) & 0x0F if level == 0 or level == 1: name, version_major, version_minor, build = _unpack('!3sBBH', fw_info[1:8]) version = '%02X.%02X' % (version_major, version_minor) if build: version += '.B%04X' % build extras = fw_info[9:].rstrip(b'\x00') or None fw_info = _FirmwareInfo(FIRMWARE_KIND[level], name.decode('ascii'), version, extras) elif level == FIRMWARE_KIND.Hardware: fw_info = _FirmwareInfo(FIRMWARE_KIND.Hardware, '', str(ord(fw_info[1:2])), None) else: fw_info = _FirmwareInfo(FIRMWARE_KIND.Other, '', '', None) fw.append(fw_info) # if _log.isEnabledFor(_DEBUG): # _log.debug("device %d firmware %s", devnumber, fw_info) return tuple(fw) def get_kind(device): """Reads a device's type. :see DEVICE_KIND: :returns: a string describing the device type, or ``None`` if the device is not available or does not support the ``DEVICE_NAME`` feature. """ kind = feature_request(device, FEATURE.DEVICE_NAME, 0x20) if kind: kind = ord(kind[:1]) # if _log.isEnabledFor(_DEBUG): # _log.debug("device %d type %d = %s", devnumber, kind, DEVICE_KIND[kind]) return DEVICE_KIND[kind] def get_name(device): """Reads a device's name. :returns: a string with the device name, or ``None`` if the device is not available or does not support the ``DEVICE_NAME`` feature. """ name_length = feature_request(device, FEATURE.DEVICE_NAME) if name_length: name_length = ord(name_length[:1]) name = b'' while len(name) < name_length: fragment = feature_request(device, FEATURE.DEVICE_NAME, 0x10, len(name)) if fragment: name += fragment[:name_length - len(name)] else: _log.error('failed to read whole name of %s (expected %d chars)', device, name_length) return None return name.decode('ascii') def get_battery(device): """Reads a device's battery level.""" battery = feature_request(device, FEATURE.BATTERY_STATUS) if battery: discharge, dischargeNext, status = _unpack('!BBB', battery[:3]) discharge = None if discharge == 0 else discharge if _log.isEnabledFor(_DEBUG): _log.debug( 'device %d battery %d%% charged, next level %d%% charge, status %d = %s', device.number, discharge, dischargeNext, status, BATTERY_STATUS[status] ) return discharge, BATTERY_STATUS[status], dischargeNext def get_voltage(device): battery_voltage = feature_request(device, FEATURE.BATTERY_VOLTAGE) if battery_voltage: return decipher_voltage(battery_voltage) # modified to be much closer to battery reports def decipher_voltage(voltage_report): voltage, flags = _unpack('>HB', voltage_report[:3]) status = BATTERY_STATUS.discharging charge_sts = ERROR.unknown charge_lvl = CHARGE_LEVEL.average charge_type = CHARGE_TYPE.standard if flags & (1 << 7): status = BATTERY_STATUS.recharging charge_sts = CHARGE_STATUS[flags & 0x03] if charge_sts is None: charge_sts = ERROR.unknown elif charge_sts == CHARGE_STATUS.full: charge_lvl = CHARGE_LEVEL.full status = BATTERY_STATUS.full if (flags & (1 << 3)): charge_type = CHARGE_TYPE.fast elif (flags & (1 << 4)): charge_type = CHARGE_TYPE.slow status = BATTERY_STATUS.slow_recharge elif (flags & (1 << 5)): charge_lvl = CHARGE_LEVEL.critical if _log.isEnabledFor(_DEBUG): _log.debug( 'device ???, battery voltage %d mV, charging = %s, charge status %d = %s, charge level %s, charge type %s', voltage, status, (flags & 0x03), charge_sts, charge_lvl, charge_type ) return charge_lvl, status, voltage, charge_sts, charge_type def get_keys(device): # TODO: add here additional variants for other REPROG_CONTROLS count = None if FEATURE.REPROG_CONTROLS in device.features: count = feature_request(device, FEATURE.REPROG_CONTROLS) elif FEATURE.REPROG_CONTROLS_V4 in device.features: count = feature_request(device, FEATURE.REPROG_CONTROLS_V4) if count: return KeysArray(device, ord(count[:1])) def get_mouse_pointer_info(device): pointer_info = feature_request(device, FEATURE.MOUSE_POINTER) if pointer_info: dpi, flags = _unpack('!HB', pointer_info[:3]) acceleration = ('none', 'low', 'med', 'high')[flags & 0x3] suggest_os_ballistics = (flags & 0x04) != 0 suggest_vertical_orientation = (flags & 0x08) != 0 return { 'dpi': dpi, 'acceleration': acceleration, 'suggest_os_ballistics': suggest_os_ballistics, 'suggest_vertical_orientation': suggest_vertical_orientation } def get_vertical_scrolling_info(device): vertical_scrolling_info = feature_request(device, FEATURE.VERTICAL_SCROLLING) if vertical_scrolling_info: roller, ratchet, lines = _unpack('!BBB', vertical_scrolling_info[:3]) roller_type = ( 'reserved', 'standard', 'reserved', '3G', 'micro', 'normal touch pad', 'inverted touch pad', 'reserved' )[roller] return {'roller': roller_type, 'ratchet': ratchet, 'lines': lines} def get_hi_res_scrolling_info(device): hi_res_scrolling_info = feature_request(device, FEATURE.HI_RES_SCROLLING) if hi_res_scrolling_info: mode, resolution = _unpack('!BB', hi_res_scrolling_info[:2]) return mode, resolution def get_pointer_speed_info(device): pointer_speed_info = feature_request(device, FEATURE.POINTER_SPEED) if pointer_speed_info: pointer_speed_hi, pointer_speed_lo = _unpack('!BB', pointer_speed_info[:2]) # if pointer_speed_lo > 0: # pointer_speed_lo = pointer_speed_lo return pointer_speed_hi + pointer_speed_lo / 256 def get_lowres_wheel_status(device): lowres_wheel_status = feature_request(device, FEATURE.LOWRES_WHEEL) if lowres_wheel_status: wheel_flag = _unpack('!B', lowres_wheel_status[:1])[0] wheel_reporting = ('HID', 'HID++')[wheel_flag & 0x01] return wheel_reporting def get_hires_wheel(device): caps = feature_request(device, FEATURE.HIRES_WHEEL, 0x00) mode = feature_request(device, FEATURE.HIRES_WHEEL, 0x10) ratchet = feature_request(device, FEATURE.HIRES_WHEEL, 0x030) if caps and mode and ratchet: # Parse caps multi, flags = _unpack('!BB', caps[:2]) has_invert = (flags & 0x08) != 0 has_ratchet = (flags & 0x04) != 0 # Parse mode wheel_mode, reserved = _unpack('!BB', mode[:2]) target = (wheel_mode & 0x01) != 0 res = (wheel_mode & 0x02) != 0 inv = (wheel_mode & 0x04) != 0 # Parse Ratchet switch ratchet_mode, reserved = _unpack('!BB', ratchet[:2]) ratchet = (ratchet_mode & 0x01) != 0 return multi, has_invert, has_ratchet, inv, res, target, ratchet def get_new_fn_inversion(device): state = feature_request(device, FEATURE.NEW_FN_INVERSION, 0x00) if state: inverted, default_inverted = _unpack('!BB', state[:2]) inverted = (inverted & 0x01) != 0 default_inverted = (default_inverted & 0x01) != 0 return inverted, default_inverted def get_host_names(device): state = feature_request(device, FEATURE.HOSTS_INFO, 0x00) host_names = {} if state: capability_flags, _ignore, numHosts, currentHost = _unpack('!BBBB', state[:4]) if capability_flags & 0x01: # device can get host names for host in range(0, numHosts): hostinfo = feature_request(device, FEATURE.HOSTS_INFO, 0x10, host) _ignore, status, _ignore, numPages, nameLen, _ignore = _unpack('!BBBBBB', hostinfo[:6]) name = '' remaining = nameLen while remaining > 0: name_piece = feature_request(device, FEATURE.HOSTS_INFO, 0x30, host, nameLen - remaining) name += name_piece[2:2 + min(remaining, 14)].decode() remaining = max(0, remaining - 14) host_names[host] = (bool(status), name) return host_names def set_host_name(device, name): state = feature_request(device, FEATURE.HOSTS_INFO, 0x00) if state: flags = _unpack('!B', state[:1])[0] if flags & 0x02: hn = name[:min(14, name.find('.'))] if name.find('.') >= 0 else name response = feature_request(device, FEATURE.HOSTS_INFO, 0x40, 0xff, 0, hn) return response def get_polling_rate(device): state = feature_request(device, FEATURE.REPORT_RATE, 0x10) if state: rate = _unpack('!B', state[:1])[0] return rate