Skip to content
Draft
Changes from 1 commit
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
f5b8489
usbd: Add USB device drivers implemented in Python.
projectgus Oct 26, 2022
e2a3e45
usbd: Add midi interface definition from @paulhamsh.
projectgus Feb 9, 2023
c8ad6ca
usbd: Major cleanup, refactor.
projectgus Feb 14, 2023
65762f6
Add basic keypad support
turmoni Jun 3, 2023
944e107
Fix report count, remove irrelevant comments
turmoni Jun 3, 2023
5b5871c
Add basic, read-only MSC support, and add LED status to keypad.
turmoni Jun 28, 2023
e8bd164
Actually add the changes methoned in the previous commit message, and…
turmoni Jun 28, 2023
eb47fa0
usbd: Bugfixes around data transfer, support using an AbstractBlockDe…
turmoni Jun 30, 2023
24f7422
usbd: Add USB device drivers implemented in Python.
projectgus Oct 26, 2022
581a662
usbd: Add midi interface definition from @paulhamsh.
projectgus Feb 9, 2023
7472ef5
Merge remote-tracking branch 'upstream/feature/usbd_python' into feat…
turmoni Jul 10, 2023
e24951a
usbd: Add copyright notices (+delete file that has gone from upstream…
turmoni Jul 10, 2023
e85b368
usbd: Run "black" with the right options for the style checker to be …
turmoni Jul 10, 2023
82f1e47
usbd: Use EP_IN_FLAG from utils for mass storage
turmoni Jul 10, 2023
5c51a9e
usbd: Re-run black to fix the missing comma
turmoni Jul 10, 2023
9d4d843
usbd: Add support for configuration open and reset callbacks.
projectgus Jul 25, 2023
29e9185
usbd: Add USB interface functions for endpoint STALL support.
projectgus Jul 25, 2023
9d7ce9f
usbd: Implement SET_REPORT support for OUT direction HID data.
projectgus Jul 26, 2023
92711ea
usbd: Rename ustruct->struct.
projectgus Jul 26, 2023
3765d04
usbd: Add hid keypad example from @turmoni .
projectgus Jul 26, 2023
756d761
usbd: Update hid_keypad example module.
projectgus Jul 26, 2023
bb389e3
usbd: Implement ruff, black linter & formatting fixes.
projectgus Jul 26, 2023
2baaf58
usbd: Add missing manifest file.
projectgus Jul 26, 2023
83364c0
Merge remote-tracking branch 'upstream/feature/usbd_python' into feat…
turmoni Aug 3, 2023
cd4f51c
usbd: Theoretically handle resets and bad CBWs better in msc
turmoni Sep 2, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
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.
  • Loading branch information
projectgus committed Jul 10, 2023
commit 581a662762e7e257071a339d4ed66942419bdd3e
306 changes: 306 additions & 0 deletions micropython/usbd/midi.py
Original file line number Diff line number Diff line change
@@ -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(
"<BBBHHBB",
9, # bLength
0x24, # bDescriptorType CS_INTERFACE
0x01, # bDescriptorSubtype MS_HEADER
0x0100, # BcdADC
0x0009, # wTotalLength
0x01, # bInCollection,
# baInterfaceNr value assumes the next interface will be MIDIInterface
itf_idx + 1, # baInterfaceNr
)

return (desc, strs)


class MIDIInterface(USBInterface):
# Base class to implement a USB MIDI device in Python.

# To be compliant two USB interfaces should be registered in series, first a
# _DummyAudioInterface() and then this one immediately after.
def __init__(self, num_rx=1, num_tx=1):
# Arguments are number of MIDI IN and OUT connections (default 1 each way).

# 'rx' and 'tx' are from the point of view of this device, i.e. a 'tx'
# connection is device to host. RX and TX are used here to avoid the
# even more confusing "MIDI IN" and "MIDI OUT", which varies depending
# on whether you look from the perspective of the device or the USB
# interface.
super().__init__(
_INTERFACE_CLASS_AUDIO, _INTERFACE_SUBCLASS_AUDIO_MIDISTREAMING, _PROTOCOL_NONE
)
self._num_rx = num_rx
self._num_tx = num_tx
self.ep_out = None # Set during enumeration
self.ep_in = None
self._rx_buf = bytearray(64)

def send_data(self, tx_data):
"""Helper function to send data."""
self.submit_xfer(self.ep_out, tx_data)

def midi_received(self):
return not self.rb.is_empty()

def get_rb(self):
return self.rb.get()

def receive_data_callback(self, ep_addr, result, xferred_bytes):
for i in range(0, xferred_bytes):
self.rb.put(self.rx_data[i])
self.submit_xfer(0x03, self.rx_data, self.receive_data_callback)

def start_receive_data(self):
self.submit_xfer(self.ep_in, self.rx_data, self.receive_data_callback)

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 interface descriptors

_JACK_IN_DESC_LEN = const(6)
_JACK_OUT_DESC_LEN = const(9)

# Midi Streaming interface descriptor
cs_ms_interface = ustruct.pack(
"<BBBHH",
7, # bLength
0x24, # bDescriptorType CS_INTERFACE
0x01, # bDescriptorSubtype MS_HEADER
0x0100, # BcdADC
# wTotalLength: this descriptor, plus length of all Jack descriptors
(7 + (2 * (_JACK_IN_DESC_LEN + _JACK_OUT_DESC_LEN) * (self._num_rx + self._num_tx))),
)

def jack_in_desc(bJackType, bJackID):
return ustruct.pack(
"<BBBBBB",
_JACK_IN_DESC_LEN, # bLength
0x24, # bDescriptorType CS_INTERFACE
0x02, # bDescriptorSubtype MIDI_IN_JACK
bJackType,
bJackID,
0x00, # iJack, no string descriptor support yet
)

def jack_out_desc(bJackType, bJackID, bSourceId, bSourcePin):
return ustruct.pack(
"<BBBBBBBBB",
_JACK_OUT_DESC_LEN, # bLength
0x24, # bDescriptorType CS_INTERFACE
0x03, # bDescriptorSubtype MIDI_OUT_JACK
bJackType,
bJackID,
0x01, # bNrInputPins
bSourceId, # baSourceID(1)
bSourcePin, # baSourcePin(1)
0x00, # iJack, no string descriptor support yet
)

jacks = bytearray() # TODO: pre-allocate this whole descriptor and pack into it

# The USB MIDI standard 1.0 allows modelling a baffling range of MIDI
# devices with different permutations of Jack descriptors, with a lot of
# scope for indicating internal connections in the device (as
# "virtualised" by the USB MIDI standard). Much of the options don't
# really change the USB behaviour but provide metadata to the host.
#
# As observed elsewhere online, the standard ends up being pretty
# complex and unclear in parts, but there is a clear simple example in
# an Appendix. So nearly everyone implements the device from the
# Appendix as-is, even when it's not a good fit for their application,
# and ignores the rest of the standard.
#
# We'll try to implement a slightly more flexible subset that's still
# very simple, without getting caught in the weeds:
#
# - For each rx (total _num_rx), we have data flowing from the USB host
# to the USB MIDI device:
# * Data comes from a MIDI OUT Endpoint (Host->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(
"<BBBB" + "B" * self._num_rx,
4 + self._num_rx, # bLength
0x25, # bDescriptorType CS_ENDPOINT
0x01, # bDescriptorSubtype MS_GENERAL
self._num_rx, # bNumEmbMIDIJack
*(self._emb_id(False, idx) for idx in range(self._num_rx)) # baSourcePin(1..._num_rx)
)

