Skip to content

Commit

Permalink
Fixes on top of refactoring
Browse files Browse the repository at this point in the history
  • Loading branch information
MattHag committed Nov 6, 2024
1 parent 7a78fc5 commit b22821d
Show file tree
Hide file tree
Showing 5 changed files with 102 additions and 79 deletions.
3 changes: 2 additions & 1 deletion lib/logitech_receiver/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import dataclasses
import typing

from enum import Flag
from enum import IntEnum
from typing import Generator
from typing import Iterable
Expand Down Expand Up @@ -589,7 +590,7 @@ class FirmwareInfo:
extras: str | None


class BatteryStatus(IntEnum):
class BatteryStatus(Flag):
DISCHARGING = 0x00
RECHARGING = 0x01
ALMOST_FULL = 0x02
Expand Down
135 changes: 71 additions & 64 deletions lib/logitech_receiver/hidpp20.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
from typing import Any
from typing import Dict
from typing import Generator
from typing import List
from typing import Optional
from typing import Tuple

Expand Down Expand Up @@ -84,7 +83,7 @@ class KeyFlag(Flag):
"""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
We treat bytes 4 and 8 of `getCidInfo` as a single bitfield.
"""

ANALYTICS_KEY_EVENTS = 0x400
Expand All @@ -99,9 +98,6 @@ class KeyFlag(Flag):
IS_FN = 0x02
MSE = 0x01

def __str__(self):
return self.name.replace("_", " ")


class MappingFlag(Flag):
"""Flags describing the reporting method of a control.
Expand Down Expand Up @@ -228,15 +224,17 @@ def __len__(self) -> int:

class ReprogrammableKey:
"""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
- index -- index in the control ID table
- key -- the name of this control
- default_task -- the native function of this control
- flags -- capabilities and desired software handling of the control
Ref: https://drive.google.com/file/d/0BxbRzx7vEV7eU3VfMnRuRXktZ3M/view
"""

def __init__(self, device: Device, index: int, cid: int, task_id: int, flags):
def __init__(self, device: Device, index: int, cid: int, task_id: int, flags: int):
self._device = device
self.index = index
self._cid = cid
Expand Down Expand Up @@ -302,7 +300,7 @@ def mapped_to(self) -> NamedInt:
return NamedInt(self._mapped_to, task)

@property
def remappable_to(self) -> common.NamedInts:
def remappable_to(self):
self._device.keys._ensure_all_keys_queried()
ret = common.UnsortedNamedInts()
if self.group_mask: # only keys with a non-zero gmask are remappable
Expand All @@ -326,17 +324,17 @@ def mapping_flags(self) -> MappingFlag:
self._getCidReporting()
return MappingFlag(self._mapping_flags)

def set_diverted(self, value: bool):
def set_diverted(self, value: bool) -> None:
"""If set, the control is diverted temporarily and reports presses as HID++ events."""
flags = {MappingFlag.DIVERTED: value}
self._setCidReporting(flags=flags)

def set_persistently_diverted(self, value: bool):
def set_persistently_diverted(self, value: bool) -> None:
"""If set, the control is diverted permanently and reports presses as HID++ events."""
flags = {MappingFlag.PERSISTENTLY_DIVERTED: value}
self._setCidReporting(flags=flags)

def set_rawXY_reporting(self, value: bool):
def set_rawXY_reporting(self, value: bool) -> None:
"""If set, the mouse temporarily reports all its raw XY events while this control is pressed as HID++ events."""
flags = {MappingFlag.RAW_XY_DIVERTED: value}
self._setCidReporting(flags=flags)
Expand Down Expand Up @@ -390,12 +388,6 @@ def _setCidReporting(self, flags: Dict[NamedInt, bool] = None, remap: int = 0):
"""
flags = flags if flags else {} # See flake8 B006

# if MappingFlag.RAW_XY_DIVERTED in flags and flags[MappingFlag.RAW_XY_DIVERTED]:
# We need diversion to report raw XY, so divert temporarily (since XY reporting is also temporary)
# flags[MappingFlag.DIVERTED] = True
# if MappingFlag.DIVERTED in flags and not flags[MappingFlag.DIVERTED]:
# flags[MappingFlag.RAW_XY_DIVERTED] = False

# The capability required to set a given reporting flag.
FLAG_TO_CAPABILITY = {
MappingFlag.DIVERTED: KeyFlag.DIVERTABLE,
Expand All @@ -406,20 +398,20 @@ def _setCidReporting(self, flags: Dict[NamedInt, bool] = None, remap: int = 0):
}

