From 24f742252f30af723863f4168b20f6d1a1bfe4bb Mon Sep 17 00:00:00 2001 From: Angus Gratton Date: Wed, 26 Oct 2022 16:55:49 +1100 Subject: [PATCH 01/17] usbd: Add USB device drivers implemented in Python. Rely on support implemented in the machine.USBD() object on the MicroPython side, providing a thin wrapper around TinyUSB "application" device class driver. --- micropython/usbd/__init__.py | 4 + micropython/usbd/device.py | 549 +++++++++++++++++++++++++++++++++++ micropython/usbd/hid.py | 255 ++++++++++++++++ micropython/usbd/utils.py | 77 +++++ 4 files changed, 885 insertions(+) create mode 100644 micropython/usbd/__init__.py create mode 100644 micropython/usbd/device.py create mode 100644 micropython/usbd/hid.py create mode 100644 micropython/usbd/utils.py diff --git a/micropython/usbd/__init__.py b/micropython/usbd/__init__.py new file mode 100644 index 000000000..90739c27e --- /dev/null +++ b/micropython/usbd/__init__.py @@ -0,0 +1,4 @@ +from .device import get_usbdevice, USBInterface +from .hid import HIDInterface, MouseInterface +from .midi import DummyAudioInterface, MIDIInterface, MidiUSB +from . import utils diff --git a/micropython/usbd/device.py b/micropython/usbd/device.py new file mode 100644 index 000000000..7045126ec --- /dev/null +++ b/micropython/usbd/device.py @@ -0,0 +1,549 @@ +# MicroPython USB device module +# MIT license; Copyright (c) 2022 Angus Gratton +from micropython import const +import machine +import ustruct + +from .utils import split_bmRequestType, EP_IN_FLAG + +# USB descriptor types +_STD_DESC_DEVICE_TYPE = const(0x1) +_STD_DESC_CONFIG_TYPE = const(0x2) +_STD_DESC_STRING_TYPE = const(0x3) +_STD_DESC_INTERFACE_TYPE = const(0x4) +_STD_DESC_INTERFACE_ASSOC = const(0xB) + +# Standard USB descriptor lengths +_STD_DESC_CONFIG_LEN = const(9) +_STD_DESC_INTERFACE_LEN = const(9) + +# Standard control request bmRequest fields, can extract by calling split_bmRequestType() +_REQ_RECIPIENT_DEVICE = const(0x0) +_REQ_RECIPIENT_INTERFACE = const(0x1) +_REQ_RECIPIENT_ENDPOINT = const(0x2) +_REQ_RECIPIENT_OTHER = const(0x3) + +# Offsets into the standard configuration descriptor, to fixup +_OFFS_CONFIG_iConfiguration = const(6) + + +# Singleton _USBDevice instance +_inst = None + + +def get_usbdevice(): + # Access the singleton instance of the MicroPython _USBDevice object. + # + # TODO: It might be better to factor this as a module-level interface? + global _inst + if not _inst: + _inst = _USBDevice() + return _inst + + +class _USBDevice: + # Class that implements the Python parts of the MicroPython USBDevice. + # + # This object represents any interfaces on the USB device that are implemented + # in Python, and also allows disabling the 'static' USB interfaces that are + # implemented in Python (if include_static property is set to False). + # + # Should be accessed via the singleton getter module function get_usbdevice(), + # not instantiated directly.. + def __init__(self): + self._eps = {} # Mapping from endpoint address to interface object + self._ep_cbs = {} # Mapping from endpoint address to Optional[xfer callback] + self._itfs = [] # List of interfaces + self.include_static = True # Include static devices when enumerating? + + # Device properties, set non-NULL to override static values + self.manufacturer_str = None + self.product_str = None + self.serial_str = None + self.id_vendor = None + self.id_product = None + self.device_class = None + self.device_subclass = None + self.device_protocol = None + self.bcd_device = None + + # Configuration properties + self.config_str = None + self.max_power_ma = 50 + + self._strs = self._get_device_strs() + + usbd = self._usbd = machine.USBD() + usbd.init( + descriptor_device_cb=self._descriptor_device_cb, + descriptor_config_cb=self._descriptor_config_cb, + descriptor_string_cb=self._descriptor_string_cb, + open_driver_cb=self._open_driver_cb, + control_xfer_cb=self._control_xfer_cb, + xfer_cb=self._xfer_cb, + ) + + def add_interface(self, itf): + # Add an instance of USBInterface to the USBDevice. + # + # The next time USB is reenumerated (by calling .reenumerate() or + # otherwise), this interface will appear to the host. + self._itfs.append(itf) + + def remove_interface(self, itf): + # Remove an instance of USBInterface from the USBDevice. + # + # If the USB device is currently enumerated to a host, and in particular + # if any endpoint transfers are pending, then this may cause it to + # misbehave as these transfers are not cancelled. + self._itfs.remove(itf) + + def reenumerate(self): + # Disconnect the USB device and then reconnect it, causing the host to + # reenumerate it. + # + # Any open USB interfaces (for example USB-CDC serial connection) will be + # temporarily terminated. + # + # This is the only way to change the composition of an existing USB device + # from the device side without disconnecting/reconnecting the port. + self._usbd.reenumerate() + + def _descriptor_device_cb(self): + # Singleton callback from TinyUSB to read the USB device descriptor. + # + # This function will build a new device descriptor based on the 'static' + # USB device values compiled into MicroPython, but many values can be + # optionally overriden by setting properties of this object. + + FMT = "= 0 # index shouldn't be in the static range + try: + return self._itfs[index] + except IndexError: + return None # host has old mappings for interfaces + + def _descriptor_config_cb(self): + # Singleton callback from TinyUSB to read the configuration descriptor. + # + # Each time this function is called (in response to a GET DESCRIPTOR - + # CONFIGURATION request from the host), it rebuilds the full configuration + # descriptor and also the list of strings stored in self._strs. + # + # This normally only happens during enumeration, but may happen more than + # once (the host will first ask for a minimum length descriptor, and then + # use the length field request to request the whole thing). + static = self._usbd.static + + # Rebuild the _strs list as we build the configuration descriptor + strs = self._get_device_strs() + + if self.include_static: + desc = bytearray(static.desc_cfg) + else: + desc = bytearray(_STD_DESC_CONFIG_LEN) + + self._eps = {} # rebuild endpoint mapping as we enumerate each interface + self._ep_cbs = {} + itf_idx = static.itf_max + ep_addr = static.ep_max + str_idx = static.str_max + len(strs) + for itf in self._itfs: + # Get the endpoint descriptors first so we know how many endpoints there are + ep_desc, ep_strs, ep_addrs = itf.get_endpoint_descriptors(ep_addr, str_idx) + strs += ep_strs + str_idx += len(ep_strs) + + # Now go back and get the interface descriptor + itf_desc, itf_strs = itf.get_itf_descriptor(len(ep_addrs), itf_idx, str_idx) + desc += itf_desc + strs += itf_strs + itf_idx += 1 + str_idx += len(itf_strs) + + desc += ep_desc + for e in ep_addrs: + self._eps[e] = itf + self._ep_cbs[e] = None # no pending callback + # TODO: check if always incrementing leaves too many gaps + ep_addr = max((e & ~EP_IN_FLAG) + 1, ep_addr) + + self._update_configuration_descriptor(desc) + + self._strs = strs + return desc + + def _update_configuration_descriptor(self, desc): + # Utility function to update the Standard Configuration Descriptor + # header supplied in the argument with values based on the current state + # of the device. + # + # See USB 2.0 specification section 9.6.3 p264 for details. + # + # Currently only one configuration per device is supported. + bmAttributes = ( + (1 << 7) # Reserved + | (0 if self.max_power_ma else (1 << 6)) # Self-Powered + # Remote Wakeup not currently supported + ) + + iConfiguration = self._get_str_index(self.config_str) + if self.include_static and not iConfiguration: + iConfiguration = desc[_OFFS_CONFIG_iConfiguration] + + bNumInterfaces = self._usbd.static.itf_max if self.include_static else 0 + bNumInterfaces += len(self._itfs) + + ustruct.pack_into( + "= 0 + ) # Shouldn't get any calls here where index is less than first dynamic string index + try: + return self._strs[index] + except IndexError: + return None + + def _open_driver_cb(self, interface_desc_view): + # Singleton callback from TinyUSB custom class driver + pass + + def _submit_xfer(self, ep_addr, data, done_cb=None): + # Singleton function to submit a USB transfer (of any type except control). + # + # Generally, drivers should call USBInterface.submit_xfer() instead. See + # that function for documentation about the possible parameter values. + cb = self._ep_cbs[ep_addr] + if cb: + raise RuntimeError(f"Pending xfer on EP {ep_addr}") + + # USBD callback may be called immediately, before Python execution + # continues + self._ep_cbs[ep_addr] = done_cb + + if not self._usbd.submit_xfer(ep_addr, data): + self._ep_cbs[ep_addr] = None + return False + return True + + def _xfer_cb(self, ep_addr, result, xferred_bytes): + # Singleton callback from TinyUSB custom class driver when a transfer completes. + cb = self._ep_cbs.get(ep_addr, None) + if cb: + self._ep_cbs[ep_addr] = None + cb(ep_addr, result, xferred_bytes) + + def _control_xfer_cb(self, stage, request): + # Singleton callback from TinyUSB custom class driver when a control + # transfer is in progress. + # + # stage determines appropriate responses (possible values + # utils.STAGE_SETUP, utils.STAGE_DATA, utils.STAGE_ACK). + # + # The TinyUSB class driver framework only calls this function for + # particular types of control transfer, other standard control transfers + # are handled by TinyUSB itself. + bmRequestType, _, _, wIndex, _ = request + recipient, _, _ = split_bmRequestType(bmRequestType) + + itf = None + result = None + + if recipient == _REQ_RECIPIENT_DEVICE: + itf = self._get_interface(wIndex & 0xFFFF) + if itf: + result = itf.handle_device_control_xfer(stage, request) + elif recipient == _REQ_RECIPIENT_INTERFACE: + itf = self._get_interface(wIndex & 0xFFFF) + if itf: + result = itf.handle_interface_control_xfer(stage, request) + elif recipient == _REQ_RECIPIENT_ENDPOINT: + ep_num = wIndex & 0xFFFF + itf = self._eps.get(ep_num, None) + if itf: + result = itf.handle_endpoint_control_xfer(stage, request) + + if not itf: + # At time this code was written, only the control transfers shown + # above are passed to the class driver callback. See + # invoke_class_control() in tinyusb usbd.c + print(f"Unexpected control request type {bmRequestType:#x}") + return False + + # Accept the following possible replies from handle_NNN_control_xfer(): + # + # True - Continue transfer, no data + # False - STALL transfer + # Object with buffer interface - submit this data for the control transfer + if type(result) == bool: + return result + + return self._usbd.control_xfer(request, result) + + +class USBInterface: + # Abstract base class to implement a USBInterface (and associated endpoints) in Python + + def __init__( + self, + bInterfaceClass=0xFF, + bInterfaceSubClass=0, + bInterfaceProtocol=0xFF, + interface_str=None, + ): + # Create a new USBInterface object. Optionally can set bInterfaceClass, + # bInterfaceSubClass, bInterfaceProtocol values to specify the interface + # type. Can also optionally set a string descriptor value interface_str to describe this + # interface. + # + # The defaults are to set 'vendor' class and protocol values, the host + # will not attempt to use any standard class driver to talk to this + # interface. + + # Defaults set "vendor" class and protocol + self.bInterfaceClass = bInterfaceClass + self.bInterfaceSubClass = bInterfaceSubClass + self.bInterfaceProtocol = bInterfaceProtocol + self.interface_str = interface_str + + def get_itf_descriptor(self, num_eps, itf_idx, str_idx): + # Return the interface descriptor binary data and associated other + # descriptors for the interface (not including endpoint descriptors), plus + # associated string descriptor data. + # + # For most types of USB interface, this function doesn't need to be + # overriden. Only override if you need to append interface-specific + # descriptors before the first endpoint descriptor. To return an Interface + # Descriptor Association, on the first interface this function should + # return the IAD descriptor followed by the Interface descriptor. + # + # Parameters: + # + # - num_eps - number of endpoints in the interface, as returned by + # get_endpoint_descriptors() which is actually called before this + # function. + # + # - itf_idx - Interface index number for this interface. + # + # - str_idx - First string index number to assign for any string + # descriptor indexes included in the result. + # + # Result: + # + # Should be a 2-tuple: + # + # - Interface descriptor binary data, to return as part of the + # configuration descriptor. + # + # - List of any strings referenced in the interface descriptor data + # (indexes in the descriptor data should start from 'str_idx'.) + # + # See USB 2.0 specification section 9.6.5 p267 for standard interface descriptors. + desc = ustruct.pack( + "<" + "B" * _STD_DESC_INTERFACE_LEN, + _STD_DESC_INTERFACE_LEN, # bLength + _STD_DESC_INTERFACE_TYPE, # bDescriptorType + itf_idx, # bInterfaceNumber + 0, # bAlternateSetting, not currently supported + num_eps, + self.bInterfaceClass, + self.bInterfaceSubClass, + self.bInterfaceProtocol, + str_idx if self.interface_str else 0, # iInterface + ) + strs = [self.interface_str] if self.interface_str else [] + + return (desc, strs) + + def get_endpoint_descriptors(self, ep_addr, str_idx): + # Similar to get_itf_descriptor, returns descriptors for any endpoints + # in this interface, plus associated other configuration descriptor data. + # + # The base class returns no endpoints, so usually this is overriden in the subclass. + # + # This function is called any time the host asks for a configuration + # descriptor. It is actually called before get_itf_descriptor(), so that + # the number of endpoints is known. + # + # Parameters: + # + # - ep_addr - Address for this endpoint, without any utils.EP_IN_FLAG (0x80) bit set. + # - str_idx - Index to use for the first string descriptor in the result, if any. + # + # Result: + # + # Should be a 3-tuple: + # + # - Endpoint descriptor binary data and associated other descriptors for + # the endpoint, to return as part of the configuration descriptor. + # + # - List of any strings referenced in the descriptor data (indexes in the + # descriptor data should start from 'str_idx'.) + # + # - List of endpoint addresses referenced in the descriptor data (should + # start from ep_addr, optionally with the utils.EP_IN_FLAG bit set.) + return (b"", [], []) + + def handle_device_control_xfer(self, stage, request): + # Control transfer callback. Override to handle a non-standard device + # control transfer where bmRequestType Recipient is Device, Type is + # utils.REQ_TYPE_CLASS, and the lower byte of wIndex indicates this interface. + # + # (See USB 2.0 specification 9.4 Standard Device Requests, p250). + # + # This particular request type seems pretty uncommon for a device class + # driver to need to handle, most hosts will not send this so most + # implementations won't need to override it. + # + # Parameters: + # + # - stage is one of utils.STAGE_SETUP, utils.STAGE_DATA, utils.STAGE_ACK. + # - request is a tuple of (bmRequestType, bRequest, wValue, wIndex, + # - wLength), as per USB 2.0 specification 9.3 USB Device Requests, p250. + # + # The function can call split_bmRequestType() to split bmRequestType into + # (Recipient, Type, Direction). + # + # Result: + # + # - True to continue the request False to STALL the endpoint A buffer + # - interface object to provide a buffer to the host as part of the + # - transfer, if possible. + return False + + def handle_interface_control_xfer(self, stage, request): + # Control transfer callback. Override to handle a device control + # transfer where bmRequestType Recipient is Interface, and the lower byte + # of wIndex indicates this interface. + # + # (See USB 2.0 specification 9.4 Standard Device Requests, p250). + # + # bmRequestType Type field may have different values. It's not necessary + # to handle the mandatory Standard requests (bmRequestType Type == + # utils.REQ_TYPE_STANDARD), if the driver returns False in these cases then + # TinyUSB will provide the necessary responses. + # + # See handle_device_control_xfer() for a description of the arguments and + # possible return values. + return False + + def handle_endpoint_control_xfer(self, stage, request): + # Control transfer callback. Override to handle a device + # control transfer where bmRequestType Recipient is Endpoint and + # the lower byte of wIndex indicates an endpoint address associated with this interface. + # + # bmRequestType Type will generally have any value except + # utils.REQ_TYPE_STANDARD, as Standard endpoint requests are handled by + # TinyUSB. The exception is the the Standard "Set Feature" request. This + # is handled by Tiny USB but also passed through to the driver in case it + # needs to change any internal state, but most drivers can ignore and + # return False in this case. + # + # (See USB 2.0 specification 9.4 Standard Device Requests, p250). + # + # See handle_device_control_xfer() for a description of the parameters and + # possible return values. + return False + + def submit_xfer(self, ep_addr, data, done_cb=None): + # Submit a USB transfer (of any type except control) + # + # Parameters: + # + # - ep_addr. Address of the endpoint to submit the transfer on. Caller is + # responsible for ensuring that ep_addr is correct and belongs to this + # interface. Only one transfer can be active at a time on each endpoint. + # + # - data. Buffer containing data to send, or for data to be read into + # (depending on endpoint direction). + # + # - done_cb. Optional callback function for when the transfer + # completes. The callback is called with arguments (ep_addr, result, + # xferred_bytes) where result is one of xfer_result_t enum (see top of + # this file), and xferred_bytes is an integer. + # + # Note that done_cb may be called immediately, possibly before this + # function has returned to the caller. + return get_usbdevice()._submit_xfer(ep_addr, data, done_cb) diff --git a/micropython/usbd/hid.py b/micropython/usbd/hid.py new file mode 100644 index 000000000..f6f3ec6ae --- /dev/null +++ b/micropython/usbd/hid.py @@ -0,0 +1,255 @@ +# MicroPython USB hid module +# MIT license; Copyright (c) 2023 Angus Gratton +from .device import ( + USBInterface, +) +from .utils import ( + endpoint_descriptor, + split_bmRequestType, + EP_IN_FLAG, + STAGE_SETUP, + REQ_TYPE_STANDARD, + REQ_TYPE_CLASS, +) +from micropython import const +import ustruct + +_DESC_HID_TYPE = const(0x21) +_DESC_REPORT_TYPE = const(0x22) +_DESC_PHYSICAL_TYPE = const(0x23) + +_INTERFACE_CLASS = const(0x03) +_INTERFACE_SUBCLASS_NONE = const(0x00) +_INTERFACE_SUBCLASS_BOOT = const(0x01) + +_INTERFACE_PROTOCOL_NONE = const(0x00) +_INTERFACE_PROTOCOL_KEYBOARD = const(0x01) +_INTERFACE_PROTOCOL_MOUSE = const(0x02) + +# bRequest values for HID control requests +_REQ_CONTROL_GET_REPORT = const(0x01) +_REQ_CONTROL_GET_IDLE = const(0x02) +_REQ_CONTROL_GET_PROTOCOL = const(0x03) +_REQ_CONTROL_GET_DESCRIPTOR = const(0x06) +_REQ_CONTROL_SET_REPORT = const(0x09) +_REQ_CONTROL_SET_IDLE = const(0x0A) +_REQ_CONTROL_SET_PROTOCOL = const(0x0B) + + +class HIDInterface(USBInterface): + # Abstract base class to implement a USB device HID interface in Python. + + def __init__( + self, + report_descriptor, + extra_descriptors=[], + protocol=_INTERFACE_PROTOCOL_NONE, + interface_str=None, + ): + # Construct a new HID interface. + # + # - report_descriptor is the only mandatory argument, which is the binary + # data consisting of the HID Report Descriptor. See Device Class + # Definition for Human Interface Devices (HID) v1.11 section 6.2.2 Report + # Descriptor, p23. + # + # - extra_descriptors is an optional argument holding additional HID + # descriptors, to append after the mandatory report descriptor. Most + # HID devices do not use these. + # + # - protocol can be set to a specific value as per HID v1.11 section 4.3 Protocols, p9. + # + # - interface_str is an optional string descriptor to associate with the HID USB interface. + super().__init__(_INTERFACE_CLASS, _INTERFACE_SUBCLASS_NONE, protocol, interface_str) + self.extra_descriptors = extra_descriptors + self.report_descriptor = report_descriptor + self._int_ep = None # set during enumeration + + def get_report(self): + return False + + def send_report(self, report_data): + # Helper function to send a HID report in the typical USB interrupt + # endpoint associated with a HID interface. return + self.submit_xfer(self._int_ep, report_data) + + def get_endpoint_descriptors(self, ep_addr, str_idx): + # Return the typical single USB interrupt endpoint descriptor associated + # with a HID interface. + # + # As per HID v1.11 section 7.1 Standard Requests, return the contents of + # the standard HID descriptor before the associated endpoint descriptor. + desc = self.get_hid_descriptor() + ep_addr |= EP_IN_FLAG + desc += endpoint_descriptor(ep_addr, "interrupt", 8, 8) + self.idle_rate = 0 + self.protocol = 0 + self._int_ep = ep_addr + return (desc, [], [ep_addr]) + + def get_hid_descriptor(self): + # Generate a full USB HID descriptor from the object's report descriptor + # and optional additional descriptors. + # + # See HID Specification Version 1.1, Section 6.2.1 HID Descriptor p22 + result = ustruct.pack( + "> 8 + if desc_type == _DESC_HID_TYPE: + return self.get_hid_descriptor() + if desc_type == _DESC_REPORT_TYPE: + return self.report_descriptor + elif req_type == REQ_TYPE_CLASS: + # HID Spec p50: 7.2 Class-Specific Requests + if bRequest == _REQ_CONTROL_GET_REPORT: + return False # Unsupported for now + if bRequest == _REQ_CONTROL_GET_IDLE: + return bytes([self.idle_rate]) + if bRequest == _REQ_CONTROL_GET_PROTOCOL: + return bytes([self.protocol]) + if bRequest == _REQ_CONTROL_SET_IDLE: + self.idle_rate = wValue >> 8 + return b"" + if bRequest == _REQ_CONTROL_SET_PROTOCOL: + self.protocol = wValue + return b"" + return False # Unsupported + + +# Basic 3-button mouse HID Report Descriptor. +# This is cribbed from Appendix E.10 of the HID v1.11 document. +_MOUSE_REPORT_DESC = bytes( + [ + 0x05, + 0x01, # Usage Page (Generic Desktop) + 0x09, + 0x02, # Usage (Mouse) + 0xA1, + 0x01, # Collection (Application) + 0x09, + 0x01, # Usage (Pointer) + 0xA1, + 0x00, # Collection (Physical) + 0x05, + 0x09, # Usage Page (Buttons) + 0x19, + 0x01, # Usage Minimum (01), + 0x29, + 0x03, # Usage Maximun (03), + 0x15, + 0x00, # Logical Minimum (0), + 0x25, + 0x01, # Logical Maximum (1), + 0x95, + 0x03, # Report Count (3), + 0x75, + 0x01, # Report Size (1), + 0x81, + 0x02, # Input (Data, Variable, Absolute), ;3 button bits + 0x95, + 0x01, # Report Count (1), + 0x75, + 0x05, # Report Size (5), + 0x81, + 0x01, # Input (Constant), ;5 bit padding + 0x05, + 0x01, # Usage Page (Generic Desktop), + 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, Variable, Relative), ;2 position bytes (X & Y) + 0xC0, # End Collection, + 0xC0, # End Collection + ] +) + + +class MouseInterface(HIDInterface): + # Very basic synchronous USB mouse HID interface + # TODO: This should be in a different package or an example + + def __init__(self): + super().__init__( + _MOUSE_REPORT_DESC, + protocol=_INTERFACE_PROTOCOL_MOUSE, + interface_str="MP Mouse!", + ) + self._l = False # Left button + self._m = False # Middle button + self._r = False # Right button + + def send_report(self, dx=0, dy=0): + b = 0 + if self._l: + b |= 1 << 0 + if self._r: + b |= 1 << 1 + if self._m: + b |= 1 << 2 + # Note: This allocates the bytes object 'report' each time a report is + # sent. + # + # However, at the moment the base class doesn't keep track of each + # transfer after it's submitted. So reusing a bytearray() creates a risk + # of a race condition if a new report transfer is submitted using the + # same buffer, before the previous one has completed. + report = ustruct.pack("Bbb", b, dx, dy) + + super().send_report(report) + + def click_left(self, down=True): + self._l = down + self.send_report() + + def click_middle(self, down=True): + self._m = down + self.send_report() + + def click_right(self, down=True): + self._r = down + self.send_report() + + def move_by(self, dx, dy): + # dx, dy are -127, 127 in range + self.send_report(dx, dy) diff --git a/micropython/usbd/utils.py b/micropython/usbd/utils.py new file mode 100644 index 000000000..017019575 --- /dev/null +++ b/micropython/usbd/utils.py @@ -0,0 +1,77 @@ +# MicroPython USB utility functions +# MIT license; Copyright (c) 2023 Angus Gratton +# +# Some constants and stateless utility functions for working with USB descriptors and requests. +from micropython import const +import ustruct + +# Shared constants +# +# It's a tough decision of when to make a constant "shared" like this. "Private" constants have no resource use, but these will take up flash space for the name. Suggest deciding on basis of: +# +# - Is this constant used in a lot of places, including potentially by users +# of this package? +# +# Otherwise, it's not the greatest sin to be copy-pasting "private" constants +# in a couple of places. I guess. :/ + +EP_IN_FLAG = const(1 << 7) + +# Control transfer stages +STAGE_IDLE = const(0) +STAGE_SETUP = const(1) +STAGE_DATA = const(2) +STAGE_ACK = const(3) + +# Request types +REQ_TYPE_STANDARD = const(0x0) +REQ_TYPE_CLASS = const(0x1) +REQ_TYPE_VENDOR = const(0x2) +REQ_TYPE_RESERVED = const(0x3) + +# TinyUSB xfer_result_t enum +RESULT_SUCCESS = const(0) +RESULT_FAILED = const(1) +RESULT_STALLED = const(2) +RESULT_TIMEOUT = const(3) +RESULT_INVALID = const(4) + + +# Non-shared constants, used in this function only +_STD_DESC_ENDPOINT_LEN = const(7) +_STD_DESC_ENDPOINT_TYPE = const(0x5) + + +def endpoint_descriptor(bEndpointAddress, bmAttributes, wMaxPacketSize, bInterval=1): + # Utility function to generate a standard Endpoint descriptor bytes object, with + # the properties specified in the parameter list. + # + # See USB 2.0 specification section 9.6.6 Endpoint p269 + # + # As well as a numeric value, bmAttributes can be a string value to represent + # common endpoint types: "control", "bulk", "interrupt". + bmAttributes = {"control": 0, "bulk": 2, "interrupt": 3}.get(bmAttributes, bmAttributes) + return ustruct.pack( + "> 5) & 0x03, + (bmRequestType >> 7) & 0x01, + ) From 581a662762e7e257071a339d4ed66942419bdd3e Mon Sep 17 00:00:00 2001 From: Angus Gratton Date: Thu, 9 Feb 2023 14:29:26 +1100 Subject: [PATCH 02/17] usbd: Add midi interface definition from @paulhamsh. Based on https://github.com/paulhamsh/Micropython-Midi-Device as of commit 2678d13. With additions/edits by me. --- micropython/usbd/midi.py | 306 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 306 insertions(+) create mode 100644 micropython/usbd/midi.py diff --git a/micropython/usbd/midi.py b/micropython/usbd/midi.py new file mode 100644 index 000000000..a72380b47 --- /dev/null +++ b/micropython/usbd/midi.py @@ -0,0 +1,306 @@ +# MicroPython USB MIDI module +# MIT license; Copyright (c) 2023 Angus Gratton, Paul Hamshere +from micropython import const +import ustruct + +from .device import USBInterface +from .utils import endpoint_descriptor, EP_IN_FLAG + +_INTERFACE_CLASS_AUDIO = const(0x01) +_INTERFACE_SUBCLASS_AUDIO_CONTROL = const(0x01) +_INTERFACE_SUBCLASS_AUDIO_MIDISTREAMING = const(0x03) +_PROTOCOL_NONE = const(0x00) + +_JACK_TYPE_EMBEDDED = const(0x01) +_JACK_TYPE_EXTERNAL = const(0x02) + + +class RingBuf: + def __init__(self, size): + self.data = bytearray(size) + self.size = size + self.index_put = 0 + self.index_get = 0 + + def put(self, value): + next_index = (self.index_put + 1) % self.size + # check for overflow + if self.index_get != next_index: + self.data[self.index_put] = value + self.index_put = next_index + return value + else: + return None + + def get(self): + if self.index_get == self.index_put: + return None # buffer empty + else: + value = self.data[self.index_get] + self.index_get = (self.index_get + 1) % self.size + return value + + def is_empty(self): + return self.index_get == self.index_put + + +class DummyAudioInterface(USBInterface): + # An Audio Class interface is mandatory for MIDI Interfaces as well, this + # class implements the minimum necessary for this. + def __init__(self): + super().__init__(_INTERFACE_CLASS_AUDIO, _INTERFACE_SUBCLASS_AUDIO_CONTROL, _PROTOCOL_NONE) + + def get_itf_descriptor(self, num_eps, itf_idx, str_idx): + # Return the MIDI USB interface descriptors. + + # Get the parent interface class + desc, strs = super().get_itf_descriptor(num_eps, itf_idx, str_idx) + + # Append the class-specific AudioControl interface descriptor + desc += ustruct.pack( + "Device) + # * Data goes via an Embedded MIDI IN Jack ("into" the USB-MIDI device) + # * Data goes out via a virtual External MIDI OUT Jack ("out" of the + # USB-MIDI device and into the world). This "out" jack may be + # theoretical, and only exists in the USB descriptor. + # + # - For each tx (total _num_tx), we have data flowing from the USB MIDI + # device to the USB host: + # * Data comes in via a virtual External MIDI IN Jack (from the + # outside world, theoretically) + # * Data goes via an Embedded MIDI OUT Jack ("out" of the USB-MIDI + # device). + # * Data goes into the host via MIDI IN Endpoint (Device->Host) + + # rx side + for idx in range(self._num_rx): + emb_id = self._emb_id(False, idx) + ext_id = emb_id + 1 + pin = idx + 1 + jacks += jack_in_desc(_JACK_TYPE_EMBEDDED, emb_id) # bJackID) + jacks += jack_out_desc( + _JACK_TYPE_EXTERNAL, + ext_id, # bJackID + emb_id, # baSourceID(1) + pin, # baSourcePin(1) + ) + + # tx side + for idx in range(self._num_tx): + emb_id = self._emb_id(True, idx) + ext_id = emb_id + 1 + pin = idx + 1 + + jacks += jack_in_desc( + _JACK_TYPE_EXTERNAL, + ext_id, # bJackID + ) + jacks += jack_out_desc( + _JACK_TYPE_EMBEDDED, + emb_id, + ext_id, # baSourceID(1) + pin, # baSourcePin(1) + ) + + iface = desc + cs_ms_interface + jacks + return (iface, strs) + + def _emb_id(self, is_tx, idx): + # Given a direction (False==rx, True==tx) and a 0-index + # of the MIDI connection, return the embedded JackID value. + # + # Embedded JackIDs take odd numbers 1,3,5,etc with all + # 'RX' jack numbers first and then all 'TX' jack numbers + # (see long comment above for explanation of RX, TX in + # this context.) + # + # This is used to keep jack IDs in sync between + # get_itf_descriptor() and get_endpoint_descriptors() + return 1 + 2 * (idx + (is_tx * self._num_rx)) + + def get_endpoint_descriptors(self, ep_addr, str_idx): + # One MIDI endpoint in each direction, plus the + # associated CS descriptors + + # The following implementation is *very* memory inefficient + # and needs optimising + + self.ep_out = ep_addr + 1 + self.ep_in = ep_addr + 2 | EP_IN_FLAG + + # rx side, USB "in" endpoint and embedded MIDI IN Jacks + e_out = endpoint_descriptor(self.ep_in, "bulk", 64, 0) + cs_out = ustruct.pack( + " Date: Tue, 14 Feb 2023 18:01:33 +1100 Subject: [PATCH 03/17] usbd: Major cleanup, refactor. - Add micropython-lib 'usbd' package (provisional). - Update midi implementation a bit. - Rearrange code to work with package structure - Convert docstrings to regular comments to save flash. --- micropython/usbd/manifest.py | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 micropython/usbd/manifest.py diff --git a/micropython/usbd/manifest.py b/micropython/usbd/manifest.py new file mode 100644 index 000000000..989174eee --- /dev/null +++ b/micropython/usbd/manifest.py @@ -0,0 +1,9 @@ +metadata(version="0.1.0") + +# TODO: split off parts of this to optional sub-packages, most people won't need +# all interface classes +package( + "usbd", + files=("__init__.py", "device.py", "hid.py", "midi.py", "utils.py"), + base_path="..", +) From 69e527bbcc12c82db765410c62c38cfcf854b291 Mon Sep 17 00:00:00 2001 From: Martin Fischer Date: Tue, 25 Apr 2023 12:45:57 +0200 Subject: [PATCH 04/17] chore: add premlinenary CDC interfaces --- micropython/usbd/cdc.py | 86 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 micropython/usbd/cdc.py diff --git a/micropython/usbd/cdc.py b/micropython/usbd/cdc.py new file mode 100644 index 000000000..4f490decc --- /dev/null +++ b/micropython/usbd/cdc.py @@ -0,0 +1,86 @@ +# MicroPython USB CDC module +# MIT license; Copyright (c) 2022 Martin Fischer +from .device import ( + USBInterface, + get_usbdevice +) +from .utils import ( + endpoint_descriptor, + EP_OUT_FLAG +) +from micropython import const +import ustruct + +_DEV_CLASS_MISC = const(0xef) +_CS_DESC_TYPE = const(0x24) # CS Interface type communication descriptor +_ITF_ASSOCIATION_DESC_TYPE = const(0xb) # Interface Association descriptor + +# CDC control interface definitions +_CDC_ITF_CONTROL_CLASS = const(2) +_CDC_ITF_CONTROL_SUBCLASS = const(2) # Abstract Control Mode +_CDC_ITF_CONTROL_PROT = const(0) # no protocol + +# CDC data interface definitions +_CDC_ITF_DATA_CLASS = const(0xa) +_CDC_ITF_DATA_SUBCLASS = const(0) +_CDC_ITF_DATA_PROT = const(0) # no protocol + + +def setup_CDC_device(): + # CDC is a composite device, consisting of multiple interfaces + # (CDC control and CDC data) + # therefore we have to make sure that the association descriptor + # is set and that it associates both interfaces to the logical cdc class + usb_device = get_usbdevice() + usb_device.device_class = _DEV_CLASS_MISC + usb_device.device_subclass = 2 + usb_device.device_protocol = 1 # Itf association descriptor + + +class CDCControlInterface(USBInterface): + # Implements the CDC Control Interface + + def __init__(self, interface_str): + super().__init__(_CDC_ITF_CONTROL_CLASS, _CDC_ITF_CONTROL_SUBCLASS, + _CDC_ITF_CONTROL_PROT) + + def get_itf_descriptor(self, num_eps, itf_idx, str_idx): + # CDC needs a Interface Association Descriptor (IAD) + # first interface is zero, two interfaces in total + desc = ustruct.pack(" Date: Thu, 8 Jun 2023 14:24:59 +0200 Subject: [PATCH 05/17] usbd: Add cdc example and a basic read function. --- micropython/usbd/cdc.py | 31 ++++++++++++++++++++++++++----- micropython/usbd/cdc_example.py | 14 ++++++++++++++ 2 files changed, 40 insertions(+), 5 deletions(-) create mode 100644 micropython/usbd/cdc_example.py diff --git a/micropython/usbd/cdc.py b/micropython/usbd/cdc.py index 4f490decc..62a5a29c9 100644 --- a/micropython/usbd/cdc.py +++ b/micropython/usbd/cdc.py @@ -10,6 +10,7 @@ ) from micropython import const import ustruct +import time _DEV_CLASS_MISC = const(0xef) _CS_DESC_TYPE = const(0x24) # CS Interface type communication descriptor @@ -69,18 +70,38 @@ def get_endpoint_descriptors(self, ep_addr, str_idx): class CDCDataInterface(USBInterface): # Implements the CDC Data Interface - def __init__(self, interface_str): + def __init__(self, interface_str, timeout=1): super().__init__(_CDC_ITF_DATA_CLASS, _CDC_ITF_DATA_SUBCLASS, _CDC_ITF_DATA_PROT) + self.rx_buf = bytearray(256) + self.mv_buf = memoryview(self.rx_buf) + self.rx_done = False + self.rx_nbytes = 0 + self.timeout = timeout def get_endpoint_descriptors(self, ep_addr, str_idx): # XXX OUT = 0x00 but is defined as 0x80? self.ep_in = (ep_addr + 2) | EP_OUT_FLAG self.ep_out = (ep_addr + 2) & ~EP_OUT_FLAG - print("cdc in={} out={}".format(self.ep_in, self.ep_out)) # one IN / OUT Endpoint e_out = endpoint_descriptor(self.ep_out, "bulk", 64, 0) e_in = endpoint_descriptor(self.ep_in, "bulk", 64, 0) - desc = e_out + e_in - return (desc, [], (self.ep_out, self.ep_in)) - + return (e_out + e_in, [], (self.ep_out, self.ep_in)) + + def write(self, data): + super().submit_xfer(self.ep_in, data) + + def read(self, nbytes=0): + # XXX PoC.. When returning, it should probably + # copy it to a ringbuffer instead of leaving it here + super().submit_xfer(self.ep_out, self.rx_buf, self._cb_rx) + now = time.time() + self.rx_done = False + self.rx_nbytes = 0 + while ((time.time() - now) < self.timeout) and not self.rx_done: + time.sleep_ms(10) + return bytes(self.mv_buf[:self.rx_nbytes]) if self.rx_done else None + + def _cb_rx(self, ep, res, num_bytes): + self.rx_done = True + self.rx_nbytes = num_bytes diff --git a/micropython/usbd/cdc_example.py b/micropython/usbd/cdc_example.py new file mode 100644 index 000000000..f2edc91c8 --- /dev/null +++ b/micropython/usbd/cdc_example.py @@ -0,0 +1,14 @@ +from usbd import device, cdc + +ud = device.get_usbdevice() +cdc.setup_CDC_device() +ctrl_cdc = cdc.CDCControlInterface('') +data_cdc = cdc.CDCDataInterface('') +ud.add_interface(ctrl_cdc) +ud.add_interface(data_cdc) +ud.reenumerate() + +# sending something over CDC +data_cdc.write(b'Hello World') +# receiving something.. +print(data_cdc.read(10)) From 02795f1d50fb695e10a4c8b566434cfea2b5e85e Mon Sep 17 00:00:00 2001 From: Martin Fischer Date: Wed, 19 Jul 2023 22:12:40 +0200 Subject: [PATCH 06/17] chore(cdc): updates regarding EP_IN_FLAG --- micropython/usbd/cdc.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/micropython/usbd/cdc.py b/micropython/usbd/cdc.py index 62a5a29c9..e19337ffa 100644 --- a/micropython/usbd/cdc.py +++ b/micropython/usbd/cdc.py @@ -6,7 +6,7 @@ ) from .utils import ( endpoint_descriptor, - EP_OUT_FLAG + EP_IN_FLAG ) from micropython import const import ustruct @@ -63,8 +63,8 @@ def get_itf_descriptor(self, num_eps, itf_idx, str_idx): return desc, strs def get_endpoint_descriptors(self, ep_addr, str_idx): - self.ep_in = endpoint_descriptor((ep_addr + 1) | EP_OUT_FLAG, "interrupt", 8, 16) - return (self.ep_in, [], ((ep_addr+1) | EP_OUT_FLAG,)) + self.ep_in = endpoint_descriptor((ep_addr + 1) | EP_IN_FLAG, "interrupt", 8, 16) + return (self.ep_in, [], ((ep_addr+1) | EP_IN_FLAG,)) class CDCDataInterface(USBInterface): @@ -80,9 +80,8 @@ def __init__(self, interface_str, timeout=1): self.timeout = timeout def get_endpoint_descriptors(self, ep_addr, str_idx): - # XXX OUT = 0x00 but is defined as 0x80? - self.ep_in = (ep_addr + 2) | EP_OUT_FLAG - self.ep_out = (ep_addr + 2) & ~EP_OUT_FLAG + self.ep_in = (ep_addr + 2) | EP_IN_FLAG + self.ep_out = (ep_addr + 2) & ~EP_IN_FLAG # one IN / OUT Endpoint e_out = endpoint_descriptor(self.ep_out, "bulk", 64, 0) e_in = endpoint_descriptor(self.ep_in, "bulk", 64, 0) From 9d4d843aad75cf032a62d79614e6665ca771d3ff Mon Sep 17 00:00:00 2001 From: Angus Gratton Date: Tue, 25 Jul 2023 15:26:53 +1000 Subject: [PATCH 07/17] usbd: Add support for configuration open and reset callbacks. Implement by overriding USBInterface.handle_open or handle_reset. --- micropython/usbd/device.py | 72 +++++++++++++++++++++++++++++++++----- 1 file changed, 63 insertions(+), 9 deletions(-) diff --git a/micropython/usbd/device.py b/micropython/usbd/device.py index 7045126ec..13a4828d4 100644 --- a/micropython/usbd/device.py +++ b/micropython/usbd/device.py @@ -78,7 +78,8 @@ def __init__(self): descriptor_device_cb=self._descriptor_device_cb, descriptor_config_cb=self._descriptor_config_cb, descriptor_string_cb=self._descriptor_string_cb, - open_driver_cb=self._open_driver_cb, + open_cb=self._open_cb, + reset_cb=self._reset_cb, control_xfer_cb=self._control_xfer_cb, xfer_cb=self._xfer_cb, ) @@ -286,9 +287,37 @@ def _descriptor_string_cb(self, index): except IndexError: return None - def _open_driver_cb(self, interface_desc_view): - # Singleton callback from TinyUSB custom class driver - pass + def _open_cb(self, interface_desc_view): + # Singleton callback from TinyUSB custom class driver, when USB host does + # Set Configuration. The "runtime class device" accepts all interfaces that + # it has sent in descriptors, and calls this callback. + + # Walk the view of the "claimed" descriptor data provided in the + # callback and call handle_open() on each claimed interface + # + # ... this may be unnecessary at the moment, as only one configuration is supported so we + # can probably assume all the interfaces will be included. + i = 0 + while i < len(interface_desc_view): + # descriptor length, type, and index (if it's an interface descriptor) + dl, dt, di = interface_desc_view[i:i+3] + if dt == _STD_DESC_INTERFACE_TYPE: + if di >= self._usbd.static.itf_max: + di -= self._usbd.static.itf_max + self._itfs[di].handle_open() + i += dl + assert dl + + def _reset_cb(self): + # Callback when the USB device is reset by the host + + # Cancel outstanding transfer callbacks + for k in self._ep_cbs.keys(): + self._ep_cbs[k] = None + + # Allow interfaces to respond to the reset + for itf in self._itfs: + itf.handle_reset() def _submit_xfer(self, ep_addr, data, done_cb=None): # Singleton function to submit a USB transfer (of any type except control). @@ -387,6 +416,7 @@ def __init__( self.bInterfaceSubClass = bInterfaceSubClass self.bInterfaceProtocol = bInterfaceProtocol self.interface_str = interface_str + self._open = False def get_itf_descriptor(self, num_eps, itf_idx, str_idx): # Return the interface descriptor binary data and associated other @@ -466,6 +496,27 @@ def get_endpoint_descriptors(self, ep_addr, str_idx): # start from ep_addr, optionally with the utils.EP_IN_FLAG bit set.) return (b"", [], []) + def handle_open(self): + # Callback called when the USB host accepts the device configuration. + # + # Override this function to initiate any operations that the USB interface + # should do when the USB device is configured to the host. + self._open = True + + def handle_reset(self): + # Callback called on every registered interface when the USB device is + # reset by the host. This can happen when the USB device is unplugged, + # or if the host triggers a reset for some other reason. + # + # At this point, no USB functionality is available - handle_open() will + # be called later if/when the USB host re-enumerates and configures the + # interface. + self._open = False + + def is_open(self): + # Returns True if the interface is in use + return self._open + def handle_device_control_xfer(self, stage, request): # Control transfer callback. Override to handle a non-standard device # control transfer where bmRequestType Recipient is Device, Type is @@ -486,11 +537,11 @@ def handle_device_control_xfer(self, stage, request): # The function can call split_bmRequestType() to split bmRequestType into # (Recipient, Type, Direction). # - # Result: + # Result, any of: # - # - True to continue the request False to STALL the endpoint A buffer - # - interface object to provide a buffer to the host as part of the - # - transfer, if possible. + # - True to continue the request, False to STALL the endpoint. + # - Buffer interface object to provide a buffer to the host as part of the + # transfer, if possible. return False def handle_interface_control_xfer(self, stage, request): @@ -512,7 +563,8 @@ def handle_interface_control_xfer(self, stage, request): def handle_endpoint_control_xfer(self, stage, request): # Control transfer callback. Override to handle a device # control transfer where bmRequestType Recipient is Endpoint and - # the lower byte of wIndex indicates an endpoint address associated with this interface. + # the lower byte of wIndex indicates an endpoint address associated + # with this interface. # # bmRequestType Type will generally have any value except # utils.REQ_TYPE_STANDARD, as Standard endpoint requests are handled by @@ -546,4 +598,6 @@ def submit_xfer(self, ep_addr, data, done_cb=None): # # Note that done_cb may be called immediately, possibly before this # function has returned to the caller. + if not self._open: + raise RuntimeError() return get_usbdevice()._submit_xfer(ep_addr, data, done_cb) From 29e918543952bbadca22270d499ce18bd0ffd8ee Mon Sep 17 00:00:00 2001 From: Angus Gratton Date: Tue, 25 Jul 2023 16:26:43 +1000 Subject: [PATCH 08/17] usbd: Add USB interface functions for endpoint STALL support. Necessary for MSC device class, possibly other purposes. --- micropython/usbd/device.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/micropython/usbd/device.py b/micropython/usbd/device.py index 13a4828d4..c757cae47 100644 --- a/micropython/usbd/device.py +++ b/micropython/usbd/device.py @@ -508,6 +508,9 @@ def handle_reset(self): # reset by the host. This can happen when the USB device is unplugged, # or if the host triggers a reset for some other reason. # + # Override this function to cancel any pending operations specific to + # the interface (outstanding USB transfers are already cancelled). + # # At this point, no USB functionality is available - handle_open() will # be called later if/when the USB host re-enumerates and configures the # interface. @@ -601,3 +604,22 @@ def submit_xfer(self, ep_addr, data, done_cb=None): if not self._open: raise RuntimeError() return get_usbdevice()._submit_xfer(ep_addr, data, done_cb) + + def set_ep_stall(self, ep_addr, stall): + # Set or clear endpoint STALL state, according to the bool "stall" parameter. + # + # Generally endpoint STALL is handled automatically by TinyUSB, but + # there are some device classes that need to explicitly stall or unstall + # an endpoint under certain conditions. + if not self._open or ep_addr not in get_usbdevice()._eps: + raise RuntimeError() + get_usbdevice()._usbd.set_ep_stall(ep_addr, stall) + + def get_ep_stall(self, ep_addr): + # Get the current endpoint STALL state. + # + # Endpoint can be stalled/unstalled by host, TinyUSB stack, or calls to + # set_ep_stall(). + if not self._open or ep_addr not in get_usbdevice()._eps: + raise RuntimeError() + return get_usbdevice()._usbd.get_ep_stall(ep_addr) From 9d7ce9fe37a79717732799adc4641a3640b99741 Mon Sep 17 00:00:00 2001 From: Angus Gratton Date: Wed, 26 Jul 2023 11:16:07 +1000 Subject: [PATCH 09/17] usbd: Implement SET_REPORT support for OUT direction HID data. --- micropython/usbd/hid.py | 100 +++++++++++++++++++++++++++++----------- 1 file changed, 73 insertions(+), 27 deletions(-) diff --git a/micropython/usbd/hid.py b/micropython/usbd/hid.py index f6f3ec6ae..28260c78c 100644 --- a/micropython/usbd/hid.py +++ b/micropython/usbd/hid.py @@ -8,6 +8,7 @@ split_bmRequestType, EP_IN_FLAG, STAGE_SETUP, + STAGE_DATA, REQ_TYPE_STANDARD, REQ_TYPE_CLASS, ) @@ -43,6 +44,7 @@ def __init__( self, report_descriptor, extra_descriptors=[], + set_report_buf=None, protocol=_INTERFACE_PROTOCOL_NONE, interface_str=None, ): @@ -57,17 +59,34 @@ def __init__( # descriptors, to append after the mandatory report descriptor. Most # HID devices do not use these. # + # - set_report_buf is an optional writable buffer object (i.e. + # bytearray), where SET_REPORT requests from the host can be + # written. Only necessary if the report_descriptor contains Output + # entries. If set, the size must be at least the size of the largest + # Output entry. + # # - protocol can be set to a specific value as per HID v1.11 section 4.3 Protocols, p9. # # - interface_str is an optional string descriptor to associate with the HID USB interface. super().__init__(_INTERFACE_CLASS, _INTERFACE_SUBCLASS_NONE, protocol, interface_str) self.extra_descriptors = extra_descriptors self.report_descriptor = report_descriptor + self._set_report_buf = set_report_buf self._int_ep = None # set during enumeration def get_report(self): return False + def handle_set_report(self, report_data, report_id, report_type): + # Override this function in order to handle SET REPORT requests from the host, + # where it sends data to the HID device. + # + # This function will only be called if the Report descriptor contains at least one Output entry, + # and the set_report_buf argument is provided to the constructor. + # + # Return True to complete the control transfer normally, False to abort it. + return True + def send_report(self, report_data): # Helper function to send a HID report in the typical USB interrupt # endpoint associated with a HID interface. return @@ -115,36 +134,63 @@ def get_hid_descriptor(self): def handle_interface_control_xfer(self, stage, request): # Handle standard and class-specific interface control transfers for HID devices. - bmRequestType, bRequest, wValue, _, _ = request + bmRequestType, bRequest, wValue, _, wLength = request recipient, req_type, _ = split_bmRequestType(bmRequestType) - if stage != STAGE_SETUP: - return True # allow request DATA/ACK stages to complete normally - - if req_type == REQ_TYPE_STANDARD: - # HID Spec p48: 7.1 Standard Requests - if bRequest == _REQ_CONTROL_GET_DESCRIPTOR: - desc_type = wValue >> 8 - if desc_type == _DESC_HID_TYPE: - return self.get_hid_descriptor() - if desc_type == _DESC_REPORT_TYPE: - return self.report_descriptor - elif req_type == REQ_TYPE_CLASS: - # HID Spec p50: 7.2 Class-Specific Requests - if bRequest == _REQ_CONTROL_GET_REPORT: - return False # Unsupported for now - if bRequest == _REQ_CONTROL_GET_IDLE: - return bytes([self.idle_rate]) - if bRequest == _REQ_CONTROL_GET_PROTOCOL: - return bytes([self.protocol]) - if bRequest == _REQ_CONTROL_SET_IDLE: - self.idle_rate = wValue >> 8 - return b"" - if bRequest == _REQ_CONTROL_SET_PROTOCOL: - self.protocol = wValue - return b"" - return False # Unsupported + if stage == STAGE_SETUP: + if req_type == REQ_TYPE_STANDARD: + # HID Spec p48: 7.1 Standard Requests + if bRequest == _REQ_CONTROL_GET_DESCRIPTOR: + desc_type = wValue >> 8 + if desc_type == _DESC_HID_TYPE: + return self.get_hid_descriptor() + if desc_type == _DESC_REPORT_TYPE: + return self.report_descriptor + elif req_type == REQ_TYPE_CLASS: + # HID Spec p50: 7.2 Class-Specific Requests + if bRequest == _REQ_CONTROL_GET_REPORT: + print("GET_REPORT?") + return False # Unsupported for now + if bRequest == _REQ_CONTROL_GET_IDLE: + return bytes([self.idle_rate]) + if bRequest == _REQ_CONTROL_GET_PROTOCOL: + return bytes([self.protocol]) + if bRequest == _REQ_CONTROL_SET_IDLE: + self.idle_rate = wValue >> 8 + return b"" + if bRequest == _REQ_CONTROL_SET_PROTOCOL: + self.protocol = wValue + return b"" + if bRequest == _REQ_CONTROL_SET_REPORT: + # Return the _set_report_buf to be filled with the + # report data + if not self._set_report_buf: + return False + elif wLength >= len(self._set_report_buf): + # Saves an allocation if the size is exactly right (or will be a short read) + return self._set_report_buf + else: + # Otherwise, need to wrap the buffer in a memoryview of the correct length + # + # TODO: check this is correct, maybe TinyUSB won't mind if we ask for more + # bytes than the host has offered us. + return memoryview(self._set_report_buf)[:wLength] + return False # Unsupported + + if stage == STAGE_DATA: + if req_type == REQ_TYPE_CLASS: + if bRequest == _REQ_CONTROL_SET_REPORT and self._set_report_buf: + report_id = wValue & 0xFF + report_type = wValue >> 8 + report_data = self._set_report_buf + if wLength < len(report_data): + # as above, need to truncate the buffer if we read less + # bytes than what was provided + report_data = memoryview(self._set_report_buf)[:wLength] + self.handle_set_report(report_data, report_id, report_type) + + return True # allow DATA/ACK stages to complete normally # Basic 3-button mouse HID Report Descriptor. From 92711eae134b37804c668e6fdf879ae53718a0a6 Mon Sep 17 00:00:00 2001 From: Angus Gratton Date: Wed, 26 Jul 2023 11:17:31 +1000 Subject: [PATCH 10/17] usbd: Rename ustruct->struct. --- micropython/usbd/device.py | 10 +++++----- micropython/usbd/hid.py | 8 ++++---- micropython/usbd/midi.py | 18 +++++++++--------- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/micropython/usbd/device.py b/micropython/usbd/device.py index c757cae47..617d3390c 100644 --- a/micropython/usbd/device.py +++ b/micropython/usbd/device.py @@ -2,7 +2,7 @@ # MIT license; Copyright (c) 2022 Angus Gratton from micropython import const import machine -import ustruct +import struct from .utils import split_bmRequestType, EP_IN_FLAG @@ -119,7 +119,7 @@ def _descriptor_device_cb(self): FMT = " Date: Wed, 26 Jul 2023 11:28:51 +1000 Subject: [PATCH 11/17] usbd: Add hid keypad example from @turmoni . As contributed in https://github.com/projectgus/micropython-lib/pull/1 commit 5c51a9ee70 This version of the hidkeypad module depends on some other code changes from the linked PR that aren't included here, so it won't work here yet. --- micropython/usbd/hidkeypad.py | 93 +++++++++++++++++++++++++++++++++++ micropython/usbd/keycodes.py | 24 +++++++++ 2 files changed, 117 insertions(+) create mode 100644 micropython/usbd/hidkeypad.py create mode 100644 micropython/usbd/keycodes.py diff --git a/micropython/usbd/hidkeypad.py b/micropython/usbd/hidkeypad.py new file mode 100644 index 000000000..bd0319fe5 --- /dev/null +++ b/micropython/usbd/hidkeypad.py @@ -0,0 +1,93 @@ +# MicroPython USB keypad module +# MIT license; Copyright (c) 2023 Dave Wickham + +from .hid import HIDInterface +from .keycodes import KEYPAD_KEYS_TO_KEYCODES +from .utils import STAGE_SETUP, split_bmRequestType +from micropython import const +import micropython + +_INTERFACE_PROTOCOL_KEYBOARD = const(0x01) +_REQ_CONTROL_SET_REPORT = const(0x09) +_REQ_CONTROL_SET_IDLE = const(0x0A) + +# fmt: off +_KEYPAD_REPORT_DESC = bytes( + [ + 0x05, 0x01, # Usage Page (Generic Desktop) + 0x09, 0x07, # Usage (Keypad) + 0xA1, 0x01, # Collection (Application) + 0x05, 0x07, # Usage Page (Keypad) + 0x19, 0x00, # Usage Minimum (00), + 0x29, 0xFF, # Usage Maximum (ff), + 0x15, 0x00, # Logical Minimum (0), + 0x25, 0xFF, # Logical Maximum (ff), + 0x95, 0x01, # Report Count (1), + 0x75, 0x08, # Report Size (8), + 0x81, 0x00, # Input (Data, Array, Absolute) + 0x05, 0x08, # Usage page (LEDs) + 0x19, 0x01, # Usage minimum (1) + 0x29, 0x05, # Usage Maximum (5), + 0x95, 0x05, # Report Count (5), + 0x75, 0x01, # Report Size (1), + 0x91, 0x02, # Output (Data, Variable, Absolute) + 0x95, 0x01, # Report Count (1), + 0x75, 0x03, # Report Size (3), + 0x91, 0x01, # Output (Constant) + 0xC0, # End Collection + ] +) +# fmt: on + + +class KeypadInterface(HIDInterface): + # Very basic synchronous USB keypad HID interface + + def __init__(self): + self.numlock = None + self.capslock = None + self.scrolllock = None + self.compose = None + self.kana = None + self.set_report_initialised = False + super().__init__( + _KEYPAD_REPORT_DESC, + protocol=_INTERFACE_PROTOCOL_KEYBOARD, + interface_str="MicroPython Keypad!", + use_out_ep=True, + ) + + def handle_interface_control_xfer(self, stage, request): + if request[1] == _REQ_CONTROL_SET_IDLE and not self.set_report_initialised: + # Hacky initialisation goes here + self.set_report() + self.set_report_initialised = True + + if stage == STAGE_SETUP: + return super().handle_interface_control_xfer(stage, request) + + bmRequestType, bRequest, wValue, _, _ = request + recipient, req_type, _ = split_bmRequestType(bmRequestType) + + return True + + def set_report(self, args=None): + self.out_buffer = bytearray(1) + self.submit_xfer(self._out_ep, self.out_buffer, self.set_report_cb) + return True + + def set_report_cb(self, ep_addr, result, xferred_bytes): + buf_result = int(self.out_buffer[0]) + self.numlock = buf_result & 1 + self.capslock = (buf_result >> 1) & 1 + self.scrolllock = (buf_result >> 2) & 1 + self.compose = (buf_result >> 3) & 1 + self.kana = (buf_result >> 4) & 1 + + micropython.schedule(self.set_report, None) + + def send_report(self, key=None): + if key is None: + super().send_report(bytes(1)) + else: + super().send_report(KEYPAD_KEYS_TO_KEYCODES[key].to_bytes(1, "big")) diff --git a/micropython/usbd/keycodes.py b/micropython/usbd/keycodes.py new file mode 100644 index 000000000..63732fda3 --- /dev/null +++ b/micropython/usbd/keycodes.py @@ -0,0 +1,24 @@ +# Keypad keycodes for use with USB HID +# MIT license; Copyright (c) 2023 Dave Wickham +_KEYPAD_KEYS = [ + "", + "/", + "*", + "-", + "+", + "", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "0", + ".", +] + +KEYPAD_KEYCODES_TO_KEYS = {k + 0x53: v for k, v in enumerate(_KEYPAD_KEYS)} +KEYPAD_KEYS_TO_KEYCODES = {v: k for k, v in KEYPAD_KEYCODES_TO_KEYS.items()} From 756d761e385c9db3258c8172f69734a594d8be0f Mon Sep 17 00:00:00 2001 From: Angus Gratton Date: Wed, 26 Jul 2023 16:39:22 +1000 Subject: [PATCH 12/17] usbd: Update hid_keypad example module. - Use SET_REPORT via control transfer. - Merge the keycodes module into hid_keypad. - Remove LEDs other than NumLock to make the report handler even simpler. --- micropython/usbd/hid_keypad.py | 96 ++++++++++++++++++++++++++++++++++ micropython/usbd/hidkeypad.py | 93 -------------------------------- micropython/usbd/keycodes.py | 24 --------- 3 files changed, 96 insertions(+), 117 deletions(-) create mode 100644 micropython/usbd/hid_keypad.py delete mode 100644 micropython/usbd/hidkeypad.py delete mode 100644 micropython/usbd/keycodes.py diff --git a/micropython/usbd/hid_keypad.py b/micropython/usbd/hid_keypad.py new file mode 100644 index 000000000..9c5f4769a --- /dev/null +++ b/micropython/usbd/hid_keypad.py @@ -0,0 +1,96 @@ +# MicroPython USB keypad module +# MIT license; Copyright (c) 2023 Dave Wickham, Angus Gratton + +from .hid import HIDInterface +from micropython import const + +_INTERFACE_PROTOCOL_KEYBOARD = const(0x01) + +# See HID Usages and Descriptions 1.4, section 10 Keyboard/Keypad Page (0x07) +# +# This keypad example has a contiguous series of keys (KEYPAD_KEY_IDS) starting +# from the NumLock/Clear keypad key (0x53), but you can send any Key IDs from +# the table in the HID Usages specification. +_KEYPAD_KEY_OFFS = const(0x53) + +_KEYPAD_KEY_IDS = [ + "", + "/", + "*", + "-", + "+", + "", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "0", + ".", +] + + +def _key_to_id(key): + # This is a little slower than making a dict for lookup, but uses + # less memory and O(n) can be fast enough when n is small. + return _KEYPAD_KEY_IDS.index(key) + _KEYPAD_KEY_OFFS + + +# fmt: off +_KEYPAD_REPORT_DESC = bytes( + [ + 0x05, 0x01, # Usage Page (Generic Desktop) + 0x09, 0x07, # Usage (Keypad) + 0xA1, 0x01, # Collection (Application) + 0x05, 0x07, # Usage Page (Keypad) + 0x19, 0x00, # Usage Minimum (0) + 0x29, 0xFF, # Usage Maximum (ff) + 0x15, 0x00, # Logical Minimum (0) + 0x25, 0xFF, # Logical Maximum (ff) + 0x95, 0x01, # Report Count (1), + 0x75, 0x08, # Report Size (8), + 0x81, 0x00, # Input (Data, Array, Absolute) + 0x05, 0x08, # Usage page (LEDs) + 0x19, 0x01, # Usage Minimum (1) + 0x29, 0x01, # Usage Maximum (1), + 0x95, 0x01, # Report Count (1), + 0x75, 0x01, # Report Size (1), + 0x91, 0x02, # Output (Data, Variable, Absolute) + 0x95, 0x01, # Report Count (1), + 0x75, 0x07, # Report Size (7), + 0x91, 0x01, # Output (Constant) - padding bits + 0xC0, # End Collection + ] +) +# fmt: on + + +class KeypadInterface(HIDInterface): + # Very basic synchronous USB keypad HID interface + + def __init__(self): + self.numlock = False + self.set_report_initialised = False + super().__init__( + _KEYPAD_REPORT_DESC, + set_report_buf=bytearray(1), + protocol=_INTERFACE_PROTOCOL_KEYBOARD, + interface_str="MicroPython Keypad", + ) + + def handle_set_report(self, report_data, _report_id, _report_type): + report = report_data[0] + b = bool(report & 1) + if b != self.numlock: + print("Numlock: ", b) + self.numlock = b + + def send_key(self, key=None): + if key is None: + self.send_report(b"\x00") + else: + self.send_report(_key_to_id(key).to_bytes(1, "big")) diff --git a/micropython/usbd/hidkeypad.py b/micropython/usbd/hidkeypad.py deleted file mode 100644 index bd0319fe5..000000000 --- a/micropython/usbd/hidkeypad.py +++ /dev/null @@ -1,93 +0,0 @@ -# MicroPython USB keypad module -# MIT license; Copyright (c) 2023 Dave Wickham - -from .hid import HIDInterface -from .keycodes import KEYPAD_KEYS_TO_KEYCODES -from .utils import STAGE_SETUP, split_bmRequestType -from micropython import const -import micropython - -_INTERFACE_PROTOCOL_KEYBOARD = const(0x01) -_REQ_CONTROL_SET_REPORT = const(0x09) -_REQ_CONTROL_SET_IDLE = const(0x0A) - -# fmt: off -_KEYPAD_REPORT_DESC = bytes( - [ - 0x05, 0x01, # Usage Page (Generic Desktop) - 0x09, 0x07, # Usage (Keypad) - 0xA1, 0x01, # Collection (Application) - 0x05, 0x07, # Usage Page (Keypad) - 0x19, 0x00, # Usage Minimum (00), - 0x29, 0xFF, # Usage Maximum (ff), - 0x15, 0x00, # Logical Minimum (0), - 0x25, 0xFF, # Logical Maximum (ff), - 0x95, 0x01, # Report Count (1), - 0x75, 0x08, # Report Size (8), - 0x81, 0x00, # Input (Data, Array, Absolute) - 0x05, 0x08, # Usage page (LEDs) - 0x19, 0x01, # Usage minimum (1) - 0x29, 0x05, # Usage Maximum (5), - 0x95, 0x05, # Report Count (5), - 0x75, 0x01, # Report Size (1), - 0x91, 0x02, # Output (Data, Variable, Absolute) - 0x95, 0x01, # Report Count (1), - 0x75, 0x03, # Report Size (3), - 0x91, 0x01, # Output (Constant) - 0xC0, # End Collection - ] -) -# fmt: on - - -class KeypadInterface(HIDInterface): - # Very basic synchronous USB keypad HID interface - - def __init__(self): - self.numlock = None - self.capslock = None - self.scrolllock = None - self.compose = None - self.kana = None - self.set_report_initialised = False - super().__init__( - _KEYPAD_REPORT_DESC, - protocol=_INTERFACE_PROTOCOL_KEYBOARD, - interface_str="MicroPython Keypad!", - use_out_ep=True, - ) - - def handle_interface_control_xfer(self, stage, request): - if request[1] == _REQ_CONTROL_SET_IDLE and not self.set_report_initialised: - # Hacky initialisation goes here - self.set_report() - self.set_report_initialised = True - - if stage == STAGE_SETUP: - return super().handle_interface_control_xfer(stage, request) - - bmRequestType, bRequest, wValue, _, _ = request - recipient, req_type, _ = split_bmRequestType(bmRequestType) - - return True - - def set_report(self, args=None): - self.out_buffer = bytearray(1) - self.submit_xfer(self._out_ep, self.out_buffer, self.set_report_cb) - return True - - def set_report_cb(self, ep_addr, result, xferred_bytes): - buf_result = int(self.out_buffer[0]) - self.numlock = buf_result & 1 - self.capslock = (buf_result >> 1) & 1 - self.scrolllock = (buf_result >> 2) & 1 - self.compose = (buf_result >> 3) & 1 - self.kana = (buf_result >> 4) & 1 - - micropython.schedule(self.set_report, None) - - def send_report(self, key=None): - if key is None: - super().send_report(bytes(1)) - else: - super().send_report(KEYPAD_KEYS_TO_KEYCODES[key].to_bytes(1, "big")) diff --git a/micropython/usbd/keycodes.py b/micropython/usbd/keycodes.py deleted file mode 100644 index 63732fda3..000000000 --- a/micropython/usbd/keycodes.py +++ /dev/null @@ -1,24 +0,0 @@ -# Keypad keycodes for use with USB HID -# MIT license; Copyright (c) 2023 Dave Wickham -_KEYPAD_KEYS = [ - "", - "/", - "*", - "-", - "+", - "", - "1", - "2", - "3", - "4", - "5", - "6", - "7", - "8", - "9", - "0", - ".", -] - -KEYPAD_KEYCODES_TO_KEYS = {k + 0x53: v for k, v in enumerate(_KEYPAD_KEYS)} -KEYPAD_KEYS_TO_KEYCODES = {v: k for k, v in KEYPAD_KEYCODES_TO_KEYS.items()} From bb389e3961ff5a6b02739999fb7b23aea9089533 Mon Sep 17 00:00:00 2001 From: Angus Gratton Date: Wed, 26 Jul 2023 17:09:27 +1000 Subject: [PATCH 13/17] usbd: Implement ruff, black linter & formatting fixes. --- micropython/usbd/device.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/micropython/usbd/device.py b/micropython/usbd/device.py index 617d3390c..492b42ea3 100644 --- a/micropython/usbd/device.py +++ b/micropython/usbd/device.py @@ -300,7 +300,7 @@ def _open_cb(self, interface_desc_view): i = 0 while i < len(interface_desc_view): # descriptor length, type, and index (if it's an interface descriptor) - dl, dt, di = interface_desc_view[i:i+3] + dl, dt, di = interface_desc_view[i : i + 3] if dt == _STD_DESC_INTERFACE_TYPE: if di >= self._usbd.static.itf_max: di -= self._usbd.static.itf_max @@ -602,7 +602,7 @@ def submit_xfer(self, ep_addr, data, done_cb=None): # Note that done_cb may be called immediately, possibly before this # function has returned to the caller. if not self._open: - raise RuntimeError() + raise RuntimeError return get_usbdevice()._submit_xfer(ep_addr, data, done_cb) def set_ep_stall(self, ep_addr, stall): @@ -612,7 +612,7 @@ def set_ep_stall(self, ep_addr, stall): # there are some device classes that need to explicitly stall or unstall # an endpoint under certain conditions. if not self._open or ep_addr not in get_usbdevice()._eps: - raise RuntimeError() + raise RuntimeError get_usbdevice()._usbd.set_ep_stall(ep_addr, stall) def get_ep_stall(self, ep_addr): @@ -621,5 +621,5 @@ def get_ep_stall(self, ep_addr): # Endpoint can be stalled/unstalled by host, TinyUSB stack, or calls to # set_ep_stall(). if not self._open or ep_addr not in get_usbdevice()._eps: - raise RuntimeError() + raise RuntimeError return get_usbdevice()._usbd.get_ep_stall(ep_addr) From 2baaf58971169473a36ffedfae4817cda7967c1e Mon Sep 17 00:00:00 2001 From: Angus Gratton Date: Wed, 26 Jul 2023 17:19:57 +1000 Subject: [PATCH 14/17] usbd: Add missing manifest file. --- micropython/usbd/manifest.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 micropython/usbd/manifest.py diff --git a/micropython/usbd/manifest.py b/micropython/usbd/manifest.py new file mode 100644 index 000000000..78b2c69fb --- /dev/null +++ b/micropython/usbd/manifest.py @@ -0,0 +1,15 @@ +metadata(version="0.1.0") + +# TODO: split this up into sub-packages, and some code in example subdirectory +package( + "usbd", + files=( + "__init__.py", + "device.py", + "hid.py", + "hid_keypad.py", + "midi.py", + "utils.py", + ), + base_path="..", +) From 3411ce14bd48704c0cc1ac2f59e656afe387cb2d Mon Sep 17 00:00:00 2001 From: Martin Fischer Date: Tue, 15 Aug 2023 22:34:26 +0200 Subject: [PATCH 15/17] chore(cdc): polish descriptor and improve rx handling of OUT Endpoint. There was also a bug in the control itf union descriptor. --- micropython/usbd/cdc.py | 117 ++++++++++++++++++++++++++++++---------- 1 file changed, 89 insertions(+), 28 deletions(-) diff --git a/micropython/usbd/cdc.py b/micropython/usbd/cdc.py index e19337ffa..37ee141c4 100644 --- a/micropython/usbd/cdc.py +++ b/micropython/usbd/cdc.py @@ -6,6 +6,10 @@ ) from .utils import ( endpoint_descriptor, + split_bmRequestType, + STAGE_SETUP, + REQ_TYPE_STANDARD, + REQ_TYPE_CLASS, EP_IN_FLAG ) from micropython import const @@ -17,9 +21,20 @@ _ITF_ASSOCIATION_DESC_TYPE = const(0xb) # Interface Association descriptor # CDC control interface definitions -_CDC_ITF_CONTROL_CLASS = const(2) -_CDC_ITF_CONTROL_SUBCLASS = const(2) # Abstract Control Mode -_CDC_ITF_CONTROL_PROT = const(0) # no protocol +_INTERFACE_CLASS_CDC = const(2) +_INTERFACE_SUBCLASS_CDC = const(2) # Abstract Control Mode +_PROTOCOL_NONE = const(0) # no protocol + +# CDC descriptor subtype +# see also CDC120.pdf, table 13 +_CDC_FUNC_DESC_HEADER = const(0) +_CDC_FUNC_DESC_CALL_MANAGEMENT = const(1) +_CDC_FUNC_DESC_ABSTRACT_CONTROL = const(2) +_CDC_FUNC_DESC_UNION = const(6) + +# Other definitions +_CDC_VERSION = const(0x0120) # release number in binary-coded decimal + # CDC data interface definitions _CDC_ITF_DATA_CLASS = const(0xa) @@ -41,31 +56,70 @@ def setup_CDC_device(): class CDCControlInterface(USBInterface): # Implements the CDC Control Interface - def __init__(self, interface_str): - super().__init__(_CDC_ITF_CONTROL_CLASS, _CDC_ITF_CONTROL_SUBCLASS, - _CDC_ITF_CONTROL_PROT) + def __init__(self, _): + super().__init__(_INTERFACE_CLASS_CDC, _INTERFACE_SUBCLASS_CDC, _PROTOCOL_NONE) def get_itf_descriptor(self, num_eps, itf_idx, str_idx): # CDC needs a Interface Association Descriptor (IAD) - # first interface is zero, two interfaces in total - desc = ustruct.pack("= nbytes: + break + time.sleep_ms(10) # XXX blocking.. could be async'd + return self.total_rx def _cb_rx(self, ep, res, num_bytes): - self.rx_done = True - self.rx_nbytes = num_bytes + self.total_rx.extend(self.mv_buf[:num_bytes]) + self.rx_nbytes += num_bytes + if self.rx_nbytes < self.rx_nbytes_requested: + # try to get more from endpoint + self._poll_rx_endpoint(self._cb_rx) From 62f20b84760dfdd4291a95a16b0bd53335800a93 Mon Sep 17 00:00:00 2001 From: Martin Fischer Date: Sun, 3 Sep 2023 11:49:46 +0200 Subject: [PATCH 16/17] usbd(cdc): handle line_coding and line state --- micropython/usbd/cdc.py | 65 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 59 insertions(+), 6 deletions(-) diff --git a/micropython/usbd/cdc.py b/micropython/usbd/cdc.py index 37ee141c4..0b6115787 100644 --- a/micropython/usbd/cdc.py +++ b/micropython/usbd/cdc.py @@ -8,6 +8,8 @@ endpoint_descriptor, split_bmRequestType, STAGE_SETUP, + STAGE_DATA, + STAGE_ACK, REQ_TYPE_STANDARD, REQ_TYPE_CLASS, EP_IN_FLAG @@ -32,6 +34,26 @@ _CDC_FUNC_DESC_ABSTRACT_CONTROL = const(2) _CDC_FUNC_DESC_UNION = const(6) +# CDC class requests, table 13, PSTN subclass +_SET_LINE_CODING_REQ = const(0x20) +_GET_LINE_CODING_REQ = const(0x21) +_SET_CONTROL_LINE_STATE = const(0x22) +_SEND_BREAK_REQ = const(0x23) + +_LINE_CODING_STOP_BIT_1 = const(0) +_LINE_CODING_STOP_BIT_1_5 = const(1) +_LINE_CODING_STOP_BIT_2 = const(2) + + +_LINE_CODING_PARITY_NONE = const(0) +_LINE_CODING_PARITY_ODD = const(1) +_LINE_CODING_PARITY_EVEN = const(2) +_LINE_CODING_PARITY_MARK = const(3) +_LINE_CODING_PARITY_SPACE = const(4) + +parity_bits_repr = ['N', 'O', 'E', 'M', 'S'] +stop_bits_repr = ['1', '1.5', '2'] + # Other definitions _CDC_VERSION = const(0x0120) # release number in binary-coded decimal @@ -58,6 +80,13 @@ class CDCControlInterface(USBInterface): def __init__(self, _): super().__init__(_INTERFACE_CLASS_CDC, _INTERFACE_SUBCLASS_CDC, _PROTOCOL_NONE) + self.rts = None + self.dtr = None + self.baudrate = None + self.stop_bits = 0 + self.parity = 0 + self.data_bits = None + self.line_coding_state = bytearray(7) def get_itf_descriptor(self, num_eps, itf_idx, str_idx): # CDC needs a Interface Association Descriptor (IAD) @@ -113,12 +142,36 @@ def get_endpoint_descriptors(self, ep_addr, str_idx): return (self.ep_in, [], ((ep_addr+1) | EP_IN_FLAG,)) def handle_interface_control_xfer(self, stage, request): - # Handle standard and class-specific interface control transfers for HID devices. - bmRequestType, bRequest, wValue, _, _ = request - recipient, req_type, _ = split_bmRequestType(bmRequestType) - - print(f'itf cntrl: {recipient}, {req_type}') - super().handle_interface_control_xfer(stage, request) + # Handle standard and class-specific interface control transfers + bmRequestType, bRequest, wValue, _, wLength = request + recipient, req_type, req_dir = split_bmRequestType(bmRequestType) + if stage == STAGE_SETUP: + if req_type == REQ_TYPE_CLASS: + if bRequest == _SET_LINE_CODING_REQ: + # XXX check against wLength + return self.line_coding_state + elif bRequest == _GET_LINE_CODING_REQ: + return self.line_coding_state + elif bRequest == _SET_CONTROL_LINE_STATE: + # DTR = BIT0, RTS = BIT1 + self.dtr = bool(wValue & 0x1) + self.rts = bool(wValue & 0x2) + return b"" + + if stage == STAGE_DATA: + if req_type == REQ_TYPE_CLASS: + if bRequest == _SET_LINE_CODING_REQ: + # Byte 0-3 Byte 4 Byte 5 Byte 6 + # dwDTERate bCharFormat bParityType bDataBits + self.baudrate, self.stop_bits, self.parity, self.data_bits = ustruct.unpack( + ' Date: Sun, 3 Sep 2023 15:32:24 +0200 Subject: [PATCH 17/17] usbd(cdc): add support for line break and add a callback to it --- micropython/usbd/cdc.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/micropython/usbd/cdc.py b/micropython/usbd/cdc.py index 0b6115787..0cc958a24 100644 --- a/micropython/usbd/cdc.py +++ b/micropython/usbd/cdc.py @@ -86,6 +86,8 @@ def __init__(self, _): self.stop_bits = 0 self.parity = 0 self.data_bits = None + self.break_cb = None # callback for break condition + self.line_coding_state = bytearray(7) def get_itf_descriptor(self, num_eps, itf_idx, str_idx): @@ -157,6 +159,10 @@ def handle_interface_control_xfer(self, stage, request): self.dtr = bool(wValue & 0x1) self.rts = bool(wValue & 0x2) return b"" + elif bRequest == _SEND_BREAK_REQ: + if self.break_cb: + self.break_cb(wValue) + return b"" if stage == STAGE_DATA: if req_type == REQ_TYPE_CLASS: @@ -167,6 +173,11 @@ def handle_interface_control_xfer(self, stage, request): '