Fixes on top of refactoring

This commit is contained in:
MattHag 2024-11-06 00:44:32 +01:00 committed by Peter F. Patel-Schneider
parent ad3916e1b8
commit 5f5c7cdcce
5 changed files with 102 additions and 81 deletions

View File

@ -20,6 +20,7 @@ import binascii
import dataclasses import dataclasses
import typing import typing
from enum import Flag
from enum import IntEnum from enum import IntEnum
from typing import Generator from typing import Generator
from typing import Iterable from typing import Iterable
@ -589,7 +590,7 @@ class FirmwareInfo:
extras: str | None extras: str | None
class BatteryStatus(IntEnum): class BatteryStatus(Flag):
DISCHARGING = 0x00 DISCHARGING = 0x00
RECHARGING = 0x01 RECHARGING = 0x01
ALMOST_FULL = 0x02 ALMOST_FULL = 0x02

View File

@ -26,7 +26,6 @@ from enum import IntEnum
from typing import Any from typing import Any
from typing import Dict from typing import Dict
from typing import Generator from typing import Generator
from typing import List
from typing import Optional from typing import Optional
from typing import Tuple from typing import Tuple
@ -84,7 +83,7 @@ class KeyFlag(Flag):
"""Capabilities and desired software handling for a control. """Capabilities and desired software handling for a control.
Ref: https://drive.google.com/file/d/10imcbmoxTJ1N510poGdsviEhoFfB_Ua4/view 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 ANALYTICS_KEY_EVENTS = 0x400
@ -99,9 +98,6 @@ class KeyFlag(Flag):
IS_FN = 0x02 IS_FN = 0x02
MSE = 0x01 MSE = 0x01
def __str__(self):
return self.name.replace("_", " ")
class MappingFlag(Flag): class MappingFlag(Flag):
"""Flags describing the reporting method of a control. """Flags describing the reporting method of a control.
@ -228,15 +224,17 @@ class FeaturesArray(dict):
class ReprogrammableKey: class ReprogrammableKey:
"""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://drive.google.com/file/d/0BxbRzx7vEV7eU3VfMnRuRXktZ3M/view
Read-only properties: Read-only properties:
- index {int} -- index in the control ID table - index -- index in the control ID table
- key {NamedInt} -- the name of this control - key -- the name of this control
- default_task {NamedInt} -- the native function of this control - default_task -- the native function of this control
- flags {List[str]} -- capabilities and desired software handling of the 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._device = device
self.index = index self.index = index
self._cid = cid self._cid = cid
@ -302,7 +300,7 @@ class ReprogrammableKeyV4(ReprogrammableKey):
return NamedInt(self._mapped_to, task) return NamedInt(self._mapped_to, task)
@property @property
def remappable_to(self) -> common.NamedInts: def remappable_to(self):
self._device.keys._ensure_all_keys_queried() self._device.keys._ensure_all_keys_queried()
ret = common.UnsortedNamedInts() ret = common.UnsortedNamedInts()
if self.group_mask: # only keys with a non-zero gmask are remappable if self.group_mask: # only keys with a non-zero gmask are remappable
@ -326,17 +324,17 @@ class ReprogrammableKeyV4(ReprogrammableKey):
self._getCidReporting() self._getCidReporting()
return MappingFlag(self._mapping_flags) 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.""" """If set, the control is diverted temporarily and reports presses as HID++ events."""
flags = {MappingFlag.DIVERTED: value} flags = {MappingFlag.DIVERTED: value}
self._setCidReporting(flags=flags) 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.""" """If set, the control is diverted permanently and reports presses as HID++ events."""
flags = {MappingFlag.PERSISTENTLY_DIVERTED: value} flags = {MappingFlag.PERSISTENTLY_DIVERTED: value}
self._setCidReporting(flags=flags) 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.""" """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} flags = {MappingFlag.RAW_XY_DIVERTED: value}
self._setCidReporting(flags=flags) self._setCidReporting(flags=flags)
@ -390,12 +388,6 @@ class ReprogrammableKeyV4(ReprogrammableKey):
""" """
flags = flags if flags else {} # See flake8 B006 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. # The capability required to set a given reporting flag.
FLAG_TO_CAPABILITY = { FLAG_TO_CAPABILITY = {
MappingFlag.DIVERTED: KeyFlag.DIVERTABLE, MappingFlag.DIVERTED: KeyFlag.DIVERTABLE,
@ -406,20 +398,20 @@ class ReprogrammableKeyV4(ReprogrammableKey):
} }
bfield = 0 bfield = 0
for f, v in flags.items(): for mapping_flag, activated in flags.items():
key_flag = FLAG_TO_CAPABILITY[f] key_flag = FLAG_TO_CAPABILITY[mapping_flag]
if v and key_flag not in self.flags: if activated and key_flag not in self.flags:
raise exceptions.FeatureNotSupported( 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}.' + f'which does not support "{key_flag}" on device {self._device}.'
) )
bfield |= int(f.value) if v else 0 bfield |= mapping_flag.value if activated else 0
bfield |= int(f.value) << 1 # The 'Xvalid' bit bfield |= mapping_flag.value << 1 # The 'Xvalid' bit
if self._mapping_flags: # update flags if already read if self._mapping_flags: # update flags if already read
if v: if activated:
self._mapping_flags |= int(f.value) self._mapping_flags |= mapping_flag.value
else: else:
self._mapping_flags &= ~int(f.value) self._mapping_flags &= ~mapping_flag.value
if remap != 0 and remap not in self.remappable_to: if remap != 0 and remap not in self.remappable_to:
raise exceptions.FeatureNotSupported( raise exceptions.FeatureNotSupported(
@ -1169,26 +1161,29 @@ class RGBEffectsInfo(LEDEffectsInfo): # effects that the LEDs can do using RGB_
ButtonBehaviors = common.NamedInts(MacroExecute=0x0, MacroStop=0x1, MacroStopAll=0x2, Send=0x8, Function=0x9) 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) ButtonMappingTypes = common.NamedInts(No_Action=0x0, Button=0x1, Modifier_And_Key=0x2, Consumer_Key=0x3)
ButtonFunctions = common.NamedInts(
No_Action=0x0,
Tilt_Left=0x1, class ButtonFunctions(IntEnum):
Tilt_Right=0x2, NO_ACTION = 0x0
Next_DPI=0x3, TILT_LEFT = 0x1
Previous_DPI=0x4, TILT_RIGHT = 0x2
Cycle_DPI=0x5, NEXT_DPI = 0x3
Default_DPI=0x6, PREVIOUS_DPI = 0x4
Shift_DPI=0x7, CYCLE_DPI = 0x5
Next_Profile=0x8, DEFAULT_DPI = 0x6
Previous_Profile=0x9, SHIFT_DPI = 0x7
Cycle_Profile=0xA, NEXT_PROFILE = 0x8
G_Shift=0xB, PREVIOUS_PROFILE = 0x9
Battery_Status=0xC, CYCLE_PROFILE = 0xA
Profile_Select=0xD, G_SHIFT = 0xB
Mode_Switch=0xE, BATTERY_STATUS = 0xC
Host_Button=0xF, PROFILE_SELECT = 0xD
Scroll_Down=0x10, MODE_SWITCH = 0xE
Scroll_Up=0x11, HOST_BUTTON = 0xF
) SCROLL_DOWN = 0x10
SCROLL_UP = 0x11
ButtonButtons = special_keys.MOUSE_BUTTONS ButtonButtons = special_keys.MOUSE_BUTTONS
ButtonModifiers = special_keys.modifiers ButtonModifiers = special_keys.modifiers
ButtonKeys = special_keys.USB_HID_KEYCODES ButtonKeys = special_keys.USB_HID_KEYCODES
@ -1213,32 +1208,37 @@ class Button:
return dumper.represent_mapping("!Button", data.__dict__, flow_style=True) return dumper.represent_mapping("!Button", data.__dict__, flow_style=True)
@classmethod @classmethod
def from_bytes(cls, bytes): def from_bytes(cls, bytes_) -> Button:
behavior = ButtonBehaviors[bytes[0] >> 4] behavior_id = bytes_[0] >> 4
behavior = ButtonBehaviors[behavior_id]
if behavior == ButtonBehaviors.MacroExecute or behavior == ButtonBehaviors.MacroStop: if behavior == ButtonBehaviors.MacroExecute or behavior == ButtonBehaviors.MacroStop:
sector = ((bytes[0] & 0x0F) << 8) + bytes[1] sector = ((bytes_[0] & 0x0F) << 8) + bytes_[1]
address = (bytes[2] << 8) + bytes[3] address = (bytes_[2] << 8) + bytes_[3]
result = cls(behavior=behavior, sector=sector, address=address) result = cls(behavior=behavior, sector=sector, address=address)
elif behavior == ButtonBehaviors.Send: elif behavior == ButtonBehaviors.Send:
mapping_type = ButtonMappingTypes[bytes[1]] mapping_type = ButtonMappingTypes[bytes_[1]]
if mapping_type == ButtonMappingTypes.Button: 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) result = cls(behavior=behavior, type=mapping_type, value=value)
elif mapping_type == ButtonMappingTypes.Modifier_And_Key: elif mapping_type == ButtonMappingTypes.Modifier_And_Key:
modifiers = bytes[2] modifiers = bytes_[2]
value = ButtonKeys[bytes[3]] value = ButtonKeys[bytes_[3]]
result = cls(behavior=behavior, type=mapping_type, modifiers=modifiers, value=value) result = cls(behavior=behavior, type=mapping_type, modifiers=modifiers, value=value)
elif mapping_type == ButtonMappingTypes.Consumer_Key: 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) result = cls(behavior=behavior, type=mapping_type, value=value)
elif mapping_type == ButtonMappingTypes.No_Action: elif mapping_type == ButtonMappingTypes.No_Action:
result = cls(behavior=behavior, type=mapping_type) result = cls(behavior=behavior, type=mapping_type)
elif behavior == ButtonBehaviors.Function: elif behavior == ButtonBehaviors.Function:
value = ButtonFunctions[bytes[1]] if ButtonFunctions[bytes[1]] is not None else bytes[1] second_byte = bytes_[1]
data = bytes[3] try:
result = cls(behavior=behavior, value=value, data=data) 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: else:
result = cls(behavior=bytes[0] >> 4, bytes=bytes) result = cls(behavior=bytes_[0] >> 4, bytes=bytes_)
return result return result
def to_bytes(self): def to_bytes(self):
@ -1381,7 +1381,14 @@ class OnboardProfiles:
return dumper.represent_mapping("!OnboardProfiles", data.__dict__) return dumper.represent_mapping("!OnboardProfiles", data.__dict__)
@classmethod @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 i = 0
headers = [] headers = []
chunk = device.feature_request(SupportedFeature.ONBOARD_PROFILES, 0x50, 0, 0, 0, i) chunk = device.feature_request(SupportedFeature.ONBOARD_PROFILES, 0x50, 0, 0, 0, i)
@ -1408,10 +1415,8 @@ class OnboardProfiles:
gbuttons = buttons if (shift & 0x3 == 0x2) else 0 gbuttons = buttons if (shift & 0x3 == 0x2) else 0
headers = OnboardProfiles.get_profile_headers(device) headers = OnboardProfiles.get_profile_headers(device)
profiles = {} profiles = {}
i = 0 for i, (sector, enabled) in enumerate(headers, start=1):
for sector, enabled in headers: profiles[i] = OnboardProfile.from_dev(device, i, sector, size, enabled, buttons, gbuttons)
profiles[i + 1] = OnboardProfile.from_dev(device, i, sector, size, enabled, buttons, gbuttons)
i += 1
return cls( return cls(
version=OnboardProfilesVersion, version=OnboardProfilesVersion,
name=device.name, name=device.name,

View File

@ -23,6 +23,8 @@ import pytest
from logitech_receiver import common from logitech_receiver import common
from logitech_receiver import device from logitech_receiver import device
from logitech_receiver import hidpp20 from logitech_receiver import hidpp20
from logitech_receiver.common import BatteryLevelApproximation
from logitech_receiver.common import BatteryStatus
from . import fake_hidpp from . import fake_hidpp
@ -325,14 +327,14 @@ def test_device_settings(device_info, responses, protocol, p, persister, setting
@pytest.mark.parametrize( @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_empty, 1.0, None, {"active": True, "alert": 0, "reason": None}),
( (
di_C318, di_C318,
fake_hidpp.r_keyboard_1, fake_hidpp.r_keyboard_1,
1.0, 1.0,
common.Battery(50, None, 0, None), common.Battery(BatteryLevelApproximation.GOOD.value, None, BatteryStatus.DISCHARGING, None),
{"active": True, "alert": 0, "reason": None}, {"active": True, "alert": 0, "reason": None},
), ),
( (
@ -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 = FakeDevice(responses, None, None, online=True, device_info=device_info)
test_device._name = "TestDevice" test_device._name = "TestDevice"
test_device._protocol = protocol test_device._protocol = protocol
spy_changed = mocker.spy(test_device, "changed") spy_changed = mocker.spy(test_device, "changed")
assert test_device.battery() == battery assert test_device.battery() == expected_battery
test_device.read_battery() test_device.read_battery()
spy_changed.assert_called_with(**changed) spy_changed.assert_called_with(**changed)

View File

@ -23,6 +23,7 @@ from logitech_receiver import hidpp20
from logitech_receiver import hidpp20_constants from logitech_receiver import hidpp20_constants
from logitech_receiver import special_keys from logitech_receiver import special_keys
from logitech_receiver.hidpp20 import KeyFlag from logitech_receiver.hidpp20 import KeyFlag
from logitech_receiver.hidpp20 import MappingFlag
from logitech_receiver.hidpp20_constants import GestureId from logitech_receiver.hidpp20_constants import GestureId
from . import fake_hidpp from . import fake_hidpp
@ -789,7 +790,7 @@ def test_LED_RGB_EffectsInfo(feature, cls, responses, readable, count, count_0):
@pytest.mark.parametrize( @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), ("05010203", 0x0, 0x501, 0x0203, None, None, None, None, None),
("15020304", 0x1, 0x502, 0x0304, None, None, None, None, None), ("15020304", 0x1, 0x502, 0x0304, None, None, None, None, None),
@ -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"), ("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)) 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, "sector", None) == sector
assert getattr(button, "address", None) == address assert getattr(button, "address", None) == address
assert getattr(button, "type", None) == typ assert getattr(button, "type", None) == typ
@ -881,7 +882,7 @@ hex3 = (
(hex3, "", 2, 1, 16, 0, [0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF], "FFFFFFFF", "FFFFFFFFFFFFFFFFFFFFFF"), (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)) profile = hidpp20.OnboardProfile.from_bytes(sector, enabled, buttons, gbuttons, bytes.fromhex(hex))
assert profile.name == name assert profile.name == name
@ -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), (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( device = fake_hidpp.Device(
name, True, 4.5, responses=responses, feature=hidpp20_constants.SupportedFeature.ONBOARD_PROFILES, offset=0x9 name, True, 4.5, responses=responses, feature=hidpp20_constants.SupportedFeature.ONBOARD_PROFILES, offset=0x9
) )
@ -919,4 +920,5 @@ def test_OnboardProfiles_device(responses, name, count, buttons, gbuttons, secto
assert profiles.size == size assert profiles.size == size
assert len(profiles.profiles) == count 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()

View File

@ -108,7 +108,7 @@ def test_get_battery_voltage():
assert feature == SupportedFeature.BATTERY_VOLTAGE assert feature == SupportedFeature.BATTERY_VOLTAGE
assert battery.level == 90 assert battery.level == 90
assert battery.status == common.BatteryStatus.RECHARGING assert common.BatteryStatus.RECHARGING in battery.status
assert battery.voltage == 0x1000 assert battery.voltage == 0x1000
@ -390,7 +390,7 @@ def test_decipher_battery_voltage():
assert feature == SupportedFeature.BATTERY_VOLTAGE assert feature == SupportedFeature.BATTERY_VOLTAGE
assert battery.level == 90 assert battery.level == 90
assert battery.status == common.BatteryStatus.RECHARGING assert common.BatteryStatus.RECHARGING in battery.status
assert battery.voltage == 0x1000 assert battery.voltage == 0x1000
@ -438,3 +438,14 @@ def test_feature_flag_names(code, expected_flags):
flags = common.flag_names(hidpp20_constants.FeatureFlag, code) flags = common.flag_names(hidpp20_constants.FeatureFlag, code)
assert list(flags) == expected_flags 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