From c8ad6cae8812d8299cf30b09194bf63dec208cbc Mon Sep 17 00:00:00 2001 From: Angus Gratton Date: Tue, 14 Feb 2023 18:01:33 +1100 Subject: [PATCH] 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/__init__.py | 4 + micropython/usbd/device.py | 525 +++++++++++++++-------------------- micropython/usbd/hid.py | 69 ++--- micropython/usbd/manifest.py | 9 + micropython/usbd/midi.py | 391 ++++++++++++++------------ micropython/usbd/utils.py | 77 +++++ 6 files changed, 563 insertions(+), 512 deletions(-) create mode 100644 micropython/usbd/__init__.py create mode 100644 micropython/usbd/manifest.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 index 166c9c2bb..262d12a98 100644 --- a/micropython/usbd/device.py +++ b/micropython/usbd/device.py @@ -4,42 +4,18 @@ import machine import ustruct -## -## Constants that are used by consumers of this module -## -## (TODO: decide if this is too expensive on code size) - -EP_OUT_FLAG = const(1 << 7) - -# Control transfer stages -STAGE_IDLE = const(0) -STAGE_SETUP = const(1) -STAGE_DATA = const(2) -STAGE_ACK = const(3) - -# 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) - -## -## Constants used only inside this module -## +from .utils import split_bmRequestType # 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_ENDPOINT_TYPE = const(0x5) _STD_DESC_INTERFACE_ASSOC = const(0xB) # Standard USB descriptor lengths _STD_DESC_CONFIG_LEN = const(9) _STD_DESC_INTERFACE_LEN = const(9) -_STD_DESC_ENDPOINT_LEN = const(7) # Standard control request bmRequest fields, can extract by calling split_bmRequestType() _REQ_RECIPIENT_DEVICE = const(0x0) @@ -47,11 +23,6 @@ _REQ_RECIPIENT_ENDPOINT = const(0x2) _REQ_RECIPIENT_OTHER = const(0x3) -REQ_TYPE_STANDARD = const(0x0) -REQ_TYPE_CLASS = const(0x1) -REQ_TYPE_VENDOR = const(0x2) -REQ_TYPE_RESERVED = const(0x3) - # Offsets into the standard configuration descriptor, to fixup _OFFS_CONFIG_iConfiguration = const(6) @@ -60,8 +31,10 @@ _inst = None -def get(): - """Access the singleton instance of the MicroPython _USBDevice object.""" +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() @@ -69,16 +42,14 @@ def get(): 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(), - not instantiated directly.. - """ - + # 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 = ( {} @@ -114,41 +85,38 @@ def __init__(self): ) 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. - - """ + # 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. - - """ + # 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. - """ + # 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. + # 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: @@ -218,17 +188,15 @@ def _get_interface(self, index): 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). - - """ + # 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 @@ -268,15 +236,13 @@ def _descriptor_config_cb(self): return desc def _write_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. - - """ + # 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 @@ -305,13 +271,11 @@ def _write_configuration_descriptor(self, desc): ) def _descriptor_string_cb(self, index): - """Singleton callback from TinyUSB to get a string descriptor. - - The self._strs list is built during enumeration (each time - _descriptor_config_cb is called), so we just return a value indexed from - it. - - """ + # Singleton callback from TinyUSB to get a string descriptor. + # + # The self._strs list is built during enumeration (each time + # _descriptor_config_cb is called), so we just return a value indexed from + # it. index -= self._usbd.static.str_max assert ( index >= 0 @@ -322,15 +286,14 @@ def _descriptor_string_cb(self, index): return None def _open_driver_cb(self, interface_desc_view): - """Singleton callback from TinyUSB custom class driver""" + # 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. - """ + # 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. itf, cb = self._eps[ep_addr] if cb: raise RuntimeError(f"Pending xfer on EP {ep_addr}") @@ -340,7 +303,7 @@ def _submit_xfer(self, ep_addr, data, done_cb=None): return False def _xfer_cb(self, ep_addr, result, xferred_bytes): - """Singleton callback from TinyUSB custom class driver when a transfer completes.""" + # Singleton callback from TinyUSB custom class driver when a transfer completes. try: itf, cb = self._eps[ep_addr] self._eps[ep_addr] = (itf, None) @@ -350,17 +313,15 @@ def _xfer_cb(self, ep_addr, result, xferred_bytes): 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 STAGE_SETUP, - STAGE_DATA, 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. - - """ + # 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) @@ -385,8 +346,9 @@ def _control_xfer_cb(self, stage, request): 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 + # 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 @@ -402,7 +364,7 @@ def _control_xfer_cb(self, stage, request): class USBInterface: - """Abstract base class to implement a USBInterface (and associated endpoints) in Python""" + # Abstract base class to implement a USBInterface (and associated endpoints) in Python def __init__( self, @@ -411,16 +373,15 @@ def __init__( 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. + # 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 @@ -428,40 +389,38 @@ def __init__( 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. - - """ + # 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 @@ -479,153 +438,109 @@ def get_itf_descriptor(self, num_eps, itf_idx, str_idx): 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 EP_OUT_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 EP_OUT_FLAG bit set.) - - """ + # 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_OUT_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_OUT_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 - 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 STAGE_SETUP, STAGE_DATA, 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. - - """ + # 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 == - 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. - - """ + # 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 - 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. - - """ + # 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. - - """ - return get()._submit_xfer(ep_addr, data, done_cb) - - -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, - ) + # 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. + return get_usbdevice()._submit_xfer(ep_addr, data, done_cb) diff --git a/micropython/usbd/hid.py b/micropython/usbd/hid.py index a5768c5e8..9687e55d3 100644 --- a/micropython/usbd/hid.py +++ b/micropython/usbd/hid.py @@ -1,10 +1,12 @@ # MicroPython USB hid module # MIT license; Copyright (c) 2022 Angus Gratton -from device import ( +from .device import ( USBInterface, - EP_OUT_FLAG, +) +from .utils import ( endpoint_descriptor, split_bmRequestType, + EP_OUT_FLAG, STAGE_SETUP, REQ_TYPE_STANDARD, REQ_TYPE_CLASS, @@ -35,7 +37,7 @@ class HIDInterface(USBInterface): - """Abstract base class to implement a USB device HID interface in Python.""" + # Abstract base class to implement a USB device HID interface in Python. def __init__( self, @@ -44,20 +46,20 @@ def __init__( 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. - - """ + # 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 @@ -67,16 +69,16 @@ 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) + # 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. - - """ + # 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_OUT_FLAG desc += endpoint_descriptor(ep_addr, "interrupt", 8, 8) @@ -86,11 +88,10 @@ def get_endpoint_descriptors(self, ep_addr, str_idx): 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 - """ + # 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( "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): - """Return the MIDI USB endpoint descriptors. - """ - - epA = ustruct.pack( - "> 5) & 0x03, + (bmRequestType >> 7) & 0x01, + )