## Copyright (C) 2024 Solaar Contributors https://pwr-solaar.github.io/Solaar/ ## ## 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. """HID++ data and functions common to several logitech_receiver test files""" import errno import threading from dataclasses import dataclass from dataclasses import field from struct import pack from typing import Any from typing import Optional from logitech_receiver import device from logitech_receiver import hidpp20 from solaar import configuration def open_path(path: Optional[str]) -> int: if path is None: raise OSError(errno.EACCES, "Fake access error") return int(path, 16) # can raise exception def ping(responses, handle, devnumber, long_message=False): for r in responses: if handle == r.handle and devnumber == r.devnumber and r.id == 0x0010: return r.response def request(responses, handle, devnumber, id, *params, no_reply=False, return_error=False, long_message=False, protocol=1.0): params = b"".join(pack("B", p) if isinstance(p, int) else p for p in params) print("REQUEST ", hex(handle), hex(devnumber), hex(id), params.hex()) for r in responses: if handle == r.handle and devnumber == r.devnumber and r.id == id and bytes.fromhex(r.params) == params: print("RESPONSE", hex(r.handle), hex(r.devnumber), hex(r.id), r.params, r.response) return bytes.fromhex(r.response) if r.response is not None else None @dataclass class Response: response: Optional[str] id: int params: str = "" handle: int = 0x11 devnumber: int = 0xFF no_reply: bool = False def replace_number(responses, number): # change the devnumber for a list of responses return [Response(r.response, r.id, r.params, r.handle, number, r.no_reply) for r in responses] def adjust_responses_index(index, responses): # change index-4 responses to index return [Response(r.response, r.id - 0x400 + (index << 8), r.params, r.handle, r.devnumber, r.no_reply) for r in responses] r_empty = [ # a HID++ device with no responses except for ping Response(1.0, 0x0010), # ping ] r_keyboard_1 = [ # a HID++ 1.0 keyboard Response(1.0, 0x0010), # ping Response("001234", 0x81F1, "01"), # firmware Response("003412", 0x81F1, "02"), # firmware Response("002345", 0x81F1, "03"), # firmware Response("003456", 0x81F1, "04"), # firmware Response("050050", 0x8107), # battery status ] r_keyboard_2 = [ # a HID++ 2.0 keyboard Response(4.2, 0x0010), # ping Response("010001", 0x0000, "0001"), # feature set at 0x01 Response("020003", 0x0000, "0020"), # CONFIG_CHANGE at 0x02 Response("030001", 0x0000, "0003"), # device information at 0x03 Response("040003", 0x0000, "0100"), # unknown 0100 at 0x04 Response("050003", 0x0000, "1B04"), # reprogrammable keys V4 at 0x05 Response("060003", 0x0000, "0007"), # device friendly name at 0x06 Response("070003", 0x0000, "0005"), # device name at 0x07 Response("080003", 0x0000, "1000"), # battery status at 0x08 Response("08", 0x0100), # 8 features Response("00010001", 0x0110, "01"), # feature set at 0x01 Response("00200003", 0x0110, "02"), # CONFIG_CHANGE at 0x02 Response("00030001", 0x0110, "03"), # device information at 0x03 Response("01000003", 0x0110, "04"), # unknown 0100 at 0x04 Response("1B040003", 0x0110, "05"), # reprogrammable keys V4 at 0x05 Response("00070003", 0x0000, "06"), # device friendly name at 0x06 Response("00050003", 0x0000, "07"), # device name at 0x07 Response("10000001", 0x0110, "08"), # battery status at 0x02 Response("0212345678000D1234567890ABAA01", 0x0300), # device information Response("04", 0x0500), # reprogrammable keys V4 Response("00110012AB010203CD00", 0x0510, "00"), # reprogrammable keys V4 Response("01110022AB010203CD00", 0x0510, "01"), # reprogrammable keys V4 Response("00010111AB010203CD00", 0x0510, "02"), # reprogrammable keys V4 Response("03110032AB010204CD00", 0x0510, "03"), # reprogrammable keys V4 Response("00030333AB010203CD00", 0x0510, "04"), # reprogrammable keys V4 Response("12", 0x0600), # friendly namme Response("004142434445464748494A4B4C4D4E", 0x0610, "00"), Response("0E4F50515253000000000000000000", 0x0610, "0E"), Response("12", 0x0700), # name and kind Response("4142434445464748494A4B4C4D4E4F", 0x0710, "00"), Response("505152530000000000000000000000", 0x0710, "0F"), Response("00", 0x0720), Response("12345678", 0x0800), # battery status ] r_mouse_1 = [ # a HID++ 1.0 mouse Response(1.0, 0x0010), # ping ] r_mouse_2 = [ # a HID++ 2.0 mouse with few responses except for ping Response(4.2, 0x0010), # ping ] r_mouse_3 = [ # a HID++ 2.0 mouse Response(4.5, 0x0010), # ping Response("010001", 0x0000, "0001"), # feature set at 0x01 Response("020002", 0x0000, "8060"), # report rate at 0x02 Response("040001", 0x0000, "0003"), # device information at 0x04 Response("050002", 0x0000, "0005"), # device type and name at 0x05 Response("08", 0x0100), # 8 features Response("00010001", 0x0110, "01"), # feature set at 0x01 Response("80600002", 0x0110, "02"), # report rate at 0x02 Response("00030001", 0x0110, "04"), # device information at 0x04 Response("00050002", 0x0110, "05"), # device type and name at 0x05 Response("09", 0x0210), # report rate - current rate Response("03123456790008123456780000AA01", 0x0400), # device information Response("0141424302030100", 0x0410, "00"), # firmware 0 Response("0241", 0x0410, "01"), # firmware 1 Response("05", 0x0410, "02"), # firmware 2 Response("12", 0x0500), # name count - 18 characters Response("414241424142414241424142414241", 0x0510, "00"), # name - first 15 characters Response("444544000000000000000000000000", 0x0510, "0F"), # name - last 3 characters ] responses_key = [ # responses for Reprogrammable Keys V4 at 0x05 Response("08", 0x0500), # Reprogrammable Keys V4 count Response("00500038010001010400000000000000", 0x0510, "00"), # left button Response("00510039010001010400000000000000", 0x0510, "01"), # right button Response("0052003A310003070500000000000000", 0x0510, "02"), # middle button Response("0053003C710002030100000000000000", 0x0510, "03"), # back button Response("0056003E710002030100000000000000", 0x0510, "04"), # forward button Response("00C300A9310003070300000000000000", 0x0510, "05"), # smart shift? Response("00C4009D310003070500000000000000", 0x0510, "06"), # ? Response("00D700B4A00004000300000000000000", 0x0510, "07"), # ? Response("00500000000000000000000000000000", 0x0520, "0050"), # left button Response("00510000000000000000000000000000", 0x0520, "0051"), # ... Response("00520100500000000000000000000000", 0x0520, "0052"), Response("00530500000000000000000000000000", 0x0520, "0053"), Response("00561100000000000000000000000000", 0x0520, "0056"), Response("00C30000000000000000000000000000", 0x0520, "00C3"), Response("00C40000500000000000000000000000", 0x0520, "00C4"), Response("00D70000510000000000000000000000", 0x0520, "00D7"), Response("0041", 0x0400), # flags Response("0401", 0x0410), # count Response("0050", 0x0420, "00FF"), # left button Response("0051", 0x0420, "01FF"), # right button Response("0052", 0x0420, "02FF"), # middle button Response("0053", 0x0420, "03FF"), # back button Response("0050000100500000", 0x0430, "0050FF"), # left button current Response("0051000100500001", 0x0430, "0051FF"), # right button current Response("0052000100500001", 0x0430, "0052FF"), # middle button current Response("0053000100500001", 0x0430, "0053FF"), # back button current Response("0050FF01005000", 0x0440, "0050FF01005000"), # left button write Response("0051FF01005000", 0x0440, "0051FF01005000"), # right button write Response("0051FF01005100", 0x0440, "0051FF01005100"), # right button set write ] responses_remap = [ # responses for Persistent Remappable Actions at 0x04 and reprogrammable keys at 0x05 Response("0041", 0x0400), Response("03", 0x0410), Response("0301", 0x0410, "00"), Response("0050", 0x0420, "00FF"), Response("0050000200010001", 0x0430, "0050FF"), # Left Button Response("0051", 0x0420, "01FF"), Response("0051000200010000", 0x0430, "0051FF"), # Left Button Response("0052", 0x0420, "02FF"), Response("0052000100510000", 0x0430, "0052FF"), # key DOWN Response("050002", 0x0000, "1B04"), # REPROGRAMMABLE_KEYS_V4 ] + responses_key responses_gestures = [ # the commented-out messages are not used by either the setting or other testing Response("4203410141020400320480148C21A301", 0x0400, "0000"), # items Response("A302A11EA30A4105822C852DAD2AAD2B", 0x0400, "0008"), Response("8F408F418F434204AF54912282558264", 0x0400, "0010"), Response("01000000000000000000000000000000", 0x0400, "0018"), Response("01000000000000000000000000000000", 0x0410, "000101"), # enable # Response("02000000000000000000000000000000", 0x0410, "000102"), # Response("04000000000000000000000000000000", 0x0410, "000104"), # Response("08000000000000000000000000000000", 0x0410, "000108"), Response("00000000000000000000000000000000", 0x0410, "000110"), # Response("20000000000000000000000000000000", 0x0410, "000120"), # Response("40000000000000000000000000000000", 0x0410, "000140"), # Response("00000000000000000000000000000000", 0x0410, "000180"), # Response("00000000000000000000000000000000", 0x0410, "010101"), # Response("00000000000000000000000000000000", 0x0410, "010102"), # Response("04000000000000000000000000000000", 0x0410, "010104"), # Response("00000000000000000000000000000000", 0x0410, "010108"), Response("6F000000000000000000000000000000", 0x0410, "0001FF"), Response("04000000000000000000000000000000", 0x0410, "01010F"), Response("00000000000000000000000000000000", 0x0430, "000101"), # divert # Response("00000000000000000000000000000000", 0x0430, "000102"), # Response("00000000000000000000000000000000", 0x0430, "000104"), # Response("00000000000000000000000000000000", 0x0430, "000108"), Response("00000000000000000000000000000000", 0x0430, "000110"), # Response("00000000000000000000000000000000", 0x0430, "000120"), # Response("00000000000000000000000000000000", 0x0430, "000140"), # Response("00000000000000000000000000000000", 0x0430, "000180"), # Response("00000000000000000000000000000000", 0x0430, "010101"), # Response("00000000000000000000000000000000", 0x0430, "010102"), Response("00000000000000000000000000000000", 0x0430, "0001FF"), Response("00000000000000000000000000000000", 0x0430, "010103"), Response("08000000000000000000000000000000", 0x0450, "01FF"), Response("08000000000000000000000000000000", 0x0450, "02FF"), Response("08000000000000000000000000000000", 0x0450, "03FF"), Response("00040000000000000000000000000000", 0x0450, "04FF"), Response("5C020000000000000000000000000000", 0x0450, "05FF"), Response("01000000000000000000000000000000", 0x0460, "00FF"), Response("01000000000000000000000000000000", 0x0470, "00FF"), Response("01", 0x0420, "00010101"), # set index 1 Response("00", 0x0420, "00010100"), # unset index 1 Response("01", 0x0420, "00011010"), # set index 4 Response("00", 0x0420, "00011000"), # unset index 4 Response("01", 0x0440, "00010101"), # divert index 1 Response("00", 0x0440, "00010100"), # undivert index 1 Response("000080FF", 0x0480, "000080FF"), # write param 0 Response("000180FF", 0x0480, "000180FF"), # write param 0 ] zone_responses_1 = [ # responses for COLOR LED EFFECTS Response("00000102", 0x0710, "00FF00"), Response("0000000300040005", 0x0720, "000000"), Response("0001000B00080009", 0x0720, "000100"), ] zone_responses_2 = [ # responses for RGB EFFECTS Response("0000000102", 0x0700, "00FF00"), Response("0000000300040005", 0x0700, "000000"), Response("0001000200080009", 0x0700, "000100"), ] effects_responses_1 = [Response("0100000001", 0x0700)] + zone_responses_1 effects_responses_2 = [Response("FFFF0100000001", 0x0700, "FFFF00")] + zone_responses_2 responses_profiles = [ # OnboardProfile in RAM Response("0104010101020100FE0200", 0x0900), Response("000101FF", 0x0950, "00000000"), Response("FFFFFFFF", 0x0950, "00000004"), Response("01010290018003000700140028FFFFFF", 0x0950, "00010000"), Response("FFFF0000000000000000000000000000", 0x0950, "00010010"), Response("8000FFFF900aFF00800204548000FFFF", 0x0950, "00010020"), Response("900aFF00800204548000FFFF900aFF00", 0x0950, "00010030"), Response("800204548000FFFF900aFF0080020454", 0x0950, "00010040"), Response("8000FFFF900aFF00800204548000FFFF", 0x0950, "00010050"), Response("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", 0x0950, "00010060"), Response("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", 0x0950, "00010070"), Response("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", 0x0950, "00010080"), Response("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", 0x0950, "00010090"), Response("54004500370000000000000000000000", 0x0950, "000100A0"), Response("00000000000000000000000000000000", 0x0950, "000100B0"), Response("00000000000000000000000000000000", 0x0950, "000100C0"), Response("0A01020300500407000000FFFFFFFFFF", 0x0950, "000100D0"), Response("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", 0x0950, "000100E0"), Response("FFFFFFFFFFFFFFFFFFFFFFFFFF7C81AB", 0x0950, "000100EE"), ] responses_profiles_rom = [ # OnboardProfile in ROM Response("0104010101020100FE0200", 0x0900), Response("00000000", 0x0950, "00000000"), Response("010101FF", 0x0950, "01000000"), Response("FFFFFFFF", 0x0950, "01000004"), Response("01010290018003000700140028FFFFFF", 0x0950, "01010000"), Response("FFFF0000000000000000000000000000", 0x0950, "01010010"), Response("8000FFFF900aFF00800204548000FFFF", 0x0950, "01010020"), Response("900aFF00800204548000FFFF900aFF00", 0x0950, "01010030"), Response("800204548000FFFF900aFF0080020454", 0x0950, "01010040"), Response("8000FFFF900aFF00800204548000FFFF", 0x0950, "01010050"), Response("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", 0x0950, "01010060"), Response("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", 0x0950, "01010070"), Response("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", 0x0950, "01010080"), Response("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", 0x0950, "01010090"), Response("54004500370000000000000000000000", 0x0950, "010100A0"), Response("00000000000000000000000000000000", 0x0950, "010100B0"), Response("00000000000000000000000000000000", 0x0950, "010100C0"), Response("0A01020300500407000000FFFFFFFFFF", 0x0950, "010100D0"), Response("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", 0x0950, "010100E0"), Response("FFFFFFFFFFFFFFFFFFFFFFFFFF7C81AB", 0x0950, "010100EE"), ] responses_profiles_rom_2 = [ # OnboardProfile in ROM Response("0104010101020100FE0200", 0x0900), Response("FFFFFFFF", 0x0950, "00000000"), Response("010101FF", 0x0950, "01000000"), Response("FFFFFFFF", 0x0950, "01000004"), Response("01010290018003000700140028FFFFFF", 0x0950, "01010000"), Response("FFFF0000000000000000000000000000", 0x0950, "01010010"), Response("8000FFFF900aFF00800204548000FFFF", 0x0950, "01010020"), Response("900aFF00800204548000FFFF900aFF00", 0x0950, "01010030"), Response("800204548000FFFF900aFF0080020454", 0x0950, "01010040"), Response("8000FFFF900aFF00800204548000FFFF", 0x0950, "01010050"), Response("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", 0x0950, "01010060"), Response("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", 0x0950, "01010070"), Response("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", 0x0950, "01010080"), Response("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", 0x0950, "01010090"), Response("54004500370000000000000000000000", 0x0950, "010100A0"), Response("00000000000000000000000000000000", 0x0950, "010100B0"), Response("00000000000000000000000000000000", 0x0950, "010100C0"), Response("0A01020300500407000000FFFFFFFFFF", 0x0950, "010100D0"), Response("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", 0x0950, "010100E0"), Response("FFFFFFFFFFFFFFFFFFFFFFFFFF7C81AB", 0x0950, "010100EE"), ] complex_responses_1 = [ # COLOR_LED_EFFECTS Response(4.2, 0x0010), # ping Response("010001", 0x0000, "0001"), # FEATURE SET at x01 Response("020001", 0x0000, "0020"), # CONFIG_CHANGE at x02 Response("0A", 0x0100), # 10 features Response("070001", 0x0000, "8070"), # COLOR_LED_EFFECTS at 0x07 *effects_responses_1, ] complex_responses_2 = [ # RGB_EFFECTS + reprogrammable keys + persistent actions Response(4.2, 0x0010), # ping Response("010001", 0x0000, "0001"), # FEATURE SET at x01 Response("020001", 0x0000, "0020"), # CONFIG_CHANGE at x02 Response("0A", 0x0100), # 10 features Response("070001", 0x0000, "8071"), # RGB_EFFECTS at 0x07 *effects_responses_2, Response("040001", 0x0000, "1C00"), # Persistent Remappable Actions at 0x04 *responses_remap, Response("080001", 0x0000, "6501"), # Gestures at 0x08 *adjust_responses_index(8, responses_gestures), Response("060003", 0x0000, "1982"), # Backlight 2 at 0x06 Response("010118000001020003000400", 0x0600), Response("090003", 0x0000, "8100"), # Onboard Profiles at 0x09 *responses_profiles, ] responses_speedchange = [ Response("0100", 0x0400), Response("010001", 0x0000, "0001"), # FEATURE SET at x01 Response("0A", 0x0100), # 10 features Response("0120", 0x0410, "0120"), Response("050001", 0x0000, "1B04"), # REPROG_CONTROLS_V4 Response("01", 0x0500), Response("00ED009D310003070500000000000000", 0x0510, "00"), # DPI Change Response("00ED0000000000000000000000000000", 0x0520, "00ED"), # DPI Change current Response("060000", 0x0000, "2205"), # POINTER_SPEED ] # A fake device that uses provided data (responses) to respond to HID++ commands. # Some methods from the real device are used to set up data structures needed for settings @dataclass class Device: name: str = "TESTD" online: bool = True protocol: float = 2.0 responses: Any = field(default_factory=list) codename: str = "TESTC" feature: Optional[int] = None offset: Optional[int] = 4 version: Optional[int] = 0 wpid: Optional[str] = "0000" setting_callback: Any = None sliding = profiles = _backlight = _keys = _remap_keys = _led_effects = _gestures = None _gestures_lock = threading.Lock() read_register = device.Device.read_register write_register = device.Device.write_register backlight = device.Device.backlight keys = device.Device.keys remap_keys = device.Device.remap_keys led_effects = device.Device.led_effects gestures = device.Device.gestures __hash__ = device.Device.__hash__ feature_request = device.Device.feature_request def __post_init__(self): self._name = self.name self._protocol = self.protocol self.persister = configuration._DeviceEntry() self.features = hidpp20.FeaturesArray(self) self.settings = [] if self.feature is not None: self.features = hidpp20.FeaturesArray(self) self.responses = [Response("010001", 0x0000, "0001"), Response("20", 0x0100)] + self.responses self.responses.append(Response(f"{self.offset:0>2X}00{self.version:0>2X}", 0x0000, f"{self.feature:0>4X}")) if self.setting_callback is None: self.setting_callback = lambda x, y, z: None self.add_notification_handler = lambda x, y: None def request(self, id, *params, no_reply=False, long_message=False, protocol=2.0): params = b"".join(pack("B", p) if isinstance(p, int) else p for p in params) print("REQUEST ", self._name, hex(id), params.hex().upper()) for r in self.responses: if id == r.id and params == bytes.fromhex(r.params): print("RESPONSE", self._name, hex(r.id), r.params, r.response) return bytes.fromhex(r.response) if isinstance(r.response, str) else r.response print("RESPONSE", self._name, None) def ping(self, handle=None, devnumber=None, long_message=False): print("PING", self._protocol) return self._protocol def match_requests(number, responses, call_args_list): for i in range(0 - number, 0): param = b"".join(pack("B", p) if isinstance(p, int) else p for p in call_args_list[i][0][1:]).hex().upper() print("MATCH", i, hex(call_args_list[i][0][0]), param, hex(responses[i].id), responses[i].params) assert call_args_list[i][0][0] == responses[i].id assert param == responses[i].params