X-Git-Url: https://git.ayoreis.com/Adafruit_Blinka-hackapet.git/blobdiff_plain/96247c0ce7d885ba5d3bf9fe8725d91df2c12f2a..e722742d4e738361cc9e0fa89466bb0f52f94a72:/src/usb_hid.py diff --git a/src/usb_hid.py b/src/usb_hid.py index dc057df..791647b 100644 --- a/src/usb_hid.py +++ b/src/usb_hid.py @@ -1,14 +1,40 @@ +# SPDX-FileCopyrightText: 2021 Melissa LeBlanc-Williams for Adafruit Industries +# +# SPDX-License-Identifier: MIT +""" +`usb_hid` - support for usb hid devices via usb_gadget driver +=========================================================== +See `CircuitPython:usb_hid` in CircuitPython for more details. +For now using report ids in the descriptor + +# regarding usb_gadget see https://www.kernel.org/doc/Documentation/usb/gadget_configfs.txt +* Author(s): Björn Bösel +""" + from typing import Sequence from pathlib import Path import os import atexit +import sys -# https://www.kernel.org/doc/Documentation/usb/gadget_configfs.txt +for module in ["dwc2", "libcomposite"]: + if Path("/proc/modules").read_text(encoding="utf-8").find(module) == -1: + raise Exception( # pylint: disable=broad-exception-raised + "%s module not present in your kernel. did you insmod it?" % module + ) +this = sys.modules[__name__] -gadget_root = "/sys/kernel/config/usb_gadget/adafruit-blinka" +this.gadget_root = "/sys/kernel/config/usb_gadget/adafruit-blinka" +this.boot_device = 0 +this.devices = [] class Device: + """ + HID Device specification: see + https://github.com/adafruit/circuitpython/blob/main/shared-bindings/usb_hid/Device.c + """ + def __init__( self, *, @@ -28,15 +54,34 @@ class Device: self._last_received_report = None def send_report(self, report: bytearray, report_id: int = None): - device_path = self.gets_device_path(report_id) + """Send an HID report. If the device descriptor specifies zero or one report id's, + you can supply `None` (the default) as the value of ``report_id``. + Otherwise you must specify which report id to use when sending the report. + """ + report_id = report_id or self.report_ids[0] + device_path = self.get_device_path(report_id) with open(device_path, "rb+") as fd: + if report_id > 0: + report = bytearray(report_id.to_bytes(1, "big")) + report fd.write(report) @property def last_received_report( self, - ): - device_path = self.gets_device_path(self.report_ids[0]) + ) -> bytes: + """The HID OUT report as a `bytes` (read-only). `None` if nothing received. + Same as `get_last_received_report()` with no argument. + + Deprecated: will be removed in CircutPython 8.0.0. Use `get_last_received_report()` instead. + """ + return self.get_last_received_report() + + def get_last_received_report(self, report_id=None) -> bytes: + """Get the last received HID OUT or feature report for the given report ID. + The report ID may be omitted if there is no report ID, or only one report ID. + Return `None` if nothing received. + """ + device_path = self.get_device_path(report_id or self.report_ids[0]) with open(device_path, "rb+") as fd: os.set_blocking(fd.fileno(), False) report = fd.read(self.out_report_lengths[0]) @@ -44,19 +89,26 @@ class Device: self._last_received_report = report return self._last_received_report - def gets_device_path(self, report_id): + def get_device_path(self, report_id): + """ + translates the /dev/hidg device from the report id + """ device = ( Path( "%s/functions/hid.usb%s/dev" - % (gadget_root, report_id or self.report_ids[0]) + % (this.gadget_root, report_id or self.report_ids[0]) ) - .read_text() + .read_text(encoding="utf-8") .strip() .split(":")[1] ) device_path = "/dev/hidg%s" % device return device_path + KEYBOARD = None + MOUSE = None + CONSUMER_CONTROL = None + Device.KEYBOARD = Device( descriptor=bytes( @@ -67,6 +119,8 @@ Device.KEYBOARD = Device( 0x06, # usage (keyboard) 0xA1, 0x01, # collection (application) + 0x85, + 0x01, # Report ID (1) 0x05, 0x07, # usage page (kbrd/keypad) 0x19, @@ -100,13 +154,15 @@ Device.KEYBOARD = Device( 0x29, 0x05, # usage maximum (kana) 0x91, - 0x02, # output (data,var,abs,no wrap,linear,preferred state,no null position,non-volatile) + 0x02, # output + # (data,var,abs,no wrap,linear,preferred state,no null position,non-volatile) 0x95, 0x01, # report count (1) 0x75, 0x05, # report size (5) 0x91, - 0x01, # output (const,array,abs,no wrap,linear,preferred state,no null position,non-volatile) + 0x01, # output + # (const,array,abs,no wrap,linear,preferred state,no null position,non-volatile) 0x95, 0x06, # report count (6) 0x75, @@ -143,6 +199,8 @@ Device.MOUSE = Device( 0x02, # Usage (Mouse) 0xA1, 0x01, # Collection (Application) + 0x85, + 0x02, # Report ID (2) 0x09, 0x01, # Usage (Pointer) 0xA1, @@ -201,10 +259,9 @@ Device.MOUSE = Device( 0xC0, # End Collection ) ), - # Omitted for brevity. usage_page=0x1, usage=0x02, - report_ids=[2], + report_ids=[0x02], in_report_lengths=[4], out_report_lengths=[0], ) @@ -218,6 +275,8 @@ Device.CONSUMER_CONTROL = Device( 0x01, # Usage (Consumer Control) 0xA1, 0x01, # Collection (Application) + 0x85, + 0x03, # Report ID (3) 0x75, 0x10, # Report Size (16) 0x95, @@ -237,212 +296,430 @@ Device.CONSUMER_CONTROL = Device( 0xC0, # End Collection ) ), - # Omitted for brevity. usage_page=0x0C, usage=0x01, report_ids=[3], in_report_lengths=[2], out_report_lengths=[0], ) -devices = [Device.KEYBOARD, Device.MOUSE, Device.CONSUMER_CONTROL] + +Device.BOOT_KEYBOARD = Device( + descriptor=bytes( + ( + 0x05, + 0x01, # usage page (generic desktop ctrls) + 0x09, + 0x06, # usage (keyboard) + 0xA1, + 0x01, # collection (application) + 0x05, + 0x07, # usage page (kbrd/keypad) + 0x19, + 0xE0, # usage minimum (0xe0) + 0x29, + 0xE7, # usage maximum (0xe7) + 0x15, + 0x00, # logical minimum (0) + 0x25, + 0x01, # logical maximum (1) + 0x75, + 0x01, # report size (1) + 0x95, + 0x08, # report count (8) + 0x81, + 0x02, # input (data,var,abs,no wrap,linear,preferred state,no null position) + 0x95, + 0x01, # report count (1) + 0x75, + 0x08, # report size (8) + 0x81, + 0x01, # input (const,array,abs,no wrap,linear,preferred state,no null position) + 0x95, + 0x03, # report count (3) + 0x75, + 0x01, # report size (1) + 0x05, + 0x08, # usage page (leds) + 0x19, + 0x01, # usage minimum (num lock) + 0x29, + 0x05, # usage maximum (kana) + 0x91, + 0x02, # output + # (data,var,abs,no wrap,linear,preferred state,no null position,non-volatile) + 0x95, + 0x01, # report count (1) + 0x75, + 0x05, # report size (5) + 0x91, + 0x01, # output + # (const,array,abs,no wrap,linear,preferred state,no null position,non-volatile) + 0x95, + 0x06, # report count (6) + 0x75, + 0x08, # report size (8) + 0x15, + 0x00, # logical minimum (0) + 0x26, + 0xFF, + 0x00, # logical maximum (255) + 0x05, + 0x07, # usage page (kbrd/keypad) + 0x19, + 0x00, # usage minimum (0x00) + 0x2A, + 0xFF, + 0x00, # usage maximum (0xff) + 0x81, + 0x00, # input (data,array,abs,no wrap,linear,preferred state,no null position) + 0xC0, # end collection + ) + ), + usage_page=0x1, + usage=0x6, + report_ids=[0x0], + in_report_lengths=[8], + out_report_lengths=[1], +) +Device.BOOT_MOUSE = Device( + descriptor=bytes( + ( + 0x05, + 0x01, # Usage Page (Generic Desktop Ctrls) + 0x09, + 0x02, # Usage (Mouse) + 0xA1, + 0x01, # Collection (Application) + 0x09, + 0x01, # Usage (Pointer) + 0xA1, + 0x00, # Collection (Physical) + 0x05, + 0x09, # Usage Page (Button) + 0x19, + 0x01, # Usage Minimum (0x01) + 0x29, + 0x05, # Usage Maximum (0x05) + 0x15, + 0x00, # Logical Minimum (0) + 0x25, + 0x01, # Logical Maximum (1) + 0x95, + 0x05, # Report Count (5) + 0x75, + 0x01, # Report Size (1) + 0x81, + 0x02, # Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) + 0x95, + 0x01, # Report Count (1) + 0x75, + 0x03, # Report Size (3) + 0x81, + 0x01, # Input (Const,Array,Abs,No Wrap,Linear,Preferred State,No Null Position) + 0x05, + 0x01, # Usage Page (Generic Desktop Ctrls) + 0x09, + 0x30, # Usage (X) + 0x09, + 0x31, # Usage (Y) + 0x15, + 0x81, # Logical Minimum (-127) + 0x25, + 0x7F, # Logical Maximum (127) + 0x75, + 0x08, # Report Size (8) + 0x95, + 0x02, # Report Count (2) + 0x81, + 0x06, # Input (Data,Var,Rel,No Wrap,Linear,Preferred State,No Null Position) + 0x09, + 0x38, # Usage (Wheel) + 0x15, + 0x81, # Logical Minimum (-127) + 0x25, + 0x7F, # Logical Maximum (127) + 0x75, + 0x08, # Report Size (8) + 0x95, + 0x01, # Report Count (1) + 0x81, + 0x06, # Input (Data,Var,Rel,No Wrap,Linear,Preferred State,No Null Position) + 0xC0, # End Collection + 0xC0, # End Collection + ) + ), + usage_page=0x1, + usage=0x02, + report_ids=[0], + in_report_lengths=[4], + out_report_lengths=[0], +) def disable() -> None: + """Do not present any USB HID devices to the host computer. + Can be called in ``boot.py``, before USB is connected. + The HID composite device is normally enabled by default, + but on some boards with limited endpoints, including STM32F4, + it is disabled by default. You must turn off another USB device such + as `usb_cdc` or `storage` to free up endpoints for use by `usb_hid`. + """ try: - Path("%s/UDC" % gadget_root).write_text("") + Path("%s/UDC" % this.gadget_root).write_text("", encoding="utf-8") except FileNotFoundError: pass - for symlink in Path(gadget_root).glob("configs/**/hid.usb*"): + for symlink in Path(this.gadget_root).glob("configs/**/hid.usb*"): symlink.unlink() - for strings_file in Path(gadget_root).rglob("configs/*/strings/*/*"): + for strings_file in Path(this.gadget_root).rglob("configs/*/strings/*/*"): if strings_file.is_dir(): strings_file.rmdir() - for strings_file in Path(gadget_root).rglob("configs/*/strings/*"): + for strings_file in Path(this.gadget_root).rglob("configs/*/strings/*"): if strings_file.is_dir(): strings_file.rmdir() - for config_dir in Path(gadget_root).rglob("configs/*"): + for config_dir in Path(this.gadget_root).rglob("configs/*"): if config_dir.is_dir(): config_dir.rmdir() - for function_dir in Path(gadget_root).rglob("functions/*"): + for function_dir in Path(this.gadget_root).rglob("functions/*"): if function_dir.is_dir(): function_dir.rmdir() try: - Path(gadget_root).rmdir() - except: + Path(this.gadget_root).rmdir() + except FileNotFoundError: pass + this.devices = [] -# atexit.register(disable) - +atexit.register(disable) -def enable(devices: Sequence[Device], boot_device: int = 0) -> None: - if len(devices) == 0: - disable() - return - """ - 1. Creating the gadgets - ----------------------- - For each gadget to be created its corresponding directory must be created:: +def enable(requested_devices: Sequence[Device], boot_device: int = 0) -> None: + """Specify which USB HID devices that will be available. + Can be called in ``boot.py``, before USB is connected. - $ mkdir $CONFIGFS_HOME/usb_gadget/ + :param Sequence devices: `Device` objects. + If `devices` is empty, HID is disabled. The order of the ``Devices`` + may matter to the host. For instance, for MacOS, put the mouse device + before any Gamepad or Digitizer HID device or else it will not work. + :param int boot_device: If non-zero, inform the host that support for a + a boot HID device is available. + If ``boot_device=1``, a boot keyboard is available. + If ``boot_device=2``, a boot mouse is available. No other values are allowed. + See below. - e.g.:: + If you enable too many devices at once, you will run out of USB endpoints. + The number of available endpoints varies by microcontroller. + CircuitPython will go into safe mode after running ``boot.py`` to inform you if + not enough endpoints are available. - $ mkdir $CONFIGFS_HOME/usb_gadget/g1 + **Boot Devices** - ... - ... - ... + Boot devices implement a fixed, predefined report descriptor, defined in + https://www.usb.org/sites/default/files/hid1_12.pdf, Appendix B. A USB host + can request to use the boot device if the USB device says it is available. + Usually only a BIOS or other kind of limited-functionality + host needs boot keyboard support. - $ cd $CONFIGFS_HOME/usb_gadget/g1 + For example, to make a boot keyboard available, you can use this code:: - Each gadget needs to have its vendor id and product id specified:: + usb_hid.enable((Device.KEYBOARD), boot_device=1) # 1 for a keyboard - $ echo > idVendor - $ echo > idProduct + If the host requests the boot keyboard, the report descriptor provided by `Device.KEYBOARD` + will be ignored, and the predefined report descriptor will be used. + But if the host does not request the boot keyboard, + the descriptor provided by `Device.KEYBOARD` will be used. - A gadget also needs its serial number, manufacturer and product strings. - In order to have a place to store them, a strings subdirectory must be created - for each language, e.g.:: - - $ mkdir strings/0x409 + The HID boot device must usually be the first or only device presented by CircuitPython. + The HID device will be USB interface number 0. + To make sure it is the first device, disable other USB devices, including CDC and MSC + (CIRCUITPY). + If you specify a non-zero ``boot_device``, and it is not the first device, CircuitPython + will enter safe mode to report this error. + """ + this.boot_device = boot_device - Then the strings can be specified:: + if len(requested_devices) == 0: + disable() + return - $ echo > strings/0x409/serialnumber - $ echo > strings/0x409/manufacturer - $ echo > strings/0x409/product - """ - Path("%s/functions" % gadget_root).mkdir(parents=True, exist_ok=True) - Path("%s/configs" % gadget_root).mkdir(parents=True, exist_ok=True) - Path("%s/bcdDevice" % gadget_root).write_text("%s" % 1) # Version 1.0.0 - Path("%s/bcdUSB" % gadget_root).write_text("%s" % 0x0200) # USB 2.0 - Path("%s/bDeviceClass" % gadget_root).write_text( - "%s" % 0x00 + if boot_device == 1: + requested_devices = [Device.BOOT_KEYBOARD] + if boot_device == 2: + requested_devices = [Device.BOOT_MOUSE] + + # """ + # 1. Creating the gadgets + # ----------------------- + # + # For each gadget to be created its corresponding directory must be created:: + # + # $ mkdir $CONFIGFS_HOME/usb_gadget/ + # + # e.g.:: + # + # $ mkdir $CONFIGFS_HOME/usb_gadget/g1 + # + # ... + # ... + # ... + # + # $ cd $CONFIGFS_HOME/usb_gadget/g1 + # + # Each gadget needs to have its vendor id and product id specified:: + # + # $ echo > idVendor + # $ echo > idProduct + # + # A gadget also needs its serial number, manufacturer and product strings. + # In order to have a place to store them, a strings subdirectory must be created + # for each language, e.g.:: + # + # $ mkdir strings/0x409 + # + # Then the strings can be specified:: + # + # $ echo > strings/0x409/serialnumber + # $ echo > strings/0x409/manufacturer + # $ echo > strings/0x409/product + # """ + Path("%s/functions" % this.gadget_root).mkdir(parents=True, exist_ok=True) + Path("%s/configs" % this.gadget_root).mkdir(parents=True, exist_ok=True) + Path("%s/bcdDevice" % this.gadget_root).write_text( + "%s" % 1, encoding="utf-8" + ) # Version 1.0.0 + Path("%s/bcdUSB" % this.gadget_root).write_text( + "%s" % 0x0200, encoding="utf-8" + ) # USB 2.0 + Path("%s/bDeviceClass" % this.gadget_root).write_text( + "%s" % 0x00, encoding="utf-8" ) # multipurpose i guess? - Path("%s/bDeviceProtocol" % gadget_root).write_text("%s" % 0x00) - Path("%s/bDeviceSubClass" % gadget_root).write_text("%s" % 0x00) - Path("%s/bMaxPacketSize0" % gadget_root).write_text("%s" % 0x08) - Path("%s/idProduct" % gadget_root).write_text( - "%s" % 0x0104 + Path("%s/bDeviceProtocol" % this.gadget_root).write_text( + "%s" % 0x00, encoding="utf-8" + ) + Path("%s/bDeviceSubClass" % this.gadget_root).write_text( + "%s" % 0x00, encoding="utf-8" + ) + Path("%s/bMaxPacketSize0" % this.gadget_root).write_text( + "%s" % 0x08, encoding="utf-8" + ) + Path("%s/idProduct" % this.gadget_root).write_text( + "%s" % 0x0104, encoding="utf-8" ) # Multifunction Composite Gadget - Path("%s/idVendor" % gadget_root).write_text("%s" % 0x1D6B) # Linux Foundation - """ - 2. Creating the configurations - ------------------------------ - - Each gadget will consist of a number of configurations, their corresponding - directories must be created: - - $ mkdir configs/. - - where can be any string which is legal in a filesystem and the - is the configuration's number, e.g.:: - - $ mkdir configs/c.1 - - ... - ... - ... - - Each configuration also needs its strings, so a subdirectory must be created - for each language, e.g.:: - - $ mkdir configs/c.1/strings/0x409 - - Then the configuration string can be specified:: - - $ echo > configs/c.1/strings/0x409/configuration - - Some attributes can also be set for a configuration, e.g.:: - - $ echo 120 > configs/c.1/MaxPower - """ - - for i, device in enumerate(devices): - - config_root = "%s/configs/device.%s" % (gadget_root, 1) + Path("%s/idVendor" % this.gadget_root).write_text( + "%s" % 0x1D6B, encoding="utf-8" + ) # Linux Foundation + # """ + # 2. Creating the configurations + # ------------------------------ + # + # Each gadget will consist of a number of configurations, their corresponding + # directories must be created: + # + # $ mkdir configs/. + # + # where can be any string which is legal in a filesystem and the + # is the configuration's number, e.g.:: + # + # $ mkdir configs/c.1 + # + # ... + # ... + # ... + # + # Each configuration also needs its strings, so a subdirectory must be created + # for each language, e.g.:: + # + # $ mkdir configs/c.1/strings/0x409 + # + # Then the configuration string can be specified:: + # + # $ echo > configs/c.1/strings/0x409/configuration + # + # Some attributes can also be set for a configuration, e.g.:: + # + # $ echo 120 > configs/c.1/MaxPower + # """ + + for device in requested_devices: + config_root = "%s/configs/device.1" % this.gadget_root Path("%s/" % config_root).mkdir(parents=True, exist_ok=True) Path("%s/strings/0x409" % config_root).mkdir(parents=True, exist_ok=True) Path("%s/strings/0x409/configuration" % config_root).write_text( - "my configuration" + "my configuration", encoding="utf-8" ) - Path("%s/MaxPower" % config_root).write_text("150") - Path("%s/bmAttributes" % config_root).write_text("%s" % 0x080) - """ - 3. Creating the functions - ------------------------- - - The gadget will provide some functions, for each function its corresponding - directory must be created:: - - $ mkdir functions/. - - where corresponds to one of allowed function names and instance name - is an arbitrary string allowed in a filesystem, e.g.:: - - $ mkdir functions/ncm.usb0 # usb_f_ncm.ko gets loaded with request_module() - - ... - ... - ... - - Each function provides its specific set of attributes, with either read-only - or read-write access. Where applicable they need to be written to as - appropriate. - Please refer to Documentation/ABI/*/configfs-usb-gadget* for more information. - - """ - - # create functions + Path("%s/MaxPower" % config_root).write_text("150", encoding="utf-8") + Path("%s/bmAttributes" % config_root).write_text("%s" % 0x080, encoding="utf-8") + this.devices.append(device) + # """ + # 3. Creating the functions + # ------------------------- + # + # The gadget will provide some functions, for each function its corresponding + # directory must be created:: + # + # $ mkdir functions/. + # + # where corresponds to one of allowed function names and instance name + # is an arbitrary string allowed in a filesystem, e.g.:: + # + # $ mkdir functions/ncm.usb0 # usb_f_ncm.ko gets loaded with request_module() + # + # ... + # ... + # ... + # + # Each function provides its specific set of attributes, with either read-only + # or read-write access. Where applicable they need to be written to as + # appropriate. + # Please refer to Documentation/ABI/*/configfs-usb-gadget* for more information. """ for report_index, report_id in enumerate(device.report_ids): - function_root = "%s/functions/hid.usb%s" % (gadget_root, report_id) + function_root = "%s/functions/hid.usb%s" % (this.gadget_root, report_id) try: Path("%s/" % function_root).mkdir(parents=True) except FileExistsError: continue - Path("%s/protocol" % function_root).write_text("%s" % report_id) + Path("%s/protocol" % function_root).write_text( + "%s" % report_id, encoding="utf-8" + ) Path("%s/report_length" % function_root).write_text( - "%s" % device.in_report_lengths[report_index] + "%s" % device.in_report_lengths[report_index], encoding="utf-8" ) - Path("%s/subclass" % function_root).write_text("%s" % 1) + Path("%s/subclass" % function_root).write_text("%s" % 1, encoding="utf-8") Path("%s/report_desc" % function_root).write_bytes(device.descriptor) - """ - 4. Associating the functions with their configurations - ------------------------------------------------------ - - At this moment a number of gadgets is created, each of which has a number of - configurations specified and a number of functions available. What remains - is specifying which function is available in which configuration (the same - function can be used in multiple configurations). This is achieved with - creating symbolic links:: - - $ ln -s functions/. configs/. - - e.g.:: - - $ ln -s functions/ncm.usb0 configs/c.1 - """ - - Path("%s/hid.usb%s" % (config_root, report_id)).symlink_to(function_root) - """ - 5. Enabling the gadget - ---------------------- - Such a gadget must be finally enabled so that the USB host can enumerate it. - - In order to enable the gadget it must be bound to a UDC (USB Device - Controller):: - - $ echo > UDC - - where is one of those found in /sys/class/udc/* - e.g.:: - - $ echo s3c-hsotg > UDC - - """ + # """ + # 4. Associating the functions with their configurations + # ------------------------------------------------------ + # + # At this moment a number of gadgets is created, each of which has a number of + # configurations specified and a number of functions available. What remains + # is specifying which function is available in which configuration (the same + # function can be used in multiple configurations). This is achieved with + # creating symbolic links:: + # + # $ ln -s functions/. configs/. + # + # e.g.:: + # + # $ ln -s functions/ncm.usb0 configs/c.1 """ + try: + Path("%s/hid.usb%s" % (config_root, report_id)).symlink_to( + function_root + ) + except FileNotFoundError: + pass + # """ 5. Enabling the gadget + # ---------------------- + # Such a gadget must be finally enabled so that the USB host can enumerate it. + # + # In order to enable the gadget it must be bound to a UDC (USB Device + # Controller):: + # + # $ echo > UDC + # + # where is one of those found in /sys/class/udc/* + # e.g.:: + # + # $ echo s3c-hsotg > UDC """ udc = next(Path("/sys/class/udc/").glob("*")) - Path("%s/UDC" % gadget_root).write_text("%s" % udc.name) + Path("%s/UDC" % this.gadget_root).write_text("%s" % udc.name, encoding="utf-8")