Compare commits
7 Commits
2dd10c70d2
...
884920d9a5
Author | SHA1 | Date |
---|---|---|
|
884920d9a5 | |
|
bdb0e9589b | |
|
0335dd003c | |
|
8bea0121cc | |
|
8e631291c1 | |
|
2781b4d878 | |
|
c9fe126c27 |
72
CHANGELOG.md
72
CHANGELOG.md
|
@ -1,3 +1,75 @@
|
|||
# 1.1.15rc1
|
||||
|
||||
* Center labels and remove buggy entry resizing logic
|
||||
* Add shape keys from Key POP Icon
|
||||
* Device and Action rule conditions match on codename and name
|
||||
* Fix listing hidpp10 devices - bytes vs string concatenation (#2856)
|
||||
* Add present flag, unset when internal error occurs, set when notification appears
|
||||
* Pause setting up features when error occurs; use ADC message to signal connection and disconnection
|
||||
* Fix listing of hidpp10 peripherals
|
||||
* Complete DEVICE_FEATURES to DeviceFeature transition for hidpp10 devices
|
||||
* Fix NOTIFICATION_FLAG to NotificationFlag transition leftovers
|
||||
* Fix github workflow stopping all matrix jobs when one of them fails
|
||||
* Fix ubuntu github CI
|
||||
* Update index.md
|
||||
* Python documentation appears to be broken so don't set it up
|
||||
* Improve documentation on onboard profiles
|
||||
* Use correct LOD values for extended adjustable dpi
|
||||
* Better support RGB Effects - not readable
|
||||
* Fix crash when asking for help about config
|
||||
* Fix error when updating ChoiceControlBig box
|
||||
* Add uninstallation docs
|
||||
* Handle unknown power switch locations again
|
||||
* Correctly handle selection of [empty] in rule editor
|
||||
* Handle `HIDError` in `hidapi.hidapi_impl._match()` (#2804)
|
||||
* Give ghost devices a path
|
||||
* Guard against typeerror when setting the value of a control box
|
||||
* Recover from errors in ping
|
||||
* Replace spaces by underscores when looking up features
|
||||
* Rewrote string concatenation/format with f strings
|
||||
* Fix logo not showing in about dialog box
|
||||
* Make typing-extensions dependency mandatory
|
||||
* Properly ignore unsupported locale
|
||||
* hidapi: skip unsupported devices and handle exception on open
|
||||
* Ignore macOS junk files and pipenv config
|
||||
* Fix ui desktop notifications test
|
||||
* hidpp20: Remove dependency to NamedInts
|
||||
* Estimate accurate battery level for some rechargable devices (#2745)
|
||||
* Upgrade desktop notifications tests to take notifications availability into account
|
||||
* Update tests to run on Python 3.13
|
||||
* Remove outdated logger enabled checks
|
||||
* Introduce GTK signal types
|
||||
* Introduce error types
|
||||
* Remove alias for SupportedFeature
|
||||
* Refactor process_device_notification
|
||||
* Refactor process_receiver_notification
|
||||
* Refactor receiver event handling
|
||||
* Introduce custom logger
|
||||
* Refactor notifications
|
||||
* Rename variable to full name notification
|
||||
* Test notifications
|
||||
* Test extraction of serial and max. devices
|
||||
* Refactor extraction of serial and max. devices
|
||||
* macOS: Fix int.from_bytes, int.to_bytes for show.py
|
||||
* macOS: Remove udev rule warning
|
||||
* macOS: Add support for Bluetooth devices
|
||||
* Add back and forward mouseclick actions
|
||||
* Speedup lookup of known receivers
|
||||
* Refactor device filtering
|
||||
* Reorder private functions and variable definitions
|
||||
* Turn filter_products_of_interest into a public function
|
||||
* Improve tests of known receivers
|
||||
* Refactor: Remove NamedInts and move enums where used
|
||||
* Add docstrings and type hints
|
||||
* Enforce rules on RuleComponentUI subclasses
|
||||
* Simplify settings UI class
|
||||
* Remove diversion alias
|
||||
* Refactor: Convert Kind to IntEnum
|
||||
* Split up huge settings module
|
||||
* Remove Python 2 specific path handling
|
||||
* Delete logging temp file on exit
|
||||
* Update Swedish translation
|
||||
|
||||
# 1.1.14
|
||||
|
||||
* Handle fake feature enums in show
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
## Version 1.1.15
|
||||
|
||||
* Device and Action rule conditions match on device codename and name
|
||||
* Solaar supports configuration of Bluetooth devices on macOS.
|
||||
|
||||
## Version 1.1.13
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
solaar show
|
||||
rules cannot access modifier keys in Wayland, accessing process only works on GNOME with Solaar Gnome extension installed
|
||||
solaar version 1.1.14-2
|
||||
|
||||
Unifying Receiver
|
||||
Device path : /dev/hidraw1
|
||||
USB id : 046d:C52B
|
||||
Serial : EC219AC2
|
||||
C Pending : ff
|
||||
0 : 12.11.B0032
|
||||
1 : 04.16
|
||||
3 : AA.AA
|
||||
Has 2 paired device(s) out of a maximum of 6.
|
||||
Notifications: wireless (0x000100)
|
||||
Device activity counters: 1=195, 2=74
|
||||
|
||||
1: Wireless Mouse M175
|
||||
Device path : /dev/hidraw2
|
||||
WPID : 4008
|
||||
Codename : M175
|
||||
Kind : mouse
|
||||
Protocol : HID++ 2.0
|
||||
Report Rate : 8ms
|
||||
Serial number: 16E46E8C
|
||||
Model ID: 000000000000
|
||||
Unit ID: 00000000
|
||||
0: RQM 40.00.B0016
|
||||
The power switch is located on the base.
|
||||
Supports 21 HID++ 2.0 features:
|
||||
0: ROOT {0000} V0
|
||||
1: FEATURE SET {0001} V0
|
||||
2: DEVICE FW VERSION {0003} V0
|
||||
Firmware: 0 RQM 40.00.B0016 4008
|
||||
Unit ID: 00000000 Model ID: 000000000000 Transport IDs: {}
|
||||
3: DEVICE NAME {0005} V0
|
||||
Name: Wireless Mouse M185
|
||||
Kind: mouse
|
||||
4: BATTERY STATUS {1000} V0
|
||||
Battery: 70%, 0, next level 5%.
|
||||
5: unknown:1830 {1830} V0 internal, hidden
|
||||
6: unknown:1850 {1850} V0 internal, hidden
|
||||
7: unknown:1860 {1860} V0 internal, hidden
|
||||
8: unknown:1890 {1890} V0 internal, hidden
|
||||
9: unknown:18A0 {18A0} V0 internal, hidden
|
||||
10: unknown:18C0 {18C0} V0 internal, hidden
|
||||
11: WIRELESS DEVICE STATUS {1D4B} V0
|
||||
12: unknown:1DF3 {1DF3} V0 internal, hidden
|
||||
13: REPROG CONTROLS {1B00} V0
|
||||
14: REMAINING PAIRING {1DF0} V0 hidden
|
||||
Remaining Pairings: 117
|
||||
15: unknown:1E00 {1E00} V0 hidden
|
||||
16: unknown:1E80 {1E80} V0 internal, hidden
|
||||
17: unknown:1E90 {1E90} V0 internal, hidden
|
||||
18: unknown:1F03 {1F03} V0 internal, hidden
|
||||
19: VERTICAL SCROLLING {2100} V0
|
||||
Roller type: standard
|
||||
Ratchet per turn: 24
|
||||
Scroll lines: 0
|
||||
20: MOUSE POINTER {2200} V0
|
||||
DPI: 1000
|
||||
Acceleration: low
|
||||
Override OS ballistics
|
||||
No vertical tuning, standard mice
|
||||
Battery: 70%, 0, next level 5%.
|
|
@ -191,7 +191,7 @@ class Hidpp10:
|
|||
def get_notification_flags(self, device: Device):
|
||||
return self._get_register(device, Registers.NOTIFICATIONS)
|
||||
|
||||
def set_notification_flags(self, device: Device, *flag_bits: NotificationFlag):
|
||||
def set_notification_flags(self, device: Device, *flag_bits: NotificationFlag) -> None:
|
||||
assert device is not None
|
||||
|
||||
# Avoid a call if the device is not online,
|
||||
|
|
|
@ -1579,7 +1579,7 @@ class Hidpp20:
|
|||
except Exception:
|
||||
return None
|
||||
|
||||
def get_name(self, device: Device):
|
||||
def get_name(self, device: Device) -> str:
|
||||
"""Reads a device's name.
|
||||
|
||||
:returns: a string with the device name, or ``None`` if the device is not
|
||||
|
@ -1695,7 +1695,7 @@ class Hidpp20:
|
|||
if SupportedFeature.ONBOARD_PROFILES in device.features:
|
||||
return OnboardProfiles.from_device(device)
|
||||
|
||||
def get_mouse_pointer_info(self, device: Device):
|
||||
def get_mouse_pointer_info(self, device: Device) -> dict[str, Any]:
|
||||
pointer_info = device.feature_request(SupportedFeature.MOUSE_POINTER)
|
||||
if pointer_info:
|
||||
dpi, flags = struct.unpack("!HB", pointer_info[:3])
|
||||
|
@ -1709,7 +1709,7 @@ class Hidpp20:
|
|||
"suggest_vertical_orientation": suggest_vertical_orientation,
|
||||
}
|
||||
|
||||
def get_vertical_scrolling_info(self, device: Device):
|
||||
def get_vertical_scrolling_info(self, device: Device) -> dict[str, Any]:
|
||||
vertical_scrolling_info = device.feature_request(SupportedFeature.VERTICAL_SCROLLING)
|
||||
if vertical_scrolling_info:
|
||||
roller, ratchet, lines = struct.unpack("!BBB", vertical_scrolling_info[:3])
|
||||
|
@ -1725,13 +1725,13 @@ class Hidpp20:
|
|||
)[roller]
|
||||
return {"roller": roller_type, "ratchet": ratchet, "lines": lines}
|
||||
|
||||
def get_hi_res_scrolling_info(self, device: Device):
|
||||
def get_hi_res_scrolling_info(self, device: Device) -> tuple[int, int]:
|
||||
hi_res_scrolling_info = device.feature_request(SupportedFeature.HI_RES_SCROLLING)
|
||||
if hi_res_scrolling_info:
|
||||
mode, resolution = struct.unpack("!BB", hi_res_scrolling_info[:2])
|
||||
return mode, resolution
|
||||
|
||||
def get_pointer_speed_info(self, device: Device):
|
||||
def get_pointer_speed_info(self, device: Device) -> float:
|
||||
pointer_speed_info = device.feature_request(SupportedFeature.POINTER_SPEED)
|
||||
if pointer_speed_info:
|
||||
pointer_speed_hi, pointer_speed_lo = struct.unpack("!BB", pointer_speed_info[:2])
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
import pytest
|
||||
|
||||
from fakes import hidpp10_device
|
||||
|
||||
from .fakes import hidpp20_device
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def device_hidpp10():
|
||||
yield hidpp10_device.Hidpp10Device()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def device():
|
||||
yield hidpp20_device.Hidpp20Device()
|
|
@ -0,0 +1,18 @@
|
|||
from logitech_receiver.hidpp10_constants import Registers
|
||||
|
||||
|
||||
class Hidpp10Device:
|
||||
def __init__(self):
|
||||
self._iteration = 0
|
||||
|
||||
@property
|
||||
def kind(self):
|
||||
pass
|
||||
|
||||
def request(self, request_id, *params, no_reply=False):
|
||||
if request_id == 0x8100 + Registers.BATTERY_STATUS:
|
||||
return b"fff"
|
||||
elif request_id == 0x8000:
|
||||
return bytes([0x10, 0x10, 0x00])
|
||||
|
||||
raise RuntimeError(f"Unsupported feature: {request_id:04X}")
|
|
@ -0,0 +1,62 @@
|
|||
from logitech_receiver.hidpp20_constants import SupportedFeature
|
||||
|
||||
|
||||
class Hidpp20Device:
|
||||
def __init__(self):
|
||||
self._iteration = 0
|
||||
|
||||
def feature_request(self, feature, function: int = 0x00, *params, no_reply: bool = False) -> bytes:
|
||||
self._iteration += 1
|
||||
if feature == SupportedFeature.DEVICE_FW_VERSION:
|
||||
if function == 0x00:
|
||||
self._iteration = 1
|
||||
return bytes([0x02, 0xFF, 0xFF])
|
||||
elif self._iteration == 2 and function == 0x10:
|
||||
return bytes.fromhex("01414243030401000101000102030405")
|
||||
elif self._iteration == 3:
|
||||
self._iteration = 0
|
||||
return bytes.fromhex("02414243030401000101000102030405")
|
||||
elif feature == SupportedFeature.DEVICE_NAME:
|
||||
if function == 0x00:
|
||||
self._iteration = 1
|
||||
return bytes([0x12])
|
||||
elif function == 0x10:
|
||||
if self._iteration == 2:
|
||||
return bytes.fromhex("4142434445464748494A4B4C4D4E4F")
|
||||
elif self._iteration == 3:
|
||||
return bytes.fromhex("505152530000000000000000000000")
|
||||
elif function == 0x20:
|
||||
keyboard = 0x00
|
||||
return bytes([keyboard])
|
||||
elif feature == SupportedFeature.DEVICE_FRIENDLY_NAME:
|
||||
if function == 0x00:
|
||||
self._iteration = 1
|
||||
return bytes([0x12])
|
||||
elif function == 0x10:
|
||||
if self._iteration == 2:
|
||||
return bytes.fromhex("004142434445464748494A4B4C4D4E")
|
||||
elif self._iteration == 3:
|
||||
return bytes.fromhex("0E4F50515253000000000000000000")
|
||||
elif feature == SupportedFeature.BATTERY_STATUS:
|
||||
if function == 0x00:
|
||||
return bytes.fromhex("502000FFFF")
|
||||
elif feature == SupportedFeature.VERTICAL_SCROLLING:
|
||||
roller_type = 0x01
|
||||
num_of_ratchet_by_turn = 0x08
|
||||
scroll_lines = 0x0C
|
||||
return bytes([roller_type, num_of_ratchet_by_turn, scroll_lines])
|
||||
elif feature == SupportedFeature.HI_RES_SCROLLING:
|
||||
mode = 0x01
|
||||
resolution = 0x02
|
||||
return bytes([mode, resolution])
|
||||
elif feature == SupportedFeature.MOUSE_POINTER:
|
||||
sensor_resolution_msb = 0x01
|
||||
sensor_resolution_lsb = 0x00
|
||||
flags = 0x0A
|
||||
return bytes([sensor_resolution_msb, sensor_resolution_lsb, flags])
|
||||
elif feature == SupportedFeature.POINTER_SPEED:
|
||||
pointer_speed_high = 0x01
|
||||
pointer_speed_low = 0x03
|
||||
return bytes([pointer_speed_high, pointer_speed_low])
|
||||
|
||||
raise RuntimeError(f"Unsupported feature: {feature.name}, func=0x{function:02X}")
|
|
@ -1,12 +1,3 @@
|
|||
from dataclasses import dataclass
|
||||
from dataclasses import field
|
||||
from typing import Any
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
|
||||
import pytest
|
||||
|
||||
from logitech_receiver import common
|
||||
from logitech_receiver import hidpp10
|
||||
from logitech_receiver import hidpp10_constants
|
||||
from logitech_receiver.hidpp10_constants import Registers
|
||||
|
@ -14,327 +5,23 @@ from logitech_receiver.hidpp10_constants import Registers
|
|||
_hidpp10 = hidpp10.Hidpp10()
|
||||
|
||||
|
||||
@dataclass
|
||||
class Response:
|
||||
response: Optional[str]
|
||||
request_id: int
|
||||
params: Any
|
||||
def test_read_register(device_hidpp10):
|
||||
result = hidpp10.read_register(
|
||||
device_hidpp10,
|
||||
register=Registers.BATTERY_STATUS,
|
||||
)
|
||||
|
||||
assert result == bytes.fromhex("666666")
|
||||
|
||||
|
||||
@dataclass
|
||||
class Device:
|
||||
name: str = "Device"
|
||||
online: bool = True
|
||||
kind: str = "fake"
|
||||
protocol: float = 1.0
|
||||
isDevice: bool = False # incorrect, but useful here
|
||||
registers: List[Registers] = field(default_factory=list)
|
||||
responses: List[Response] = field(default_factory=list)
|
||||
|
||||
def request(self, id, params=None, no_reply=False):
|
||||
if params is None:
|
||||
params = []
|
||||
print("REQUEST ", self.name, hex(id), params)
|
||||
for r in self.responses:
|
||||
if id == r.request_id and params == r.params:
|
||||
print("RESPONSE", self.name, hex(r.request_id), r.params, r.response)
|
||||
return bytes.fromhex(r.response) if r.response is not None else None
|
||||
|
||||
|
||||
device_offline = Device("OFFLINE", False)
|
||||
device_leds = Device("LEDS", True, registers=[Registers.THREE_LEDS, Registers.BATTERY_STATUS])
|
||||
device_features = Device("FEATURES", True, protocol=4.5)
|
||||
|
||||
registers_standard = [Registers.BATTERY_STATUS, Registers.FIRMWARE]
|
||||
responses_standard = [
|
||||
Response("555555", 0x8100 | Registers.BATTERY_STATUS, 0x00),
|
||||
Response("666666", 0x8100 | Registers.BATTERY_STATUS, 0x10),
|
||||
Response("777777", 0x8000 | Registers.BATTERY_STATUS, 0x00),
|
||||
Response("888888", 0x8000 | Registers.BATTERY_STATUS, 0x10),
|
||||
Response("052100", 0x8100 | Registers.BATTERY_STATUS, []),
|
||||
Response("ABCDEF", 0x8100 | Registers.FIRMWARE, 0x01),
|
||||
Response("ABCDEF", 0x8100 | Registers.FIRMWARE, 0x02),
|
||||
Response("ABCDEF", 0x8100 | Registers.FIRMWARE, 0x03),
|
||||
Response("ABCDEF", 0x8100 | Registers.FIRMWARE, 0x04),
|
||||
Response("000900", 0x8100 | Registers.NOTIFICATIONS, []),
|
||||
Response("101010", 0x8100 | Registers.MOUSE_BUTTON_FLAGS, []),
|
||||
Response("010101", 0x8100 | Registers.KEYBOARD_FN_SWAP, []),
|
||||
Response("020202", 0x8100 | Registers.DEVICES_CONFIGURATION, []),
|
||||
Response("030303", 0x8000 | Registers.DEVICES_CONFIGURATION, 0x00),
|
||||
]
|
||||
device_standard = Device("STANDARD", True, registers=registers_standard, responses=responses_standard)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"device, register, param, expected_result",
|
||||
[
|
||||
(device_offline, Registers.THREE_LEDS, 0x00, None),
|
||||
(device_standard, Registers.THREE_LEDS, 0x00, None),
|
||||
(device_standard, Registers.BATTERY_STATUS, 0x00, "555555"),
|
||||
(device_standard, Registers.BATTERY_STATUS, 0x10, "666666"),
|
||||
],
|
||||
)
|
||||
def test_read_register(device, register, param, expected_result, mocker):
|
||||
spy_request = mocker.spy(device, "request")
|
||||
|
||||
result = hidpp10.read_register(device, register, param)
|
||||
|
||||
assert result == (bytes.fromhex(expected_result) if expected_result else None)
|
||||
spy_request.assert_called_once_with(0x8100 | register, param)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"device, register, param, expected_result",
|
||||
[
|
||||
(device_offline, Registers.THREE_LEDS, 0x00, None),
|
||||
(device_standard, Registers.THREE_LEDS, 0x00, None),
|
||||
(device_standard, Registers.BATTERY_STATUS, 0x00, "777777"),
|
||||
(device_standard, Registers.BATTERY_STATUS, 0x10, "888888"),
|
||||
],
|
||||
)
|
||||
def test_write_register(device, register, param, expected_result, mocker):
|
||||
spy_request = mocker.spy(device, "request")
|
||||
|
||||
result = hidpp10.write_register(device, register, param)
|
||||
|
||||
assert result == (bytes.fromhex(expected_result) if expected_result else None)
|
||||
spy_request.assert_called_once_with(0x8000 | register, param)
|
||||
|
||||
|
||||
def device_charge(name, response):
|
||||
responses = [Response(response, 0x8100 | Registers.BATTERY_CHARGE, [])]
|
||||
return Device(name, registers=[], responses=responses)
|
||||
|
||||
|
||||
device_charge1 = device_charge("DISCHARGING", "550030")
|
||||
device_charge2 = device_charge("RECHARGING", "440050")
|
||||
device_charge3 = device_charge("FULL", "600090")
|
||||
device_charge4 = device_charge("OTHER", "220000")
|
||||
|
||||
|
||||
def device_status(name, response):
|
||||
responses = [Response(response, 0x8100 | Registers.BATTERY_STATUS, [])]
|
||||
return Device(name, registers=[], responses=responses)
|
||||
|
||||
|
||||
device_status1 = device_status("FULL", "072200")
|
||||
device_status2 = device_status("GOOD", "052100")
|
||||
device_status3 = device_status("LOW", "032200")
|
||||
device_status4 = device_status("CRITICAL", "010100")
|
||||
device_status5 = device_status("EMPTY", "000000")
|
||||
device_status6 = device_status("NOSTATUS", "002200")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"device, expected_result, expected_register",
|
||||
[
|
||||
(device_offline, None, None),
|
||||
(device_features, None, None),
|
||||
(device_leds, None, None),
|
||||
(
|
||||
device_standard,
|
||||
common.Battery(common.BatteryLevelApproximation.GOOD, None, common.BatteryStatus.RECHARGING, None),
|
||||
Registers.BATTERY_STATUS,
|
||||
),
|
||||
(device_charge1, common.Battery(0x55, None, common.BatteryStatus.DISCHARGING, None), Registers.BATTERY_CHARGE),
|
||||
(
|
||||
device_charge2,
|
||||
common.Battery(0x44, None, common.BatteryStatus.RECHARGING, None),
|
||||
Registers.BATTERY_CHARGE,
|
||||
),
|
||||
(
|
||||
device_charge3,
|
||||
common.Battery(0x60, None, common.BatteryStatus.FULL, None),
|
||||
Registers.BATTERY_CHARGE,
|
||||
),
|
||||
(device_charge4, common.Battery(0x22, None, None, None), Registers.BATTERY_CHARGE),
|
||||
(
|
||||
device_status1,
|
||||
common.Battery(common.BatteryLevelApproximation.FULL, None, common.BatteryStatus.FULL, None),
|
||||
Registers.BATTERY_STATUS,
|
||||
),
|
||||
(
|
||||
device_status2,
|
||||
common.Battery(common.BatteryLevelApproximation.GOOD, None, common.BatteryStatus.RECHARGING, None),
|
||||
Registers.BATTERY_STATUS,
|
||||
),
|
||||
(
|
||||
device_status3,
|
||||
common.Battery(common.BatteryLevelApproximation.LOW, None, common.BatteryStatus.FULL, None),
|
||||
Registers.BATTERY_STATUS,
|
||||
),
|
||||
(
|
||||
device_status4,
|
||||
common.Battery(common.BatteryLevelApproximation.CRITICAL, None, None, None),
|
||||
Registers.BATTERY_STATUS,
|
||||
),
|
||||
(
|
||||
device_status5,
|
||||
common.Battery(common.BatteryLevelApproximation.EMPTY, None, common.BatteryStatus.DISCHARGING, None),
|
||||
Registers.BATTERY_STATUS,
|
||||
),
|
||||
(
|
||||
device_status6,
|
||||
common.Battery(None, None, common.BatteryStatus.FULL, None),
|
||||
Registers.BATTERY_STATUS,
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_hidpp10_get_battery(device, expected_result, expected_register):
|
||||
result = _hidpp10.get_battery(device)
|
||||
|
||||
assert result == expected_result
|
||||
if expected_register is not None:
|
||||
assert expected_register in device.registers
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"device, expected_firmwares",
|
||||
[
|
||||
(device_offline, []),
|
||||
(
|
||||
device_standard,
|
||||
[
|
||||
common.FirmwareKind.Firmware,
|
||||
common.FirmwareKind.Bootloader,
|
||||
common.FirmwareKind.Other,
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_hidpp10_get_firmware(device, expected_firmwares):
|
||||
firmwares = _hidpp10.get_firmware(device)
|
||||
|
||||
if not expected_firmwares:
|
||||
assert firmwares is None
|
||||
else:
|
||||
firmware_types = [firmware.kind for firmware in firmwares]
|
||||
assert firmware_types == expected_firmwares
|
||||
assert len(firmwares) == len(expected_firmwares)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"device, level, charging, warning, p1, p2",
|
||||
[
|
||||
(device_leds, common.BatteryLevelApproximation.EMPTY, False, False, 0x33, 0x00),
|
||||
(device_leds, common.BatteryLevelApproximation.CRITICAL, False, False, 0x22, 0x00),
|
||||
(device_leds, common.BatteryLevelApproximation.LOW, False, False, 0x20, 0x00),
|
||||
(device_leds, common.BatteryLevelApproximation.GOOD, False, False, 0x20, 0x02),
|
||||
(device_leds, common.BatteryLevelApproximation.FULL, False, False, 0x20, 0x22),
|
||||
(device_leds, None, True, False, 0x30, 0x33),
|
||||
(device_leds, None, False, True, 0x02, 0x00),
|
||||
(device_leds, None, False, False, 0x11, 0x11),
|
||||
],
|
||||
)
|
||||
def test_set_3leds(device, level, charging, warning, p1, p2, mocker):
|
||||
spy_request = mocker.spy(device, "request")
|
||||
|
||||
_hidpp10.set_3leds(device, level, charging, warning)
|
||||
|
||||
spy_request.assert_called_once_with(0x8000 | Registers.THREE_LEDS, p1, p2)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("device", [device_offline, device_features])
|
||||
def test_set_3leds_missing(device, mocker):
|
||||
spy_request = mocker.spy(device, "request")
|
||||
|
||||
_hidpp10.set_3leds(device)
|
||||
|
||||
assert spy_request.call_count == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize("device", [device_standard])
|
||||
def test_get_notification_flags(device):
|
||||
result = _hidpp10.get_notification_flags(device)
|
||||
|
||||
assert result == int("000900", 16)
|
||||
|
||||
|
||||
def test_set_notification_flags(mocker):
|
||||
device = device_standard
|
||||
spy_request = mocker.spy(device, "request")
|
||||
def test_set_notification_flags(mocker, device_hidpp10):
|
||||
spy_request = mocker.spy(device_hidpp10, "request")
|
||||
|
||||
result = _hidpp10.set_notification_flags(
|
||||
device, hidpp10_constants.NotificationFlag.BATTERY_STATUS, hidpp10_constants.NotificationFlag.WIRELESS
|
||||
device_hidpp10,
|
||||
hidpp10_constants.NotificationFlag.BATTERY_STATUS,
|
||||
hidpp10_constants.NotificationFlag.WIRELESS,
|
||||
)
|
||||
|
||||
spy_request.assert_called_once_with(0x8000 | Registers.NOTIFICATIONS, b"\x10\x01\x00")
|
||||
assert result is not None
|
||||
|
||||
|
||||
def test_set_notification_flags_bad(mocker):
|
||||
device = device_features
|
||||
spy_request = mocker.spy(device, "request")
|
||||
|
||||
result = _hidpp10.set_notification_flags(
|
||||
device, hidpp10_constants.NotificationFlag.BATTERY_STATUS, hidpp10_constants.NotificationFlag.WIRELESS
|
||||
)
|
||||
|
||||
assert spy_request.call_count == 0
|
||||
assert result is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"flag_bits, expected_names",
|
||||
[
|
||||
(None, ""),
|
||||
(0x0, "none"),
|
||||
(0x009020, "multi touch\n unknown:008020"),
|
||||
(0x080000, "mouse extra buttons"),
|
||||
(
|
||||
0x080000 + 0x000400,
|
||||
("link quality\n mouse extra buttons"),
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_notification_flag_str(flag_bits, expected_names):
|
||||
flag_names = hidpp10_constants.flags_to_str(flag_bits, fallback="none")
|
||||
|
||||
assert flag_names == expected_names
|
||||
|
||||
|
||||
def test_get_device_features():
|
||||
result = _hidpp10.get_device_features(device_standard)
|
||||
|
||||
assert result == int("101010", 16)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"device, register, expected_result",
|
||||
[
|
||||
(device_standard, Registers.BATTERY_STATUS, "052100"),
|
||||
(device_standard, Registers.MOUSE_BUTTON_FLAGS, "101010"),
|
||||
(device_standard, Registers.KEYBOARD_ILLUMINATION, None),
|
||||
(device_features, Registers.KEYBOARD_ILLUMINATION, None),
|
||||
],
|
||||
)
|
||||
def test_get_register(device, register, expected_result):
|
||||
result = _hidpp10._get_register(device, register)
|
||||
|
||||
assert result == (int(expected_result, 16) if expected_result is not None else None)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"device, expected_result",
|
||||
[
|
||||
(device_standard, 2),
|
||||
(device_features, None),
|
||||
],
|
||||
)
|
||||
def test_get_configuration_pending_flags(device, expected_result):
|
||||
result = hidpp10.get_configuration_pending_flags(device)
|
||||
|
||||
assert result == expected_result
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"device, expected_result",
|
||||
[
|
||||
(device_standard, True),
|
||||
(device_features, False),
|
||||
],
|
||||
)
|
||||
def test_set_configuration_pending_flags(device, expected_result):
|
||||
result = hidpp10.set_configuration_pending_flags(device, 0x00)
|
||||
|
||||
assert result == expected_result
|
||||
|
|
|
@ -0,0 +1,340 @@
|
|||
from dataclasses import dataclass
|
||||
from dataclasses import field
|
||||
from typing import Any
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
|
||||
import pytest
|
||||
|
||||
from logitech_receiver import common
|
||||
from logitech_receiver import hidpp10
|
||||
from logitech_receiver import hidpp10_constants
|
||||
from logitech_receiver.hidpp10_constants import Registers
|
||||
|
||||
_hidpp10 = hidpp10.Hidpp10()
|
||||
|
||||
|
||||
@dataclass
|
||||
class Response:
|
||||
response: Optional[str]
|
||||
request_id: int
|
||||
params: Any
|
||||
|
||||
|
||||
@dataclass
|
||||
class Device:
|
||||
name: str = "Device"
|
||||
online: bool = True
|
||||
kind: str = "fake"
|
||||
protocol: float = 1.0
|
||||
isDevice: bool = False # incorrect, but useful here
|
||||
registers: List[Registers] = field(default_factory=list)
|
||||
responses: List[Response] = field(default_factory=list)
|
||||
|
||||
def request(self, id, params=None, no_reply=False):
|
||||
if params is None:
|
||||
params = []
|
||||
print("REQUEST ", self.name, hex(id), params)
|
||||
for r in self.responses:
|
||||
if id == r.request_id and params == r.params:
|
||||
print("RESPONSE", self.name, hex(r.request_id), r.params, r.response)
|
||||
return bytes.fromhex(r.response) if r.response is not None else None
|
||||
|
||||
|
||||
device_offline = Device("OFFLINE", False)
|
||||
device_leds = Device("LEDS", True, registers=[Registers.THREE_LEDS, Registers.BATTERY_STATUS])
|
||||
device_features = Device("FEATURES", True, protocol=4.5)
|
||||
|
||||
registers_standard = [Registers.BATTERY_STATUS, Registers.FIRMWARE]
|
||||
responses_standard = [
|
||||
Response("555555", 0x8100 | Registers.BATTERY_STATUS, 0x00),
|
||||
Response("666666", 0x8100 | Registers.BATTERY_STATUS, 0x10),
|
||||
Response("777777", 0x8000 | Registers.BATTERY_STATUS, 0x00),
|
||||
Response("888888", 0x8000 | Registers.BATTERY_STATUS, 0x10),
|
||||
Response("052100", 0x8100 | Registers.BATTERY_STATUS, []),
|
||||
Response("ABCDEF", 0x8100 | Registers.FIRMWARE, 0x01),
|
||||
Response("ABCDEF", 0x8100 | Registers.FIRMWARE, 0x02),
|
||||
Response("ABCDEF", 0x8100 | Registers.FIRMWARE, 0x03),
|
||||
Response("ABCDEF", 0x8100 | Registers.FIRMWARE, 0x04),
|
||||
Response("000900", 0x8100 | Registers.NOTIFICATIONS, []),
|
||||
Response("101010", 0x8100 | Registers.MOUSE_BUTTON_FLAGS, []),
|
||||
Response("010101", 0x8100 | Registers.KEYBOARD_FN_SWAP, []),
|
||||
Response("020202", 0x8100 | Registers.DEVICES_CONFIGURATION, []),
|
||||
Response("030303", 0x8000 | Registers.DEVICES_CONFIGURATION, 0x00),
|
||||
]
|
||||
device_standard = Device("STANDARD", True, registers=registers_standard, responses=responses_standard)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"device, register, param, expected_result",
|
||||
[
|
||||
(device_offline, Registers.THREE_LEDS, 0x00, None),
|
||||
(device_standard, Registers.THREE_LEDS, 0x00, None),
|
||||
(device_standard, Registers.BATTERY_STATUS, 0x00, "555555"),
|
||||
(device_standard, Registers.BATTERY_STATUS, 0x10, "666666"),
|
||||
],
|
||||
)
|
||||
def test_read_register(device, register, param, expected_result, mocker):
|
||||
spy_request = mocker.spy(device, "request")
|
||||
|
||||
result = hidpp10.read_register(device, register, param)
|
||||
|
||||
assert result == (bytes.fromhex(expected_result) if expected_result else None)
|
||||
spy_request.assert_called_once_with(0x8100 | register, param)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"device, register, param, expected_result",
|
||||
[
|
||||
(device_offline, Registers.THREE_LEDS, 0x00, None),
|
||||
(device_standard, Registers.THREE_LEDS, 0x00, None),
|
||||
(device_standard, Registers.BATTERY_STATUS, 0x00, "777777"),
|
||||
(device_standard, Registers.BATTERY_STATUS, 0x10, "888888"),
|
||||
],
|
||||
)
|
||||
def test_write_register(device, register, param, expected_result, mocker):
|
||||
spy_request = mocker.spy(device, "request")
|
||||
|
||||
result = hidpp10.write_register(device, register, param)
|
||||
|
||||
assert result == (bytes.fromhex(expected_result) if expected_result else None)
|
||||
spy_request.assert_called_once_with(0x8000 | register, param)
|
||||
|
||||
|
||||
def device_charge(name, response):
|
||||
responses = [Response(response, 0x8100 | Registers.BATTERY_CHARGE, [])]
|
||||
return Device(name, registers=[], responses=responses)
|
||||
|
||||
|
||||
device_charge1 = device_charge("DISCHARGING", "550030")
|
||||
device_charge2 = device_charge("RECHARGING", "440050")
|
||||
device_charge3 = device_charge("FULL", "600090")
|
||||
device_charge4 = device_charge("OTHER", "220000")
|
||||
|
||||
|
||||
def device_status(name, response):
|
||||
responses = [Response(response, 0x8100 | Registers.BATTERY_STATUS, [])]
|
||||
return Device(name, registers=[], responses=responses)
|
||||
|
||||
|
||||
device_status1 = device_status("FULL", "072200")
|
||||
device_status2 = device_status("GOOD", "052100")
|
||||
device_status3 = device_status("LOW", "032200")
|
||||
device_status4 = device_status("CRITICAL", "010100")
|
||||
device_status5 = device_status("EMPTY", "000000")
|
||||
device_status6 = device_status("NOSTATUS", "002200")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"device, expected_result, expected_register",
|
||||
[
|
||||
(device_offline, None, None),
|
||||
(device_features, None, None),
|
||||
(device_leds, None, None),
|
||||
(
|
||||
device_standard,
|
||||
common.Battery(common.BatteryLevelApproximation.GOOD, None, common.BatteryStatus.RECHARGING, None),
|
||||
Registers.BATTERY_STATUS,
|
||||
),
|
||||
(device_charge1, common.Battery(0x55, None, common.BatteryStatus.DISCHARGING, None), Registers.BATTERY_CHARGE),
|
||||
(
|
||||
device_charge2,
|
||||
common.Battery(0x44, None, common.BatteryStatus.RECHARGING, None),
|
||||
Registers.BATTERY_CHARGE,
|
||||
),
|
||||
(
|
||||
device_charge3,
|
||||
common.Battery(0x60, None, common.BatteryStatus.FULL, None),
|
||||
Registers.BATTERY_CHARGE,
|
||||
),
|
||||
(device_charge4, common.Battery(0x22, None, None, None), Registers.BATTERY_CHARGE),
|
||||
(
|
||||
device_status1,
|
||||
common.Battery(common.BatteryLevelApproximation.FULL, None, common.BatteryStatus.FULL, None),
|
||||
Registers.BATTERY_STATUS,
|
||||
),
|
||||
(
|
||||
device_status2,
|
||||
common.Battery(common.BatteryLevelApproximation.GOOD, None, common.BatteryStatus.RECHARGING, None),
|
||||
Registers.BATTERY_STATUS,
|
||||
),
|
||||
(
|
||||
device_status3,
|
||||
common.Battery(common.BatteryLevelApproximation.LOW, None, common.BatteryStatus.FULL, None),
|
||||
Registers.BATTERY_STATUS,
|
||||
),
|
||||
(
|
||||
device_status4,
|
||||
common.Battery(common.BatteryLevelApproximation.CRITICAL, None, None, None),
|
||||
Registers.BATTERY_STATUS,
|
||||
),
|
||||
(
|
||||
device_status5,
|
||||
common.Battery(common.BatteryLevelApproximation.EMPTY, None, common.BatteryStatus.DISCHARGING, None),
|
||||
Registers.BATTERY_STATUS,
|
||||
),
|
||||
(
|
||||
device_status6,
|
||||
common.Battery(None, None, common.BatteryStatus.FULL, None),
|
||||
Registers.BATTERY_STATUS,
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_hidpp10_get_battery(device, expected_result, expected_register):
|
||||
result = _hidpp10.get_battery(device)
|
||||
|
||||
assert result == expected_result
|
||||
if expected_register is not None:
|
||||
assert expected_register in device.registers
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"device, expected_firmwares",
|
||||
[
|
||||
(device_offline, []),
|
||||
(
|
||||
device_standard,
|
||||
[
|
||||
common.FirmwareKind.Firmware,
|
||||
common.FirmwareKind.Bootloader,
|
||||
common.FirmwareKind.Other,
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_hidpp10_get_firmware(device, expected_firmwares):
|
||||
firmwares = _hidpp10.get_firmware(device)
|
||||
|
||||
if not expected_firmwares:
|
||||
assert firmwares is None
|
||||
else:
|
||||
firmware_types = [firmware.kind for firmware in firmwares]
|
||||
assert firmware_types == expected_firmwares
|
||||
assert len(firmwares) == len(expected_firmwares)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"device, level, charging, warning, p1, p2",
|
||||
[
|
||||
(device_leds, common.BatteryLevelApproximation.EMPTY, False, False, 0x33, 0x00),
|
||||
(device_leds, common.BatteryLevelApproximation.CRITICAL, False, False, 0x22, 0x00),
|
||||
(device_leds, common.BatteryLevelApproximation.LOW, False, False, 0x20, 0x00),
|
||||
(device_leds, common.BatteryLevelApproximation.GOOD, False, False, 0x20, 0x02),
|
||||
(device_leds, common.BatteryLevelApproximation.FULL, False, False, 0x20, 0x22),
|
||||
(device_leds, None, True, False, 0x30, 0x33),
|
||||
(device_leds, None, False, True, 0x02, 0x00),
|
||||
(device_leds, None, False, False, 0x11, 0x11),
|
||||
],
|
||||
)
|
||||
def test_set_3leds(device, level, charging, warning, p1, p2, mocker):
|
||||
spy_request = mocker.spy(device, "request")
|
||||
|
||||
_hidpp10.set_3leds(device, level, charging, warning)
|
||||
|
||||
spy_request.assert_called_once_with(0x8000 | Registers.THREE_LEDS, p1, p2)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("device", [device_offline, device_features])
|
||||
def test_set_3leds_missing(device, mocker):
|
||||
spy_request = mocker.spy(device, "request")
|
||||
|
||||
_hidpp10.set_3leds(device)
|
||||
|
||||
assert spy_request.call_count == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize("device", [device_standard])
|
||||
def test_get_notification_flags(device):
|
||||
result = _hidpp10.get_notification_flags(device)
|
||||
|
||||
assert result == int("000900", 16)
|
||||
|
||||
|
||||
def test_set_notification_flags(mocker):
|
||||
device = device_standard
|
||||
spy_request = mocker.spy(device, "request")
|
||||
|
||||
result = _hidpp10.set_notification_flags(
|
||||
device, hidpp10_constants.NotificationFlag.BATTERY_STATUS, hidpp10_constants.NotificationFlag.WIRELESS
|
||||
)
|
||||
|
||||
spy_request.assert_called_once_with(0x8000 | Registers.NOTIFICATIONS, b"\x10\x01\x00")
|
||||
assert result is not None
|
||||
|
||||
|
||||
def test_set_notification_flags_bad(mocker):
|
||||
device = device_features
|
||||
spy_request = mocker.spy(device, "request")
|
||||
|
||||
result = _hidpp10.set_notification_flags(
|
||||
device, hidpp10_constants.NotificationFlag.BATTERY_STATUS, hidpp10_constants.NotificationFlag.WIRELESS
|
||||
)
|
||||
|
||||
assert spy_request.call_count == 0
|
||||
assert result is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"flag_bits, expected_names",
|
||||
[
|
||||
(None, ""),
|
||||
(0x0, "none"),
|
||||
(0x009020, "multi touch\n unknown:008020"),
|
||||
(0x080000, "mouse extra buttons"),
|
||||
(
|
||||
0x080000 + 0x000400,
|
||||
("link quality\n mouse extra buttons"),
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_notification_flag_str(flag_bits, expected_names):
|
||||
flag_names = hidpp10_constants.flags_to_str(flag_bits, fallback="none")
|
||||
|
||||
assert flag_names == expected_names
|
||||
|
||||
|
||||
def test_get_device_features():
|
||||
result = _hidpp10.get_device_features(device_standard)
|
||||
|
||||
assert result == int("101010", 16)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"device, register, expected_result",
|
||||
[
|
||||
(device_standard, Registers.BATTERY_STATUS, "052100"),
|
||||
(device_standard, Registers.MOUSE_BUTTON_FLAGS, "101010"),
|
||||
(device_standard, Registers.KEYBOARD_ILLUMINATION, None),
|
||||
(device_features, Registers.KEYBOARD_ILLUMINATION, None),
|
||||
],
|
||||
)
|
||||
def test_get_register(device, register, expected_result):
|
||||
result = _hidpp10._get_register(device, register)
|
||||
|
||||
assert result == (int(expected_result, 16) if expected_result is not None else None)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"device, expected_result",
|
||||
[
|
||||
(device_standard, 2),
|
||||
(device_features, None),
|
||||
],
|
||||
)
|
||||
def test_get_configuration_pending_flags(device, expected_result):
|
||||
result = hidpp10.get_configuration_pending_flags(device)
|
||||
|
||||
assert result == expected_result
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"device, expected_result",
|
||||
[
|
||||
(device_standard, True),
|
||||
(device_features, False),
|
||||
],
|
||||
)
|
||||
def test_set_configuration_pending_flags(device, expected_result):
|
||||
result = hidpp10.set_configuration_pending_flags(device, 0x00)
|
||||
|
||||
assert result == expected_result
|
|
@ -0,0 +1,83 @@
|
|||
from logitech_receiver import common
|
||||
from logitech_receiver import hidpp20
|
||||
from logitech_receiver.common import FirmwareKind
|
||||
from logitech_receiver.hidpp20_constants import SupportedFeature
|
||||
|
||||
_hidpp20 = hidpp20.Hidpp20()
|
||||
|
||||
|
||||
def test_get_firmware(device):
|
||||
result = _hidpp20.get_firmware(device)
|
||||
|
||||
assert result == (
|
||||
common.FirmwareInfo(
|
||||
kind=FirmwareKind.Bootloader,
|
||||
name="ABC",
|
||||
version="03.04.B0100",
|
||||
extras=b"\x01\x00\x01\x02\x03\x04\x05",
|
||||
),
|
||||
common.FirmwareInfo(
|
||||
kind=FirmwareKind.Hardware,
|
||||
name="",
|
||||
version="65",
|
||||
extras=None,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def test_get_kind(device):
|
||||
result = _hidpp20.get_kind(device)
|
||||
|
||||
assert result == "keyboard"
|
||||
assert result == 1
|
||||
|
||||
|
||||
def test_get_name(device):
|
||||
result = _hidpp20.get_name(device)
|
||||
|
||||
assert result == "ABCDEFGHIJKLMNOPQR"
|
||||
|
||||
|
||||
def test_get_friendly_name(device):
|
||||
result = _hidpp20.get_friendly_name(device)
|
||||
|
||||
assert result == "ABCDEFGHIJKLMNOPQR"
|
||||
|
||||
|
||||
def test_get_battery_status(device):
|
||||
feature, battery = _hidpp20.get_battery_status(device)
|
||||
|
||||
assert feature == SupportedFeature.BATTERY_STATUS
|
||||
assert battery.level == 80
|
||||
assert battery.next_level == 32
|
||||
assert battery.status == common.BatteryStatus.DISCHARGING
|
||||
|
||||
|
||||
def test_get_vertical_scrolling_info(device):
|
||||
result = _hidpp20.get_vertical_scrolling_info(device)
|
||||
|
||||
assert result == {"roller": "standard", "ratchet": 8, "lines": 12}
|
||||
|
||||
|
||||
def test_get_high_resolution_scrolling_info(device):
|
||||
mode, resolution = _hidpp20.get_hi_res_scrolling_info(device)
|
||||
|
||||
assert mode == 0x1
|
||||
assert resolution == 0x2
|
||||
|
||||
|
||||
def test_get_mouse_pointer_info(device):
|
||||
result = _hidpp20.get_mouse_pointer_info(device)
|
||||
|
||||
assert result == {
|
||||
"dpi": 0x100,
|
||||
"acceleration": "med",
|
||||
"suggest_os_ballistics": False,
|
||||
"suggest_vertical_orientation": True,
|
||||
}
|
||||
|
||||
|
||||
def test_get_pointer_speed_info(device):
|
||||
result = _hidpp20.get_pointer_speed_info(device)
|
||||
|
||||
assert result == 0x0103 / 256
|
Loading…
Reference in New Issue