# tx side, USB "out" endpoint and embedded MIDI OUT jacks
e_in = endpoint_descriptor(self.ep_out, "bulk", 64, 0)
cs_in = ustruct.pack(
"<BBBB" + "B" * self._num_tx,
4 + self._num_tx, # bLength
0x25, # bDescriptorType CS_ENDPOINT
0x01, # bDescriptorSubtype MS_GENERAL
self._num_tx, # bNumEmbMIDIJack
*(self._emb_id(True, idx) for idx in range(self._num_tx)) # baSourcePin(1..._num_rx)
)

desc = e_out + cs_out + e_in + cs_in

return (desc, [], (self.ep_out, self.ep_in))


class MidiUSB(MIDIInterface):
# Very basic synchronous USB MIDI interface

def __init__(self):
super().__init__()

def note_on(self, channel, pitch, vel):
obuf = ustruct.pack("<BBBB", 0x09, 0x90 | channel, pitch, vel)
super().send_data(obuf)

def note_off(self, channel, pitch, vel):
obuf = ustruct.pack("<BBBB", 0x08, 0x80 | channel, pitch, vel)
super().send_data(obuf)

def start(self):
super().start_receive_data()

def midi_received(self):
return super().midi_received()

def get_midi(self):
if super().midi_received():
cin = super().get_rb()
cmd = super().get_rb()
val1 = super().get_rb()
val2 = super().get_rb()
return (cin, cmd, val1, val2)
else:
return (None, None, None, None)