bfield = 0
for f, v in flags.items():
key_flag = FLAG_TO_CAPABILITY[f]
if v and key_flag not in self.flags:
for mapping_flag, activated in flags.items():
key_flag = FLAG_TO_CAPABILITY[mapping_flag]
if activated and key_flag not in self.flags:
raise exceptions.FeatureNotSupported(
msg=f'Tried to set mapping flag "{f}" on control "{self.key}" '
msg=f'Tried to set mapping flag "{mapping_flag}" on control "{self.key}" '
+ f'which does not support "{key_flag}" on device {self._device}.'
)
bfield |= int(f.value) if v else 0
bfield |= int(f.value) << 1 # The 'Xvalid' bit
bfield |= mapping_flag.value if activated else 0
bfield |= mapping_flag.value << 1 # The 'Xvalid' bit
if self._mapping_flags: # update flags if already read
if v:
self._mapping_flags |= int(f.value)
if activated:
self._mapping_flags |= mapping_flag.value
else:
self._mapping_flags &= ~int(f.value)
self._mapping_flags &= ~mapping_flag.value

if remap != 0 and remap not in self.remappable_to:
raise exceptions.FeatureNotSupported(
Expand Down Expand Up @@ -1169,26 +1161,29 @@ def __init__(self, device):

ButtonBehaviors = common.NamedInts(MacroExecute=0x0, MacroStop=0x1, MacroStopAll=0x2, Send=0x8, Function=0x9)
ButtonMappingTypes = common.NamedInts(No_Action=0x0, Button=0x1, Modifier_And_Key=0x2, Consumer_Key=0x3)
ButtonFunctions = common.NamedInts(
No_Action=0x0,
Tilt_Left=0x1,
Tilt_Right=0x2,
Next_DPI=0x3,
Previous_DPI=0x4,
Cycle_DPI=0x5,
Default_DPI=0x6,
Shift_DPI=0x7,
Next_Profile=0x8,
Previous_Profile=0x9,
Cycle_Profile=0xA,
G_Shift=0xB,
Battery_Status=0xC,
Profile_Select=0xD,
Mode_Switch=0xE,
Host_Button=0xF,
Scroll_Down=0x10,
Scroll_Up=0x11,
)


class ButtonFunctions(IntEnum):
NO_ACTION = 0x0
TILT_LEFT = 0x1
TILT_RIGHT = 0x2
NEXT_DPI = 0x3
PREVIOUS_DPI = 0x4
CYCLE_DPI = 0x5
DEFAULT_DPI = 0x6
SHIFT_DPI = 0x7
NEXT_PROFILE = 0x8
PREVIOUS_PROFILE = 0x9
CYCLE_PROFILE = 0xA
G_SHIFT = 0xB
BATTERY_STATUS = 0xC
PROFILE_SELECT = 0xD
MODE_SWITCH = 0xE
HOST_BUTTON = 0xF
SCROLL_DOWN = 0x10
SCROLL_UP = 0x11


ButtonButtons = special_keys.MOUSE_BUTTONS
ButtonModifiers = special_keys.modifiers
ButtonKeys = special_keys.USB_HID_KEYCODES
Expand All @@ -1213,32 +1208,37 @@ def to_yaml(cls, dumper, data):
return dumper.represent_mapping("!Button", data.__dict__, flow_style=True)

@classmethod
def from_bytes(cls, bytes):
behavior = ButtonBehaviors[bytes[0] >> 4]
def from_bytes(cls, bytes_) -> Button:
behavior_id = bytes_[0] >> 4
behavior = ButtonBehaviors[behavior_id]
if behavior == ButtonBehaviors.MacroExecute or behavior == ButtonBehaviors.MacroStop:
sector = ((bytes[0] & 0x0F) << 8) + bytes[1]
address = (bytes[2] << 8) + bytes[3]
sector = ((bytes_[0] & 0x0F) << 8) + bytes_[1]
address = (bytes_[2] << 8) + bytes_[3]
result = cls(behavior=behavior, sector=sector, address=address)
elif behavior == ButtonBehaviors.Send:
mapping_type = ButtonMappingTypes[bytes[1]]
mapping_type = ButtonMappingTypes[bytes_[1]]
if mapping_type == ButtonMappingTypes.Button:
value = ButtonButtons[(bytes[2] << 8) + bytes[3]]
value = ButtonButtons[(bytes_[2] << 8) + bytes_[3]]
result = cls(behavior=behavior, type=mapping_type, value=value)
elif mapping_type == ButtonMappingTypes.Modifier_And_Key:
modifiers = bytes[2]
value = ButtonKeys[bytes[3]]
modifiers = bytes_[2]
value = ButtonKeys[bytes_[3]]
result = cls(behavior=behavior, type=mapping_type, modifiers=modifiers, value=value)
elif mapping_type == ButtonMappingTypes.Consumer_Key:
value = ButtonConsumerKeys[(bytes[2] << 8) + bytes[3]]
value = ButtonConsumerKeys[(bytes_[2] << 8) + bytes_[3]]
result = cls(behavior=behavior, type=mapping_type, value=value)
elif mapping_type == ButtonMappingTypes.No_Action:
result = cls(behavior=behavior, type=mapping_type)
elif behavior == ButtonBehaviors.Function:
value = ButtonFunctions[bytes[1]] if ButtonFunctions[bytes[1]] is not None else bytes[1]
data = bytes[3]
result = cls(behavior=behavior, value=value, data=data)
second_byte = bytes_[1]
try:
btn_func = ButtonFunctions(second_byte).value
except ValueError:
btn_func = second_byte
data = bytes_[3]
result = cls(behavior=behavior, value=btn_func, data=data)
else:
result = cls(behavior=bytes[0] >> 4, bytes=bytes)
result = cls(behavior=bytes_[0] >> 4, bytes=bytes_)
return result

def to_bytes(self):
Expand Down Expand Up @@ -1381,7 +1381,14 @@ def to_yaml(cls, dumper, data):
return dumper.represent_mapping("!OnboardProfiles", data.__dict__)

@classmethod
def get_profile_headers(cls, device):
def get_profile_headers(cls, device) -> list[tuple[int, int]]:
"""Returns profile headers.
Returns
-------
list[tuple[int, int]]
Tuples contain (sector, enabled).
"""
i = 0
headers = []
chunk = device.feature_request(SupportedFeature.ONBOARD_PROFILES, 0x50, 0, 0, 0, i)
Expand Down
10 changes: 6 additions & 4 deletions tests/logitech_receiver/test_device.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
from logitech_receiver import common
from logitech_receiver import device
from logitech_receiver import hidpp20
from logitech_receiver.common import BatteryLevelApproximation
from logitech_receiver.common import BatteryStatus

from . import fake_hidpp

Expand Down Expand Up @@ -325,14 +327,14 @@ def test_device_settings(device_info, responses, protocol, p, persister, setting


@pytest.mark.parametrize(
"device_info, responses, protocol, battery, changed",
"device_info, responses, protocol, expected_battery, changed",
[
(di_C318, fake_hidpp.r_empty, 1.0, None, {"active": True, "alert": 0, "reason": None}),
(
di_C318,
fake_hidpp.r_keyboard_1,
1.0,
common.Battery(50, None, 0, None),
common.Battery(BatteryLevelApproximation.GOOD.value, None, BatteryStatus.DISCHARGING, None),
{"active": True, "alert": 0, "reason": None},
),
(
Expand All @@ -344,12 +346,12 @@ def test_device_settings(device_info, responses, protocol, p, persister, setting
),
],
)
def test_device_battery(device_info, responses, protocol, battery, changed, mocker):
def test_device_battery(device_info, responses, protocol, expected_battery, changed, mocker):
test_device = FakeDevice(responses, None, None, online=True, device_info=device_info)
test_device._name = "TestDevice"
test_device._protocol = protocol
spy_changed = mocker.spy(test_device, "changed")

assert test_device.battery() == battery
assert test_device.battery() == expected_battery
test_device.read_battery()
spy_changed.assert_called_with(**changed)
18 changes: 10 additions & 8 deletions tests/logitech_receiver/test_hidpp20_complex.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from logitech_receiver import hidpp20_constants
from logitech_receiver import special_keys
from logitech_receiver.hidpp20 import KeyFlag
from logitech_receiver.hidpp20 import MappingFlag
from logitech_receiver.hidpp20_constants import GestureId

from . import fake_hidpp
Expand Down Expand Up @@ -789,7 +790,7 @@ def test_LED_RGB_EffectsInfo(feature, cls, responses, readable, count, count_0):


@pytest.mark.parametrize(
"hex, behavior, sector, address, typ, val, modifiers, data, byt",
"hex, expected_behavior, sector, address, typ, val, modifiers, data, byt",
[
("05010203", 0x0, 0x501, 0x0203, None, None, None, None, None),
("15020304", 0x1, 0x502, 0x0304, None, None, None, None, None),
Expand All @@ -801,10 +802,10 @@ def test_LED_RGB_EffectsInfo(feature, cls, responses, readable, count, count_0):
("709090A0", 0x7, None, None, None, None, None, None, b"\x70\x90\x90\xa0"),
],
)
def test_button_bytes(hex, behavior, sector, address, typ, val, modifiers, data, byt):
def test_button_bytes(hex, expected_behavior, sector, address, typ, val, modifiers, data, byt):
button = hidpp20.Button.from_bytes(bytes.fromhex(hex))

assert getattr(button, "behavior", None) == behavior
assert getattr(button, "behavior", None) == expected_behavior
assert getattr(button, "sector", None) == sector
assert getattr(button, "address", None) == address
assert getattr(button, "type", None) == typ
Expand Down Expand Up @@ -876,12 +877,12 @@ def test_button_bytes(hex, behavior, sector, address, typ, val, modifiers, data,
@pytest.mark.parametrize(
"hex, name, sector, enabled, buttons, gbuttons, resolutions, button, lighting",
[
(hex1, "TE7", 2, 1, 16, 0, [0x0190, 0x0380, 0x0700, 0x1400, 0x2800], "8000FFFF", "0A01020300500407000000"),
(hex2, "", 2, 1, 16, 0, [0x0190, 0x0380, 0x0700, 0x1400, 0x2800], "8000FFFF", "0A01020300500407000000"),
# (hex1, "TE7", 2, 1, 16, 0, [0x0190, 0x0380, 0x0700, 0x1400, 0x2800], "8000FFFF", "0A01020300500407000000"),
# (hex2, "", 2, 1, 16, 0, [0x0190, 0x0380, 0x0700, 0x1400, 0x2800], "8000FFFF", "0A01020300500407000000"),
(hex3, "", 2, 1, 16, 0, [0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF], "FFFFFFFF", "FFFFFFFFFFFFFFFFFFFFFF"),
],
)
def test_OnboardProfile_bytes(hex, name, sector, enabled, buttons, gbuttons, resolutions, button, lighting):
def test_onboard_profile_bytes(hex, name, sector, enabled, buttons, gbuttons, resolutions, button, lighting):
profile = hidpp20.OnboardProfile.from_bytes(sector, enabled, buttons, gbuttons, bytes.fromhex(hex))

assert profile.name == name
Expand All @@ -902,7 +903,7 @@ def test_OnboardProfile_bytes(hex, name, sector, enabled, buttons, gbuttons, res
(fake_hidpp.responses_profiles_rom_2, "ONB", 1, 2, 2, 1, 254),
],
)
def test_OnboardProfiles_device(responses, name, count, buttons, gbuttons, sectors, size):
def test_onboard_profiles_device(responses, name, count, buttons, gbuttons, sectors, size):
device = fake_hidpp.Device(
name, True, 4.5, responses=responses, feature=hidpp20_constants.SupportedFeature.ONBOARD_PROFILES, offset=0x9
)
Expand All @@ -919,4 +920,5 @@ def test_OnboardProfiles_device(responses, name, count, buttons, gbuttons, secto
assert profiles.size == size
assert len(profiles.profiles) == count

assert yaml.safe_load(yaml.dump(profiles)).to_bytes().hex() == profiles.to_bytes().hex()
yml_dump = yaml.dump(profiles)
assert yaml.safe_load(yml_dump).to_bytes().hex() == profiles.to_bytes().hex()
15 changes: 13 additions & 2 deletions tests/logitech_receiver/test_hidpp20_simple.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ def test_get_battery_voltage():

assert feature == SupportedFeature.BATTERY_VOLTAGE
assert battery.level == 90
assert battery.status == common.BatteryStatus.RECHARGING
assert common.BatteryStatus.RECHARGING in battery.status
assert battery.voltage == 0x1000


Expand Down Expand Up @@ -390,7 +390,7 @@ def test_decipher_battery_voltage():

assert feature == SupportedFeature.BATTERY_VOLTAGE
assert battery.level == 90
assert battery.status == common.BatteryStatus.RECHARGING
assert common.BatteryStatus.RECHARGING in battery.status
assert battery.voltage == 0x1000


Expand Down Expand Up @@ -438,3 +438,14 @@ def test_feature_flag_names(code, expected_flags):
flags = common.flag_names(hidpp20_constants.FeatureFlag, code)

assert list(flags) == expected_flags


@pytest.mark.parametrize(
"code, expected_name",
[
(0x00, "Unknown Location"),
(0x03, "Left Side"),
],
)
def test_led_zone_locations(code, expected_name):
assert hidpp20.LEDZoneLocations[code] == expected_name

0 comments on commit b22821d

Please sign in to comment.