diff --git a/fw/adafruit_hid/__init__.py b/fw/adafruit_hid/__init__.py new file mode 100644 index 0000000..8452d3f --- /dev/null +++ b/fw/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__ = "6.1.1" +__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/fw/adafruit_hid/consumer_control.py b/fw/adafruit_hid/consumer_control.py new file mode 100644 index 0000000..535fae1 --- /dev/null +++ b/fw/adafruit_hid/consumer_control.py @@ -0,0 +1,102 @@ +# SPDX-FileCopyrightText: 2018 Dan Halbert for Adafruit Industries +# +# SPDX-License-Identifier: MIT + +""" +`adafruit_hid.consumer_control.ConsumerControl` +==================================================== + +* Author(s): Dan Halbert +""" + +import sys + +if sys.implementation.version[0] < 3: + raise ImportError( + "{0} is not supported in CircuitPython 2.x or lower".format(__name__) + ) + +# pylint: disable=wrong-import-position +import struct +from . import find_device + +try: + from typing import Sequence + import usb_hid +except ImportError: + pass + + +class ConsumerControl: + """Send ConsumerControl code reports, used by multimedia keyboards, remote controls, etc.""" + + def __init__(self, devices: Sequence[usb_hid.Device], timeout: int = None) -> None: + """Create a ConsumerControl object that will send Consumer Control Device 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 Consumer Control device or a CC device + itself. A device is any object that implements ``send_report()``, ``usage_page`` and + ``usage``. + """ + self._consumer_device = find_device( + devices, usage_page=0x0C, usage=0x01, timeout=timeout + ) + + # Reuse this bytearray to send consumer reports. + self._report = bytearray(2) + + def send(self, consumer_code: int) -> None: + """Send a report to do the specified consumer control action, + and then stop the action (so it will not repeat). + + :param consumer_code: a 16-bit consumer control code. + + Examples:: + + from adafruit_hid.consumer_control_code import ConsumerControlCode + + # Raise volume. + consumer_control.send(ConsumerControlCode.VOLUME_INCREMENT) + + # Advance to next track (song). + consumer_control.send(ConsumerControlCode.SCAN_NEXT_TRACK) + """ + self.press(consumer_code) + self.release() + + def press(self, consumer_code: int) -> None: + """Send a report to indicate that the given key has been pressed. + Only one consumer control action can be pressed at a time, so any one + that was previously pressed will be released. + + :param consumer_code: a 16-bit consumer control code. + + Examples:: + + from adafruit_hid.consumer_control_code import ConsumerControlCode + + # Raise volume for 0.5 seconds + consumer_control.press(ConsumerControlCode.VOLUME_INCREMENT) + time.sleep(0.5) + consumer_control.release() + """ + struct.pack_into(" None: + """Send a report indicating that the consumer control key has been + released. Only one consumer control key can be pressed at a time. + + Examples:: + + from adafruit_hid.consumer_control_code import ConsumerControlCode + + # Raise volume for 0.5 seconds + consumer_control.press(ConsumerControlCode.VOLUME_INCREMENT) + time.sleep(0.5) + consumer_control.release() + """ + self._report[0] = self._report[1] = 0x0 + self._consumer_device.send_report(self._report) diff --git a/fw/adafruit_hid/consumer_control_code.py b/fw/adafruit_hid/consumer_control_code.py new file mode 100644 index 0000000..e49137e --- /dev/null +++ b/fw/adafruit_hid/consumer_control_code.py @@ -0,0 +1,47 @@ +# SPDX-FileCopyrightText: 2018 Dan Halbert for Adafruit Industries +# +# SPDX-License-Identifier: MIT + +""" +`adafruit_hid.consumer_control_code.ConsumerControlCode` +======================================================== + +* Author(s): Dan Halbert +""" + + +class ConsumerControlCode: + """USB HID Consumer Control Device constants. + + This list includes a few common consumer control codes from + https://www.usb.org/sites/default/files/hut1_21_0.pdf#page=118. + """ + + # pylint: disable-msg=too-few-public-methods + + RECORD = 0xB2 + """Record""" + FAST_FORWARD = 0xB3 + """Fast Forward""" + REWIND = 0xB4 + """Rewind""" + SCAN_NEXT_TRACK = 0xB5 + """Skip to next track""" + SCAN_PREVIOUS_TRACK = 0xB6 + """Go back to previous track""" + STOP = 0xB7 + """Stop""" + EJECT = 0xB8 + """Eject""" + PLAY_PAUSE = 0xCD + """Play/Pause toggle""" + MUTE = 0xE2 + """Mute""" + VOLUME_DECREMENT = 0xEA + """Decrease volume""" + VOLUME_INCREMENT = 0xE9 + """Increase volume""" + BRIGHTNESS_DECREMENT = 0x70 + """Decrease Brightness""" + BRIGHTNESS_INCREMENT = 0x6F + """Increase Brightness""" diff --git a/fw/adafruit_hid/keyboard.py b/fw/adafruit_hid/keyboard.py new file mode 100644 index 0000000..db7079e --- /dev/null +++ b/fw/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/fw/adafruit_hid/keyboard_layout_base.py b/fw/adafruit_hid/keyboard_layout_base.py new file mode 100644 index 0000000..984e5f3 --- /dev/null +++ b/fw/adafruit_hid/keyboard_layout_base.py @@ -0,0 +1,191 @@ +# SPDX-FileCopyrightText: 2017 Dan Halbert for Adafruit Industries +# +# SPDX-License-Identifier: MIT + +""" +`adafruit_hid.keyboard_layout_base.KeyboardLayoutBase` +======================================================= + +* Author(s): Dan Halbert, AngainorDev, Neradoc +""" + + +try: + from typing import Tuple + from .keyboard import Keyboard +except ImportError: + pass + +from time import sleep + +__version__ = "6.1.1" +__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_HID.git" + + +class KeyboardLayoutBase: + """Base class for keyboard layouts. Uses the tables defined in the subclass + to map UTF-8 characters to appropriate keypresses. + + Non-supported characters and most control characters will raise an exception. + """ + + SHIFT_FLAG = 0x80 + """Bit set in any keycode byte if the shift key is required for the character.""" + ALTGR_FLAG = 0x80 + """Bit set in the combined keys table if altgr is required for the first key.""" + SHIFT_CODE = 0xE1 + """The SHIFT keycode, to avoid dependency to the Keycode class.""" + RIGHT_ALT_CODE = 0xE6 + """The ALTGR keycode, to avoid dependency to the Keycode class.""" + ASCII_TO_KEYCODE = () + """Bytes string of keycodes for low ASCII characters, indexed by the ASCII value. + Keycodes use the `SHIFT_FLAG` if needed. + Dead keys are excluded by assigning the keycode 0.""" + HIGHER_ASCII = {} + """Dictionary that associates the ord() int value of high ascii and utf8 characters + to their keycode. Keycodes use the `SHIFT_FLAG` if needed.""" + NEED_ALTGR = "" + """Characters in `ASCII_TO_KEYCODE` and `HIGHER_ASCII` that need + the ALTGR key pressed to type.""" + COMBINED_KEYS = {} + """ + Dictionary of characters (indexed by ord() value) that can be accessed by typing first + a dead key followed by a regular key, like ``ñ`` as ``~ + n``. The value is a 2-bytes int: + the high byte is the dead-key keycode (including SHIFT_FLAG), the low byte is the ascii code + of the second character, with ALTGR_FLAG set if the dead key (the first key) needs ALTGR. + + The combined-key codes bits are: ``0b SDDD DDDD AKKK KKKK``: + ``S`` is the shift flag for the **first** key, + ``DDD DDDD`` is the keycode for the **first** key, + ``A`` is the altgr flag for the **first** key, + ``KKK KKKK`` is the (low) ASCII code for the second character. + """ + + def __init__(self, keyboard: Keyboard) -> None: + """Specify the layout for the given keyboard. + + :param keyboard: a Keyboard object. Write characters to this keyboard when requested. + + Example:: + + kbd = Keyboard(usb_hid.devices) + layout = KeyboardLayout(kbd) + """ + self.keyboard = keyboard + + def _write(self, keycode: int, altgr: bool = False) -> None: + """Type a key combination based on shift bit and altgr bool + + :param keycode: int value of the keycode, with the shift bit. + :param altgr: bool indicating if the altgr key should be pressed too. + """ + # Add altgr modifier if needed + if altgr: + self.keyboard.press(self.RIGHT_ALT_CODE) + # If this is a shifted char, clear the SHIFT flag and press the SHIFT key. + if keycode & self.SHIFT_FLAG: + keycode &= ~self.SHIFT_FLAG + self.keyboard.press(self.SHIFT_CODE) + self.keyboard.press(keycode) + self.keyboard.release_all() + + def write(self, string: str, delay: float = None) -> None: + """Type the string by pressing and releasing keys on my keyboard. + + :param string: A string of UTF-8 characters to convert to key presses and send. + :param float delay: Optional delay in seconds between key presses. + :raises ValueError: if any of the characters has no keycode + (such as some control characters). + + Example:: + + # Write abc followed by Enter to the keyboard + layout.write('abc\\n') + """ + for char in string: + # find easy ones first + keycode = self._char_to_keycode(char) + if keycode > 0: + self._write(keycode, char in self.NEED_ALTGR) + # find combined keys + elif ord(char) in self.COMBINED_KEYS: + # first key (including shift bit) + cchar = self.COMBINED_KEYS[ord(char)] + self._write(cchar >> 8, cchar & self.ALTGR_FLAG) + # second key (removing the altgr bit) + char = chr(cchar & 0xFF & (~self.ALTGR_FLAG)) + keycode = self._char_to_keycode(char) + # assume no altgr needed for second key + self._write(keycode, False) + else: + raise ValueError( + "No keycode available for character {letter} ({num}/0x{num:02x}).".format( + letter=repr(char), num=ord(char) + ) + ) + + if delay is not None: + sleep(delay) + + def keycodes(self, char: str) -> Tuple[int, ...]: + """Return a tuple of keycodes needed to type the given character. + + :param char: A single UTF8 character in a string. + :type char: str of length one. + :returns: tuple of Keycode keycodes. + :raises ValueError: if there is no keycode for ``char``. + + Examples:: + + # Returns (Keycode.TAB,) + keycodes('\t') + # Returns (Keycode.A,) + keycode('a') + # Returns (Keycode.SHIFT, Keycode.A) + keycode('A') + # Raises ValueError with a US layout because it's an unknown character + keycode('é') + """ + keycode = self._char_to_keycode(char) + if keycode == 0: + raise ValueError( + "No keycode available for character {letter} ({num}/0x{num:02x}).".format( + letter=repr(char), num=ord(char) + ) + ) + + codes = [] + if char in self.NEED_ALTGR: + codes.append(self.RIGHT_ALT_CODE) + if keycode & self.SHIFT_FLAG: + codes.extend((self.SHIFT_CODE, keycode & ~self.SHIFT_FLAG)) + else: + codes.append(keycode) + + return codes + + def _above128char_to_keycode(self, char: str) -> int: + """Return keycode for above 128 utf8 codes. + + A character can be indexed by the char itself or its int ord() value. + + :param char_val: char value + :return: keycode, with modifiers if needed + """ + if ord(char) in self.HIGHER_ASCII: + return self.HIGHER_ASCII[ord(char)] + if char in self.HIGHER_ASCII: + return self.HIGHER_ASCII[char] + return 0 + + def _char_to_keycode(self, char: str) -> int: + """Return the HID keycode for the given character, with the SHIFT_FLAG possibly set. + + If the character requires pressing the Shift key, the SHIFT_FLAG bit is set. + You must clear this bit before passing the keycode in a USB report. + """ + char_val = ord(char) + if char_val > len(self.ASCII_TO_KEYCODE): + return self._above128char_to_keycode(char) + keycode = self.ASCII_TO_KEYCODE[char_val] + return keycode diff --git a/fw/adafruit_hid/keyboard_layout_us.py b/fw/adafruit_hid/keyboard_layout_us.py new file mode 100644 index 0000000..35b163b --- /dev/null +++ b/fw/adafruit_hid/keyboard_layout_us.py @@ -0,0 +1,166 @@ +# SPDX-FileCopyrightText: 2017 Dan Halbert for Adafruit Industries +# +# SPDX-License-Identifier: MIT + +""" +`adafruit_hid.keyboard_layout_us.KeyboardLayoutUS` +======================================================= + +* Author(s): Dan Halbert +""" + +from .keyboard_layout_base import KeyboardLayoutBase + + +class KeyboardLayoutUS(KeyboardLayoutBase): + """Map ASCII characters to appropriate keypresses on a standard US PC keyboard. + + Non-ASCII characters and most control characters will raise an exception. + """ + + # The ASCII_TO_KEYCODE bytes object is used as a table to maps ASCII 0-127 + # to the corresponding # keycode on a US 104-key keyboard. + # The user should not normally need to use this table, + # but it is not marked as private. + # + # Because the table only goes to 127, we use the top bit of each byte (ox80) to indicate + # that the shift key should be pressed. So any values 0x{8,9,a,b}* are shifted characters. + # + # The Python compiler will concatenate all these bytes literals into a single bytes object. + # Micropython/CircuitPython will store the resulting bytes constant in flash memory + # if it's in a .mpy file, so it doesn't use up valuable RAM. + # + # \x00 entries have no keyboard key and so won't be sent. + ASCII_TO_KEYCODE = ( + b"\x00" # NUL + b"\x00" # SOH + b"\x00" # STX + b"\x00" # ETX + b"\x00" # EOT + b"\x00" # ENQ + b"\x00" # ACK + b"\x00" # BEL \a + b"\x2a" # BS BACKSPACE \b (called DELETE in the usb.org document) + b"\x2b" # TAB \t + b"\x28" # LF \n (called Return or ENTER in the usb.org document) + b"\x00" # VT \v + b"\x00" # FF \f + b"\x00" # CR \r + b"\x00" # SO + b"\x00" # SI + b"\x00" # DLE + b"\x00" # DC1 + b"\x00" # DC2 + b"\x00" # DC3 + b"\x00" # DC4 + b"\x00" # NAK + b"\x00" # SYN + b"\x00" # ETB + b"\x00" # CAN + b"\x00" # EM + b"\x00" # SUB + b"\x29" # ESC + b"\x00" # FS + b"\x00" # GS + b"\x00" # RS + b"\x00" # US + b"\x2c" # SPACE + b"\x9e" # ! x1e|SHIFT_FLAG (shift 1) + b"\xb4" # " x34|SHIFT_FLAG (shift ') + b"\xa0" # # x20|SHIFT_FLAG (shift 3) + b"\xa1" # $ x21|SHIFT_FLAG (shift 4) + b"\xa2" # % x22|SHIFT_FLAG (shift 5) + b"\xa4" # & x24|SHIFT_FLAG (shift 7) + b"\x34" # ' + b"\xa6" # ( x26|SHIFT_FLAG (shift 9) + b"\xa7" # ) x27|SHIFT_FLAG (shift 0) + b"\xa5" # * x25|SHIFT_FLAG (shift 8) + b"\xae" # + x2e|SHIFT_FLAG (shift =) + b"\x36" # , + b"\x2d" # - + b"\x37" # . + b"\x38" # / + b"\x27" # 0 + b"\x1e" # 1 + b"\x1f" # 2 + b"\x20" # 3 + b"\x21" # 4 + b"\x22" # 5 + b"\x23" # 6 + b"\x24" # 7 + b"\x25" # 8 + b"\x26" # 9 + b"\xb3" # : x33|SHIFT_FLAG (shift ;) + b"\x33" # ; + b"\xb6" # < x36|SHIFT_FLAG (shift ,) + b"\x2e" # = + b"\xb7" # > x37|SHIFT_FLAG (shift .) + b"\xb8" # ? x38|SHIFT_FLAG (shift /) + b"\x9f" # @ x1f|SHIFT_FLAG (shift 2) + b"\x84" # A x04|SHIFT_FLAG (shift a) + b"\x85" # B x05|SHIFT_FLAG (etc.) + b"\x86" # C x06|SHIFT_FLAG + b"\x87" # D x07|SHIFT_FLAG + b"\x88" # E x08|SHIFT_FLAG + b"\x89" # F x09|SHIFT_FLAG + b"\x8a" # G x0a|SHIFT_FLAG + b"\x8b" # H x0b|SHIFT_FLAG + b"\x8c" # I x0c|SHIFT_FLAG + b"\x8d" # J x0d|SHIFT_FLAG + b"\x8e" # K x0e|SHIFT_FLAG + b"\x8f" # L x0f|SHIFT_FLAG + b"\x90" # M x10|SHIFT_FLAG + b"\x91" # N x11|SHIFT_FLAG + b"\x92" # O x12|SHIFT_FLAG + b"\x93" # P x13|SHIFT_FLAG + b"\x94" # Q x14|SHIFT_FLAG + b"\x95" # R x15|SHIFT_FLAG + b"\x96" # S x16|SHIFT_FLAG + b"\x97" # T x17|SHIFT_FLAG + b"\x98" # U x18|SHIFT_FLAG + b"\x99" # V x19|SHIFT_FLAG + b"\x9a" # W x1a|SHIFT_FLAG + b"\x9b" # X x1b|SHIFT_FLAG + b"\x9c" # Y x1c|SHIFT_FLAG + b"\x9d" # Z x1d|SHIFT_FLAG + b"\x2f" # [ + b"\x31" # \ backslash + b"\x30" # ] + b"\xa3" # ^ x23|SHIFT_FLAG (shift 6) + b"\xad" # _ x2d|SHIFT_FLAG (shift -) + b"\x35" # ` + b"\x04" # a + b"\x05" # b + b"\x06" # c + b"\x07" # d + b"\x08" # e + b"\x09" # f + b"\x0a" # g + b"\x0b" # h + b"\x0c" # i + b"\x0d" # j + b"\x0e" # k + b"\x0f" # l + b"\x10" # m + b"\x11" # n + b"\x12" # o + b"\x13" # p + b"\x14" # q + b"\x15" # r + b"\x16" # s + b"\x17" # t + b"\x18" # u + b"\x19" # v + b"\x1a" # w + b"\x1b" # x + b"\x1c" # y + b"\x1d" # z + b"\xaf" # { x2f|SHIFT_FLAG (shift [) + b"\xb1" # | x31|SHIFT_FLAG (shift \) + b"\xb0" # } x30|SHIFT_FLAG (shift ]) + b"\xb5" # ~ x35|SHIFT_FLAG (shift `) + b"\x4c" # DEL DELETE (called Forward Delete in usb.org document) + ) + + +KeyboardLayout = KeyboardLayoutUS diff --git a/fw/adafruit_hid/keycode.py b/fw/adafruit_hid/keycode.py new file mode 100644 index 0000000..12bc7b8 --- /dev/null +++ b/fw/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/fw/adafruit_hid/mouse.py b/fw/adafruit_hid/mouse.py new file mode 100644 index 0000000..775dcf4 --- /dev/null +++ b/fw/adafruit_hid/mouse.py @@ -0,0 +1,151 @@ +# SPDX-FileCopyrightText: 2017 Dan Halbert for Adafruit Industries +# +# SPDX-License-Identifier: MIT + +""" +`adafruit_hid.mouse.Mouse` +==================================================== + +* Author(s): Dan Halbert +""" +from . import find_device + +try: + from typing import Sequence + import usb_hid +except ImportError: + pass + + +class Mouse: + """Send USB HID mouse reports.""" + + LEFT_BUTTON = 1 + """Left mouse button.""" + RIGHT_BUTTON = 2 + """Right mouse button.""" + MIDDLE_BUTTON = 4 + """Middle mouse button.""" + BACK_BUTTON = 8 + """Back mouse button.""" + FORWARD_BUTTON = 16 + """Forward mouse button.""" + + def __init__(self, devices: Sequence[usb_hid.Device], timeout: int = None) -> None: + """Create a Mouse object that will send USB mouse 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._mouse_device = find_device( + devices, usage_page=0x1, usage=0x02, timeout=timeout + ) + + # Reuse this bytearray to send mouse reports. + # report[0] buttons pressed (LEFT, MIDDLE, RIGHT) + # report[1] x movement + # report[2] y movement + # report[3] wheel movement + self.report = bytearray(4) + + def press(self, buttons: int) -> None: + """Press the given mouse buttons. + + :param buttons: a bitwise-or'd combination of ``LEFT_BUTTON``, + ``MIDDLE_BUTTON``, and ``RIGHT_BUTTON``. + + Examples:: + + # Press the left button. + m.press(Mouse.LEFT_BUTTON) + + # Press the left and right buttons simultaneously. + m.press(Mouse.LEFT_BUTTON | Mouse.RIGHT_BUTTON) + """ + self.report[0] |= buttons + self._send_no_move() + + def release(self, buttons: int) -> None: + """Release the given mouse buttons. + + :param buttons: a bitwise-or'd combination of ``LEFT_BUTTON``, + ``MIDDLE_BUTTON``, and ``RIGHT_BUTTON``. + """ + self.report[0] &= ~buttons + self._send_no_move() + + def release_all(self) -> None: + """Release all the mouse buttons.""" + self.report[0] = 0 + self._send_no_move() + + def click(self, buttons: int) -> None: + """Press and release the given mouse buttons. + + :param buttons: a bitwise-or'd combination of ``LEFT_BUTTON``, + ``MIDDLE_BUTTON``, and ``RIGHT_BUTTON``. + + Examples:: + + # Click the left button. + m.click(Mouse.LEFT_BUTTON) + + # Double-click the left button. + m.click(Mouse.LEFT_BUTTON) + m.click(Mouse.LEFT_BUTTON) + """ + self.press(buttons) + self.release(buttons) + + def move(self, x: int = 0, y: int = 0, wheel: int = 0) -> None: + """Move the mouse and turn the wheel as directed. + + :param x: Move the mouse along the x axis. Negative is to the left, positive + is to the right. + :param y: Move the mouse along the y axis. Negative is upwards on the display, + positive is downwards. + :param wheel: Rotate the wheel this amount. Negative is toward the user, positive + is away from the user. The scrolling effect depends on the host. + + Examples:: + + # Move 100 to the left. Do not move up and down. Do not roll the scroll wheel. + m.move(-100, 0, 0) + # Same, with keyword arguments. + m.move(x=-100) + + # Move diagonally to the upper right. + m.move(50, 20) + # Same. + m.move(x=50, y=-20) + + # Roll the mouse wheel away from the user. + m.move(wheel=1) + """ + # Send multiple reports if necessary to move or scroll requested amounts. + while x != 0 or y != 0 or wheel != 0: + partial_x = self._limit(x) + partial_y = self._limit(y) + partial_wheel = self._limit(wheel) + self.report[1] = partial_x & 0xFF + self.report[2] = partial_y & 0xFF + self.report[3] = partial_wheel & 0xFF + self._mouse_device.send_report(self.report) + x -= partial_x + y -= partial_y + wheel -= partial_wheel + + def _send_no_move(self) -> None: + """Send a button-only report.""" + self.report[1] = 0 + self.report[2] = 0 + self.report[3] = 0 + self._mouse_device.send_report(self.report) + + @staticmethod + def _limit(dist: int) -> int: + return min(127, max(-127, dist))