From d4aa504a0104de656f027f072ed9afe1f84c5853 Mon Sep 17 00:00:00 2001 From: Rune Haugaard Date: Mon, 17 Jun 2024 09:27:26 +0200 Subject: [PATCH 1/5] Initial commit for support for pico passthrough Signed-off-by: Rune Haugaard --- pico_passthrough/boot.py | 2 + pico_passthrough/boot_out.txt | 4 + pico_passthrough/code.py | 38 ++++++++ pico_passthrough/sd/placeholder.txt | 1 + pico_passthrough/settings.toml | 0 src/constants.py | 1 + src/controller.py | 3 + src/executer_pico.py | 138 +++++++++++++++++++++++++++ src/view/pyqt5/edit_config_dialog.py | 3 +- 9 files changed, 189 insertions(+), 1 deletion(-) create mode 100644 pico_passthrough/boot.py create mode 100644 pico_passthrough/boot_out.txt create mode 100644 pico_passthrough/code.py create mode 100644 pico_passthrough/sd/placeholder.txt create mode 100644 pico_passthrough/settings.toml create mode 100644 src/executer_pico.py diff --git a/pico_passthrough/boot.py b/pico_passthrough/boot.py new file mode 100644 index 0000000..730d544 --- /dev/null +++ b/pico_passthrough/boot.py @@ -0,0 +1,2 @@ +import usb_cdc +usb_cdc.enable(console=True, data=True) diff --git a/pico_passthrough/boot_out.txt b/pico_passthrough/boot_out.txt new file mode 100644 index 0000000..c322318 --- /dev/null +++ b/pico_passthrough/boot_out.txt @@ -0,0 +1,4 @@ +Adafruit CircuitPython 9.0.5 on 2024-05-22; Raspberry Pi Pico with rp2040 +Board ID:raspberry_pi_pico +UID:E66098F29B4F3239 +boot.py output: diff --git a/pico_passthrough/code.py b/pico_passthrough/code.py new file mode 100644 index 0000000..c45db9a --- /dev/null +++ b/pico_passthrough/code.py @@ -0,0 +1,38 @@ +import time +import usb_cdc +import usb_hid +from adafruit_hid.keyboard import Keyboard +from adafruit_hid.keycode import Keycode + +# Initialize serial +serial = usb_cdc.data + +# Initialize USB HID keyboard +keyboard = Keyboard(usb_hid.devices) + +def emulate_key_press(key_char): + # Convert the key_char to an appropriate keycode if needed + # This example assumes key_char is a string that maps directly to a key + if key_char.isalpha(): # For alphabetic characters + keycode = getattr(Keycode, key_char.upper()) + keyboard.press(keycode) + keyboard.release_all() + else: + # For other characters, you'd need a mapping or handle accordingly + pass + +while True: + if serial.in_waiting >= 1: # Wait for at least 3 bytes (1 byte key + 2 bytes delay) + key_byte = serial.read(1) # Read 1 byte for the key + #print("bytes: "+ str(key_byte)) + key = key_byte.decode("utf-8") + print("key: "+key) + delay_bytes = serial.read(2) # Read 2 bytes for the delay + delay = int.from_bytes(delay_bytes, 'big') # Convert bytes to integer (big-endian) + + print(str(delay)) + # Emulate the key press + #emulate_key_press(key) + + # Wait for the specified delay + #time.sleep(delay / 1000) # Delay is in milliseconds diff --git a/pico_passthrough/sd/placeholder.txt b/pico_passthrough/sd/placeholder.txt new file mode 100644 index 0000000..6e9754c --- /dev/null +++ b/pico_passthrough/sd/placeholder.txt @@ -0,0 +1 @@ +SD cards mounted at /sd will hide this file from Python. SD cards are not visible via USB CIRCUITPY. diff --git a/pico_passthrough/settings.toml b/pico_passthrough/settings.toml new file mode 100644 index 0000000..e69de29 diff --git a/src/constants.py b/src/constants.py index 650bc4c..3d1a582 100644 --- a/src/constants.py +++ b/src/constants.py @@ -13,6 +13,7 @@ EXECUTOR_PYAUTOGUI = "pyautogui" EXECUTOR_ARDUINO = "arduino" EXECUTOR_XDOTOOL = "xdotool" +EXECUTOR_PICO = "pico" # SETTINGS CONSTANTS ## GLOBAL ARM diff --git a/src/controller.py b/src/controller.py index 2cce96c..6f4b8e2 100644 --- a/src/controller.py +++ b/src/controller.py @@ -27,6 +27,9 @@ def set_executor(self): elif selectedExecutor == constants.EXECUTOR_XDOTOOL: from src.executer_xdotool import XdotoolExecuter self.executer = XdotoolExecuter() + elif selectedExecutor == constants.EXECUTOR_PICO: + from src.executer_pico import PicoPassthroughExecuter + self.executer = PicoPassthroughExecuter(self) else: raise ModuleNotFoundError self.view.update_executor_menu() diff --git a/src/executer_pico.py b/src/executer_pico.py new file mode 100644 index 0000000..624d27a --- /dev/null +++ b/src/executer_pico.py @@ -0,0 +1,138 @@ +from src.executer_base import BaseExecutor +import serial.tools.list_ports +from src import utilities, constants +from src.view.view_base import SettingsItem, MenuItem +from src.classes.settings import Settings +import struct + +KEY_DELAY = "pico_stratagemKeyDelay" +KEY_DELAY_DEFAULT = 30 +KEY_DELAY_JITTER = "pico_stratagemKeyDelayJitter" +KEY_DELAY_JITTER_DEFAULT = 20 +TRIGGER_DELAY = "pico_triggerKeyDelay" +TRIGGER_DELAY_DEFAULT = 100 +TRIGGER_DELAY_JITTER = "pico_triggerKeyDelayJitter" +TRIGGER_DELAY_JITTER_DEFAULT = 30 +KEY_LAST_CONNECTED = "pico_lastConnectedDevice" +KEY_LAST_CONNECTED_DEFAULT = None +KEY_AUTO_RECONNECT = "pico_autoReconnect" +KEY_AUTO_RECONNECT_DEFAULT = True + +class PicoPassthroughExecuter(BaseExecutor): + def __init__(self, controller): + super().__init__() + self.pico = None + self.settings = Settings.getInstance() + self.controller = controller + + def start(self): + if getattr(self.settings, KEY_AUTO_RECONNECT, KEY_AUTO_RECONNECT_DEFAULT): + self.attempt_auto_connect() + self.prepare() + + def stop(self): + if self.pico is not None: + self.pico.close() + self.pico = None + + def attempt_auto_connect(self): + last_connected = getattr(self.settings, KEY_LAST_CONNECTED, KEY_LAST_CONNECTED_DEFAULT) + if last_connected is not None: + ports = self.get_physical_addresses() + for port in ports: + id = str(port.vid)+"-"+str(port.pid) + if id == last_connected: + self.connect_to_pico(port) + return + + def connect_to_pico(self, port): + # Ensure any existing serial connection is properly closed before establishing a new one + if self.pico is not None: + self.pico.close() + self.pico = None + + self.pico = serial.Serial(port.device, baudrate=115200, timeout=.1) + self.controller.update_executor_menu() + setattr(self.settings, KEY_LAST_CONNECTED, str(port.vid) + "-" + str(port.pid)) + self.controller.update_title_description("Connected to: " + port.name) + + # TODO Send connection test message + + def on_macro_triggered(self, macro): + #Sending a negative number indicates that the key should be pressed but not released + bytesToSend = self.triggerKey.encode('utf-8') + int(utilities.getDelayWithJitterMs(self.triggerDelay, self.triggerDelayJitter)*-1).to_bytes(2, 'big', signed = True) # Trigger stratagem + for key in macro.commandArray: + bytesToSend += key.encode('utf-8') # Key press + bytesToSend += int(utilities.getDelayWithJitterMs(self.keyDelay, self.keyDelayJitter)).to_bytes(2,'big', signed = True) # Key press delay + bytesToSend += self.triggerKey.encode('utf-8') + int(utilities.getDelayWithJitterMs(self.triggerDelay, self.triggerDelayJitter)).to_bytes(2, 'big', signed = True) # Release trigger + + bytesToSend = bytes.fromhex("04") + + self.send_bytes(bytesToSend) + + def get_menu_items(self): + menu_items = [] + + select_serial = MenuItem("Select serial", None, None, constants.MENU_TYPE_MENU) + connection = self.get_current_connection() + physical_addresses = self.get_physical_addresses() + for port in sorted(physical_addresses): + if port.device == connection: + icon = constants.ICON_BASE_PATH+"serial_connected" + else: + icon = None + select_serial.children.append(MenuItem(port.description, icon, lambda checked, port=port: self.connect_to_pico(port), constants.MENU_TYPE_ACTION)) + + menu_items.append(select_serial) + + return menu_items + + def get_settings_items(self): + settings = [] + settings.append(SettingsItem("Pico passthrough settings", None, None, constants.SETTINGS_VALUE_TYPE_HEADER)) + settings.append(SettingsItem("Trigger delay", TRIGGER_DELAY_DEFAULT, TRIGGER_DELAY, constants.SETTINGS_VALUE_TYPE_INT)) + settings.append(SettingsItem("Trigger delay jitter", TRIGGER_DELAY_JITTER_DEFAULT, TRIGGER_DELAY_JITTER, constants.SETTINGS_VALUE_TYPE_INT)) + settings.append(SettingsItem("Stratagem key delay", KEY_DELAY_DEFAULT, KEY_DELAY, constants.SETTINGS_VALUE_TYPE_INT)) + settings.append(SettingsItem("Stratagem key delay jitter", KEY_DELAY_JITTER_DEFAULT, KEY_DELAY_JITTER, constants.SETTINGS_VALUE_TYPE_INT)) + settings.append(SettingsItem("Hardware Connection", None, None, constants.SETTINGS_VALUE_TYPE_HEADER)) + settings.append(SettingsItem("Auto re-connect to latest device", KEY_AUTO_RECONNECT_DEFAULT, KEY_AUTO_RECONNECT, constants.SETTINGS_VALUE_TYPE_BOOL)) + + return settings + + def prepare(self): + self.triggerKey = self.parse_macro_key(self.settings.triggerKey) + self.keyDelay = getattr(self.settings, KEY_DELAY, KEY_DELAY_DEFAULT) + self.keyDelayJitter = getattr(self.settings, KEY_DELAY_JITTER, KEY_DELAY_JITTER_DEFAULT) + self.triggerDelay = getattr(self.settings, TRIGGER_DELAY, TRIGGER_DELAY_DEFAULT) + self.triggerDelayJitter = getattr(self.settings, TRIGGER_DELAY_JITTER, TRIGGER_DELAY_JITTER_DEFAULT) + + def send_bytes(self, bytes): + print(bytes) + if self.pico is not None: + self.pico.write(bytes) + + def get_physical_addresses(self): + ports = serial.tools.list_ports.comports() + return ports + + def get_current_connection(self): + if self.pico is None: + return None + else: + return self.pico.port + + def parse_macro_key(self, key): + if key in self.key_map: + return self.key_map[key] + else: + return key + + key_map = { + "shift":"a", + "ctrl":"b", + "up":"c", + "down":"d", + "left":"e", + "right":"f", + "caps_lock":"g" + } \ No newline at end of file diff --git a/src/view/pyqt5/edit_config_dialog.py b/src/view/pyqt5/edit_config_dialog.py index 401f9dd..68bdb6c 100644 --- a/src/view/pyqt5/edit_config_dialog.py +++ b/src/view/pyqt5/edit_config_dialog.py @@ -164,7 +164,8 @@ def open_executor_selector_dialog(self): constants.EXECUTOR_PYNPUT: 'pynput', constants.EXECUTOR_PYAUTOGUI: 'pyautogui', constants.EXECUTOR_ARDUINO: 'Arduino passthrough', - constants.EXECUTOR_XDOTOOL: 'xdotool' + constants.EXECUTOR_XDOTOOL: 'xdotool', + constants.EXECUTOR_PICO: 'Pico passthrough' } dialog = DropdownDialog(items, self.change_selected_executor) From ad787ea508dcf42e83d59335ad9de3bdff563c12 Mon Sep 17 00:00:00 2001 From: Rune Haugaard Date: Tue, 25 Jun 2024 22:00:48 +0200 Subject: [PATCH 2/5] Add functional implementation of Pico passthrough The protocol is similar to the Arduino passthrough except it needs an explicit mapping for each key This includes all required libs for running the code on the Pico - Add pico /lib exception to Add functional implementation of Pico passthrough The protocol is similar to the Arduino passthrough except it needs an explicit mapping for each key This includes all required libs for running the code on the Pico - Add pico /lib exception to .gitignore Signed-off-by: Rune Haugaard --- .gitignore | 3 +- pico_passthrough/code.py | 41 ++- pico_passthrough/lib/adafruit_hid/__init__.py | 90 +++++ pico_passthrough/lib/adafruit_hid/keyboard.py | 207 ++++++++++++ pico_passthrough/lib/adafruit_hid/keycode.py | 307 ++++++++++++++++++ src/executer_pico.py | 64 +++- 6 files changed, 675 insertions(+), 37 deletions(-) create mode 100644 pico_passthrough/lib/adafruit_hid/__init__.py create mode 100644 pico_passthrough/lib/adafruit_hid/keyboard.py create mode 100644 pico_passthrough/lib/adafruit_hid/keycode.py diff --git a/.gitignore b/.gitignore index 97c8ce9..2b5673e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ #Hell snake specific ignores /settings.json +!/pico_passthrough/lib/** # Created by https://www.toptal.com/developers/gitignore/api/python # Edit at https://www.toptal.com/developers/gitignore?templates=python @@ -24,7 +25,7 @@ dist/ downloads/ eggs/ .eggs/ -lib/ +lib/* lib64/ parts/ sdist/ diff --git a/pico_passthrough/code.py b/pico_passthrough/code.py index c45db9a..79ad99f 100644 --- a/pico_passthrough/code.py +++ b/pico_passthrough/code.py @@ -1,6 +1,7 @@ import time import usb_cdc import usb_hid +import binascii from adafruit_hid.keyboard import Keyboard from adafruit_hid.keycode import Keycode @@ -10,29 +11,25 @@ # Initialize USB HID keyboard keyboard = Keyboard(usb_hid.devices) -def emulate_key_press(key_char): - # Convert the key_char to an appropriate keycode if needed - # This example assumes key_char is a string that maps directly to a key - if key_char.isalpha(): # For alphabetic characters - keycode = getattr(Keycode, key_char.upper()) - keyboard.press(keycode) - keyboard.release_all() +def unsigned_to_signed(unsigned_val): + # Ensure the input is within the range of a 2-byte unsigned integer + if not (0 <= unsigned_val <= 0xFFFF): + raise ValueError("Input should be a 2-byte unsigned integer (0 to 65535)") + + # If the value is greater than the maximum for a signed 2-byte integer, convert it + if unsigned_val > 0x7FFF: # 32767 in decimal + return unsigned_val - 0x10000 # 65536 in decimal else: - # For other characters, you'd need a mapping or handle accordingly - pass + return unsigned_val while True: - if serial.in_waiting >= 1: # Wait for at least 3 bytes (1 byte key + 2 bytes delay) - key_byte = serial.read(1) # Read 1 byte for the key - #print("bytes: "+ str(key_byte)) - key = key_byte.decode("utf-8") - print("key: "+key) + if serial.in_waiting >= 3: # Wait for at least 3 bytes (1 byte key + 2 bytes delay) + key_byte = int.from_bytes(serial.read(1), 'big') # Read 1 byte for the key + keyboard.press(int(key_byte)) + delay_bytes = serial.read(2) # Read 2 bytes for the delay - delay = int.from_bytes(delay_bytes, 'big') # Convert bytes to integer (big-endian) - - print(str(delay)) - # Emulate the key press - #emulate_key_press(key) - - # Wait for the specified delay - #time.sleep(delay / 1000) # Delay is in milliseconds + delay = unsigned_to_signed(int.from_bytes(delay_bytes, 'big')) # Convert bytes to signed integer (big-endian) + time.sleep(abs(delay)/1000) + + if delay >= 0: + keyboard.release(int(key_byte)) diff --git a/pico_passthrough/lib/adafruit_hid/__init__.py b/pico_passthrough/lib/adafruit_hid/__init__.py new file mode 100644 index 0000000..06c220d --- /dev/null +++ b/pico_passthrough/lib/adafruit_hid/__init__.py @@ -0,0 +1,90 @@ +# SPDX-FileCopyrightText: 2017 Scott Shawcroft for Adafruit Industries +# +# SPDX-License-Identifier: MIT + +""" +`adafruit_hid` +==================================================== + +This driver simulates USB HID devices. + +* Author(s): Scott Shawcroft, Dan Halbert + +Implementation Notes +-------------------- +**Software and Dependencies:** +* Adafruit CircuitPython firmware for the supported boards: + https://github.com/adafruit/circuitpython/releases +""" + +# imports +from __future__ import annotations +import time + +try: + import supervisor +except ImportError: + supervisor = None + +try: + from typing import Sequence +except ImportError: + pass + +# usb_hid may not exist on some boards that still provide BLE or other HID devices. +try: + from usb_hid import Device +except ImportError: + Device = None + +__version__ = "0.0.0+auto.0" +__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_HID.git" + + +def find_device( + devices: Sequence[object], + *, + usage_page: int, + usage: int, + timeout: int = None, +) -> object: + """Search through the provided sequence of devices to find the one with the matching + usage_page and usage. + + :param timeout: Time in seconds to wait for USB to become ready before timing out. + Defaults to None to wait indefinitely. + Ignored if device is not a `usb_hid.Device`; it might be BLE, for instance.""" + + if hasattr(devices, "send_report"): + devices = [devices] # type: ignore + device = None + for dev in devices: + if ( + dev.usage_page == usage_page + and dev.usage == usage + and hasattr(dev, "send_report") + ): + device = dev + break + if device is None: + raise ValueError("Could not find matching HID device.") + + # Wait for USB to be connected only if this is a usb_hid.Device. + if Device and isinstance(device, Device): + if supervisor is None: + # Blinka doesn't have supervisor (see issue Adafruit_Blinka#711), so wait + # one second for USB to become ready + time.sleep(1.0) + elif timeout is None: + # default behavior: wait indefinitely for USB to become ready + while not supervisor.runtime.usb_connected: + time.sleep(1.0) + else: + # wait up to timeout seconds for USB to become ready + for _ in range(timeout): + if supervisor.runtime.usb_connected: + return device + time.sleep(1.0) + raise OSError("Failed to initialize HID device. Is USB connected?") + + return device diff --git a/pico_passthrough/lib/adafruit_hid/keyboard.py b/pico_passthrough/lib/adafruit_hid/keyboard.py new file mode 100644 index 0000000..db7079e --- /dev/null +++ b/pico_passthrough/lib/adafruit_hid/keyboard.py @@ -0,0 +1,207 @@ +# SPDX-FileCopyrightText: 2017 Dan Halbert for Adafruit Industries +# +# SPDX-License-Identifier: MIT + +""" +`adafruit_hid.keyboard.Keyboard` +==================================================== + +* Author(s): Scott Shawcroft, Dan Halbert +""" + +from micropython import const +import usb_hid + +from .keycode import Keycode + +from . import find_device + +try: + from typing import Sequence +except: # pylint: disable=bare-except + pass + +_MAX_KEYPRESSES = const(6) + + +class Keyboard: + """Send HID keyboard reports.""" + + LED_NUM_LOCK = 0x01 + """LED Usage ID for Num Lock""" + LED_CAPS_LOCK = 0x02 + """LED Usage ID for Caps Lock""" + LED_SCROLL_LOCK = 0x04 + """LED Usage ID for Scroll Lock""" + LED_COMPOSE = 0x08 + """LED Usage ID for Compose""" + + # No more than _MAX_KEYPRESSES regular keys may be pressed at once. + + def __init__(self, devices: Sequence[usb_hid.Device], timeout: int = None) -> None: + """Create a Keyboard object that will send keyboard HID reports. + + :param timeout: Time in seconds to wait for USB to become ready before timing out. + Defaults to None to wait indefinitely. + + Devices can be a sequence of devices that includes a keyboard device or a keyboard device + itself. A device is any object that implements ``send_report()``, ``usage_page`` and + ``usage``. + """ + self._keyboard_device = find_device( + devices, usage_page=0x1, usage=0x06, timeout=timeout + ) + + # Reuse this bytearray to send keyboard reports. + self.report = bytearray(8) + + # report[0] modifiers + # report[1] unused + # report[2:8] regular key presses + + # View onto byte 0 in report. + self.report_modifier = memoryview(self.report)[0:1] + + # List of regular keys currently pressed. + # View onto bytes 2-7 in report. + self.report_keys = memoryview(self.report)[2:] + + # No keyboard LEDs on. + self._led_status = b"\x00" + + def press(self, *keycodes: int) -> None: + """Send a report indicating that the given keys have been pressed. + + :param keycodes: Press these keycodes all at once. + :raises ValueError: if more than six regular keys are pressed. + + Keycodes may be modifiers or regular keys. + No more than six regular keys may be pressed simultaneously. + + Examples:: + + from adafruit_hid.keycode import Keycode + + # Press ctrl-x. + kbd.press(Keycode.LEFT_CONTROL, Keycode.X) + + # Or, more conveniently, use the CONTROL alias for LEFT_CONTROL: + kbd.press(Keycode.CONTROL, Keycode.X) + + # Press a, b, c keys all at once. + kbd.press(Keycode.A, Keycode.B, Keycode.C) + """ + for keycode in keycodes: + self._add_keycode_to_report(keycode) + self._keyboard_device.send_report(self.report) + + def release(self, *keycodes: int) -> None: + """Send a USB HID report indicating that the given keys have been released. + + :param keycodes: Release these keycodes all at once. + + If a keycode to be released was not pressed, it is ignored. + + Example:: + + # release SHIFT key + kbd.release(Keycode.SHIFT) + """ + for keycode in keycodes: + self._remove_keycode_from_report(keycode) + self._keyboard_device.send_report(self.report) + + def release_all(self) -> None: + """Release all pressed keys.""" + for i in range(8): + self.report[i] = 0 + self._keyboard_device.send_report(self.report) + + def send(self, *keycodes: int) -> None: + """Press the given keycodes and then release all pressed keys. + + :param keycodes: keycodes to send together + """ + self.press(*keycodes) + self.release_all() + + def _add_keycode_to_report(self, keycode: int) -> None: + """Add a single keycode to the USB HID report.""" + modifier = Keycode.modifier_bit(keycode) + if modifier: + # Set bit for this modifier. + self.report_modifier[0] |= modifier + else: + report_keys = self.report_keys + # Don't press twice. + for i in range(_MAX_KEYPRESSES): + report_key = report_keys[i] + if report_key == 0: + # Put keycode in first empty slot. Since the report_keys + # are compact and unique, this is not a repeated key + report_keys[i] = keycode + return + if report_key == keycode: + # Already pressed. + return + # All slots are filled. Shuffle down and reuse last slot + for i in range(_MAX_KEYPRESSES - 1): + report_keys[i] = report_keys[i + 1] + report_keys[-1] = keycode + + def _remove_keycode_from_report(self, keycode: int) -> None: + """Remove a single keycode from the report.""" + modifier = Keycode.modifier_bit(keycode) + if modifier: + # Turn off the bit for this modifier. + self.report_modifier[0] &= ~modifier + else: + report_keys = self.report_keys + # Clear the at most one matching slot and move remaining keys down + j = 0 + for i in range(_MAX_KEYPRESSES): + pressed = report_keys[i] + if not pressed: + break # Handled all used report slots + if pressed == keycode: + continue # Remove this entry + if i != j: + report_keys[j] = report_keys[i] + j += 1 + # Clear any remaining slots + while j < _MAX_KEYPRESSES and report_keys[j]: + report_keys[j] = 0 + j += 1 + + @property + def led_status(self) -> bytes: + """Returns the last received report""" + # get_last_received_report() returns None when nothing was received + led_report = self._keyboard_device.get_last_received_report() + if led_report is not None: + self._led_status = led_report + return self._led_status + + def led_on(self, led_code: int) -> bool: + """Returns whether an LED is on based on the led code + + Examples:: + + import usb_hid + from adafruit_hid.keyboard import Keyboard + from adafruit_hid.keycode import Keycode + import time + + # Initialize Keyboard + kbd = Keyboard(usb_hid.devices) + + # Press and release CapsLock. + kbd.press(Keycode.CAPS_LOCK) + time.sleep(.09) + kbd.release(Keycode.CAPS_LOCK) + + # Check status of the LED_CAPS_LOCK + print(kbd.led_on(Keyboard.LED_CAPS_LOCK)) + + """ + return bool(self.led_status[0] & led_code) diff --git a/pico_passthrough/lib/adafruit_hid/keycode.py b/pico_passthrough/lib/adafruit_hid/keycode.py new file mode 100644 index 0000000..12bc7b8 --- /dev/null +++ b/pico_passthrough/lib/adafruit_hid/keycode.py @@ -0,0 +1,307 @@ +# SPDX-FileCopyrightText: 2017 Scott Shawcroft for Adafruit Industries +# +# SPDX-License-Identifier: MIT + +""" +`adafruit_hid.keycode.Keycode` +==================================================== + +* Author(s): Scott Shawcroft, Dan Halbert +""" + + +class Keycode: + """USB HID Keycode constants. + + This list is modeled after the names for USB keycodes defined in + https://usb.org/sites/default/files/hut1_21_0.pdf#page=83. + This list does not include every single code, but does include all the keys on + a regular PC or Mac keyboard. + + Remember that keycodes are the names for key *positions* on a US keyboard, and may + not correspond to the character that you mean to send if you want to emulate non-US keyboard. + For instance, on a French keyboard (AZERTY instead of QWERTY), + the keycode for 'q' is used to indicate an 'a'. Likewise, 'y' represents 'z' on + a German keyboard. This is historical: the idea was that the keycaps could be changed + without changing the keycodes sent, so that different firmware was not needed for + different variations of a keyboard. + """ + + # pylint: disable-msg=invalid-name + A = 0x04 + """``a`` and ``A``""" + B = 0x05 + """``b`` and ``B``""" + C = 0x06 + """``c`` and ``C``""" + D = 0x07 + """``d`` and ``D``""" + E = 0x08 + """``e`` and ``E``""" + F = 0x09 + """``f`` and ``F``""" + G = 0x0A + """``g`` and ``G``""" + H = 0x0B + """``h`` and ``H``""" + I = 0x0C + """``i`` and ``I``""" + J = 0x0D + """``j`` and ``J``""" + K = 0x0E + """``k`` and ``K``""" + L = 0x0F + """``l`` and ``L``""" + M = 0x10 + """``m`` and ``M``""" + N = 0x11 + """``n`` and ``N``""" + O = 0x12 + """``o`` and ``O``""" + P = 0x13 + """``p`` and ``P``""" + Q = 0x14 + """``q`` and ``Q``""" + R = 0x15 + """``r`` and ``R``""" + S = 0x16 + """``s`` and ``S``""" + T = 0x17 + """``t`` and ``T``""" + U = 0x18 + """``u`` and ``U``""" + V = 0x19 + """``v`` and ``V``""" + W = 0x1A + """``w`` and ``W``""" + X = 0x1B + """``x`` and ``X``""" + Y = 0x1C + """``y`` and ``Y``""" + Z = 0x1D + """``z`` and ``Z``""" + + ONE = 0x1E + """``1`` and ``!``""" + TWO = 0x1F + """``2`` and ``@``""" + THREE = 0x20 + """``3`` and ``#``""" + FOUR = 0x21 + """``4`` and ``$``""" + FIVE = 0x22 + """``5`` and ``%``""" + SIX = 0x23 + """``6`` and ``^``""" + SEVEN = 0x24 + """``7`` and ``&``""" + EIGHT = 0x25 + """``8`` and ``*``""" + NINE = 0x26 + """``9`` and ``(``""" + ZERO = 0x27 + """``0`` and ``)``""" + ENTER = 0x28 + """Enter (Return)""" + RETURN = ENTER + """Alias for ``ENTER``""" + ESCAPE = 0x29 + """Escape""" + BACKSPACE = 0x2A + """Delete backward (Backspace)""" + TAB = 0x2B + """Tab and Backtab""" + SPACEBAR = 0x2C + """Spacebar""" + SPACE = SPACEBAR + """Alias for SPACEBAR""" + MINUS = 0x2D + """``-` and ``_``""" + EQUALS = 0x2E + """``=` and ``+``""" + LEFT_BRACKET = 0x2F + """``[`` and ``{``""" + RIGHT_BRACKET = 0x30 + """``]`` and ``}``""" + BACKSLASH = 0x31 + r"""``\`` and ``|``""" + POUND = 0x32 + """``#`` and ``~`` (Non-US keyboard)""" + SEMICOLON = 0x33 + """``;`` and ``:``""" + QUOTE = 0x34 + """``'`` and ``"``""" + GRAVE_ACCENT = 0x35 + r""":literal:`\`` and ``~``""" + COMMA = 0x36 + """``,`` and ``<``""" + PERIOD = 0x37 + """``.`` and ``>``""" + FORWARD_SLASH = 0x38 + """``/`` and ``?``""" + + CAPS_LOCK = 0x39 + """Caps Lock""" + + F1 = 0x3A + """Function key F1""" + F2 = 0x3B + """Function key F2""" + F3 = 0x3C + """Function key F3""" + F4 = 0x3D + """Function key F4""" + F5 = 0x3E + """Function key F5""" + F6 = 0x3F + """Function key F6""" + F7 = 0x40 + """Function key F7""" + F8 = 0x41 + """Function key F8""" + F9 = 0x42 + """Function key F9""" + F10 = 0x43 + """Function key F10""" + F11 = 0x44 + """Function key F11""" + F12 = 0x45 + """Function key F12""" + + PRINT_SCREEN = 0x46 + """Print Screen (SysRq)""" + SCROLL_LOCK = 0x47 + """Scroll Lock""" + PAUSE = 0x48 + """Pause (Break)""" + + INSERT = 0x49 + """Insert""" + HOME = 0x4A + """Home (often moves to beginning of line)""" + PAGE_UP = 0x4B + """Go back one page""" + DELETE = 0x4C + """Delete forward""" + END = 0x4D + """End (often moves to end of line)""" + PAGE_DOWN = 0x4E + """Go forward one page""" + + RIGHT_ARROW = 0x4F + """Move the cursor right""" + LEFT_ARROW = 0x50 + """Move the cursor left""" + DOWN_ARROW = 0x51 + """Move the cursor down""" + UP_ARROW = 0x52 + """Move the cursor up""" + + KEYPAD_NUMLOCK = 0x53 + """Num Lock (Clear on Mac)""" + KEYPAD_FORWARD_SLASH = 0x54 + """Keypad ``/``""" + KEYPAD_ASTERISK = 0x55 + """Keypad ``*``""" + KEYPAD_MINUS = 0x56 + """Keyapd ``-``""" + KEYPAD_PLUS = 0x57 + """Keypad ``+``""" + KEYPAD_ENTER = 0x58 + """Keypad Enter""" + KEYPAD_ONE = 0x59 + """Keypad ``1`` and End""" + KEYPAD_TWO = 0x5A + """Keypad ``2`` and Down Arrow""" + KEYPAD_THREE = 0x5B + """Keypad ``3`` and PgDn""" + KEYPAD_FOUR = 0x5C + """Keypad ``4`` and Left Arrow""" + KEYPAD_FIVE = 0x5D + """Keypad ``5``""" + KEYPAD_SIX = 0x5E + """Keypad ``6`` and Right Arrow""" + KEYPAD_SEVEN = 0x5F + """Keypad ``7`` and Home""" + KEYPAD_EIGHT = 0x60 + """Keypad ``8`` and Up Arrow""" + KEYPAD_NINE = 0x61 + """Keypad ``9`` and PgUp""" + KEYPAD_ZERO = 0x62 + """Keypad ``0`` and Ins""" + KEYPAD_PERIOD = 0x63 + """Keypad ``.`` and Del""" + KEYPAD_BACKSLASH = 0x64 + """Keypad ``\\`` and ``|`` (Non-US)""" + + APPLICATION = 0x65 + """Application: also known as the Menu key (Windows)""" + POWER = 0x66 + """Power (Mac)""" + KEYPAD_EQUALS = 0x67 + """Keypad ``=`` (Mac)""" + F13 = 0x68 + """Function key F13 (Mac)""" + F14 = 0x69 + """Function key F14 (Mac)""" + F15 = 0x6A + """Function key F15 (Mac)""" + F16 = 0x6B + """Function key F16 (Mac)""" + F17 = 0x6C + """Function key F17 (Mac)""" + F18 = 0x6D + """Function key F18 (Mac)""" + F19 = 0x6E + """Function key F19 (Mac)""" + + F20 = 0x6F + """Function key F20""" + F21 = 0x70 + """Function key F21""" + F22 = 0x71 + """Function key F22""" + F23 = 0x72 + """Function key F23""" + F24 = 0x73 + """Function key F24""" + + LEFT_CONTROL = 0xE0 + """Control modifier left of the spacebar""" + CONTROL = LEFT_CONTROL + """Alias for LEFT_CONTROL""" + LEFT_SHIFT = 0xE1 + """Shift modifier left of the spacebar""" + SHIFT = LEFT_SHIFT + """Alias for LEFT_SHIFT""" + LEFT_ALT = 0xE2 + """Alt modifier left of the spacebar""" + ALT = LEFT_ALT + """Alias for LEFT_ALT; Alt is also known as Option (Mac)""" + OPTION = ALT + """Labeled as Option on some Mac keyboards""" + LEFT_GUI = 0xE3 + """GUI modifier left of the spacebar""" + GUI = LEFT_GUI + """Alias for LEFT_GUI; GUI is also known as the Windows key, Command (Mac), or Meta""" + WINDOWS = GUI + """Labeled with a Windows logo on Windows keyboards""" + COMMAND = GUI + """Labeled as Command on Mac keyboards, with a clover glyph""" + RIGHT_CONTROL = 0xE4 + """Control modifier right of the spacebar""" + RIGHT_SHIFT = 0xE5 + """Shift modifier right of the spacebar""" + RIGHT_ALT = 0xE6 + """Alt modifier right of the spacebar""" + RIGHT_GUI = 0xE7 + """GUI modifier right of the spacebar""" + + # pylint: enable-msg=invalid-name + @classmethod + def modifier_bit(cls, keycode: int) -> int: + """Return the modifer bit to be set in an HID keycode report if this is a + modifier key; otherwise return 0.""" + return ( + 1 << (keycode - 0xE0) if cls.LEFT_CONTROL <= keycode <= cls.RIGHT_GUI else 0 + ) diff --git a/src/executer_pico.py b/src/executer_pico.py index 624d27a..93e83e4 100644 --- a/src/executer_pico.py +++ b/src/executer_pico.py @@ -60,13 +60,11 @@ def connect_to_pico(self, port): def on_macro_triggered(self, macro): #Sending a negative number indicates that the key should be pressed but not released - bytesToSend = self.triggerKey.encode('utf-8') + int(utilities.getDelayWithJitterMs(self.triggerDelay, self.triggerDelayJitter)*-1).to_bytes(2, 'big', signed = True) # Trigger stratagem + bytesToSend = bytes.fromhex(self.triggerKey) + int(utilities.getDelayWithJitterMs(self.triggerDelay, self.triggerDelayJitter)*-1).to_bytes(2, 'big', signed = True) # Trigger stratagem for key in macro.commandArray: - bytesToSend += key.encode('utf-8') # Key press + bytesToSend += bytes.fromhex(self.parse_macro_key(key)) # Key press bytesToSend += int(utilities.getDelayWithJitterMs(self.keyDelay, self.keyDelayJitter)).to_bytes(2,'big', signed = True) # Key press delay - bytesToSend += self.triggerKey.encode('utf-8') + int(utilities.getDelayWithJitterMs(self.triggerDelay, self.triggerDelayJitter)).to_bytes(2, 'big', signed = True) # Release trigger - - bytesToSend = bytes.fromhex("04") + bytesToSend += bytes.fromhex(self.triggerKey) + int(utilities.getDelayWithJitterMs(self.triggerDelay, self.triggerDelayJitter)).to_bytes(2, 'big', signed = True) # Release trigger self.send_bytes(bytesToSend) @@ -107,9 +105,11 @@ def prepare(self): self.triggerDelayJitter = getattr(self.settings, TRIGGER_DELAY_JITTER, TRIGGER_DELAY_JITTER_DEFAULT) def send_bytes(self, bytes): - print(bytes) if self.pico is not None: self.pico.write(bytes) + + def parse_to_hex(self, key): + return hex(ord(key))[2:] def get_physical_addresses(self): ports = serial.tools.list_ports.comports() @@ -125,14 +125,50 @@ def parse_macro_key(self, key): if key in self.key_map: return self.key_map[key] else: - return key + return self.parse_to_hex(key) key_map = { - "shift":"a", - "ctrl":"b", - "up":"c", - "down":"d", - "left":"e", - "right":"f", - "caps_lock":"g" + "a":"04", + "b":"05", + "c":"06", + "d":"07", + "e":"08", + "f":"09", + "g":"0A", + "h":"0B", + "i":"0C", + "j":"0D", + "k":"0E", + "l":"0F", + "m":"10", + "n":"11", + "o":"12", + "p":"13", + "q":"14", + "r":"15", + "s":"16", + "t":"17", + "u":"18", + "v":"19", + "w":"1A", + "x":"1B", + "y":"1C", + "z":"1D", + "1":"1E", + "2":"1F", + "3":"20", + "4":"21", + "5":"22", + "6":"23", + "7":"24", + "8":"25", + "9":"26", + "0":"27", + "shift":"E1", + "ctrl":"E0", + "up":"52", + "down":"51", + "left":"50", + "right":"4F", + "caps_lock":"39" } \ No newline at end of file From acfad2027ce3fdf79d2092b40cff81196a349128 Mon Sep 17 00:00:00 2001 From: Rune Haugaard Date: Thu, 27 Jun 2024 16:08:17 +0200 Subject: [PATCH 3/5] Move shared serial code to executor helper class - Fix issue with double encoding - Add exception for pico executor when attempting to use unsupported key - Remove serial devices from list that doesn't have a pid or vid - Remove redundant code Signed-off-by: Rune Haugaard --- src/executer_arduino.py | 10 +++------- src/executer_pico.py | 18 ++++++------------ src/executer_utilities.py | 8 ++++++++ 3 files changed, 17 insertions(+), 19 deletions(-) create mode 100644 src/executer_utilities.py diff --git a/src/executer_arduino.py b/src/executer_arduino.py index bea742b..c529648 100644 --- a/src/executer_arduino.py +++ b/src/executer_arduino.py @@ -1,9 +1,9 @@ from src.executer_base import BaseExecutor -import serial.tools.list_ports from src import utilities, constants from src.view.view_base import SettingsItem, MenuItem from src.classes.settings import Settings import struct +from src.executer_utilities import get_physical_addresses KEY_DELAY = "arduino_stratagemKeyDelay" KEY_DELAY_DEFAULT = 30 @@ -37,7 +37,7 @@ def stop(self): def attempt_auto_connect(self): if getattr(self.settings, KEY_LAST_CONNECTED, KEY_LAST_CONNECTED_DEFAULT) is not None: - ports = self.get_physical_addresses() + ports = get_physical_addresses() for port in ports: id = str(port.vid)+"-"+str(port.pid) if id == self.settings.arduino_lastConnectedDevice: @@ -72,7 +72,7 @@ def get_menu_items(self): select_serial = MenuItem("Select serial", None, None, constants.MENU_TYPE_MENU) connection = self.get_current_connection() - physical_addresses = self.get_physical_addresses() + physical_addresses = get_physical_addresses() for port in sorted(physical_addresses): if port.device == connection: icon = constants.ICON_BASE_PATH+"serial_connected" @@ -122,10 +122,6 @@ def delay_to_hex(self, delay): hex_representation = packed_delay.hex() return hex_representation - def get_physical_addresses(self): - ports = serial.tools.list_ports.comports() - return ports - def get_current_connection(self): if self.arduino is None: return None diff --git a/src/executer_pico.py b/src/executer_pico.py index 93e83e4..34a68a4 100644 --- a/src/executer_pico.py +++ b/src/executer_pico.py @@ -1,9 +1,9 @@ from src.executer_base import BaseExecutor -import serial.tools.list_ports from src import utilities, constants from src.view.view_base import SettingsItem, MenuItem from src.classes.settings import Settings import struct +from src.executer_utilities import get_physical_addresses KEY_DELAY = "pico_stratagemKeyDelay" KEY_DELAY_DEFAULT = 30 @@ -38,7 +38,7 @@ def stop(self): def attempt_auto_connect(self): last_connected = getattr(self.settings, KEY_LAST_CONNECTED, KEY_LAST_CONNECTED_DEFAULT) if last_connected is not None: - ports = self.get_physical_addresses() + ports = get_physical_addresses() for port in ports: id = str(port.vid)+"-"+str(port.pid) if id == last_connected: @@ -62,7 +62,7 @@ def on_macro_triggered(self, macro): #Sending a negative number indicates that the key should be pressed but not released bytesToSend = bytes.fromhex(self.triggerKey) + int(utilities.getDelayWithJitterMs(self.triggerDelay, self.triggerDelayJitter)*-1).to_bytes(2, 'big', signed = True) # Trigger stratagem for key in macro.commandArray: - bytesToSend += bytes.fromhex(self.parse_macro_key(key)) # Key press + bytesToSend += bytes.fromhex(key) # Key press bytesToSend += int(utilities.getDelayWithJitterMs(self.keyDelay, self.keyDelayJitter)).to_bytes(2,'big', signed = True) # Key press delay bytesToSend += bytes.fromhex(self.triggerKey) + int(utilities.getDelayWithJitterMs(self.triggerDelay, self.triggerDelayJitter)).to_bytes(2, 'big', signed = True) # Release trigger @@ -73,7 +73,7 @@ def get_menu_items(self): select_serial = MenuItem("Select serial", None, None, constants.MENU_TYPE_MENU) connection = self.get_current_connection() - physical_addresses = self.get_physical_addresses() + physical_addresses = get_physical_addresses() for port in sorted(physical_addresses): if port.device == connection: icon = constants.ICON_BASE_PATH+"serial_connected" @@ -108,13 +108,6 @@ def send_bytes(self, bytes): if self.pico is not None: self.pico.write(bytes) - def parse_to_hex(self, key): - return hex(ord(key))[2:] - - def get_physical_addresses(self): - ports = serial.tools.list_ports.comports() - return ports - def get_current_connection(self): if self.pico is None: return None @@ -125,7 +118,8 @@ def parse_macro_key(self, key): if key in self.key_map: return self.key_map[key] else: - return self.parse_to_hex(key) + print("Does not support: "+str(key)) + raise KeyError key_map = { "a":"04", diff --git a/src/executer_utilities.py b/src/executer_utilities.py new file mode 100644 index 0000000..4f8db28 --- /dev/null +++ b/src/executer_utilities.py @@ -0,0 +1,8 @@ +import serial.tools.list_ports + +def get_physical_addresses(): + ports = [] + for port in serial.tools.list_ports.comports(): + if port.vid != None and port.pid != None: + ports.append(port) + return ports \ No newline at end of file From 2c85d1320c5a51d59f81f0b37e7b4cee40e27de3 Mon Sep 17 00:00:00 2001 From: Rune Haugaard Date: Thu, 27 Jun 2024 16:18:09 +0200 Subject: [PATCH 4/5] Fix issue after merge with main Signed-off-by: Rune Haugaard --- src/executer_arduino.py | 1 - src/executer_pico.py | 5 +++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/executer_arduino.py b/src/executer_arduino.py index ac4fa3f..8f249c8 100644 --- a/src/executer_arduino.py +++ b/src/executer_arduino.py @@ -1,4 +1,3 @@ -import serial.tools.list_ports import struct import constants import utilities diff --git a/src/executer_pico.py b/src/executer_pico.py index 34a68a4..4314f51 100644 --- a/src/executer_pico.py +++ b/src/executer_pico.py @@ -1,7 +1,8 @@ from src.executer_base import BaseExecutor -from src import utilities, constants +import utilities +import constants from src.view.view_base import SettingsItem, MenuItem -from src.classes.settings import Settings +from src.settings import Settings import struct from src.executer_utilities import get_physical_addresses From 5dad26b9534ff144ca9696a91e1c3d598338cafa Mon Sep 17 00:00:00 2001 From: Rune Haugaard Date: Sat, 29 Jun 2024 00:23:38 +0200 Subject: [PATCH 5/5] Fix missing import issue. Signed-off-by: Rune Haugaard --- src/executer_arduino.py | 1 + src/executer_pico.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/executer_arduino.py b/src/executer_arduino.py index 8f249c8..f1320f1 100644 --- a/src/executer_arduino.py +++ b/src/executer_arduino.py @@ -1,6 +1,7 @@ import struct import constants import utilities +import serial from src.executer_base import BaseExecutor from src.view.view_base import SettingsItem, MenuItem from src.settings import Settings diff --git a/src/executer_pico.py b/src/executer_pico.py index 4314f51..5c17b33 100644 --- a/src/executer_pico.py +++ b/src/executer_pico.py @@ -5,6 +5,7 @@ from src.settings import Settings import struct from src.executer_utilities import get_physical_addresses +import serial KEY_DELAY = "pico_stratagemKeyDelay" KEY_DELAY_DEFAULT = 30 @@ -115,7 +116,7 @@ def get_current_connection(self): else: return self.pico.port - def parse_macro_key(self, key): + def parse_macro_key(self, key): if key in self.key_map: return self.key_map[key] else: