From 6ca17e874b219652073e4ab6dfd0eb69b3fc7e7e Mon Sep 17 00:00:00 2001 From: Angus Gratton Date: Wed, 16 Sep 2026 11:07:31 +1000 Subject: [PATCH 1/2] usb-device-midi: Queue at most one micropython.schedule() for rx. Prevents situation where a high MIDI RX load fills up the schedule() queue with redundant calls to _on_rx() and causes a RuntimeError. Also refactor the callback handler so that a RuntimeError when scheduling doesn't stop RX endpoint transfers from continuing. This work was funded through GitHub Sponsors. Signed-off-by: Angus Gratton --- micropython/usb/usb-device-midi/manifest.py | 2 +- .../usb/usb-device-midi/usb/device/midi.py | 22 +++++++++++++++---- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/micropython/usb/usb-device-midi/manifest.py b/micropython/usb/usb-device-midi/manifest.py index af9b8cb84..4520325e3 100644 --- a/micropython/usb/usb-device-midi/manifest.py +++ b/micropython/usb/usb-device-midi/manifest.py @@ -1,3 +1,3 @@ -metadata(version="0.1.0") +metadata(version="0.1.1") require("usb-device") package("usb") diff --git a/micropython/usb/usb-device-midi/usb/device/midi.py b/micropython/usb/usb-device-midi/usb/device/midi.py index 55bfbd08b..8c99da08b 100644 --- a/micropython/usb/usb-device-midi/usb/device/midi.py +++ b/micropython/usb/usb-device-midi/usb/device/midi.py @@ -72,6 +72,7 @@ def __init__(self, rxlen=16, txlen=16): self.ep_in = None # TX direction (device to host) self._rx = Buffer(rxlen) self._tx = Buffer(txlen) + self._on_rx_schedule = False # Callbacks for handling received MIDI messages. # @@ -147,10 +148,22 @@ def _rx_xfer(self): self.submit_xfer(self.ep_out, self._rx.pend_write(), self._rx_cb) def _rx_cb(self, ep, res, num_bytes): - if res == 0: - self._rx.finish_write(num_bytes) - schedule(self._on_rx, None) - self._rx_xfer() + # This function assumes it's only called via an irq (soft or hard), and therefore + # it won't be interrupted by its own scheduled callback until it finishes. + try: + if res == 0: + # Queue at most one concurrent call to self._on_rx, as each execution + # will read all the bytes queued in self._rx buffer + if not self._on_rx_schedule: + schedule(self._on_rx, None) + self._on_rx_schedule = True + # Ordering so that if the schedule() call fails, the bytes + # already in the buffer will be lost (overwritten by the next + # xfer). This prevents wedging the MIDI RX path with a full buffer + # and no call to _on_rx pending. + self._rx.finish_write(num_bytes) + finally: + self._rx_xfer() def on_open(self): super().on_open() @@ -160,6 +173,7 @@ def on_open(self): def _on_rx(self, _): # Receive MIDI events. Called via micropython.schedule, outside of the USB callback function. + self._on_rx_schedule = False m = self._rx.pend_read() i = 0 while i <= len(m) - 4: From bdecf00bdd1eb6ecb2d58d1d8845f9e0af79d435 Mon Sep 17 00:00:00 2001 From: Angus Gratton Date: Wed, 16 Sep 2026 11:08:59 +1000 Subject: [PATCH 2/2] usb-device-midi: Resume RX endpoint transfer after handling callback. If the _rx buffer is full then no OUT transfer is submitted to TinyUSB. This meant that completely filling the RX buffer could permanently stall the OUT direction endpoint, as the scheduled callback handler wouldn't re-queue it. Signed-off-by: Angus Gratton --- micropython/usb/usb-device-midi/usb/device/midi.py | 1 + 1 file changed, 1 insertion(+) diff --git a/micropython/usb/usb-device-midi/usb/device/midi.py b/micropython/usb/usb-device-midi/usb/device/midi.py index 8c99da08b..f484a079f 100644 --- a/micropython/usb/usb-device-midi/usb/device/midi.py +++ b/micropython/usb/usb-device-midi/usb/device/midi.py @@ -181,6 +181,7 @@ def _on_rx(self, _): self.on_midi_event(cin, m[i + 1], m[i + 2], m[i + 3]) i += 4 self._rx.finish_read(i) + self._rx_xfer() # resume xfer if needed because _rx buffer was previously full def desc_cfg(self, desc, itf_num, ep_num, strs): # Start by registering a USB Audio Control interface, that is required to point to the