From 4c160d1723be9af42d8cb835387a9b4494d4550b Mon Sep 17 00:00:00 2001 From: MattHag <16444067+MattHag@users.noreply.github.com> Date: Mon, 4 Nov 2024 23:27:50 +0100 Subject: [PATCH] Remove NamedInts: Convert Task to enum Refactor code related to task and task ID. Related #2273 --- lib/logitech_receiver/hidpp20.py | 33 +- lib/logitech_receiver/special_keys.py | 494 +++++++++--------- tests/logitech_receiver/test_device.py | 6 +- .../logitech_receiver/test_hidpp20_complex.py | 28 +- 4 files changed, 289 insertions(+), 272 deletions(-) diff --git a/lib/logitech_receiver/hidpp20.py b/lib/logitech_receiver/hidpp20.py index b2fa67dd..3f142927 100644 --- a/lib/logitech_receiver/hidpp20.py +++ b/lib/logitech_receiver/hidpp20.py @@ -192,11 +192,11 @@ class ReprogrammableKey: - flags {List[str]} -- capabilities and desired software handling of the control """ - def __init__(self, device: Device, index, cid, tid, flags): + def __init__(self, device: Device, index, cid, task_id, flags): self._device = device self.index = index self._cid = cid - self._tid = tid + self._tid = task_id self._flags = flags @property @@ -209,7 +209,10 @@ class ReprogrammableKey: while the name is the Control ID's native task. But this makes more sense than presenting details of controls vs tasks in the interface. The same convention applies to `mapped_to`, `remappable_to`, `remap` in `ReprogrammableKeyV4`.""" - task = str(special_keys.TASK[self._tid]) + try: + task = str(special_keys.Task(self._tid)) + except ValueError: + task = f"unknown:{self._tid:04X}" return NamedInt(self._cid, task) @property @@ -234,8 +237,8 @@ class ReprogrammableKeyV4(ReprogrammableKey): - mapping_flags {List[str]} -- mapping flags set on the control """ - def __init__(self, device: Device, index, cid, tid, flags, pos, group, gmask): - ReprogrammableKey.__init__(self, device, index, cid, tid, flags) + def __init__(self, device: Device, index, cid, task_id, flags, pos, group, gmask): + ReprogrammableKey.__init__(self, device, index, cid, task_id, flags) self.pos = pos self.group = group self._gmask = gmask @@ -251,7 +254,7 @@ class ReprogrammableKeyV4(ReprogrammableKey): if self._mapped_to is None: self._getCidReporting() self._device.keys._ensure_all_keys_queried() - task = str(special_keys.TASK[self._device.keys.cid_to_tid[self._mapped_to]]) + task = str(special_keys.Task(self._device.keys.cid_to_tid[self._mapped_to])) return NamedInt(self._mapped_to, task) @property @@ -263,7 +266,11 @@ class ReprogrammableKeyV4(ReprogrammableKey): for g in self.group_mask: g = special_keys.CidGroup[str(g)] for tgt_cid in self._device.keys.group_cids[g]: - tgt_task = str(special_keys.TASK[self._device.keys.cid_to_tid[tgt_cid]]) + cid = self._device.keys.cid_to_tid[tgt_cid] + try: + tgt_task = str(special_keys.Task(cid)) + except ValueError: + tgt_task = f"unknown:{cid:04X}" tgt_task = NamedInt(tgt_cid, tgt_task) if tgt_task != self.default_task: # don't put itself in twice ret[tgt_task] = tgt_task @@ -524,9 +531,9 @@ class KeysArrayV2(KeysArray): raise IndexError(index) keydata = self.device.feature_request(SupportedFeature.REPROG_CONTROLS, 0x10, index) if keydata: - cid, tid, flags = struct.unpack("!HHB", keydata[:5]) - self.keys[index] = ReprogrammableKey(self.device, index, cid, tid, flags) - self.cid_to_tid[cid] = tid + cid, task_id, flags = struct.unpack("!HHB", keydata[:5]) + self.keys[index] = ReprogrammableKey(self.device, index, cid, task_id, flags) + self.cid_to_tid[cid] = task_id elif logger.isEnabledFor(logging.WARNING): logger.warning(f"Key with index {index} was expected to exist but device doesn't report it.") @@ -540,10 +547,10 @@ class KeysArrayV4(KeysArrayV2): raise IndexError(index) keydata = self.device.feature_request(SupportedFeature.REPROG_CONTROLS_V4, 0x10, index) if keydata: - cid, tid, flags1, pos, group, gmask, flags2 = struct.unpack("!HHBBBBB", keydata[:9]) + cid, task_id, flags1, pos, group, gmask, flags2 = struct.unpack("!HHBBBBB", keydata[:9]) flags = flags1 | (flags2 << 8) - self.keys[index] = ReprogrammableKeyV4(self.device, index, cid, tid, flags, pos, group, gmask) - self.cid_to_tid[cid] = tid + self.keys[index] = ReprogrammableKeyV4(self.device, index, cid, task_id, flags, pos, group, gmask) + self.cid_to_tid[cid] = task_id if group != 0: # 0 = does not belong to a group self.group_cids[special_keys.CidGroup(group)].append(cid) elif logger.isEnabledFor(logging.WARNING): diff --git a/lib/logitech_receiver/special_keys.py b/lib/logitech_receiver/special_keys.py index 281512ec..9547ec34 100644 --- a/lib/logitech_receiver/special_keys.py +++ b/lib/logitech_receiver/special_keys.py @@ -323,255 +323,263 @@ CONTROL[0x1200] = "MR" # add in MR key - this is not really a Logitech Control CONTROL._fallback = lambda x: f"unknown:{x:04X}" -# tasks.py - Switch_Presentation__Switch_Screen=0x0093, # on K400 Plus - Minimize_Window=0x0094, - Maximize_Window=0x0095, # on K400 Plus - MultiPlatform_App_Switch=0x0096, - MultiPlatform_Home=0x0097, - MultiPlatform_Menu=0x0098, - MultiPlatform_Back=0x0099, - Switch_Language=0x009A, # Mac_switch_language - Screen_Capture=0x009B, # Mac_screen_Capture, on Craft Keyboard - Gesture_Button=0x009C, - Smart_Shift=0x009D, - AppExpose=0x009E, - Smart_Zoom=0x009F, - Lookup=0x00A0, - Microphone_on__off=0x00A1, - Wifi_on__off=0x00A2, - Brightness_Down=0x00A3, - Brightness_Up=0x00A4, - Display_Out=0x00A5, - View_Open_Apps=0x00A6, - View_All_Open_Apps=0x00A7, - AppSwitch=0x00A8, - Gesture_Button_Navigation=0x00A9, # Mouse_Thumb_Button on MX Master - Fn_inversion=0x00AA, - Multiplatform_Back=0x00AB, - Multiplatform_Forward=0x00AC, - Multiplatform_Gesture_Button=0x00AD, - HostSwitch_Channel_1=0x00AE, - HostSwitch_Channel_2=0x00AF, - HostSwitch_Channel_3=0x00B0, - Multiplatform_Search=0x00B1, - Multiplatform_Home__Mission_Control=0x00B2, - Multiplatform_Menu__Launchpad=0x00B3, - Virtual_Gesture_Button=0x00B4, - Cursor=0x00B5, - Keyboard_Right_Arrow=0x00B6, - SW_Custom_Highlight=0x00B7, - Keyboard_Left_Arrow=0x00B8, - TBD=0x00B9, - Multiplatform_Language_Switch=0x00BA, - SW_Custom_Highlight_2=0x00BB, - Fast_Forward=0x00BC, - Fast_Backward=0x00BD, - Switch_Highlighting=0x00BE, - Mission_Control__Task_View=0x00BF, # Switch_Workspace on Craft Keyboard - Dashboard_Launchpad__Action_Center=0x00C0, # Application_Launcher on Craft Keyboard - Backlight_Down=0x00C1, # Backlight_Down_FW_internal_function - Backlight_Up=0x00C2, # Backlight_Up_FW_internal_function - Right_Click__App_Contextual_Menu=0x00C3, # Context_Menu on Craft Keyboard - DPI_Change=0x00C4, - New_Tab=0x00C5, - F2=0x00C6, - F3=0x00C7, - F4=0x00C8, - F5=0x00C9, - F6=0x00CA, - F7=0x00CB, - F8=0x00CC, - F1=0x00CD, - Laser_Button=0x00CE, - Laser_Button_Long_Press=0x00CF, - Start_Presentation=0x00D0, - Blank_Screen=0x00D1, - DPI_Switch=0x00D2, # AdjustDPI on MX Vertical - Home__Show_Desktop=0x00D3, - App_Switch__Dashboard=0x00D4, - App_Switch=0x00D5, - Fn_Inversion=0x00D6, - LeftAndRightClick=0x00D7, - Voice_Dictation=0x00D8, - Emoji_Smiling_Face_With_Heart_Shaped_Eyes=0x00D9, - Emoji_Loudly_Crying_Face=0x00DA, - Emoji_Smiley=0x00DB, - Emoji_Smiley_With_Tears=0x00DC, - Open_Emoji_Panel=0x00DD, - Multiplatform_App_Switch__Launchpad=0x00DE, - Snipping_Tool=0x00DF, - Grave_Accent=0x00E0, - Standard_Tab_Key=0x00E1, - Caps_Lock=0x00E2, - Left_Shift=0x00E3, - Left_Control=0x00E4, - Left_Option__Start=0x00E5, - Left_Command__Alt=0x00E6, - Right_Command__Alt=0x00E7, - Right_Option__Start=0x00E8, - Right_Control=0x00E9, - Right_Shift=0x0EA, - Insert=0x00EB, - Delete=0x00EC, - Home=0x00ED, - End=0x00EE, - Page_Up=0x00EF, - Page_Down=0x00F0, - Mute_Microphone=0x00F1, - Do_Not_Disturb=0x00F2, - Backslash=0x00F3, - Refresh=0x00F4, - Close_Tab=0x00F5, - Lang_Switch=0x00F6, - Standard_Alphabetical_Key=0x00F7, - Right_Option__Start__2=0x00F8, - Left_Option=0x00F9, - Right_Option=0x00FA, - Left_Cmd=0x00FB, - Right_Cmd=0x00FC, -) -TASK._fallback = lambda x: f"unknown:{x:04X}" + SWITCH_PRESENTATION_SWITCH_SCREEN = 0x0093 # on K400 Plus + MINIMIZE_WINDOW = 0x0094 + MAXIMIZE_WINDOW = 0x0095 # on K400 Plus + MULTI_PLATFORM_APP_SWITCH = 0x0096 + MULTI_PLATFORM_HOME = 0x0097 + MULTI_PLATFORM_MENU = 0x0098 + MULTI_PLATFORM_BACK = 0x0099 + SWITCH_LANGUAGE = 0x009A # Mac_switch_language + SCREEN_CAPTURE = 0x009B # Mac_screen_Capture, on Craft Keyboard + GESTURE_BUTTON = 0x009C + SMART_SHIFT = 0x009D + APP_EXPOSE = 0x009E + SMART_ZOOM = 0x009F + LOOKUP = 0x00A0 + MICROPHEON_ON_OFF = 0x00A1 + WIFI_ON_OFF = 0x00A2 + BRIGHTNESS_DOWN = 0x00A3 + BRIGHTNESS_UP = 0x00A4 + DISPLAY_OUT = 0x00A5 + VIEW_OPEN_APPS = 0x00A6 + VIEW_ALL_OPEN_APPS = 0x00A7 + APP_SWITCH = 0x00A8 + GESTURE_BUTTON_NAVIGATION = 0x00A9 # Mouse_Thumb_Button on MX Master + FN_INVERSION = 0x00AA + MULTI_PLATFORM_BACK_2 = 0x00AB # Alternative + MULTI_PLATFORM_FORWARD = 0x00AC + MULTI_PLATFORM_Gesture_Button = 0x00AD + HostSwitch_Channel_1 = 0x00AE + HostSwitch_Channel_2 = 0x00AF + HostSwitch_Channel_3 = 0x00B0 + MULTI_PLATFORM_SEARCH = 0x00B1 + MULTI_PLATFORM_HOME_MISSION_CONTROL = 0x00B2 + MULTI_PLATFORM_MENU_LAUNCHPAD = 0x00B3 + VIRTUAL_GESTURE_BUTTON = 0x00B4 + CURSOR = 0x00B5 + KEYBOARD_RIGHT_ARROW = 0x00B6 + SW_CUSTOM_HIGHLIGHT = 0x00B7 + KEYBOARD_LEFT_ARROW = 0x00B8 + TBD = 0x00B9 + MULTI_PLATFORM_Language_Switch = 0x00BA + SW_CUSTOM_HIGHLIGHT_2 = 0x00BB + FAST_FORWARD = 0x00BC + FAST_BACKWARD = 0x00BD + SWITCH_HIGHLIGHTING = 0x00BE + MISSION_CONTROL_TASK_VIEW = 0x00BF # Switch_Workspace on Craft Keyboard + DASHBOARD_LAUNCHPAD_ACTION_CENTER = 0x00C0 # Application_Launcher on Craft + # Keyboard + BACKLIGHT_DOWN = 0x00C1 # Backlight_Down_FW_internal_function + BACKLIGHT_UP = 0x00C2 # Backlight_Up_FW_internal_function + RIGHT_CLICK_APP_CONTEXT_MENU = 0x00C3 # Context_Menu on Craft Keyboard + DPI_Change = 0x00C4 + NEW_TAB = 0x00C5 + F2 = 0x00C6 + F3 = 0x00C7 + F4 = 0x00C8 + F5 = 0x00C9 + F6 = 0x00CA + F7 = 0x00CB + F8 = 0x00CC + F1 = 0x00CD + LASER_BUTTON = 0x00CE + LASER_BUTTON_LONG_PRESS = 0x00CF + START_PRESENTATION = 0x00D0 + BLANK_SCREEN = 0x00D1 + DPI_Switch = 0x00D2 # AdjustDPI on MX Vertical + HOME_SHOW_DESKTOP = 0x00D3 + APP_SWITCH_DASHBOARD = 0x00D4 + APP_SWITCH_2 = 0x00D5 # Alternative + FN_INVERSION_2 = 0x00D6 # Alternative + LEFT_AND_RIGHT_CLICK = 0x00D7 + VOICE_DICTATION = 0x00D8 + EMOJI_SMILING_FACE_WITH_HEART_SHAPED_EYES = 0x00D9 + EMOJI_LOUDLY_CRYING_FACE = 0x00DA + EMOJI_SMILEY = 0x00DB + EMOJI_SMILE_WITH_TEARS = 0x00DC + OPEN_EMOJI_PANEL = 0x00DD + MULTI_PLATFORM_APP_SWITCH_LAUNCHPAD = 0x00DE + SNIPPING_TOOL = 0x00DF + GRAVE_ACCENT = 0x00E0 + STANDARD_TAB_KEY = 0x00E1 + CAPS_LOCK = 0x00E2 + LEFT_SHIFT = 0x00E3 + LEFT_CONTROL = 0x00E4 + LEFT_OPTION_START = 0x00E5 + LEFT_COMMAND_ALT = 0x00E6 + RIGHT_COMMAND_ALT = 0x00E7 + RIGHT_OPTION_START = 0x00E8 + RIGHT_CONTROL = 0x00E9 + RIGHT_SHIFT = 0x0EA + INSERT = 0x00EB + DELETE = 0x00EC + HOME = 0x00ED + END = 0x00EE + PAGE_UP_2 = 0x00EF # Alternative + PAGE_DOWN_2 = 0x00F0 # Alternative + MUTE_MICROPHONE = 0x00F1 + DO_NOT_DISTURB = 0x00F2 + BACKSLASH = 0x00F3 + REFRESH = 0x00F4 + CLOSE_TAB = 0x00F5 + LANG_SWITCH = 0x00F6 + STANDARD_ALPHABETICAL_KEY = 0x00F7 + RRIGH_OPTION_START_2 = 0x00F8 + LEFT_OPTION = 0x00F9 + RIGHT_OPTION = 0x00FA + LEFT_CMD = 0x00FB + RIGHT_CMD = 0x00FC + + def __str__(self): + return self.name.replace("_", " ").title() + + # 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 diff --git a/tests/logitech_receiver/test_device.py b/tests/logitech_receiver/test_device.py index 2dd0dc02..f419b051 100644 --- a/tests/logitech_receiver/test_device.py +++ b/tests/logitech_receiver/test_device.py @@ -215,7 +215,7 @@ def test_device_receiver(number, pairing_info, responses, handle, _name, codenam @pytest.mark.parametrize( - "number, info, responses, handle, unitId, modelId, tid, kind, firmware, serial, id, psl, rate", + "number, info, responses, handle, unitId, modelId, task_id, kind, firmware, serial, id, psl, rate", zip( range(1, 7), [pi_CCCC, pi_2011, pi_4066, pi_1007, pi_407B, pi_DDDD], @@ -239,7 +239,7 @@ def test_device_receiver(number, pairing_info, responses, handle, _name, codenam ["1ms", "2ms", "4ms", "8ms", "1ms", "9ms"], # polling rate ), ) -def test_device_ids(number, info, responses, handle, unitId, modelId, tid, kind, firmware, serial, id, psl, rate): +def test_device_ids(number, info, responses, handle, unitId, modelId, task_id, kind, firmware, serial, id, psl, rate): low_level = LowLevelInterfaceFake(responses) low_level.request = partial(fake_hidpp.request, fake_hidpp.replace_number(responses, number)) low_level.ping = partial(fake_hidpp.ping, fake_hidpp.replace_number(responses, number)) @@ -248,7 +248,7 @@ def test_device_ids(number, info, responses, handle, unitId, modelId, tid, kind, assert test_device.unitId == unitId assert test_device.modelId == modelId - assert test_device.tid_map == tid + assert test_device.tid_map == task_id assert test_device.kind == kind assert test_device.firmware == firmware or len(test_device.firmware) > 0 and firmware is True assert test_device.id == id diff --git a/tests/logitech_receiver/test_hidpp20_complex.py b/tests/logitech_receiver/test_hidpp20_complex.py index 8ebcbec9..3a79d71f 100644 --- a/tests/logitech_receiver/test_hidpp20_complex.py +++ b/tests/logitech_receiver/test_hidpp20_complex.py @@ -154,19 +154,19 @@ def test_FeaturesArray_getitem(device, expected0, expected1, expected2, expected @pytest.mark.parametrize( - "device, index, cid, tid, flags, default_task, flag_names", + "device, index, cid, task_id, flags, default_task, flag_names", [ (device_standard, 2, 1, 1, 0x30, "Volume Up", ["reprogrammable", "divertable"]), (device_standard, 1, 2, 2, 0x20, "Volume Down", ["divertable"]), ], ) -def test_ReprogrammableKey_key(device, index, cid, tid, flags, default_task, flag_names): - key = hidpp20.ReprogrammableKey(device, index, cid, tid, flags) +def test_reprogrammable_key_key(device, index, cid, task_id, flags, default_task, flag_names): + key = hidpp20.ReprogrammableKey(device, index, cid, task_id, flags) assert key._device == device assert key.index == index assert key._cid == cid - assert key._tid == tid + assert key._tid == task_id assert key._flags == flags assert key.key == special_keys.CONTROL[cid] assert key.default_task == common.NamedInt(cid, default_task) @@ -174,7 +174,7 @@ def test_ReprogrammableKey_key(device, index, cid, tid, flags, default_task, fla @pytest.mark.parametrize( - "device, index, cid, tid, flags, pos, group, gmask, default_task, flag_names, group_names", + "device, index, cid, task_id, flags, pos, group, gmask, default_task, flag_names, group_names", [ (device_standard, 1, 0x51, 0x39, 0x60, 0, 1, 1, "Right Click", ["divertable", "persistently divertable"], ["g1"]), (device_standard, 2, 0x52, 0x3A, 0x11, 1, 2, 3, "Mouse Middle Button", ["mse", "reprogrammable"], ["g1", "g2"]), @@ -193,13 +193,15 @@ def test_ReprogrammableKey_key(device, index, cid, tid, flags, default_task, fla ), ], ) -def test_reprogrammable_key_v4_key(device, index, cid, tid, flags, pos, group, gmask, default_task, flag_names, group_names): - key = hidpp20.ReprogrammableKeyV4(device, index, cid, tid, flags, pos, group, gmask) +def test_reprogrammable_key_v4_key( + device, index, cid, task_id, flags, pos, group, gmask, default_task, flag_names, group_names +): + key = hidpp20.ReprogrammableKeyV4(device, index, cid, task_id, flags, pos, group, gmask) assert key._device == device assert key.index == index assert key._cid == cid - assert key._tid == tid + assert key._tid == task_id assert key._flags == flags assert key.pos == pos assert key.group == group @@ -220,7 +222,7 @@ def test_reprogrammable_key_v4_key(device, index, cid, tid, flags, pos, group, g ], ) # these fields need access all the key data, so start by setting up a device and its key data -def test_ReprogrammableKeyV4_query(responses, index, mapped_to, remappable_to, mapping_flags): +def test_reprogrammable_key_v4_query(responses, index, mapped_to, remappable_to, mapping_flags): device = fake_hidpp.Device( "KEY", responses=responses, feature=hidpp20_constants.SupportedFeature.REPROG_CONTROLS_V4, offset=5 ) @@ -360,14 +362,14 @@ def test_KeysArrayV4_query_key(device, index, top, cid): @pytest.mark.parametrize( - "device, count, index, cid, tid, flags, pos, group, gmask", + "device, count, index, cid, task_id, flags, pos, group, gmask", [ (device_standard, 4, 0, 0x0011, 0x0012, 0xCDAB, 1, 2, 3), (device_standard, 6, 1, 0x0111, 0x0022, 0xCDAB, 1, 2, 3), (device_standard, 8, 3, 0x0311, 0x0032, 0xCDAB, 1, 2, 4), ], ) -def test_KeysArrayV4__getitem(device, count, index, cid, tid, flags, pos, group, gmask): +def test_KeysArrayV4__getitem(device, count, index, cid, task_id, flags, pos, group, gmask): keysarray = hidpp20.KeysArrayV4(device, count) result = keysarray[index] @@ -375,7 +377,7 @@ def test_KeysArrayV4__getitem(device, count, index, cid, tid, flags, pos, group, assert result._device == device assert result.index == index assert result._cid == cid - assert result._tid == tid + assert result._tid == task_id assert result._flags == flags assert result.pos == pos assert result.group == group @@ -421,7 +423,7 @@ device_key = fake_hidpp.Device( (special_keys.CONTROL.Virtual_Gesture_Button, 7, common.NamedInt(0x51, "Right Click"), None), ], ) -def test_KeysArrayV4_key(key, expected_index, expected_mapped_to, expected_remappable_to): +def test_keys_array_v4_key(key, expected_index, expected_mapped_to, expected_remappable_to): device_key._keys = _hidpp20.get_keys(device_key) device_key._keys._ensure_all_keys_queried()