diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e5501d2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# Build artifacts +.pio/ +build/ +*.o +*.a +*.elf +*.bin +*.map + +# IDE +.vscode/ +*.swp +*~ + +# Python +*.pyc +__pycache__/ + +# Internal working documents (not for upstream) +tusb_ump_esp32s3_handoff.md +tusb_ump_rp2040_handoff.md +*_handoff.md +CHANGES.md +CHANGES_pt-BR.md diff --git a/examples/T-Display-S3-ESP32-S3-MIDI2-PingPong/arduino/tdisplay_s3_midi2/README.md b/examples/T-Display-S3-ESP32-S3-MIDI2-PingPong/arduino/tdisplay_s3_midi2/README.md new file mode 100644 index 0000000..60a5935 --- /dev/null +++ b/examples/T-Display-S3-ESP32-S3-MIDI2-PingPong/arduino/tdisplay_s3_midi2/README.md @@ -0,0 +1,37 @@ +# USB MIDI 2.0 Device — Arduino IDE + +ESP32-S3 USB MIDI 2.0 device example using tusb_ump. +Tested on LilyGO T-Display-S3. + +## Setup + +1. **Board:** ESP32S3 Dev Module +2. **Tools > USB Mode:** USB-OTG (TinyUSB) +3. **Tools > USB CDC On Boot:** Disabled +4. **LovyanGFX:** install via Library Manager +5. **tusb_ump:** copy repo into `~/Arduino/libraries/tusb_ump/` +6. **Build flags:** copy `platform.local.txt.example` as `platform.local.txt` to: + - Linux: `~/.arduino15/packages/esp32/hardware/esp32//` + - macOS: `~/Library/Arduino15/packages/esp32/hardware/esp32//` + - Windows: `%LOCALAPPDATA%\Arduino15\packages\esp32\hardware\esp32\\` +7. **Restart Arduino IDE**, then open `tdisplay_s3_midi2.ino` and upload. + +## Files + +| File | Description | +|------|-------------| +| `tdisplay_s3_midi2.ino` | Main sketch (setup/loop, buttons, UMP RX/TX) | +| `usb_descriptors.cpp` | USB MIDI 2.0 config descriptor (dual alt settings) | +| `UMPDisplay.h` | LovyanGFX display handler | +| `mapping.h` | Pin assignments, colours, layout | +| `platform.local.txt.example` | Build flags template for Arduino IDE | + +## Controls + +| Button | Action | +|--------|--------| +| BTN1 (GPIO0) | Toggle NoteOn / NoteOff (C5) | +| BTN2 (GPIO14) | Cycle velocity: 32 > 64 > 96 > 127 | + +See the [PlatformIO version](../../platformio/tdisplay_s3_midi2/) for an +alternative build setup that doesn't require `platform.local.txt`. diff --git a/examples/T-Display-S3-ESP32-S3-MIDI2-PingPong/arduino/tdisplay_s3_midi2/UMPDisplay.h b/examples/T-Display-S3-ESP32-S3-MIDI2-PingPong/arduino/tdisplay_s3_midi2/UMPDisplay.h new file mode 100644 index 0000000..3d80a93 --- /dev/null +++ b/examples/T-Display-S3-ESP32-S3-MIDI2-PingPong/arduino/tdisplay_s3_midi2/UMPDisplay.h @@ -0,0 +1,393 @@ +// UMPDisplay.h — Visual UMP monitor for T-Display-S3 (ST7789V 320x170) +// +// Displays real-time UMP traffic with decoded message types, velocity +// bar, scrolling event log with RX/TX direction indicators, and +// protocol negotiation timeline. +// +// Copyright (c) 2026 Saulo Verissimo +// SPDX-License-Identifier: MIT + +#ifndef UMP_DISPLAY_H +#define UMP_DISPLAY_H + +#include +#include +#include +#include +#include "mapping.h" +#include "ump_stream_handler.h" + +class UMPDisplay { +public: + + void init() { + pinMode(PIN_POWER_ON, OUTPUT); + digitalWrite(PIN_POWER_ON, HIGH); + + _tft.init(); + _tft.setRotation(2); + _tft.setBrightness(255); + _tft.fillScreen(C_BG); + pinMode(TFT_BL_PIN, OUTPUT); + digitalWrite(TFT_BL_PIN, HIGH); + + _proto = 0; _conn = false; + _rx = 0; _tx = 0; _vel = 0; _nw = 0; + memset(_w, 0, sizeof(_w)); + memset(_log, 0, sizeof(_log)); + _logN = 0; + + _drawAll(); + } + + void setConnected(bool c) { _conn = c; _drawHeader(); } + + void setProtocol(uint8_t alt) { + _proto = alt; + _drawProto(); + _drawStatus(); + } + + // ── RX: push a received UMP packet for display ────────── + void pushRxUMP(const uint32_t* words, uint8_t nw) { + _rx++; + _storeWords(words, nw); + + uint8_t b0 = _w[0] & 0xFF; + uint8_t b1 = (_w[0] >> 8) & 0xFF; + uint8_t b2 = (_w[0] >> 16) & 0xFF; + uint8_t b3 = (_w[0] >> 24) & 0xFF; + uint8_t mt = (b0 >> 4) & 0xF; + + // Update velocity from NoteOn/NoteOff (MT=2 or MT=4) + if (mt == 0x2 || mt == 0x4) { + uint8_t st = b1 & 0xF0; + if (st == 0x90 && b3 > 0) { + _vel = (mt == 0x4 && nw >= 2) + ? (uint8_t)((_w[1] >> 17) & 0x7F) // MT=4: 16-bit vel → 7-bit + : b3; // MT=2: 7-bit vel + } else if (st == 0x80 || (st == 0x90 && b3 == 0)) { + _vel = 0; + } + } + + _drawHex(); + _drawVel(); + _addLog(b0, b1, b2, b3, mt, false); + _drawStatus(); + } + + // ── TX: push a transmitted UMP packet for display ─────── + void pushTxUMP(const uint32_t* words, uint8_t nw) { + _tx++; + + uint8_t b0 = words[0] & 0xFF; + uint8_t b1 = (words[0] >> 8) & 0xFF; + uint8_t b2 = (words[0] >> 16) & 0xFF; + uint8_t b3 = (words[0] >> 24) & 0xFF; + uint8_t mt = (b0 >> 4) & 0xF; + + _addLog(b0, b1, b2, b3, mt, true); + _drawStatus(); + } + + // ── RX activity indicator ─────────────────────────────── + void pulseRx() { + static bool s = false; s = !s; + _tft.fillCircle(SCR_W - MARGIN - 4, Y_HEADER + H_HEADER / 2, + 4, s ? C_CYAN : C_HEADER); + } + +private: + // ── LGFX — T-Display-S3 (ST7789V 170x320, parallel 8-bit) ── + class LGFX : public lgfx::LGFX_Device { + public: + LGFX() { + { auto c = _bus.config(); + c.pin_wr=8; c.pin_rd=9; c.pin_rs=7; + c.pin_d0=39; c.pin_d1=40; c.pin_d2=41; c.pin_d3=42; + c.pin_d4=45; c.pin_d5=46; c.pin_d6=47; c.pin_d7=48; + _bus.config(c); _panel.setBus(&_bus); } + { auto c = _panel.config(); + c.pin_cs=6; c.pin_rst=5; c.pin_busy=-1; + c.offset_rotation=1; c.offset_x=40; + c.readable=false; c.invert=true; + c.rgb_order=false; c.dlen_16bit=false; c.bus_shared=false; + c.panel_width=170; c.panel_height=320; + _panel.config(c); } + setPanel(&_panel); + { auto c = _bl.config(); + c.pin_bl=38; c.invert=false; c.freq=22000; c.pwm_channel=7; + _bl.config(c); _panel.setLight(&_bl); } + } + private: + lgfx::Bus_Parallel8 _bus; + lgfx::Panel_ST7789 _panel; + lgfx::Light_PWM _bl; + }; + + LGFX _tft; + uint8_t _proto = 0; + bool _conn = false; + uint32_t _rx = 0, _tx = 0; + uint8_t _vel = 0, _nw = 0; + uint32_t _w[4] = {}; + + struct LogLine { char t[48]; uint32_t c; }; + LogLine _log[N_LOG]; + int _logN = 0; + + // ── helpers ───────────────────────────────────────────── + void _storeWords(const uint32_t* words, uint8_t nw) { + _w[0] = words[0]; + _w[1] = (nw >= 2) ? words[1] : 0; + _w[2] = (nw >= 3) ? words[2] : 0; + _w[3] = (nw >= 4) ? words[3] : 0; + _nw = nw; + } + + void _fill(int y, int h, uint32_t col) { + _tft.fillRect(0, y, SCR_W, h, col); + } + void _hline(int y) { + _tft.drawFastHLine(0, y, SCR_W, C_DIV); + } + void _txt(int x, int y, uint32_t fg, uint32_t bg, + const lgfx::IFont* f, const char* s) { + _tft.setFont(f); + _tft.setTextColor(fg, bg); + _tft.drawString(s, x, y); + } + + // ── full repaint ──────────────────────────────────────── + void _drawAll() { + _drawHeader(); + _drawProto(); + _drawHex(); + _drawVel(); + _drawLog(); + _drawStatus(); + } + + // ── header ────────────────────────────────────────────── + void _drawHeader() { + _fill(Y_HEADER, H_HEADER, C_HEADER); + _txt(MARGIN + 2, Y_HEADER + 4, C_WHITE, C_HEADER, + &lgfx::fonts::Font2, "USB MIDI 2.0"); + _txt(SCR_W / 2 - 30, Y_HEADER + 4, C_DIV, C_HEADER, + &lgfx::fonts::Font2, "T-Display-S3"); + uint32_t dot = _conn ? C_GREEN : C_ORANGE; + _tft.fillCircle(SCR_W - MARGIN - 4, Y_HEADER + H_HEADER / 2, 4, dot); + } + + // ── protocol tabs ─────────────────────────────────────── + void _drawProto() { + int half = SCR_W / 2; + uint32_t l_bg = (_proto == 0) ? C_ORANGE : C_DIV; + uint32_t l_fg = (_proto == 0) ? C_BG : C_GRAY; + uint32_t r_bg = (_proto == 1) ? C_GREEN : C_DIV; + uint32_t r_fg = (_proto == 1) ? C_BG : C_GRAY; + + _tft.fillRect(0, Y_PROTO, half, H_PROTO, l_bg); + _tft.fillRect(half, Y_PROTO, half, H_PROTO, r_bg); + + _txt(MARGIN + 4, Y_PROTO + 2, l_fg, l_bg, + &lgfx::fonts::Font2, "MIDI 1.0"); + _txt(half + MARGIN + 4, Y_PROTO + 2, r_fg, r_bg, + &lgfx::fonts::Font2, "MIDI 2.0 / UMP"); + + _hline(Y_PROTO + H_PROTO); + } + + // ── UMP hex view ──────────────────────────────────────── + void _drawHex() { + _fill(Y_HEX, H_HEX, C_BG); + _hline(Y_HEX + H_HEX); + + if (_nw == 0) { + _txt(MARGIN, Y_HEX + 8, C_DIV, C_BG, &lgfx::fonts::Font2, + "waiting for data..."); + return; + } + + uint8_t b0 = _w[0] & 0xFF; + uint8_t b1 = (_w[0] >> 8) & 0xFF; + uint8_t b2 = (_w[0] >> 16) & 0xFF; + uint8_t b3 = (_w[0] >> 24) & 0xFF; + uint8_t mt = (b0 >> 4) & 0xF; + uint8_t grp = b0 & 0xF; + uint8_t st = b1 & 0xF0; + uint8_t ch = b1 & 0x0F; + + // Line 1: color-coded hex bytes + char hex[48]; int cx = MARGIN; + _tft.setFont(&lgfx::fonts::Font2); + + _tft.setTextColor(C_CYAN, C_BG); + snprintf(hex, 4, "%02X ", b0); + _tft.drawString(hex, cx, Y_HEX + 2); cx += 24; + + _tft.setTextColor(C_ORANGE, C_BG); + snprintf(hex, 4, "%02X ", b1); + _tft.drawString(hex, cx, Y_HEX + 2); cx += 24; + + _tft.setTextColor(C_DIV, C_BG); + snprintf(hex, 8, "%02X %02X", b2, b3); + _tft.drawString(hex, cx, Y_HEX + 2); + + if (_nw >= 2) { + _tft.setTextColor(C_DIV, C_BG); + snprintf(hex, 12, " %08lX", (unsigned long)_w[1]); + _tft.drawString(hex, cx + 48, Y_HEX + 2); + } + + // Line 2: decoded description + char desc[48]; uint32_t col = C_DIV; + const char* tag = (mt == 0x4) ? "M2" : "M1"; + + if (mt == 0x2 || mt == 0x4) { + if (st == 0x90 && b3 > 0) { + snprintf(desc, 48, "%s NoteOn G%u Ch%u N=%u V=%u", + tag, grp, ch, b2, b3); + col = C_GREEN; + } + else if (st == 0x80 || (st == 0x90 && b3 == 0)) { + snprintf(desc, 48, "%s NoteOff G%u Ch%u N=%u", + tag, grp, ch, b2); + col = C_GRAY; + } + else if (st == 0xB0) { + snprintf(desc, 48, "%s CC G%u Ch%u CC=%u V=%u", + tag, grp, ch, b2, b3); + col = C_CYAN; + } + else if (st == 0xE0) { + snprintf(desc, 48, "%s PBend G%u Ch%u %02X%02X", + tag, grp, ch, b2, b3); + col = C_MAGENTA; + } + else { + snprintf(desc, 48, "%s Msg G%u Ch%u %02X %02X", + tag, grp, ch, b2, b3); + } + } + else if (mt == 0x3 || mt == 0x5) { + snprintf(desc, 48, "SysEx G%u [%u words]", grp, _nw); + col = C_YELLOW; + } + else if (mt == 0xF) { + uint16_t sts = streamStatus(_w[0]); + const char* lbl = umpStreamLabel(sts); + snprintf(desc, 48, "<< %s", lbl); + col = C_CYAN; + } + else { + snprintf(desc, 48, "MT=%01X G%u %02X %02X %02X", + mt, grp, b1, b2, b3); + } + + _txt(MARGIN, Y_HEX + 17, col, C_BG, &lgfx::fonts::Font2, desc); + } + + // ── velocity bar ──────────────────────────────────────── + void _drawVel() { + _fill(Y_VEL, H_VEL, C_BG); + _hline(Y_VEL + H_VEL); + + int bx = MARGIN, bw = SCR_W - 72, bh = H_VEL - 4, by = Y_VEL + 2; + int fill = (_vel * bw) / 127; + _tft.fillRect(bx, by, fill, bh, C_CYAN); + _tft.fillRect(bx + fill, by, bw - fill, bh, C_GRAY); + + char buf[12]; snprintf(buf, sizeof(buf), "V:%3u", _vel); + _txt(bx + bw + 6, Y_VEL + 1, C_DIV, C_BG, &lgfx::fonts::Font2, buf); + } + + // ── event log ─────────────────────────────────────────── + void _addLog(uint8_t b0, uint8_t b1, uint8_t b2, uint8_t b3, + uint8_t mt, bool isTx) + { + LogLine& e = _log[_logN % N_LOG]; + uint8_t st = b1 & 0xF0; + uint8_t grp = b0 & 0xF; + uint8_t ch = b1 & 0x0F; + const char* dir = isTx ? "T:" : "R:"; + e.c = C_DIV; + + if (mt == 0x2 || mt == 0x4) { + if (st == 0x90 && b3 > 0) { + snprintf(e.t, 48, "%sNoteOn G%u Ch%u N%u V%u", + dir, grp, ch, b2, b3); + e.c = C_GREEN; + } + else if (st == 0x80 || (st == 0x90 && b3 == 0)) { + snprintf(e.t, 48, "%sNoteOff G%u Ch%u N%u", + dir, grp, ch, b2); + e.c = C_GRAY; + } + else if (st == 0xB0) { + snprintf(e.t, 48, "%sCC G%u Ch%u CC%u V%u", + dir, grp, ch, b2, b3); + e.c = C_CYAN; + } + else if (st == 0xE0) { + snprintf(e.t, 48, "%sPBend G%u Ch%u %02X%02X", + dir, grp, ch, b2, b3); + e.c = C_MAGENTA; + } + else { + snprintf(e.t, 48, "%s%02X %02X %02X %02X", + dir, b0, b1, b2, b3); + } + } + else if (mt == 0x3 || mt == 0x5) { + snprintf(e.t, 48, "%sSysEx G%u %02X", dir, grp, b1); + e.c = C_YELLOW; + } + else if (mt == 0xF) { + // Decode Stream message subtype + uint16_t sts = ((uint16_t)(b0 & 0x03) << 8) | b1; + const char* lbl = umpStreamLabel(sts); + snprintf(e.t, 48, "%s%s", dir, lbl); + e.c = isTx ? C_GREEN : C_CYAN; + } + else { + snprintf(e.t, 48, "%sMT%01X %02X %02X %02X %02X", + dir, mt, b0, b1, b2, b3); + } + + _logN++; + _drawLog(); + } + + void _drawLog() { + _fill(Y_LOG, H_LOG, C_BG); + _hline(Y_LOG + H_LOG); + + for (int i = 0; i < N_LOG; i++) { + if (_logN < N_LOG - i) continue; + int idx = (_logN - N_LOG + i) % N_LOG; + if (idx < 0) idx += N_LOG; + _txt(MARGIN, Y_LOG + 1 + i * H_LLINE, _log[idx].c, C_BG, + &lgfx::fonts::Font0, _log[idx].t); + } + } + + // ── status bar ────────────────────────────────────────── + void _drawStatus() { + _fill(Y_STATUS, H_STATUS, C_HEADER); + + char buf[52]; + snprintf(buf, sizeof(buf), "RX: %-5lu TX: %-5lu", _rx, _tx); + _txt(MARGIN, Y_STATUS + 4, C_WHITE, C_HEADER, + &lgfx::fonts::Font2, buf); + + const char* ps = (_proto == 1) ? "MIDI 2.0" : "MIDI 1.0"; + uint32_t pc = (_proto == 1) ? C_GREEN : C_ORANGE; + int pw = strlen(ps) * 12; + _txt(SCR_W - MARGIN - pw, Y_STATUS + 4, pc, C_HEADER, + &lgfx::fonts::Font2, ps); + } +}; + +#endif // UMP_DISPLAY_H diff --git a/examples/T-Display-S3-ESP32-S3-MIDI2-PingPong/arduino/tdisplay_s3_midi2/mapping.h b/examples/T-Display-S3-ESP32-S3-MIDI2-PingPong/arduino/tdisplay_s3_midi2/mapping.h new file mode 100644 index 0000000..ebbada1 --- /dev/null +++ b/examples/T-Display-S3-ESP32-S3-MIDI2-PingPong/arduino/tdisplay_s3_midi2/mapping.h @@ -0,0 +1,68 @@ +#ifndef MAPPING_H +#define MAPPING_H + +// ── T-Display-S3 hardware ──────────────────────────────────────────────────── +#define TFT_BL_PIN 38 +#define PIN_POWER_ON 15 +#define BTN1 0 // GPIO0 — toggle NoteOn/Off +#define BTN2 14 // GPIO14 — cycle velocity + +// ── Screen (landscape 320 × 170) ──────────────────────────────────────────── +#define SCR_W 320 +#define SCR_H 170 +#define MARGIN 4 + +// ── Color palette ──────────────────────────────────────────────────────────── +#define C_BG 0x0000 // Black +#define C_HEADER 0x0821 // Very dark blue — header bg +#define C_DIV 0x7BEF // Mid slate — dividers, dim labels +#define C_PANEL 0x1082 // Dark navy — section panels +#define C_WHITE 0xFFFF // White +#define C_GREEN 0x07E0 // Green +#define C_CYAN 0x07FF // Cyan +#define C_ORANGE 0xFD20 // Orange +#define C_YELLOW 0xFFE0 // Yellow +#define C_GRAY 0x8C71 // Gray +#define C_BLUE 0x001F // Blue +#define C_MAGENTA 0xF81F // Magenta + +// ── Layout rows ────────────────────────────────────────────────────────────── +// +// Y= 0 ┌───────────────────────────────────┐ +// │ USB MIDI 2.0 • T-Display-S3 ● │ 24 HEADER +// Y= 24 ├────────────────┬──────────────────┤ +// │ MIDI 1.0 │ MIDI 2.0 / UMP │ 18 PROTO +// Y= 42 ├────────────────┴──────────────────┤ +// │ 20 90 48 60 ── NoteOn info │ 30 HEX +// Y= 72 ├───────────────────────────────────┤ +// │ ████████████░░░░░░░░ Vel: 96 │ 14 VEL +// Y= 86 ├───────────────────────────────────┤ +// │ NoteOn G0 Ch0 N72 V96 │ +// │ NoteOff G0 Ch0 N72 │ 56 LOG (4×14) +// │ CC G0 Ch0 CC7 V64 │ +// │ NoteOn G0 Ch0 N60 V80 │ +// Y=142 ├───────────────────────────────────┤ +// │ RX:0 TX:0 MIDI 1.0 │ 28 STATUS +// Y=170 └───────────────────────────────────┘ + +#define Y_HEADER 0 +#define H_HEADER 24 + +#define Y_PROTO 24 +#define H_PROTO 18 + +#define Y_HEX 42 +#define H_HEX 30 + +#define Y_VEL 72 +#define H_VEL 14 + +#define Y_LOG 86 +#define H_LOG 56 +#define N_LOG 4 +#define H_LLINE 14 + +#define Y_STATUS 142 +#define H_STATUS 28 + +#endif // MAPPING_H diff --git a/examples/T-Display-S3-ESP32-S3-MIDI2-PingPong/arduino/tdisplay_s3_midi2/platform.local.txt.example b/examples/T-Display-S3-ESP32-S3-MIDI2-PingPong/arduino/tdisplay_s3_midi2/platform.local.txt.example new file mode 100644 index 0000000..ad1f57c --- /dev/null +++ b/examples/T-Display-S3-ESP32-S3-MIDI2-PingPong/arduino/tdisplay_s3_midi2/platform.local.txt.example @@ -0,0 +1,19 @@ +# platform.local.txt — Arduino IDE build flags for tusb_ump +# +# Copy as "platform.local.txt" (drop .example) to: +# Linux: ~/.arduino15/packages/esp32/hardware/esp32// +# macOS: ~/Library/Arduino15/packages/esp32/hardware/esp32// +# Windows: %LOCALAPPDATA%\Arduino15\packages\esp32\hardware\esp32\\ +# +# Restart Arduino IDE after copying. Remove when no longer needed. + +compiler.cpp.extra_flags=-DCFG_TUD_UMP=1 -DCFG_TUD_UMP_RX_BUFSIZE=512 -DCFG_TUD_UMP_TX_BUFSIZE=512 -DCFG_TUD_MIDI=0 -DCONFIG_TINYUSB_MIDI_ENABLED=0 +compiler.c.extra_flags=-DCFG_TUD_UMP=1 -DCFG_TUD_UMP_RX_BUFSIZE=512 -DCFG_TUD_UMP_TX_BUFSIZE=512 -DCFG_TUD_MIDI=0 -DCONFIG_TINYUSB_MIDI_ENABLED=0 + +# Force the linker to keep our USB descriptor callback overrides. +# arduino-esp32 links with -Wl,--gc-sections -ffunction-sections, which +# eliminates "unreachable" functions. Our strong-symbol overrides of the +# WEAK esp32-hal-tinyusb.c callbacks would otherwise be discarded, +# causing the device to enumerate with VID=0x303A and a MIDI 1.0-only +# descriptor instead of our custom MIDI 2.0 dual-alt-setting descriptor. +compiler.c.elf.extra_flags=-Wl,-u,tud_descriptor_device_cb -Wl,-u,tud_descriptor_configuration_cb -Wl,-u,tud_descriptor_string_cb diff --git a/examples/T-Display-S3-ESP32-S3-MIDI2-PingPong/arduino/tdisplay_s3_midi2/tdisplay_s3_midi2.ino b/examples/T-Display-S3-ESP32-S3-MIDI2-PingPong/arduino/tdisplay_s3_midi2/tdisplay_s3_midi2.ino new file mode 100644 index 0000000..a919702 --- /dev/null +++ b/examples/T-Display-S3-ESP32-S3-MIDI2-PingPong/arduino/tdisplay_s3_midi2/tdisplay_s3_midi2.ino @@ -0,0 +1,239 @@ +// ============================================================ +// USB MIDI 2.0 Device — T-Display-S3 (Arduino IDE) +// ============================================================ +// +// Setup: +// 1) Board: "ESP32S3 Dev Module" (or LilyGO T-Display-S3) +// 2) Tools > USB Mode > "USB-OTG (TinyUSB)" +// 3) Tools > USB CDC On Boot > "Disabled" +// 4) Install LovyanGFX via Library Manager +// 5) Copy tusb_ump repo into ~/Arduino/libraries/tusb_ump/ +// 6) Copy platform.local.txt.example as platform.local.txt to: +// ~/.arduino15/packages/esp32/hardware/esp32// +// Then restart Arduino IDE. +// +// Controls: +// BTN1 (GPIO0) — toggle NoteOn / NoteOff (C5) +// BTN2 (GPIO14) — cycle test velocity: 32 → 64 → 96 → 127 +// +// Copyright (c) 2026 Saulo Verissimo +// SPDX-License-Identifier: MIT +// ============================================================ + +#include +#include "USB.h" +#include "ump_device.h" +#include "ump_stream_handler.h" +#include "UMPDisplay.h" +#include "mapping.h" + +// ── Device configuration ───────────────────────────────────── + +static const UMPStreamConfig streamCfg = { + .umpVersionMajor = 1, + .umpVersionMinor = 1, + .numFunctionBlocks = 1, + .staticFunctionBlocks = true, + .protocolCaps = UMP_PROTO_CAP_MIDI1 | UMP_PROTO_CAP_MIDI2, + .manufacturerId = { 0x00, 0x00, 0x7D }, // 0x7D = educational/dev + .familyId = 0x0001, + .modelId = 0x0001, + .swRevision = { '0', '1', '0', '0' }, + .endpointName = "ESP32-S3 MIDI2", + .productInstanceId = "ESP32S3MIDI001", + .fbName = "Group 0", + .fbDirection = UMP_FB_DIR_BIDIRECTIONAL, + .fbFirstGroup = 0, + .fbNumGroups = 1, + .fbUIHint = 0x00, + .fbMidi10 = 0x00, + .fbMidiCIVer = 0x00, + .fbSysEx8 = 0x00, +}; + +// ── State ──────────────────────────────────────────────────── + +static UMPDisplay display; +static bool usbWasConnected = false; +static uint8_t currentAlt = 0xFF; + +static uint32_t btn1_last = 0; +static uint32_t btn2_last = 0; +static const uint32_t DEBOUNCE_MS = 80; + +static bool noteIsOn = false; +static uint8_t testNote = 72; +static uint8_t testVelIdx = 1; +static const uint8_t velocities[] = { 32, 64, 96, 127 }; +static uint8_t testVel = 64; + +// ── tusb_ump callbacks ─────────────────────────────────────── + +extern "C" void tud_ump_set_itf_cb(uint8_t itf, uint8_t alt) { + (void)itf; + currentAlt = alt; + display.setProtocol(alt); +} + +extern "C" void tud_ump_rx_cb(uint8_t itf) { + (void)itf; +} + +// ── Stream handler TX callback ─────────────────────────────── + +static void onStreamTx(const uint32_t* words, uint8_t nw, + const char* /* label */) +{ + display.pushTxUMP(words, nw); +} + +// ── UMP helpers ────────────────────────────────────────────── + +static inline uint8_t umpWordsForMT(uint8_t mt) { + switch (mt) { + case 0x0: case 0x1: case 0x2: case 0x6: case 0x7: return 1; + case 0x3: case 0x4: case 0x8: case 0x9: case 0xA: case 0xD: return 2; + case 0xB: case 0xC: return 3; + case 0x5: case 0xE: case 0xF: return 4; + default: return 1; + } +} + +static inline uint32_t makeUMP_MIDI1CV(uint8_t group, uint8_t status, + uint8_t ch, uint8_t d0, uint8_t d1) +{ + return (uint32_t)d1 << 24 | + (uint32_t)d0 << 16 | + (uint32_t)((status & 0xF0) | (ch & 0x0F)) << 8 | + (uint32_t)(0x20 | (group & 0x0F)); +} + +// ── Send ───────────────────────────────────────────────────── + +static void sendNoteOn(uint8_t group, uint8_t ch, + uint8_t note, uint8_t vel) +{ + if (!tud_ump_n_mounted(0)) return; + uint32_t w = makeUMP_MIDI1CV(group, 0x90, ch, note, vel); + tud_ump_write(0, &w, 1); + display.pushTxUMP(&w, 1); +} + +static void sendNoteOff(uint8_t group, uint8_t ch, uint8_t note) +{ + if (!tud_ump_n_mounted(0)) return; + uint32_t w = makeUMP_MIDI1CV(group, 0x80, ch, note, 0); + tud_ump_write(0, &w, 1); + display.pushTxUMP(&w, 1); +} + +static void sendCC(uint8_t group, uint8_t ch, uint8_t cc, uint8_t val) +{ + if (!tud_ump_n_mounted(0)) return; + uint32_t w = makeUMP_MIDI1CV(group, 0xB0, ch, cc, val); + tud_ump_write(0, &w, 1); + display.pushTxUMP(&w, 1); +} + +// ── RX ─────────────────────────────────────────────────────── + +static void processRxUMP() +{ + for (int pass = 0; pass < 16; pass++) { + if (tud_ump_n_available(0) == 0) break; + + uint32_t firstWord = 0; + if (tud_ump_read(0, &firstWord, 1) == 0) break; + + uint8_t mt = (uint8_t)((firstWord & 0xF0) >> 4); + uint8_t numWords = umpWordsForMT(mt); + uint32_t words[4] = { firstWord, 0, 0, 0 }; + + // Wait for the remaining words before processing. + // Avoids passing a truncated packet to umpStreamHandleRx(). + if (numWords > 1 && tud_ump_n_available(0) < (numWords - 1)) break; + + for (uint8_t w = 1; w < numWords; w++) { + tud_ump_read(0, &words[w], 1); + } + + display.pushRxUMP(words, numWords); + display.pulseRx(); + + if (mt == 0xF) { + // MT=0xF Stream messages — handle with Discovery protocol + umpStreamHandleRx(0, words, streamCfg, onStreamTx); + } else { + // All other message types — echo back (loopback for testing) + if (tud_ump_n_writeable(0) >= numWords) { + tud_ump_write(0, words, numWords); + display.pushTxUMP(words, numWords); + } + } + } +} + +// ── Buttons ────────────────────────────────────────────────── + +static void handleButtons() +{ + uint32_t now = millis(); + + if (digitalRead(BTN1) == LOW && (now - btn1_last) > DEBOUNCE_MS) { + btn1_last = now; + if (!noteIsOn) { sendNoteOn(0, 0, testNote, testVel); noteIsOn = true; } + else { sendNoteOff(0, 0, testNote); noteIsOn = false; } + } + + if (digitalRead(BTN2) == LOW && (now - btn2_last) > DEBOUNCE_MS) { + btn2_last = now; + testVelIdx = (testVelIdx + 1) % (sizeof(velocities) / sizeof(velocities[0])); + testVel = velocities[testVelIdx]; + sendCC(0, 0, 7, testVel); + } +} + +// ── Arduino ────────────────────────────────────────────────── + +void setup() +{ + USB.begin(); + Serial.begin(115200); + pinMode(BTN1, INPUT_PULLUP); + pinMode(BTN2, INPUT_PULLUP); + testVel = velocities[testVelIdx]; + + display.init(); + display.setConnected(false); + display.setProtocol(0); + + Serial.println("[MIDI2] USB MIDI 2.0 device started"); + Serial.printf("[MIDI2] EP name: %s\n", streamCfg.endpointName); + Serial.printf("[MIDI2] Product: %s\n", streamCfg.productInstanceId); +} + +void loop() +{ + bool mounted = tud_ump_n_mounted(0); + + if (mounted && !usbWasConnected) { + usbWasConnected = true; + display.setConnected(true); + } else if (!mounted && usbWasConnected) { + usbWasConnected = false; + currentAlt = 0xFF; + display.setConnected(false); + } + + if (mounted) { + uint8_t alt = tud_alt_setting(0); + if (alt != currentAlt) { + currentAlt = alt; + display.setProtocol(alt); + } + processRxUMP(); + } + + handleButtons(); + delay(2); +} diff --git a/examples/T-Display-S3-ESP32-S3-MIDI2-PingPong/arduino/tdisplay_s3_midi2/ump_stream_handler.h b/examples/T-Display-S3-ESP32-S3-MIDI2-PingPong/arduino/tdisplay_s3_midi2/ump_stream_handler.h new file mode 100644 index 0000000..c0215b9 --- /dev/null +++ b/examples/T-Display-S3-ESP32-S3-MIDI2-PingPong/arduino/tdisplay_s3_midi2/ump_stream_handler.h @@ -0,0 +1,436 @@ +// ump_stream_handler.h — UMP Stream Message (MT=0xF) handler +// +// Implements the device-side UMP Endpoint Discovery protocol per the +// UMP and MIDI 2.0 Protocol Specification v1.1 (M2-104-UM). +// +// When Windows MIDI Services (or any UMP-capable host) switches to +// Alternate Setting 1 (MIDI Streaming 2.0), it sends a series of +// MT=0xF Stream Messages to discover the device's capabilities: +// +// Host → Device EP Discovery (status 0x000) +// Device → Host EP Info Notification (0x001) +// Device → Host Device Identity Notification (0x002) +// Device → Host Endpoint Name Notification (0x003) +// Device → Host Product Instance ID Notification (0x004) +// +// Host → Device Stream Configuration Request (0x005) +// Device → Host Stream Configuration Notification (0x006) +// +// Host → Device Function Block Discovery (0x010) +// Device → Host Function Block Info Notification (0x011) +// Device → Host Function Block Name Notification (0x012) +// +// This handler parses incoming requests and generates the correct +// responses. All responses are sent via tud_ump_write(). +// +// Copyright (c) 2026 Saulo Verissimo +// SPDX-License-Identifier: MIT + +#ifndef UMP_STREAM_HANDLER_H +#define UMP_STREAM_HANDLER_H + +#include +#include +#include "ump_device.h" + +// ───────────────────────────────────────────────────────────── +// UMP Stream Message status codes (10-bit) +// Reference: M2-104-UM v1.1, Section 7.1 +// ───────────────────────────────────────────────────────────── + +#define UMP_STREAM_STATUS_EP_DISCOVERY 0x000 +#define UMP_STREAM_STATUS_EP_INFO 0x001 +#define UMP_STREAM_STATUS_DEVICE_INFO 0x002 +#define UMP_STREAM_STATUS_EP_NAME 0x003 +#define UMP_STREAM_STATUS_PRODUCT_ID 0x004 +#define UMP_STREAM_STATUS_STREAM_CFG_REQ 0x005 +#define UMP_STREAM_STATUS_STREAM_CFG 0x006 +#define UMP_STREAM_STATUS_FB_DISCOVERY 0x010 +#define UMP_STREAM_STATUS_FB_INFO 0x011 +#define UMP_STREAM_STATUS_FB_NAME 0x012 +#define UMP_STREAM_STATUS_START_CLIP 0x020 +#define UMP_STREAM_STATUS_END_CLIP 0x021 + +// ── Format field (bits [3:2] of wire byte 0) ───────────────── +#define UMP_STREAM_FMT_COMPLETE 0 +#define UMP_STREAM_FMT_START 1 +#define UMP_STREAM_FMT_CONTINUE 2 +#define UMP_STREAM_FMT_END 3 + +// ── Endpoint Discovery filter bitmap (wire byte 4) ─────────── +#define UMP_FILTER_EP_INFO 0x01 +#define UMP_FILTER_DEVICE_ID 0x02 +#define UMP_FILTER_EP_NAME 0x04 +#define UMP_FILTER_PRODUCT_ID 0x08 +#define UMP_FILTER_STREAM_CFG 0x10 + +// ── Protocol capabilities (EP Info byte 6) ─────────────────── +#define UMP_PROTO_CAP_MIDI2 0x01 // supports MIDI 2.0 Protocol +#define UMP_PROTO_CAP_MIDI1 0x02 // supports MIDI 1.0 Protocol + +// ── Protocol selection (Stream Configuration) ──────────────── +#define UMP_PROTO_MIDI1 0x01 +#define UMP_PROTO_MIDI2 0x02 + +// ── Function Block direction ───────────────────────────────── +#define UMP_FB_DIR_INPUT 0x01 // receives from host +#define UMP_FB_DIR_OUTPUT 0x02 // sends to host +#define UMP_FB_DIR_BIDIRECTIONAL 0x03 + +// ───────────────────────────────────────────────────────────── +// Wire byte packing — ESP32 little-endian ↔ UMP wire order +// +// UMP words are transmitted MSB-first on USB. On ESP32 (LE), +// the LSB of a uint32_t occupies the lowest memory address, +// which is the first byte sent by the USB bulk endpoint. +// +// packWord(b0, b1, b2, b3) places wire byte 0 at the LSB: +// address+0 = b0 (first on wire — contains MT nibble) +// address+1 = b1 +// address+2 = b2 +// address+3 = b3 (last on wire) +// ───────────────────────────────────────────────────────────── + +static inline uint32_t packWord(uint8_t b0, uint8_t b1, + uint8_t b2, uint8_t b3) +{ + return (uint32_t)b0 + | ((uint32_t)b1 << 8) + | ((uint32_t)b2 << 16) + | ((uint32_t)b3 << 24); +} + +static inline uint8_t wireB0(uint32_t w) { return (uint8_t)(w); } +static inline uint8_t wireB1(uint32_t w) { return (uint8_t)(w >> 8); } +static inline uint8_t wireB2(uint32_t w) { return (uint8_t)(w >> 16); } +static inline uint8_t wireB3(uint32_t w) { return (uint8_t)(w >> 24); } + +// ── Stream header helpers ──────────────────────────────────── + +static inline uint32_t streamWord0(uint8_t format, uint16_t status, + uint8_t d2, uint8_t d3) +{ + uint8_t b0 = 0xF0 | ((format & 0x03) << 2) | ((status >> 8) & 0x03); + uint8_t b1 = status & 0xFF; + return packWord(b0, b1, d2, d3); +} + +static inline uint16_t streamStatus(uint32_t w0) +{ + return ((uint16_t)(wireB0(w0) & 0x03) << 8) | wireB1(w0); +} + +static inline uint8_t streamFormat(uint32_t w0) +{ + return (wireB0(w0) >> 2) & 0x03; +} + +// ───────────────────────────────────────────────────────────── +// Device configuration — all fields the host may discover +// ───────────────────────────────────────────────────────────── + +struct UMPStreamConfig { + // UMP protocol version + uint8_t umpVersionMajor = 1; + uint8_t umpVersionMinor = 1; + + // Function blocks + uint8_t numFunctionBlocks = 1; + bool staticFunctionBlocks = true; + + // Protocol capabilities + uint8_t protocolCaps = UMP_PROTO_CAP_MIDI1 | UMP_PROTO_CAP_MIDI2; + + // SysEx Manufacturer ID (3 bytes — 1-byte IDs use 0x00 0x00 ID) + uint8_t manufacturerId[3] = { 0x00, 0x00, 0x7D }; // 0x7D = educational + uint16_t familyId = 0x0001; + uint16_t modelId = 0x0001; + char swRevision[4] = { '0', '1', '0', '0' }; + + // Endpoint strings (UTF-8, null-terminated) + const char* endpointName = "MIDI 2.0 Device"; + const char* productInstanceId = "MIDI2-001"; + + // Function Block 0 + const char* fbName = "Group 0"; + uint8_t fbDirection = UMP_FB_DIR_BIDIRECTIONAL; + uint8_t fbFirstGroup = 0; + uint8_t fbNumGroups = 1; + uint8_t fbUIHint = 0x00; // 0=unknown + uint8_t fbMidi10 = 0x00; // 0=not a MIDI 1.0 stream + uint8_t fbMidiCIVer = 0x00; + uint8_t fbSysEx8 = 0x00; +}; + +// ───────────────────────────────────────────────────────────── +// TX callback — invoked after each response is sent +// ───────────────────────────────────────────────────────────── + +typedef void (*UMPStreamTxCb)(const uint32_t* words, uint8_t nw, + const char* label); + +// ───────────────────────────────────────────────────────────── +// Human-readable label for any Stream message status +// ───────────────────────────────────────────────────────────── + +__attribute__((unused)) +static const char* umpStreamLabel(uint16_t status) +{ + switch (status) { + case UMP_STREAM_STATUS_EP_DISCOVERY: return "EP Discovery"; + case UMP_STREAM_STATUS_EP_INFO: return "EP Info"; + case UMP_STREAM_STATUS_DEVICE_INFO: return "Device ID"; + case UMP_STREAM_STATUS_EP_NAME: return "EP Name"; + case UMP_STREAM_STATUS_PRODUCT_ID: return "Product ID"; + case UMP_STREAM_STATUS_STREAM_CFG_REQ: return "StreamCfg Req"; + case UMP_STREAM_STATUS_STREAM_CFG: return "StreamCfg OK"; + case UMP_STREAM_STATUS_FB_DISCOVERY: return "FB Discovery"; + case UMP_STREAM_STATUS_FB_INFO: return "FB Info"; + case UMP_STREAM_STATUS_FB_NAME: return "FB Name"; + case UMP_STREAM_STATUS_START_CLIP: return "Start Clip"; + case UMP_STREAM_STATUS_END_CLIP: return "End Clip"; + default: return "Stream ???"; + } +} + +// ───────────────────────────────────────────────────────────── +// Internal — send a 4-word Stream message +// ───────────────────────────────────────────────────────────── + +static void _streamSend(uint8_t itf, uint32_t w[4], + UMPStreamTxCb onTx, const char* label) +{ + tud_ump_write(itf, w, 4); + if (onTx) onTx(w, 4, label); +} + +// ───────────────────────────────────────────────────────────── +// Internal — send a UTF-8 string as one or more Stream packets +// +// status : UMP_STREAM_STATUS_EP_NAME / PRODUCT_ID / FB_NAME +// str : null-terminated UTF-8 string +// prefixByte: ≥0 to insert a prefix byte (e.g., FB index) at +// wire byte 2, shifting chars to byte 3 onward. +// <0 means no prefix — chars start at byte 2. +// ───────────────────────────────────────────────────────────── + +static void _streamSendString(uint8_t itf, uint16_t status, + const char* str, int prefixByte, + UMPStreamTxCb onTx, const char* label) +{ + const size_t len = strlen(str); + const size_t charsPerPkt = (prefixByte >= 0) ? 13 : 14; + const size_t nPkts = (len == 0) ? 1 : (len + charsPerPkt - 1) / charsPerPkt; + + size_t pos = 0; + for (size_t pkt = 0; pkt < nPkts; pkt++) { + + uint8_t fmt; + if (nPkts == 1) fmt = UMP_STREAM_FMT_COMPLETE; + else if (pkt == 0) fmt = UMP_STREAM_FMT_START; + else if (pkt == nPkts - 1) fmt = UMP_STREAM_FMT_END; + else fmt = UMP_STREAM_FMT_CONTINUE; + + uint8_t buf[16]; + memset(buf, 0, sizeof(buf)); + + // Header (bytes 0-1) + buf[0] = 0xF0 | ((fmt & 0x03) << 2) | ((status >> 8) & 0x03); + buf[1] = status & 0xFF; + + // Optional prefix (byte 2 for FB Name = FB index) + size_t off = 2; + if (prefixByte >= 0) + buf[off++] = (uint8_t)prefixByte; + + // Fill remaining bytes with string characters + while (off < 16 && pos < len) + buf[off++] = (uint8_t)str[pos++]; + + uint32_t w[4]; + w[0] = packWord(buf[0], buf[1], buf[2], buf[3]); + w[1] = packWord(buf[4], buf[5], buf[6], buf[7]); + w[2] = packWord(buf[8], buf[9], buf[10], buf[11]); + w[3] = packWord(buf[12], buf[13], buf[14], buf[15]); + + _streamSend(itf, w, onTx, label); + } +} + +// ───────────────────────────────────────────────────────────── +// Response generators +// ───────────────────────────────────────────────────────────── + +static void _replyEndpointInfo(uint8_t itf, const UMPStreamConfig& cfg, + UMPStreamTxCb onTx) +{ + uint32_t w[4]; + w[0] = streamWord0(UMP_STREAM_FMT_COMPLETE, UMP_STREAM_STATUS_EP_INFO, + cfg.umpVersionMajor, cfg.umpVersionMinor); + w[1] = packWord( + (cfg.staticFunctionBlocks ? 0x80 : 0x00) | (cfg.numFunctionBlocks & 0x7F), + 0x00, // JRTS capabilities (none) + cfg.protocolCaps, + 0x00 // extensions + ); + w[2] = 0; + w[3] = 0; + _streamSend(itf, w, onTx, "EP Info"); +} + +static void _replyDeviceIdentity(uint8_t itf, const UMPStreamConfig& cfg, + UMPStreamTxCb onTx) +{ + uint32_t w[4]; + w[0] = streamWord0(UMP_STREAM_FMT_COMPLETE, UMP_STREAM_STATUS_DEVICE_INFO, + 0x00, 0x00); + w[1] = packWord( + 0x00, + cfg.manufacturerId[0], + cfg.manufacturerId[1], + cfg.manufacturerId[2] + ); + w[2] = packWord( + (cfg.familyId >> 8) & 0xFF, cfg.familyId & 0xFF, + (cfg.modelId >> 8) & 0xFF, cfg.modelId & 0xFF + ); + w[3] = packWord( + cfg.swRevision[0], cfg.swRevision[1], + cfg.swRevision[2], cfg.swRevision[3] + ); + _streamSend(itf, w, onTx, "Device ID"); +} + +static void _replyEndpointName(uint8_t itf, const UMPStreamConfig& cfg, + UMPStreamTxCb onTx) +{ + _streamSendString(itf, UMP_STREAM_STATUS_EP_NAME, + cfg.endpointName, -1, onTx, "EP Name"); +} + +static void _replyProductInstanceId(uint8_t itf, const UMPStreamConfig& cfg, + UMPStreamTxCb onTx) +{ + _streamSendString(itf, UMP_STREAM_STATUS_PRODUCT_ID, + cfg.productInstanceId, -1, onTx, "Product ID"); +} + +static void _replyStreamConfig(uint8_t itf, uint8_t requestedProto, + uint8_t jrts, const UMPStreamConfig& cfg, + UMPStreamTxCb onTx) +{ + // Confirm the requested protocol if we support it + uint8_t proto = requestedProto; + if (proto == UMP_PROTO_MIDI2 && !(cfg.protocolCaps & UMP_PROTO_CAP_MIDI2)) + proto = UMP_PROTO_MIDI1; + else if (proto == UMP_PROTO_MIDI1 && !(cfg.protocolCaps & UMP_PROTO_CAP_MIDI1)) + proto = UMP_PROTO_MIDI2; + + uint32_t w[4]; + w[0] = streamWord0(UMP_STREAM_FMT_COMPLETE, UMP_STREAM_STATUS_STREAM_CFG, + proto, 0x00); // JRTS: 0 (not supported) + w[1] = 0; + w[2] = 0; + w[3] = 0; + _streamSend(itf, w, onTx, "StreamCfg OK"); +} + +static void _replyFBInfo(uint8_t itf, uint8_t fbIdx, + const UMPStreamConfig& cfg, UMPStreamTxCb onTx) +{ + // Word 0 byte 2: [active (1 bit)] [fb_index (7 bits)] + // Word 1 byte 0: [ui_hint (2)] [midi1.0 (2)] [direction (2)] [rsvd (2)] + // Word 1 byte 1: first_group + // Word 1 byte 2: num_groups + // Word 1 byte 3: midi_ci_version + // Word 2 byte 0: max_sysex8_streams + + uint32_t w[4]; + w[0] = streamWord0(UMP_STREAM_FMT_COMPLETE, UMP_STREAM_STATUS_FB_INFO, + 0x80 | (fbIdx & 0x7F), // active=1 + 0x00); // reserved + w[1] = packWord( + (cfg.fbUIHint << 6) | + (cfg.fbMidi10 << 4) | + (cfg.fbDirection << 2), + cfg.fbFirstGroup, + cfg.fbNumGroups, + cfg.fbMidiCIVer + ); + w[2] = packWord(cfg.fbSysEx8, 0x00, 0x00, 0x00); + w[3] = 0; + _streamSend(itf, w, onTx, "FB Info"); +} + +static void _replyFBName(uint8_t itf, uint8_t fbIdx, + const UMPStreamConfig& cfg, UMPStreamTxCb onTx) +{ + _streamSendString(itf, UMP_STREAM_STATUS_FB_NAME, + cfg.fbName, (int)fbIdx, onTx, "FB Name"); +} + +// ───────────────────────────────────────────────────────────── +// Main entry point — process a received MT=0xF Stream message +// +// Call this from processRxUMP() when MT == 0xF. +// Returns a human-readable label for the received request, +// or nullptr if the message was not recognized. +// ───────────────────────────────────────────────────────────── + +static const char* umpStreamHandleRx(uint8_t itf, const uint32_t* words, + const UMPStreamConfig& cfg, + UMPStreamTxCb onTx) +{ + const uint16_t status = streamStatus(words[0]); + + switch (status) { + + // ── Endpoint Discovery ─────────────────────────────────── + case UMP_STREAM_STATUS_EP_DISCOVERY: { + const uint8_t filter = wireB0(words[1]); + + if (filter & UMP_FILTER_EP_INFO) + _replyEndpointInfo(itf, cfg, onTx); + if (filter & UMP_FILTER_DEVICE_ID) + _replyDeviceIdentity(itf, cfg, onTx); + if (filter & UMP_FILTER_EP_NAME) + _replyEndpointName(itf, cfg, onTx); + if (filter & UMP_FILTER_PRODUCT_ID) + _replyProductInstanceId(itf, cfg, onTx); + if (filter & UMP_FILTER_STREAM_CFG) + _replyStreamConfig(itf, UMP_PROTO_MIDI2, 0x00, cfg, onTx); + + return "EP Discovery"; + } + + // ── Stream Configuration Request ───────────────────────── + case UMP_STREAM_STATUS_STREAM_CFG_REQ: { + const uint8_t protocol = wireB2(words[0]); + const uint8_t jrts = wireB3(words[0]); + _replyStreamConfig(itf, protocol, jrts, cfg, onTx); + return "StreamCfg Req"; + } + + // ── Function Block Discovery ───────────────────────────── + case UMP_STREAM_STATUS_FB_DISCOVERY: { + const uint8_t fbNum = wireB2(words[0]); // 0xFF = all + const uint8_t filter = wireB3(words[0]); + + uint8_t first = (fbNum == 0xFF) ? 0 : fbNum; + uint8_t last = (fbNum == 0xFF) ? cfg.numFunctionBlocks : (fbNum + 1); + if (last > cfg.numFunctionBlocks) + last = cfg.numFunctionBlocks; + + for (uint8_t i = first; i < last; i++) { + if (filter & 0x01) _replyFBInfo(itf, i, cfg, onTx); + if (filter & 0x02) _replyFBName(itf, i, cfg, onTx); + } + return "FB Discovery"; + } + + default: + return nullptr; + } +} + +#endif // UMP_STREAM_HANDLER_H diff --git a/examples/T-Display-S3-ESP32-S3-MIDI2-PingPong/arduino/tdisplay_s3_midi2/usb_descriptors.cpp b/examples/T-Display-S3-ESP32-S3-MIDI2-PingPong/arduino/tdisplay_s3_midi2/usb_descriptors.cpp new file mode 100644 index 0000000..a2c3d31 --- /dev/null +++ b/examples/T-Display-S3-ESP32-S3-MIDI2-PingPong/arduino/tdisplay_s3_midi2/usb_descriptors.cpp @@ -0,0 +1,230 @@ +// usb_descriptors.cpp — USB MIDI 2.0 descriptor callbacks for tusb_ump +// +// Overrides the WEAK symbols from arduino-esp32's esp32-hal-tinyusb.c: +// tud_descriptor_device_cb() +// tud_descriptor_configuration_cb() +// tud_descriptor_string_cb() +// +// USB MIDI 2.0 requires DUAL ALTERNATE SETTINGS on Interface 1: +// Alt 0x00 — MIDI Streaming 1.0 (legacy hosts) +// Alt 0x01 — MIDI Streaming 2.0 (UMP, modern hosts) +// +// Host selects alt setting via SET_INTERFACE. tusb_ump handles this +// automatically via umpd_control_xfer_cb() and exposes tud_alt_setting(). +// +// Group Terminal Block (GTB) descriptor is served by tusb_ump via a +// class-specific GET_DESCRIPTOR request (0x2601), not via the config +// descriptor — so we don't include it in the config blob below. + +#include +#include "tusb.h" +#include "ump.h" + +// ───────────────────────────────────────────────────────────── +// Interface and endpoint numbering +// ───────────────────────────────────────────────────────────── +#define ITF_NUM_AUDIO_CONTROL 0 +#define ITF_NUM_MIDI_STREAMING 1 +#define ITF_NUM_TOTAL 2 + +#define EPNUM_MIDI_OUT 0x01 +#define EPNUM_MIDI_IN 0x81 + +#define EP_SIZE_FS 64 + +// ───────────────────────────────────────────────────────────── +// String descriptor indices +// ───────────────────────────────────────────────────────────── +enum { + STR_IDX_LANGID = 0, + STR_IDX_MANUFACTURER = 1, + STR_IDX_PRODUCT = 2, + STR_IDX_SERIAL = 3, + STR_IDX_MIDI_STREAMING = 4, + STR_IDX_BLOCK_1 = 5, +}; + +// ───────────────────────────────────────────────────────────── +// Device descriptor +// ───────────────────────────────────────────────────────────── +static const tusb_desc_device_t desc_device = { + .bLength = sizeof(tusb_desc_device_t), + .bDescriptorType = TUSB_DESC_DEVICE, + .bcdUSB = 0x0200, + .bDeviceClass = TUSB_CLASS_MISC, // 0xEF — required for IAD on Windows + .bDeviceSubClass = 0x02, + .bDeviceProtocol = 0x01, + .bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE, + .idVendor = 0x1209, // pid.codes (open-source VID) + .idProduct = 0x0001, // change for production + .bcdDevice = 0x0100, + .iManufacturer = STR_IDX_MANUFACTURER, + .iProduct = STR_IDX_PRODUCT, + .iSerialNumber = STR_IDX_SERIAL, + .bNumConfigurations = 1, +}; + +// ───────────────────────────────────────────────────────────── +// Configuration descriptor (153 bytes) +// +// Config(9) + IAD(8) + ITF0_AC(9) + CS_AC(9) +// + ITF1_Alt0(9) + CS_MS(7) + 2xJackIN(12) + 2xJackOUT(18) + 2xEP(18) + 2xCS_EP(10) +// + ITF1_Alt1(9) + CS_MS(7) + 2xEP(18) + 2xCS_EP(10) +// = 153 +// ───────────────────────────────────────────────────────────── + +#define CONFIG_TOTAL_LEN 153 +#define AC_CS_HEADER_LEN 9 + +static const uint8_t desc_configuration[CONFIG_TOTAL_LEN] = { + + // ── Configuration ───────────────────────────────────────── + 9, TUSB_DESC_CONFIGURATION, + U16_TO_U8S_LE(CONFIG_TOTAL_LEN), + ITF_NUM_TOTAL, 1, 0, + TU_BIT(7) | TUSB_DESC_CONFIG_ATT_SELF_POWERED, 50, + + // ── IAD ─────────────────────────────────────────────────── + 8, TUSB_DESC_INTERFACE_ASSOCIATION, + ITF_NUM_AUDIO_CONTROL, 2, + TUSB_CLASS_AUDIO, 0x03, 0x00, 0, + + // ── Interface 0: AudioControl ───────────────────────────── + 9, TUSB_DESC_INTERFACE, + ITF_NUM_AUDIO_CONTROL, 0, 0, + TUSB_CLASS_AUDIO, 0x01, 0x00, 0, // protocol must be 0x00 (umpd_open checks AUDIO_FUNC_PROTOCOL_CODE_UNDEF) + + // ── CS AC Header (UAC2) ─────────────────────────────────── + AC_CS_HEADER_LEN, TUSB_DESC_CS_INTERFACE, 0x01, + U16_TO_U8S_LE(0x0200), 0x03, + U16_TO_U8S_LE(AC_CS_HEADER_LEN), 0x00, + + // ─── Interface 1, Alt 0: MIDI Streaming 1.0 ─────────────── + 9, TUSB_DESC_INTERFACE, + ITF_NUM_MIDI_STREAMING, 0, 2, + TUSB_CLASS_AUDIO, 0x03, 0x00, STR_IDX_MIDI_STREAMING, + + // CS MS Header (bcdMSC=1.00, wTotalLength=37) + 7, TUSB_DESC_CS_INTERFACE, MIDI_1_CS_INTERFACE_HEADER, + U16_TO_U8S_LE(0x0100), U16_TO_U8S_LE(37), + + // Embedded IN Jack (ID=1) + 6, TUSB_DESC_CS_INTERFACE, MIDI_1_CS_INTERFACE_IN_JACK, + MIDI_1_JACK_EMBEDDED, 1, 0, + + // External IN Jack (ID=3) + 6, TUSB_DESC_CS_INTERFACE, MIDI_1_CS_INTERFACE_IN_JACK, + MIDI_1_JACK_EXTERNAL, 3, 0, + + // Embedded OUT Jack (ID=2, src=ExtIN 3) + 9, TUSB_DESC_CS_INTERFACE, MIDI_1_CS_INTERFACE_OUT_JACK, + MIDI_1_JACK_EMBEDDED, 2, 1, 3, 1, 0, + + // External OUT Jack (ID=4, src=EmbIN 1) + 9, TUSB_DESC_CS_INTERFACE, MIDI_1_CS_INTERFACE_OUT_JACK, + MIDI_1_JACK_EXTERNAL, 4, 1, 1, 1, 0, + + // Bulk OUT endpoint Alt0 + 9, TUSB_DESC_ENDPOINT, EPNUM_MIDI_OUT, TUSB_XFER_BULK, + U16_TO_U8S_LE(EP_SIZE_FS), 0, 0, 0, + + // CS Endpoint (assoc Embedded OUT jack 2) + 5, TUSB_DESC_CS_ENDPOINT, MIDI_1_CS_ENDPOINT_GENERAL, 1, 2, + + // Bulk IN endpoint Alt0 + 9, TUSB_DESC_ENDPOINT, EPNUM_MIDI_IN, TUSB_XFER_BULK, + U16_TO_U8S_LE(EP_SIZE_FS), 0, 0, 0, + + // CS Endpoint (assoc Embedded IN jack 1) + 5, TUSB_DESC_CS_ENDPOINT, MIDI_1_CS_ENDPOINT_GENERAL, 1, 1, + + // ─── Interface 1, Alt 1: MIDI Streaming 2.0 (UMP) ───────── + 9, TUSB_DESC_INTERFACE, + ITF_NUM_MIDI_STREAMING, 1, 2, + TUSB_CLASS_AUDIO, 0x03, 0x00, STR_IDX_MIDI_STREAMING, + + // CS MS Header (bcdMSC=2.00, wTotalLength=7) + 7, TUSB_DESC_CS_INTERFACE, MIDI_1_CS_INTERFACE_HEADER, + U16_TO_U8S_LE(0x0200), U16_TO_U8S_LE(7), + + // Bulk OUT endpoint Alt1 + 9, TUSB_DESC_ENDPOINT, EPNUM_MIDI_OUT, TUSB_XFER_BULK, + U16_TO_U8S_LE(EP_SIZE_FS), 0, 0, 0, + + // CS Endpoint MIDI 2.0 (assoc GTB ID=1) + 5, TUSB_DESC_CS_ENDPOINT, MIDI20_CS_ENDPOINT_GENERAL, 1, 1, + + // Bulk IN endpoint Alt1 + 9, TUSB_DESC_ENDPOINT, EPNUM_MIDI_IN, TUSB_XFER_BULK, + U16_TO_U8S_LE(EP_SIZE_FS), 0, 0, 0, + + // CS Endpoint MIDI 2.0 (assoc GTB ID=1) + 5, TUSB_DESC_CS_ENDPOINT, MIDI20_CS_ENDPOINT_GENERAL, 1, 1, +}; + +static_assert(sizeof(desc_configuration) == CONFIG_TOTAL_LEN, + "CONFIG_TOTAL_LEN mismatch"); + +// ───────────────────────────────────────────────────────────── +// String descriptors +// ───────────────────────────────────────────────────────────── +static const char *const string_desc_arr[] = { + (const char[]){ 0x09, 0x04 }, + "sauloverissimo", + "ESP32-S3 MIDI 2.0 Device", + "ESP32S3-MIDI2-001", + "MIDI Streaming", + "Group 0", +}; + +static uint16_t _string_desc_buf[32]; + +// ───────────────────────────────────────────────────────────── +// Callbacks (override WEAK symbols from arduino-esp32) +// +// __attribute__((used)) prevents --gc-sections from eliminating +// these functions. arduino-esp32 links with -Wl,--gc-sections +// and -ffunction-sections, so any function not reachable from a +// link root (setup/loop/app_main) would otherwise be discarded. +// The weak definitions in esp32-hal-tinyusb.c would then be used +// instead, causing VID=0x303A and a MIDI 1.0-only descriptor. +// ───────────────────────────────────────────────────────────── + +extern "C" { + +__attribute__((used)) +uint8_t const *tud_descriptor_device_cb(void) { + return (uint8_t const *)&desc_device; +} + +__attribute__((used)) +uint8_t const *tud_descriptor_configuration_cb(uint8_t index) { + (void)index; + return desc_configuration; +} + +__attribute__((used)) +uint16_t const *tud_descriptor_string_cb(uint8_t index, uint16_t langid) { + (void)langid; + uint8_t chr_count; + const uint8_t max_str = sizeof(string_desc_arr) / sizeof(string_desc_arr[0]); + + if (index == 0) { + memcpy(&_string_desc_buf[1], string_desc_arr[0], 2); + chr_count = 1; + } else if (index < max_str) { + const char *str = string_desc_arr[index]; + chr_count = (uint8_t)strlen(str); + if (chr_count > 31) chr_count = 31; + for (uint8_t i = 0; i < chr_count; i++) { + _string_desc_buf[1 + i] = str[i]; + } + } else { + return NULL; + } + + _string_desc_buf[0] = (TUSB_DESC_STRING << 8) | (2 * chr_count + 2); + return _string_desc_buf; +} + +} // extern "C" diff --git a/examples/T-Display-S3-ESP32-S3-MIDI2-PingPong/images/T-Display-S3-ESP32-S3-MIDI2-2.jpeg b/examples/T-Display-S3-ESP32-S3-MIDI2-PingPong/images/T-Display-S3-ESP32-S3-MIDI2-2.jpeg new file mode 100644 index 0000000..668263a Binary files /dev/null and b/examples/T-Display-S3-ESP32-S3-MIDI2-PingPong/images/T-Display-S3-ESP32-S3-MIDI2-2.jpeg differ diff --git a/examples/T-Display-S3-ESP32-S3-MIDI2-PingPong/images/T-Display-S3-ESP32-S3-MIDI2.jpeg b/examples/T-Display-S3-ESP32-S3-MIDI2-PingPong/images/T-Display-S3-ESP32-S3-MIDI2.jpeg new file mode 100644 index 0000000..3aa5e12 Binary files /dev/null and b/examples/T-Display-S3-ESP32-S3-MIDI2-PingPong/images/T-Display-S3-ESP32-S3-MIDI2.jpeg differ diff --git a/examples/T-Display-S3-ESP32-S3-MIDI2-PingPong/platformio/tdisplay_s3_midi2/README.md b/examples/T-Display-S3-ESP32-S3-MIDI2-PingPong/platformio/tdisplay_s3_midi2/README.md new file mode 100644 index 0000000..03de5ce --- /dev/null +++ b/examples/T-Display-S3-ESP32-S3-MIDI2-PingPong/platformio/tdisplay_s3_midi2/README.md @@ -0,0 +1,45 @@ +# USB MIDI 2.0 Device — PlatformIO + +ESP32-S3 USB MIDI 2.0 device example using tusb_ump. +Tested on LilyGO T-Display-S3. + +## Build + +```bash +pio run -e T-Display-S3-MIDI2 # compile +pio run -e T-Display-S3-MIDI2 -t upload # flash +pio device monitor # serial output +``` + +All build flags are in `platformio.ini` — no extra configuration needed. + +## Files + +``` +tdisplay_s3_midi2/ +├── platformio.ini +└── src/ + ├── main.cpp — setup/loop, buttons, UMP RX/TX + ├── usb_descriptors.cpp — USB MIDI 2.0 config descriptor (dual alt settings) + ├── UMPDisplay.h — LovyanGFX display handler + └── mapping.h — Pin assignments, colours, layout +``` + +## Controls + +| Button | Action | +|--------|--------| +| BTN1 (GPIO0) | Toggle NoteOn / NoteOff (C5) | +| BTN2 (GPIO14) | Cycle velocity: 32 > 64 > 96 > 127 | + +## Build flags + +| Flag | Purpose | +|------|---------| +| `ARDUINO_USB_MODE=0` | TinyUSB device mode | +| `ARDUINO_USB_CDC_ON_BOOT=0` | MIDI 2.0 owns the USB port | +| `CFG_TUD_UMP=1` | Enable tusb_ump class driver | +| `CFG_TUD_MIDI=0` | Disable built-in MIDI 1.0 driver | + +See the [Arduino IDE version](../../arduino/tdisplay_s3_midi2/) if you prefer +Arduino IDE over PlatformIO. diff --git a/examples/T-Display-S3-ESP32-S3-MIDI2-PingPong/platformio/tdisplay_s3_midi2/platformio.ini b/examples/T-Display-S3-ESP32-S3-MIDI2-PingPong/platformio/tdisplay_s3_midi2/platformio.ini new file mode 100644 index 0000000..b5bfb5c --- /dev/null +++ b/examples/T-Display-S3-ESP32-S3-MIDI2-PingPong/platformio/tdisplay_s3_midi2/platformio.ini @@ -0,0 +1,43 @@ +; tusb_ump — USB MIDI 2.0 device example for ESP32-S3 +; Tested on: LilyGO T-Display-S3 (ST7789, 320x170) +; +; Build: pio run -e T-Display-S3-MIDI2 +; Flash: pio run -e T-Display-S3-MIDI2 -t upload +; Serial: pio device monitor + +[platformio] +default_envs = T-Display-S3-MIDI2 + +[env:T-Display-S3-MIDI2] +platform = https://github.com/pioarduino/platform-espressif32/releases/download/53.03.13/platform-espressif32.zip +board = lilygo-t-display-s3 +framework = arduino + +; Remove board-JSON USB flags (they come first on the command line and +; GCC's first-definition-wins would override our values). +build_unflags = + -DARDUINO_USB_MODE=1 + -DARDUINO_USB_CDC_ON_BOOT=1 + +build_flags = + -DARDUINO_USB_MODE=0 + -DARDUINO_USB_CDC_ON_BOOT=0 + -DCFG_TUD_UMP=1 + -DCFG_TUD_UMP_RX_BUFSIZE=512 + -DCFG_TUD_UMP_TX_BUFSIZE=512 + -DCONFIG_TINYUSB_MIDI_ENABLED=0 + -DCFG_TUD_MIDI=0 + ; Keep our USB descriptor callback overrides. arduino-esp32 links with + ; --gc-sections, which would discard our strong-symbol overrides and fall + ; back to the WEAK esp32-hal-tinyusb.c versions (VID=0x303A, MIDI 1.0 only). + -Wl,-u,tud_descriptor_device_cb + -Wl,-u,tud_descriptor_configuration_cb + -Wl,-u,tud_descriptor_string_cb + +lib_deps = + lovyan03/LovyanGFX @ ^1.1.16 + ; tusb_ump library (4 levels up: src/ -> device/ -> platformio/ -> esp32_s3/ -> examples/ -> repo root) + symlink://../../../../ + +monitor_speed = 115200 +upload_speed = 921600 diff --git a/examples/T-Display-S3-ESP32-S3-MIDI2-PingPong/platformio/tdisplay_s3_midi2/src/UMPDisplay.h b/examples/T-Display-S3-ESP32-S3-MIDI2-PingPong/platformio/tdisplay_s3_midi2/src/UMPDisplay.h new file mode 100644 index 0000000..3d80a93 --- /dev/null +++ b/examples/T-Display-S3-ESP32-S3-MIDI2-PingPong/platformio/tdisplay_s3_midi2/src/UMPDisplay.h @@ -0,0 +1,393 @@ +// UMPDisplay.h — Visual UMP monitor for T-Display-S3 (ST7789V 320x170) +// +// Displays real-time UMP traffic with decoded message types, velocity +// bar, scrolling event log with RX/TX direction indicators, and +// protocol negotiation timeline. +// +// Copyright (c) 2026 Saulo Verissimo +// SPDX-License-Identifier: MIT + +#ifndef UMP_DISPLAY_H +#define UMP_DISPLAY_H + +#include +#include +#include +#include +#include "mapping.h" +#include "ump_stream_handler.h" + +class UMPDisplay { +public: + + void init() { + pinMode(PIN_POWER_ON, OUTPUT); + digitalWrite(PIN_POWER_ON, HIGH); + + _tft.init(); + _tft.setRotation(2); + _tft.setBrightness(255); + _tft.fillScreen(C_BG); + pinMode(TFT_BL_PIN, OUTPUT); + digitalWrite(TFT_BL_PIN, HIGH); + + _proto = 0; _conn = false; + _rx = 0; _tx = 0; _vel = 0; _nw = 0; + memset(_w, 0, sizeof(_w)); + memset(_log, 0, sizeof(_log)); + _logN = 0; + + _drawAll(); + } + + void setConnected(bool c) { _conn = c; _drawHeader(); } + + void setProtocol(uint8_t alt) { + _proto = alt; + _drawProto(); + _drawStatus(); + } + + // ── RX: push a received UMP packet for display ────────── + void pushRxUMP(const uint32_t* words, uint8_t nw) { + _rx++; + _storeWords(words, nw); + + uint8_t b0 = _w[0] & 0xFF; + uint8_t b1 = (_w[0] >> 8) & 0xFF; + uint8_t b2 = (_w[0] >> 16) & 0xFF; + uint8_t b3 = (_w[0] >> 24) & 0xFF; + uint8_t mt = (b0 >> 4) & 0xF; + + // Update velocity from NoteOn/NoteOff (MT=2 or MT=4) + if (mt == 0x2 || mt == 0x4) { + uint8_t st = b1 & 0xF0; + if (st == 0x90 && b3 > 0) { + _vel = (mt == 0x4 && nw >= 2) + ? (uint8_t)((_w[1] >> 17) & 0x7F) // MT=4: 16-bit vel → 7-bit + : b3; // MT=2: 7-bit vel + } else if (st == 0x80 || (st == 0x90 && b3 == 0)) { + _vel = 0; + } + } + + _drawHex(); + _drawVel(); + _addLog(b0, b1, b2, b3, mt, false); + _drawStatus(); + } + + // ── TX: push a transmitted UMP packet for display ─────── + void pushTxUMP(const uint32_t* words, uint8_t nw) { + _tx++; + + uint8_t b0 = words[0] & 0xFF; + uint8_t b1 = (words[0] >> 8) & 0xFF; + uint8_t b2 = (words[0] >> 16) & 0xFF; + uint8_t b3 = (words[0] >> 24) & 0xFF; + uint8_t mt = (b0 >> 4) & 0xF; + + _addLog(b0, b1, b2, b3, mt, true); + _drawStatus(); + } + + // ── RX activity indicator ─────────────────────────────── + void pulseRx() { + static bool s = false; s = !s; + _tft.fillCircle(SCR_W - MARGIN - 4, Y_HEADER + H_HEADER / 2, + 4, s ? C_CYAN : C_HEADER); + } + +private: + // ── LGFX — T-Display-S3 (ST7789V 170x320, parallel 8-bit) ── + class LGFX : public lgfx::LGFX_Device { + public: + LGFX() { + { auto c = _bus.config(); + c.pin_wr=8; c.pin_rd=9; c.pin_rs=7; + c.pin_d0=39; c.pin_d1=40; c.pin_d2=41; c.pin_d3=42; + c.pin_d4=45; c.pin_d5=46; c.pin_d6=47; c.pin_d7=48; + _bus.config(c); _panel.setBus(&_bus); } + { auto c = _panel.config(); + c.pin_cs=6; c.pin_rst=5; c.pin_busy=-1; + c.offset_rotation=1; c.offset_x=40; + c.readable=false; c.invert=true; + c.rgb_order=false; c.dlen_16bit=false; c.bus_shared=false; + c.panel_width=170; c.panel_height=320; + _panel.config(c); } + setPanel(&_panel); + { auto c = _bl.config(); + c.pin_bl=38; c.invert=false; c.freq=22000; c.pwm_channel=7; + _bl.config(c); _panel.setLight(&_bl); } + } + private: + lgfx::Bus_Parallel8 _bus; + lgfx::Panel_ST7789 _panel; + lgfx::Light_PWM _bl; + }; + + LGFX _tft; + uint8_t _proto = 0; + bool _conn = false; + uint32_t _rx = 0, _tx = 0; + uint8_t _vel = 0, _nw = 0; + uint32_t _w[4] = {}; + + struct LogLine { char t[48]; uint32_t c; }; + LogLine _log[N_LOG]; + int _logN = 0; + + // ── helpers ───────────────────────────────────────────── + void _storeWords(const uint32_t* words, uint8_t nw) { + _w[0] = words[0]; + _w[1] = (nw >= 2) ? words[1] : 0; + _w[2] = (nw >= 3) ? words[2] : 0; + _w[3] = (nw >= 4) ? words[3] : 0; + _nw = nw; + } + + void _fill(int y, int h, uint32_t col) { + _tft.fillRect(0, y, SCR_W, h, col); + } + void _hline(int y) { + _tft.drawFastHLine(0, y, SCR_W, C_DIV); + } + void _txt(int x, int y, uint32_t fg, uint32_t bg, + const lgfx::IFont* f, const char* s) { + _tft.setFont(f); + _tft.setTextColor(fg, bg); + _tft.drawString(s, x, y); + } + + // ── full repaint ──────────────────────────────────────── + void _drawAll() { + _drawHeader(); + _drawProto(); + _drawHex(); + _drawVel(); + _drawLog(); + _drawStatus(); + } + + // ── header ────────────────────────────────────────────── + void _drawHeader() { + _fill(Y_HEADER, H_HEADER, C_HEADER); + _txt(MARGIN + 2, Y_HEADER + 4, C_WHITE, C_HEADER, + &lgfx::fonts::Font2, "USB MIDI 2.0"); + _txt(SCR_W / 2 - 30, Y_HEADER + 4, C_DIV, C_HEADER, + &lgfx::fonts::Font2, "T-Display-S3"); + uint32_t dot = _conn ? C_GREEN : C_ORANGE; + _tft.fillCircle(SCR_W - MARGIN - 4, Y_HEADER + H_HEADER / 2, 4, dot); + } + + // ── protocol tabs ─────────────────────────────────────── + void _drawProto() { + int half = SCR_W / 2; + uint32_t l_bg = (_proto == 0) ? C_ORANGE : C_DIV; + uint32_t l_fg = (_proto == 0) ? C_BG : C_GRAY; + uint32_t r_bg = (_proto == 1) ? C_GREEN : C_DIV; + uint32_t r_fg = (_proto == 1) ? C_BG : C_GRAY; + + _tft.fillRect(0, Y_PROTO, half, H_PROTO, l_bg); + _tft.fillRect(half, Y_PROTO, half, H_PROTO, r_bg); + + _txt(MARGIN + 4, Y_PROTO + 2, l_fg, l_bg, + &lgfx::fonts::Font2, "MIDI 1.0"); + _txt(half + MARGIN + 4, Y_PROTO + 2, r_fg, r_bg, + &lgfx::fonts::Font2, "MIDI 2.0 / UMP"); + + _hline(Y_PROTO + H_PROTO); + } + + // ── UMP hex view ──────────────────────────────────────── + void _drawHex() { + _fill(Y_HEX, H_HEX, C_BG); + _hline(Y_HEX + H_HEX); + + if (_nw == 0) { + _txt(MARGIN, Y_HEX + 8, C_DIV, C_BG, &lgfx::fonts::Font2, + "waiting for data..."); + return; + } + + uint8_t b0 = _w[0] & 0xFF; + uint8_t b1 = (_w[0] >> 8) & 0xFF; + uint8_t b2 = (_w[0] >> 16) & 0xFF; + uint8_t b3 = (_w[0] >> 24) & 0xFF; + uint8_t mt = (b0 >> 4) & 0xF; + uint8_t grp = b0 & 0xF; + uint8_t st = b1 & 0xF0; + uint8_t ch = b1 & 0x0F; + + // Line 1: color-coded hex bytes + char hex[48]; int cx = MARGIN; + _tft.setFont(&lgfx::fonts::Font2); + + _tft.setTextColor(C_CYAN, C_BG); + snprintf(hex, 4, "%02X ", b0); + _tft.drawString(hex, cx, Y_HEX + 2); cx += 24; + + _tft.setTextColor(C_ORANGE, C_BG); + snprintf(hex, 4, "%02X ", b1); + _tft.drawString(hex, cx, Y_HEX + 2); cx += 24; + + _tft.setTextColor(C_DIV, C_BG); + snprintf(hex, 8, "%02X %02X", b2, b3); + _tft.drawString(hex, cx, Y_HEX + 2); + + if (_nw >= 2) { + _tft.setTextColor(C_DIV, C_BG); + snprintf(hex, 12, " %08lX", (unsigned long)_w[1]); + _tft.drawString(hex, cx + 48, Y_HEX + 2); + } + + // Line 2: decoded description + char desc[48]; uint32_t col = C_DIV; + const char* tag = (mt == 0x4) ? "M2" : "M1"; + + if (mt == 0x2 || mt == 0x4) { + if (st == 0x90 && b3 > 0) { + snprintf(desc, 48, "%s NoteOn G%u Ch%u N=%u V=%u", + tag, grp, ch, b2, b3); + col = C_GREEN; + } + else if (st == 0x80 || (st == 0x90 && b3 == 0)) { + snprintf(desc, 48, "%s NoteOff G%u Ch%u N=%u", + tag, grp, ch, b2); + col = C_GRAY; + } + else if (st == 0xB0) { + snprintf(desc, 48, "%s CC G%u Ch%u CC=%u V=%u", + tag, grp, ch, b2, b3); + col = C_CYAN; + } + else if (st == 0xE0) { + snprintf(desc, 48, "%s PBend G%u Ch%u %02X%02X", + tag, grp, ch, b2, b3); + col = C_MAGENTA; + } + else { + snprintf(desc, 48, "%s Msg G%u Ch%u %02X %02X", + tag, grp, ch, b2, b3); + } + } + else if (mt == 0x3 || mt == 0x5) { + snprintf(desc, 48, "SysEx G%u [%u words]", grp, _nw); + col = C_YELLOW; + } + else if (mt == 0xF) { + uint16_t sts = streamStatus(_w[0]); + const char* lbl = umpStreamLabel(sts); + snprintf(desc, 48, "<< %s", lbl); + col = C_CYAN; + } + else { + snprintf(desc, 48, "MT=%01X G%u %02X %02X %02X", + mt, grp, b1, b2, b3); + } + + _txt(MARGIN, Y_HEX + 17, col, C_BG, &lgfx::fonts::Font2, desc); + } + + // ── velocity bar ──────────────────────────────────────── + void _drawVel() { + _fill(Y_VEL, H_VEL, C_BG); + _hline(Y_VEL + H_VEL); + + int bx = MARGIN, bw = SCR_W - 72, bh = H_VEL - 4, by = Y_VEL + 2; + int fill = (_vel * bw) / 127; + _tft.fillRect(bx, by, fill, bh, C_CYAN); + _tft.fillRect(bx + fill, by, bw - fill, bh, C_GRAY); + + char buf[12]; snprintf(buf, sizeof(buf), "V:%3u", _vel); + _txt(bx + bw + 6, Y_VEL + 1, C_DIV, C_BG, &lgfx::fonts::Font2, buf); + } + + // ── event log ─────────────────────────────────────────── + void _addLog(uint8_t b0, uint8_t b1, uint8_t b2, uint8_t b3, + uint8_t mt, bool isTx) + { + LogLine& e = _log[_logN % N_LOG]; + uint8_t st = b1 & 0xF0; + uint8_t grp = b0 & 0xF; + uint8_t ch = b1 & 0x0F; + const char* dir = isTx ? "T:" : "R:"; + e.c = C_DIV; + + if (mt == 0x2 || mt == 0x4) { + if (st == 0x90 && b3 > 0) { + snprintf(e.t, 48, "%sNoteOn G%u Ch%u N%u V%u", + dir, grp, ch, b2, b3); + e.c = C_GREEN; + } + else if (st == 0x80 || (st == 0x90 && b3 == 0)) { + snprintf(e.t, 48, "%sNoteOff G%u Ch%u N%u", + dir, grp, ch, b2); + e.c = C_GRAY; + } + else if (st == 0xB0) { + snprintf(e.t, 48, "%sCC G%u Ch%u CC%u V%u", + dir, grp, ch, b2, b3); + e.c = C_CYAN; + } + else if (st == 0xE0) { + snprintf(e.t, 48, "%sPBend G%u Ch%u %02X%02X", + dir, grp, ch, b2, b3); + e.c = C_MAGENTA; + } + else { + snprintf(e.t, 48, "%s%02X %02X %02X %02X", + dir, b0, b1, b2, b3); + } + } + else if (mt == 0x3 || mt == 0x5) { + snprintf(e.t, 48, "%sSysEx G%u %02X", dir, grp, b1); + e.c = C_YELLOW; + } + else if (mt == 0xF) { + // Decode Stream message subtype + uint16_t sts = ((uint16_t)(b0 & 0x03) << 8) | b1; + const char* lbl = umpStreamLabel(sts); + snprintf(e.t, 48, "%s%s", dir, lbl); + e.c = isTx ? C_GREEN : C_CYAN; + } + else { + snprintf(e.t, 48, "%sMT%01X %02X %02X %02X %02X", + dir, mt, b0, b1, b2, b3); + } + + _logN++; + _drawLog(); + } + + void _drawLog() { + _fill(Y_LOG, H_LOG, C_BG); + _hline(Y_LOG + H_LOG); + + for (int i = 0; i < N_LOG; i++) { + if (_logN < N_LOG - i) continue; + int idx = (_logN - N_LOG + i) % N_LOG; + if (idx < 0) idx += N_LOG; + _txt(MARGIN, Y_LOG + 1 + i * H_LLINE, _log[idx].c, C_BG, + &lgfx::fonts::Font0, _log[idx].t); + } + } + + // ── status bar ────────────────────────────────────────── + void _drawStatus() { + _fill(Y_STATUS, H_STATUS, C_HEADER); + + char buf[52]; + snprintf(buf, sizeof(buf), "RX: %-5lu TX: %-5lu", _rx, _tx); + _txt(MARGIN, Y_STATUS + 4, C_WHITE, C_HEADER, + &lgfx::fonts::Font2, buf); + + const char* ps = (_proto == 1) ? "MIDI 2.0" : "MIDI 1.0"; + uint32_t pc = (_proto == 1) ? C_GREEN : C_ORANGE; + int pw = strlen(ps) * 12; + _txt(SCR_W - MARGIN - pw, Y_STATUS + 4, pc, C_HEADER, + &lgfx::fonts::Font2, ps); + } +}; + +#endif // UMP_DISPLAY_H diff --git a/examples/T-Display-S3-ESP32-S3-MIDI2-PingPong/platformio/tdisplay_s3_midi2/src/main.cpp b/examples/T-Display-S3-ESP32-S3-MIDI2-PingPong/platformio/tdisplay_s3_midi2/src/main.cpp new file mode 100644 index 0000000..def2e9f --- /dev/null +++ b/examples/T-Display-S3-ESP32-S3-MIDI2-PingPong/platformio/tdisplay_s3_midi2/src/main.cpp @@ -0,0 +1,244 @@ +// ============================================================ +// USB MIDI 2.0 Device — T-Display-S3 (PlatformIO) +// ============================================================ +// +// Build: pio run -e T-Display-S3-MIDI2 +// Flash: pio run -e T-Display-S3-MIDI2 -t upload +// Serial: pio device monitor +// +// Controls: +// BTN1 (GPIO0) — toggle NoteOn / NoteOff (C5) +// BTN2 (GPIO14) — cycle test velocity: 32 → 64 → 96 → 127 +// +// This example demonstrates a fully compliant USB MIDI 2.0 +// device with: +// • Dual alternate settings (Alt 0 = MIDI 1.0, Alt 1 = UMP) +// • UMP Endpoint Discovery responses (EP Info, Device ID, +// Endpoint Name, Product Instance ID) +// • Function Block Discovery responses (FB Info, FB Name) +// • Stream Configuration negotiation +// • Real-time display of the negotiation timeline +// +// Copyright (c) 2026 Saulo Verissimo +// SPDX-License-Identifier: MIT +// ============================================================ + +#include +#include "USB.h" +#include "ump_device.h" +#include "ump_stream_handler.h" +#include "UMPDisplay.h" +#include "mapping.h" + +// ── Device configuration ───────────────────────────────────── + +static const UMPStreamConfig streamCfg = { + .umpVersionMajor = 1, + .umpVersionMinor = 1, + .numFunctionBlocks = 1, + .staticFunctionBlocks = true, + .protocolCaps = UMP_PROTO_CAP_MIDI1 | UMP_PROTO_CAP_MIDI2, + .manufacturerId = { 0x00, 0x00, 0x7D }, // 0x7D = educational/dev + .familyId = 0x0001, + .modelId = 0x0001, + .swRevision = { '0', '1', '0', '0' }, + .endpointName = "ESP32-S3 MIDI2", // ≤14 chars → 1 packet + .productInstanceId = "ESP32S3MIDI001", // ≤14 chars → 1 packet + .fbName = "Group 0", + .fbDirection = UMP_FB_DIR_BIDIRECTIONAL, + .fbFirstGroup = 0, + .fbNumGroups = 1, + .fbUIHint = 0x00, + .fbMidi10 = 0x00, + .fbMidiCIVer = 0x00, + .fbSysEx8 = 0x00, +}; + +// ── State ──────────────────────────────────────────────────── + +static UMPDisplay display; +static bool usbWasConnected = false; +static uint8_t currentAlt = 0xFF; + +static uint32_t btn1_last = 0; +static uint32_t btn2_last = 0; +static const uint32_t DEBOUNCE_MS = 80; + +static bool noteIsOn = false; +static uint8_t testNote = 72; +static uint8_t testVelIdx = 1; +static const uint8_t velocities[] = { 32, 64, 96, 127 }; +static uint8_t testVel = 64; + +// ── tusb_ump callbacks ─────────────────────────────────────── + +extern "C" void tud_ump_set_itf_cb(uint8_t itf, uint8_t alt) { + (void)itf; + currentAlt = alt; + display.setProtocol(alt); +} + +extern "C" void tud_ump_rx_cb(uint8_t itf) { + (void)itf; +} + +// ── Stream handler TX callback ─────────────────────────────── +// Called by umpStreamHandleRx() after each response is sent. + +static void onStreamTx(const uint32_t* words, uint8_t nw, + const char* /* label */) +{ + display.pushTxUMP(words, nw); +} + +// ── UMP helpers ────────────────────────────────────────────── + +static inline uint8_t umpWordsForMT(uint8_t mt) { + switch (mt) { + case 0x0: case 0x1: case 0x2: case 0x6: case 0x7: return 1; + case 0x3: case 0x4: case 0x8: case 0x9: case 0xA: case 0xD: return 2; + case 0xB: case 0xC: return 3; + case 0x5: case 0xE: case 0xF: return 4; + default: return 1; + } +} + +static inline uint32_t makeUMP_MIDI1CV(uint8_t group, uint8_t status, + uint8_t ch, uint8_t d0, uint8_t d1) +{ + return (uint32_t)d1 << 24 | + (uint32_t)d0 << 16 | + (uint32_t)((status & 0xF0) | (ch & 0x0F)) << 8 | + (uint32_t)(0x20 | (group & 0x0F)); +} + +// ── Send ───────────────────────────────────────────────────── + +static void sendNoteOn(uint8_t group, uint8_t ch, + uint8_t note, uint8_t vel) +{ + if (!tud_ump_n_mounted(0)) return; + uint32_t w = makeUMP_MIDI1CV(group, 0x90, ch, note, vel); + tud_ump_write(0, &w, 1); + display.pushTxUMP(&w, 1); +} + +static void sendNoteOff(uint8_t group, uint8_t ch, uint8_t note) +{ + if (!tud_ump_n_mounted(0)) return; + uint32_t w = makeUMP_MIDI1CV(group, 0x80, ch, note, 0); + tud_ump_write(0, &w, 1); + display.pushTxUMP(&w, 1); +} + +static void sendCC(uint8_t group, uint8_t ch, uint8_t cc, uint8_t val) +{ + if (!tud_ump_n_mounted(0)) return; + uint32_t w = makeUMP_MIDI1CV(group, 0xB0, ch, cc, val); + tud_ump_write(0, &w, 1); + display.pushTxUMP(&w, 1); +} + +// ── RX ─────────────────────────────────────────────────────── + +static void processRxUMP() +{ + for (int pass = 0; pass < 16; pass++) { + if (tud_ump_n_available(0) == 0) break; + + uint32_t firstWord = 0; + if (tud_ump_read(0, &firstWord, 1) == 0) break; + + uint8_t mt = (uint8_t)((firstWord & 0xF0) >> 4); + uint8_t numWords = umpWordsForMT(mt); + uint32_t words[4] = { firstWord, 0, 0, 0 }; + + // Wait for the remaining words before processing. + // Avoids passing a truncated packet to umpStreamHandleRx(). + if (numWords > 1 && tud_ump_n_available(0) < (numWords - 1)) break; + + for (uint8_t w = 1; w < numWords; w++) { + tud_ump_read(0, &words[w], 1); + } + + display.pushRxUMP(words, numWords); + display.pulseRx(); + + if (mt == 0xF) { + // MT=0xF Stream messages — handle with the Discovery protocol. + // Do NOT echo these back; the stream handler sends proper responses. + umpStreamHandleRx(0, words, streamCfg, onStreamTx); + } else { + // All other message types — echo back (loopback for testing) + if (tud_ump_n_writeable(0) >= numWords) { + tud_ump_write(0, words, numWords); + display.pushTxUMP(words, numWords); + } + } + } +} + +// ── Buttons ────────────────────────────────────────────────── + +static void handleButtons() +{ + uint32_t now = millis(); + + if (digitalRead(BTN1) == LOW && (now - btn1_last) > DEBOUNCE_MS) { + btn1_last = now; + if (!noteIsOn) { sendNoteOn(0, 0, testNote, testVel); noteIsOn = true; } + else { sendNoteOff(0, 0, testNote); noteIsOn = false; } + } + + if (digitalRead(BTN2) == LOW && (now - btn2_last) > DEBOUNCE_MS) { + btn2_last = now; + testVelIdx = (testVelIdx + 1) % (sizeof(velocities) / sizeof(velocities[0])); + testVel = velocities[testVelIdx]; + sendCC(0, 0, 7, testVel); + } +} + +// ── Arduino ────────────────────────────────────────────────── + +void setup() +{ + USB.begin(); + Serial.begin(115200); + pinMode(BTN1, INPUT_PULLUP); + pinMode(BTN2, INPUT_PULLUP); + testVel = velocities[testVelIdx]; + + display.init(); + display.setConnected(false); + display.setProtocol(0); + + Serial.println("[MIDI2] USB MIDI 2.0 device started"); + Serial.printf("[MIDI2] EP name: %s\n", streamCfg.endpointName); + Serial.printf("[MIDI2] Product: %s\n", streamCfg.productInstanceId); +} + +void loop() +{ + bool mounted = tud_ump_n_mounted(0); + + if (mounted && !usbWasConnected) { + usbWasConnected = true; + display.setConnected(true); + } else if (!mounted && usbWasConnected) { + usbWasConnected = false; + currentAlt = 0xFF; + display.setConnected(false); + } + + if (mounted) { + uint8_t alt = tud_alt_setting(0); + if (alt != currentAlt) { + currentAlt = alt; + display.setProtocol(alt); + } + processRxUMP(); + } + + handleButtons(); + delay(2); +} diff --git a/examples/T-Display-S3-ESP32-S3-MIDI2-PingPong/platformio/tdisplay_s3_midi2/src/mapping.h b/examples/T-Display-S3-ESP32-S3-MIDI2-PingPong/platformio/tdisplay_s3_midi2/src/mapping.h new file mode 100644 index 0000000..b08302f --- /dev/null +++ b/examples/T-Display-S3-ESP32-S3-MIDI2-PingPong/platformio/tdisplay_s3_midi2/src/mapping.h @@ -0,0 +1,68 @@ +#ifndef MAPPING_H +#define MAPPING_H + +// ── T-Display-S3 hardware ──────────────────────────────────────────────────── +#define TFT_BL_PIN 38 +#define PIN_POWER_ON 15 +#define BTN1 0 // GPIO0 — toggle NoteOn/Off +#define BTN2 14 // GPIO14 — cycle velocity + +// ── Screen (landscape 320 × 170) ──────────────────────────────────────────── +#define SCR_W 320 +#define SCR_H 170 +#define MARGIN 4 + +// ── Color palette ──────────────────────────────────────────────────────────── +#define C_BG 0x0000 // Black +#define C_HEADER 0x0821 // Very dark blue — header bg +#define C_DIV 0x2945 // Dark slate — dividers, dim labels +#define C_PANEL 0x1082 // Dark navy — section panels +#define C_WHITE 0xFFFF // White +#define C_GREEN 0x07E0 // Green +#define C_CYAN 0x07FF // Cyan +#define C_ORANGE 0xFD20 // Orange +#define C_YELLOW 0xFFE0 // Yellow +#define C_GRAY 0x4208 // Gray +#define C_BLUE 0x001F // Blue +#define C_MAGENTA 0xF81F // Magenta + +// ── Layout rows ────────────────────────────────────────────────────────────── +// +// Y= 0 ┌───────────────────────────────────┐ +// │ USB MIDI 2.0 • T-Display-S3 ● │ 24 HEADER +// Y= 24 ├────────────────┬──────────────────┤ +// │ MIDI 1.0 │ MIDI 2.0 / UMP │ 18 PROTO +// Y= 42 ├────────────────┴──────────────────┤ +// │ 20 90 48 60 ── NoteOn info │ 30 HEX +// Y= 72 ├───────────────────────────────────┤ +// │ ████████████░░░░░░░░ Vel: 96 │ 14 VEL +// Y= 86 ├───────────────────────────────────┤ +// │ NoteOn G0 Ch0 N72 V96 │ +// │ NoteOff G0 Ch0 N72 │ 56 LOG (4×14) +// │ CC G0 Ch0 CC7 V64 │ +// │ NoteOn G0 Ch0 N60 V80 │ +// Y=142 ├───────────────────────────────────┤ +// │ RX:0 TX:0 MIDI 1.0 │ 28 STATUS +// Y=170 └───────────────────────────────────┘ + +#define Y_HEADER 0 +#define H_HEADER 24 + +#define Y_PROTO 24 +#define H_PROTO 18 + +#define Y_HEX 42 +#define H_HEX 30 + +#define Y_VEL 72 +#define H_VEL 14 + +#define Y_LOG 86 +#define H_LOG 56 +#define N_LOG 4 +#define H_LLINE 14 + +#define Y_STATUS 142 +#define H_STATUS 28 + +#endif // MAPPING_H diff --git a/examples/T-Display-S3-ESP32-S3-MIDI2-PingPong/platformio/tdisplay_s3_midi2/src/ump_stream_handler.h b/examples/T-Display-S3-ESP32-S3-MIDI2-PingPong/platformio/tdisplay_s3_midi2/src/ump_stream_handler.h new file mode 100644 index 0000000..c0215b9 --- /dev/null +++ b/examples/T-Display-S3-ESP32-S3-MIDI2-PingPong/platformio/tdisplay_s3_midi2/src/ump_stream_handler.h @@ -0,0 +1,436 @@ +// ump_stream_handler.h — UMP Stream Message (MT=0xF) handler +// +// Implements the device-side UMP Endpoint Discovery protocol per the +// UMP and MIDI 2.0 Protocol Specification v1.1 (M2-104-UM). +// +// When Windows MIDI Services (or any UMP-capable host) switches to +// Alternate Setting 1 (MIDI Streaming 2.0), it sends a series of +// MT=0xF Stream Messages to discover the device's capabilities: +// +// Host → Device EP Discovery (status 0x000) +// Device → Host EP Info Notification (0x001) +// Device → Host Device Identity Notification (0x002) +// Device → Host Endpoint Name Notification (0x003) +// Device → Host Product Instance ID Notification (0x004) +// +// Host → Device Stream Configuration Request (0x005) +// Device → Host Stream Configuration Notification (0x006) +// +// Host → Device Function Block Discovery (0x010) +// Device → Host Function Block Info Notification (0x011) +// Device → Host Function Block Name Notification (0x012) +// +// This handler parses incoming requests and generates the correct +// responses. All responses are sent via tud_ump_write(). +// +// Copyright (c) 2026 Saulo Verissimo +// SPDX-License-Identifier: MIT + +#ifndef UMP_STREAM_HANDLER_H +#define UMP_STREAM_HANDLER_H + +#include +#include +#include "ump_device.h" + +// ───────────────────────────────────────────────────────────── +// UMP Stream Message status codes (10-bit) +// Reference: M2-104-UM v1.1, Section 7.1 +// ───────────────────────────────────────────────────────────── + +#define UMP_STREAM_STATUS_EP_DISCOVERY 0x000 +#define UMP_STREAM_STATUS_EP_INFO 0x001 +#define UMP_STREAM_STATUS_DEVICE_INFO 0x002 +#define UMP_STREAM_STATUS_EP_NAME 0x003 +#define UMP_STREAM_STATUS_PRODUCT_ID 0x004 +#define UMP_STREAM_STATUS_STREAM_CFG_REQ 0x005 +#define UMP_STREAM_STATUS_STREAM_CFG 0x006 +#define UMP_STREAM_STATUS_FB_DISCOVERY 0x010 +#define UMP_STREAM_STATUS_FB_INFO 0x011 +#define UMP_STREAM_STATUS_FB_NAME 0x012 +#define UMP_STREAM_STATUS_START_CLIP 0x020 +#define UMP_STREAM_STATUS_END_CLIP 0x021 + +// ── Format field (bits [3:2] of wire byte 0) ───────────────── +#define UMP_STREAM_FMT_COMPLETE 0 +#define UMP_STREAM_FMT_START 1 +#define UMP_STREAM_FMT_CONTINUE 2 +#define UMP_STREAM_FMT_END 3 + +// ── Endpoint Discovery filter bitmap (wire byte 4) ─────────── +#define UMP_FILTER_EP_INFO 0x01 +#define UMP_FILTER_DEVICE_ID 0x02 +#define UMP_FILTER_EP_NAME 0x04 +#define UMP_FILTER_PRODUCT_ID 0x08 +#define UMP_FILTER_STREAM_CFG 0x10 + +// ── Protocol capabilities (EP Info byte 6) ─────────────────── +#define UMP_PROTO_CAP_MIDI2 0x01 // supports MIDI 2.0 Protocol +#define UMP_PROTO_CAP_MIDI1 0x02 // supports MIDI 1.0 Protocol + +// ── Protocol selection (Stream Configuration) ──────────────── +#define UMP_PROTO_MIDI1 0x01 +#define UMP_PROTO_MIDI2 0x02 + +// ── Function Block direction ───────────────────────────────── +#define UMP_FB_DIR_INPUT 0x01 // receives from host +#define UMP_FB_DIR_OUTPUT 0x02 // sends to host +#define UMP_FB_DIR_BIDIRECTIONAL 0x03 + +// ───────────────────────────────────────────────────────────── +// Wire byte packing — ESP32 little-endian ↔ UMP wire order +// +// UMP words are transmitted MSB-first on USB. On ESP32 (LE), +// the LSB of a uint32_t occupies the lowest memory address, +// which is the first byte sent by the USB bulk endpoint. +// +// packWord(b0, b1, b2, b3) places wire byte 0 at the LSB: +// address+0 = b0 (first on wire — contains MT nibble) +// address+1 = b1 +// address+2 = b2 +// address+3 = b3 (last on wire) +// ───────────────────────────────────────────────────────────── + +static inline uint32_t packWord(uint8_t b0, uint8_t b1, + uint8_t b2, uint8_t b3) +{ + return (uint32_t)b0 + | ((uint32_t)b1 << 8) + | ((uint32_t)b2 << 16) + | ((uint32_t)b3 << 24); +} + +static inline uint8_t wireB0(uint32_t w) { return (uint8_t)(w); } +static inline uint8_t wireB1(uint32_t w) { return (uint8_t)(w >> 8); } +static inline uint8_t wireB2(uint32_t w) { return (uint8_t)(w >> 16); } +static inline uint8_t wireB3(uint32_t w) { return (uint8_t)(w >> 24); } + +// ── Stream header helpers ──────────────────────────────────── + +static inline uint32_t streamWord0(uint8_t format, uint16_t status, + uint8_t d2, uint8_t d3) +{ + uint8_t b0 = 0xF0 | ((format & 0x03) << 2) | ((status >> 8) & 0x03); + uint8_t b1 = status & 0xFF; + return packWord(b0, b1, d2, d3); +} + +static inline uint16_t streamStatus(uint32_t w0) +{ + return ((uint16_t)(wireB0(w0) & 0x03) << 8) | wireB1(w0); +} + +static inline uint8_t streamFormat(uint32_t w0) +{ + return (wireB0(w0) >> 2) & 0x03; +} + +// ───────────────────────────────────────────────────────────── +// Device configuration — all fields the host may discover +// ───────────────────────────────────────────────────────────── + +struct UMPStreamConfig { + // UMP protocol version + uint8_t umpVersionMajor = 1; + uint8_t umpVersionMinor = 1; + + // Function blocks + uint8_t numFunctionBlocks = 1; + bool staticFunctionBlocks = true; + + // Protocol capabilities + uint8_t protocolCaps = UMP_PROTO_CAP_MIDI1 | UMP_PROTO_CAP_MIDI2; + + // SysEx Manufacturer ID (3 bytes — 1-byte IDs use 0x00 0x00 ID) + uint8_t manufacturerId[3] = { 0x00, 0x00, 0x7D }; // 0x7D = educational + uint16_t familyId = 0x0001; + uint16_t modelId = 0x0001; + char swRevision[4] = { '0', '1', '0', '0' }; + + // Endpoint strings (UTF-8, null-terminated) + const char* endpointName = "MIDI 2.0 Device"; + const char* productInstanceId = "MIDI2-001"; + + // Function Block 0 + const char* fbName = "Group 0"; + uint8_t fbDirection = UMP_FB_DIR_BIDIRECTIONAL; + uint8_t fbFirstGroup = 0; + uint8_t fbNumGroups = 1; + uint8_t fbUIHint = 0x00; // 0=unknown + uint8_t fbMidi10 = 0x00; // 0=not a MIDI 1.0 stream + uint8_t fbMidiCIVer = 0x00; + uint8_t fbSysEx8 = 0x00; +}; + +// ───────────────────────────────────────────────────────────── +// TX callback — invoked after each response is sent +// ───────────────────────────────────────────────────────────── + +typedef void (*UMPStreamTxCb)(const uint32_t* words, uint8_t nw, + const char* label); + +// ───────────────────────────────────────────────────────────── +// Human-readable label for any Stream message status +// ───────────────────────────────────────────────────────────── + +__attribute__((unused)) +static const char* umpStreamLabel(uint16_t status) +{ + switch (status) { + case UMP_STREAM_STATUS_EP_DISCOVERY: return "EP Discovery"; + case UMP_STREAM_STATUS_EP_INFO: return "EP Info"; + case UMP_STREAM_STATUS_DEVICE_INFO: return "Device ID"; + case UMP_STREAM_STATUS_EP_NAME: return "EP Name"; + case UMP_STREAM_STATUS_PRODUCT_ID: return "Product ID"; + case UMP_STREAM_STATUS_STREAM_CFG_REQ: return "StreamCfg Req"; + case UMP_STREAM_STATUS_STREAM_CFG: return "StreamCfg OK"; + case UMP_STREAM_STATUS_FB_DISCOVERY: return "FB Discovery"; + case UMP_STREAM_STATUS_FB_INFO: return "FB Info"; + case UMP_STREAM_STATUS_FB_NAME: return "FB Name"; + case UMP_STREAM_STATUS_START_CLIP: return "Start Clip"; + case UMP_STREAM_STATUS_END_CLIP: return "End Clip"; + default: return "Stream ???"; + } +} + +// ───────────────────────────────────────────────────────────── +// Internal — send a 4-word Stream message +// ───────────────────────────────────────────────────────────── + +static void _streamSend(uint8_t itf, uint32_t w[4], + UMPStreamTxCb onTx, const char* label) +{ + tud_ump_write(itf, w, 4); + if (onTx) onTx(w, 4, label); +} + +// ───────────────────────────────────────────────────────────── +// Internal — send a UTF-8 string as one or more Stream packets +// +// status : UMP_STREAM_STATUS_EP_NAME / PRODUCT_ID / FB_NAME +// str : null-terminated UTF-8 string +// prefixByte: ≥0 to insert a prefix byte (e.g., FB index) at +// wire byte 2, shifting chars to byte 3 onward. +// <0 means no prefix — chars start at byte 2. +// ───────────────────────────────────────────────────────────── + +static void _streamSendString(uint8_t itf, uint16_t status, + const char* str, int prefixByte, + UMPStreamTxCb onTx, const char* label) +{ + const size_t len = strlen(str); + const size_t charsPerPkt = (prefixByte >= 0) ? 13 : 14; + const size_t nPkts = (len == 0) ? 1 : (len + charsPerPkt - 1) / charsPerPkt; + + size_t pos = 0; + for (size_t pkt = 0; pkt < nPkts; pkt++) { + + uint8_t fmt; + if (nPkts == 1) fmt = UMP_STREAM_FMT_COMPLETE; + else if (pkt == 0) fmt = UMP_STREAM_FMT_START; + else if (pkt == nPkts - 1) fmt = UMP_STREAM_FMT_END; + else fmt = UMP_STREAM_FMT_CONTINUE; + + uint8_t buf[16]; + memset(buf, 0, sizeof(buf)); + + // Header (bytes 0-1) + buf[0] = 0xF0 | ((fmt & 0x03) << 2) | ((status >> 8) & 0x03); + buf[1] = status & 0xFF; + + // Optional prefix (byte 2 for FB Name = FB index) + size_t off = 2; + if (prefixByte >= 0) + buf[off++] = (uint8_t)prefixByte; + + // Fill remaining bytes with string characters + while (off < 16 && pos < len) + buf[off++] = (uint8_t)str[pos++]; + + uint32_t w[4]; + w[0] = packWord(buf[0], buf[1], buf[2], buf[3]); + w[1] = packWord(buf[4], buf[5], buf[6], buf[7]); + w[2] = packWord(buf[8], buf[9], buf[10], buf[11]); + w[3] = packWord(buf[12], buf[13], buf[14], buf[15]); + + _streamSend(itf, w, onTx, label); + } +} + +// ───────────────────────────────────────────────────────────── +// Response generators +// ───────────────────────────────────────────────────────────── + +static void _replyEndpointInfo(uint8_t itf, const UMPStreamConfig& cfg, + UMPStreamTxCb onTx) +{ + uint32_t w[4]; + w[0] = streamWord0(UMP_STREAM_FMT_COMPLETE, UMP_STREAM_STATUS_EP_INFO, + cfg.umpVersionMajor, cfg.umpVersionMinor); + w[1] = packWord( + (cfg.staticFunctionBlocks ? 0x80 : 0x00) | (cfg.numFunctionBlocks & 0x7F), + 0x00, // JRTS capabilities (none) + cfg.protocolCaps, + 0x00 // extensions + ); + w[2] = 0; + w[3] = 0; + _streamSend(itf, w, onTx, "EP Info"); +} + +static void _replyDeviceIdentity(uint8_t itf, const UMPStreamConfig& cfg, + UMPStreamTxCb onTx) +{ + uint32_t w[4]; + w[0] = streamWord0(UMP_STREAM_FMT_COMPLETE, UMP_STREAM_STATUS_DEVICE_INFO, + 0x00, 0x00); + w[1] = packWord( + 0x00, + cfg.manufacturerId[0], + cfg.manufacturerId[1], + cfg.manufacturerId[2] + ); + w[2] = packWord( + (cfg.familyId >> 8) & 0xFF, cfg.familyId & 0xFF, + (cfg.modelId >> 8) & 0xFF, cfg.modelId & 0xFF + ); + w[3] = packWord( + cfg.swRevision[0], cfg.swRevision[1], + cfg.swRevision[2], cfg.swRevision[3] + ); + _streamSend(itf, w, onTx, "Device ID"); +} + +static void _replyEndpointName(uint8_t itf, const UMPStreamConfig& cfg, + UMPStreamTxCb onTx) +{ + _streamSendString(itf, UMP_STREAM_STATUS_EP_NAME, + cfg.endpointName, -1, onTx, "EP Name"); +} + +static void _replyProductInstanceId(uint8_t itf, const UMPStreamConfig& cfg, + UMPStreamTxCb onTx) +{ + _streamSendString(itf, UMP_STREAM_STATUS_PRODUCT_ID, + cfg.productInstanceId, -1, onTx, "Product ID"); +} + +static void _replyStreamConfig(uint8_t itf, uint8_t requestedProto, + uint8_t jrts, const UMPStreamConfig& cfg, + UMPStreamTxCb onTx) +{ + // Confirm the requested protocol if we support it + uint8_t proto = requestedProto; + if (proto == UMP_PROTO_MIDI2 && !(cfg.protocolCaps & UMP_PROTO_CAP_MIDI2)) + proto = UMP_PROTO_MIDI1; + else if (proto == UMP_PROTO_MIDI1 && !(cfg.protocolCaps & UMP_PROTO_CAP_MIDI1)) + proto = UMP_PROTO_MIDI2; + + uint32_t w[4]; + w[0] = streamWord0(UMP_STREAM_FMT_COMPLETE, UMP_STREAM_STATUS_STREAM_CFG, + proto, 0x00); // JRTS: 0 (not supported) + w[1] = 0; + w[2] = 0; + w[3] = 0; + _streamSend(itf, w, onTx, "StreamCfg OK"); +} + +static void _replyFBInfo(uint8_t itf, uint8_t fbIdx, + const UMPStreamConfig& cfg, UMPStreamTxCb onTx) +{ + // Word 0 byte 2: [active (1 bit)] [fb_index (7 bits)] + // Word 1 byte 0: [ui_hint (2)] [midi1.0 (2)] [direction (2)] [rsvd (2)] + // Word 1 byte 1: first_group + // Word 1 byte 2: num_groups + // Word 1 byte 3: midi_ci_version + // Word 2 byte 0: max_sysex8_streams + + uint32_t w[4]; + w[0] = streamWord0(UMP_STREAM_FMT_COMPLETE, UMP_STREAM_STATUS_FB_INFO, + 0x80 | (fbIdx & 0x7F), // active=1 + 0x00); // reserved + w[1] = packWord( + (cfg.fbUIHint << 6) | + (cfg.fbMidi10 << 4) | + (cfg.fbDirection << 2), + cfg.fbFirstGroup, + cfg.fbNumGroups, + cfg.fbMidiCIVer + ); + w[2] = packWord(cfg.fbSysEx8, 0x00, 0x00, 0x00); + w[3] = 0; + _streamSend(itf, w, onTx, "FB Info"); +} + +static void _replyFBName(uint8_t itf, uint8_t fbIdx, + const UMPStreamConfig& cfg, UMPStreamTxCb onTx) +{ + _streamSendString(itf, UMP_STREAM_STATUS_FB_NAME, + cfg.fbName, (int)fbIdx, onTx, "FB Name"); +} + +// ───────────────────────────────────────────────────────────── +// Main entry point — process a received MT=0xF Stream message +// +// Call this from processRxUMP() when MT == 0xF. +// Returns a human-readable label for the received request, +// or nullptr if the message was not recognized. +// ───────────────────────────────────────────────────────────── + +static const char* umpStreamHandleRx(uint8_t itf, const uint32_t* words, + const UMPStreamConfig& cfg, + UMPStreamTxCb onTx) +{ + const uint16_t status = streamStatus(words[0]); + + switch (status) { + + // ── Endpoint Discovery ─────────────────────────────────── + case UMP_STREAM_STATUS_EP_DISCOVERY: { + const uint8_t filter = wireB0(words[1]); + + if (filter & UMP_FILTER_EP_INFO) + _replyEndpointInfo(itf, cfg, onTx); + if (filter & UMP_FILTER_DEVICE_ID) + _replyDeviceIdentity(itf, cfg, onTx); + if (filter & UMP_FILTER_EP_NAME) + _replyEndpointName(itf, cfg, onTx); + if (filter & UMP_FILTER_PRODUCT_ID) + _replyProductInstanceId(itf, cfg, onTx); + if (filter & UMP_FILTER_STREAM_CFG) + _replyStreamConfig(itf, UMP_PROTO_MIDI2, 0x00, cfg, onTx); + + return "EP Discovery"; + } + + // ── Stream Configuration Request ───────────────────────── + case UMP_STREAM_STATUS_STREAM_CFG_REQ: { + const uint8_t protocol = wireB2(words[0]); + const uint8_t jrts = wireB3(words[0]); + _replyStreamConfig(itf, protocol, jrts, cfg, onTx); + return "StreamCfg Req"; + } + + // ── Function Block Discovery ───────────────────────────── + case UMP_STREAM_STATUS_FB_DISCOVERY: { + const uint8_t fbNum = wireB2(words[0]); // 0xFF = all + const uint8_t filter = wireB3(words[0]); + + uint8_t first = (fbNum == 0xFF) ? 0 : fbNum; + uint8_t last = (fbNum == 0xFF) ? cfg.numFunctionBlocks : (fbNum + 1); + if (last > cfg.numFunctionBlocks) + last = cfg.numFunctionBlocks; + + for (uint8_t i = first; i < last; i++) { + if (filter & 0x01) _replyFBInfo(itf, i, cfg, onTx); + if (filter & 0x02) _replyFBName(itf, i, cfg, onTx); + } + return "FB Discovery"; + } + + default: + return nullptr; + } +} + +#endif // UMP_STREAM_HANDLER_H diff --git a/examples/T-Display-S3-ESP32-S3-MIDI2-PingPong/platformio/tdisplay_s3_midi2/src/usb_descriptors.cpp b/examples/T-Display-S3-ESP32-S3-MIDI2-PingPong/platformio/tdisplay_s3_midi2/src/usb_descriptors.cpp new file mode 100644 index 0000000..a2c3d31 --- /dev/null +++ b/examples/T-Display-S3-ESP32-S3-MIDI2-PingPong/platformio/tdisplay_s3_midi2/src/usb_descriptors.cpp @@ -0,0 +1,230 @@ +// usb_descriptors.cpp — USB MIDI 2.0 descriptor callbacks for tusb_ump +// +// Overrides the WEAK symbols from arduino-esp32's esp32-hal-tinyusb.c: +// tud_descriptor_device_cb() +// tud_descriptor_configuration_cb() +// tud_descriptor_string_cb() +// +// USB MIDI 2.0 requires DUAL ALTERNATE SETTINGS on Interface 1: +// Alt 0x00 — MIDI Streaming 1.0 (legacy hosts) +// Alt 0x01 — MIDI Streaming 2.0 (UMP, modern hosts) +// +// Host selects alt setting via SET_INTERFACE. tusb_ump handles this +// automatically via umpd_control_xfer_cb() and exposes tud_alt_setting(). +// +// Group Terminal Block (GTB) descriptor is served by tusb_ump via a +// class-specific GET_DESCRIPTOR request (0x2601), not via the config +// descriptor — so we don't include it in the config blob below. + +#include +#include "tusb.h" +#include "ump.h" + +// ───────────────────────────────────────────────────────────── +// Interface and endpoint numbering +// ───────────────────────────────────────────────────────────── +#define ITF_NUM_AUDIO_CONTROL 0 +#define ITF_NUM_MIDI_STREAMING 1 +#define ITF_NUM_TOTAL 2 + +#define EPNUM_MIDI_OUT 0x01 +#define EPNUM_MIDI_IN 0x81 + +#define EP_SIZE_FS 64 + +// ───────────────────────────────────────────────────────────── +// String descriptor indices +// ───────────────────────────────────────────────────────────── +enum { + STR_IDX_LANGID = 0, + STR_IDX_MANUFACTURER = 1, + STR_IDX_PRODUCT = 2, + STR_IDX_SERIAL = 3, + STR_IDX_MIDI_STREAMING = 4, + STR_IDX_BLOCK_1 = 5, +}; + +// ───────────────────────────────────────────────────────────── +// Device descriptor +// ───────────────────────────────────────────────────────────── +static const tusb_desc_device_t desc_device = { + .bLength = sizeof(tusb_desc_device_t), + .bDescriptorType = TUSB_DESC_DEVICE, + .bcdUSB = 0x0200, + .bDeviceClass = TUSB_CLASS_MISC, // 0xEF — required for IAD on Windows + .bDeviceSubClass = 0x02, + .bDeviceProtocol = 0x01, + .bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE, + .idVendor = 0x1209, // pid.codes (open-source VID) + .idProduct = 0x0001, // change for production + .bcdDevice = 0x0100, + .iManufacturer = STR_IDX_MANUFACTURER, + .iProduct = STR_IDX_PRODUCT, + .iSerialNumber = STR_IDX_SERIAL, + .bNumConfigurations = 1, +}; + +// ───────────────────────────────────────────────────────────── +// Configuration descriptor (153 bytes) +// +// Config(9) + IAD(8) + ITF0_AC(9) + CS_AC(9) +// + ITF1_Alt0(9) + CS_MS(7) + 2xJackIN(12) + 2xJackOUT(18) + 2xEP(18) + 2xCS_EP(10) +// + ITF1_Alt1(9) + CS_MS(7) + 2xEP(18) + 2xCS_EP(10) +// = 153 +// ───────────────────────────────────────────────────────────── + +#define CONFIG_TOTAL_LEN 153 +#define AC_CS_HEADER_LEN 9 + +static const uint8_t desc_configuration[CONFIG_TOTAL_LEN] = { + + // ── Configuration ───────────────────────────────────────── + 9, TUSB_DESC_CONFIGURATION, + U16_TO_U8S_LE(CONFIG_TOTAL_LEN), + ITF_NUM_TOTAL, 1, 0, + TU_BIT(7) | TUSB_DESC_CONFIG_ATT_SELF_POWERED, 50, + + // ── IAD ─────────────────────────────────────────────────── + 8, TUSB_DESC_INTERFACE_ASSOCIATION, + ITF_NUM_AUDIO_CONTROL, 2, + TUSB_CLASS_AUDIO, 0x03, 0x00, 0, + + // ── Interface 0: AudioControl ───────────────────────────── + 9, TUSB_DESC_INTERFACE, + ITF_NUM_AUDIO_CONTROL, 0, 0, + TUSB_CLASS_AUDIO, 0x01, 0x00, 0, // protocol must be 0x00 (umpd_open checks AUDIO_FUNC_PROTOCOL_CODE_UNDEF) + + // ── CS AC Header (UAC2) ─────────────────────────────────── + AC_CS_HEADER_LEN, TUSB_DESC_CS_INTERFACE, 0x01, + U16_TO_U8S_LE(0x0200), 0x03, + U16_TO_U8S_LE(AC_CS_HEADER_LEN), 0x00, + + // ─── Interface 1, Alt 0: MIDI Streaming 1.0 ─────────────── + 9, TUSB_DESC_INTERFACE, + ITF_NUM_MIDI_STREAMING, 0, 2, + TUSB_CLASS_AUDIO, 0x03, 0x00, STR_IDX_MIDI_STREAMING, + + // CS MS Header (bcdMSC=1.00, wTotalLength=37) + 7, TUSB_DESC_CS_INTERFACE, MIDI_1_CS_INTERFACE_HEADER, + U16_TO_U8S_LE(0x0100), U16_TO_U8S_LE(37), + + // Embedded IN Jack (ID=1) + 6, TUSB_DESC_CS_INTERFACE, MIDI_1_CS_INTERFACE_IN_JACK, + MIDI_1_JACK_EMBEDDED, 1, 0, + + // External IN Jack (ID=3) + 6, TUSB_DESC_CS_INTERFACE, MIDI_1_CS_INTERFACE_IN_JACK, + MIDI_1_JACK_EXTERNAL, 3, 0, + + // Embedded OUT Jack (ID=2, src=ExtIN 3) + 9, TUSB_DESC_CS_INTERFACE, MIDI_1_CS_INTERFACE_OUT_JACK, + MIDI_1_JACK_EMBEDDED, 2, 1, 3, 1, 0, + + // External OUT Jack (ID=4, src=EmbIN 1) + 9, TUSB_DESC_CS_INTERFACE, MIDI_1_CS_INTERFACE_OUT_JACK, + MIDI_1_JACK_EXTERNAL, 4, 1, 1, 1, 0, + + // Bulk OUT endpoint Alt0 + 9, TUSB_DESC_ENDPOINT, EPNUM_MIDI_OUT, TUSB_XFER_BULK, + U16_TO_U8S_LE(EP_SIZE_FS), 0, 0, 0, + + // CS Endpoint (assoc Embedded OUT jack 2) + 5, TUSB_DESC_CS_ENDPOINT, MIDI_1_CS_ENDPOINT_GENERAL, 1, 2, + + // Bulk IN endpoint Alt0 + 9, TUSB_DESC_ENDPOINT, EPNUM_MIDI_IN, TUSB_XFER_BULK, + U16_TO_U8S_LE(EP_SIZE_FS), 0, 0, 0, + + // CS Endpoint (assoc Embedded IN jack 1) + 5, TUSB_DESC_CS_ENDPOINT, MIDI_1_CS_ENDPOINT_GENERAL, 1, 1, + + // ─── Interface 1, Alt 1: MIDI Streaming 2.0 (UMP) ───────── + 9, TUSB_DESC_INTERFACE, + ITF_NUM_MIDI_STREAMING, 1, 2, + TUSB_CLASS_AUDIO, 0x03, 0x00, STR_IDX_MIDI_STREAMING, + + // CS MS Header (bcdMSC=2.00, wTotalLength=7) + 7, TUSB_DESC_CS_INTERFACE, MIDI_1_CS_INTERFACE_HEADER, + U16_TO_U8S_LE(0x0200), U16_TO_U8S_LE(7), + + // Bulk OUT endpoint Alt1 + 9, TUSB_DESC_ENDPOINT, EPNUM_MIDI_OUT, TUSB_XFER_BULK, + U16_TO_U8S_LE(EP_SIZE_FS), 0, 0, 0, + + // CS Endpoint MIDI 2.0 (assoc GTB ID=1) + 5, TUSB_DESC_CS_ENDPOINT, MIDI20_CS_ENDPOINT_GENERAL, 1, 1, + + // Bulk IN endpoint Alt1 + 9, TUSB_DESC_ENDPOINT, EPNUM_MIDI_IN, TUSB_XFER_BULK, + U16_TO_U8S_LE(EP_SIZE_FS), 0, 0, 0, + + // CS Endpoint MIDI 2.0 (assoc GTB ID=1) + 5, TUSB_DESC_CS_ENDPOINT, MIDI20_CS_ENDPOINT_GENERAL, 1, 1, +}; + +static_assert(sizeof(desc_configuration) == CONFIG_TOTAL_LEN, + "CONFIG_TOTAL_LEN mismatch"); + +// ───────────────────────────────────────────────────────────── +// String descriptors +// ───────────────────────────────────────────────────────────── +static const char *const string_desc_arr[] = { + (const char[]){ 0x09, 0x04 }, + "sauloverissimo", + "ESP32-S3 MIDI 2.0 Device", + "ESP32S3-MIDI2-001", + "MIDI Streaming", + "Group 0", +}; + +static uint16_t _string_desc_buf[32]; + +// ───────────────────────────────────────────────────────────── +// Callbacks (override WEAK symbols from arduino-esp32) +// +// __attribute__((used)) prevents --gc-sections from eliminating +// these functions. arduino-esp32 links with -Wl,--gc-sections +// and -ffunction-sections, so any function not reachable from a +// link root (setup/loop/app_main) would otherwise be discarded. +// The weak definitions in esp32-hal-tinyusb.c would then be used +// instead, causing VID=0x303A and a MIDI 1.0-only descriptor. +// ───────────────────────────────────────────────────────────── + +extern "C" { + +__attribute__((used)) +uint8_t const *tud_descriptor_device_cb(void) { + return (uint8_t const *)&desc_device; +} + +__attribute__((used)) +uint8_t const *tud_descriptor_configuration_cb(uint8_t index) { + (void)index; + return desc_configuration; +} + +__attribute__((used)) +uint16_t const *tud_descriptor_string_cb(uint8_t index, uint16_t langid) { + (void)langid; + uint8_t chr_count; + const uint8_t max_str = sizeof(string_desc_arr) / sizeof(string_desc_arr[0]); + + if (index == 0) { + memcpy(&_string_desc_buf[1], string_desc_arr[0], 2); + chr_count = 1; + } else if (index < max_str) { + const char *str = string_desc_arr[index]; + chr_count = (uint8_t)strlen(str); + if (chr_count > 31) chr_count = 31; + for (uint8_t i = 0; i < chr_count; i++) { + _string_desc_buf[1 + i] = str[i]; + } + } else { + return NULL; + } + + _string_desc_buf[0] = (TUSB_DESC_STRING << 8) | (2 * chr_count + 2); + return _string_desc_buf; +} + +} // extern "C" diff --git a/examples/T-Display-S3-ESP32-S3-MIDI2-PingPong/platformio/tdisplay_s3_midi2/tools/ump_ping_pong.sh b/examples/T-Display-S3-ESP32-S3-MIDI2-PingPong/platformio/tdisplay_s3_midi2/tools/ump_ping_pong.sh new file mode 100644 index 0000000..fbff7d5 --- /dev/null +++ b/examples/T-Display-S3-ESP32-S3-MIDI2-PingPong/platformio/tdisplay_s3_midi2/tools/ump_ping_pong.sh @@ -0,0 +1,63 @@ +#!/bin/bash +# ump_ping_pong.sh — Bidirectional UMP message exchange between two ESP32-S3 devices +# +# Sends alternating NoteOn/NoteOff messages to both MIDI 2.0 endpoints, +# creating a visual ping-pong effect on the T-Display-S3 screens. +# +# Usage: bash ump_ping_pong.sh [rounds] +# rounds: number of note cycles (default: 20) +# +# Copyright (c) 2026 Saulo Verissimo +# SPDX-License-Identifier: MIT + +EP_A='\\\\?\\swd#midisrv#midiu_ks_9345629463034498376_outpin.0_inpin.2#{e7cce071-3c03-423f-88d3-f1045d02552b}' +EP_B='\\\\?\\swd#midisrv#midiu_ks_13319646191189016750_outpin.0_inpin.2#{e7cce071-3c03-423f-88d3-f1045d02552b}' + +ROUNDS=${1:-20} + +# C major scale across 2 octaves +NOTES=(60 64 67 72 76 79 84 79 76 72 67 64) +VELS=(40 55 70 85 100 115 127 115 100 85 70 55) + +send() { + midi endpoint "$1" send-message "$2" 2>/dev/null | grep -c "Sent" >/dev/null +} + +# Build UMP MIDI 1.0 Channel Voice word: MT=2, Group=0, Status+Ch, Data1, Data2 +ump_word() { + printf "0x%02X%02X%02X%02X" 0x20 "$1" "$2" "$3" +} + +echo "=== UMP Ping-Pong: $ROUNDS rounds ===" +echo " Device A ←→ Device B" +echo "" + +for (( r=0; r/dev/null | grep -c "Sent" >/dev/null; } + +NOTES_A=(48 55 60 64 67 72 76 79 84 88 91 96) +NOTES_B=(96 91 88 84 79 76 72 67 64 60 55 48) + +echo "=== UMP Storm: $ROUNDS rounds (6 msgs/round) ===" +echo " Device A ←→ Device B" +echo "" + +for (( r=0; r 127 )); then pb=127; fi + else + pb=$(( 64 - r )); if (( pb < 0 )); then pb=0; fi + fi + send "$EP_A" "$(printf '0x%02X%02X%02X%02X' 0x20 0xE0 0x00 $pb)" + send "$EP_B" "$(printf '0x%02X%02X%02X%02X' 0x20 0xE1 0x00 $((127 - pb)) )" + sleep 0.08 + + # NoteOff both + send "$EP_A" "$(printf '0x%02X%02X%02X%02X' 0x20 0x80 $na 0)" + send "$EP_B" "$(printf '0x%02X%02X%02X%02X' 0x20 0x81 $nb 0)" + sleep 0.1 +done + +echo "" +echo "=== Storm complete: $ROUNDS rounds × 6 = $((ROUNDS * 6)) UMP packets ===" diff --git a/examples/T-PicoC3-MIDI2-PingPong/files/HARDWARE.md b/examples/T-PicoC3-MIDI2-PingPong/files/HARDWARE.md new file mode 100644 index 0000000..6e6b7aa --- /dev/null +++ b/examples/T-PicoC3-MIDI2-PingPong/files/HARDWARE.md @@ -0,0 +1,96 @@ +# T-PicoC3 — Hardware Reference + +## Overview + +LilyGO T-PicoC3 is a dual-chip development board combining a **Raspberry Pi RP2040** +and an **Espressif ESP32-C3** on a single PCB, sharing one USB Type-C connector. + +- **RP2040** — ARM Cortex-M0+, 133 MHz, 264 KB SRAM, 2 MB Flash + → handles USB MIDI 2.0 device (TinyUSB native) + +- **ESP32-C3** — RISC-V 32-bit, 160 MHz, Wi-Fi 2.4 GHz, BLE 5 + → available for debug Serial or wireless features + +--- + +## Display + +| Parameter | Value | +|-----------|-------| +| Driver IC | ST7789V (rev. of ST7789, same TFT_eSPI driver) | +| Resolution | 240 × 135 px | +| Interface | SPI | +| Backlight pin (RP2040) | GPIO 4 (TFT_BL) | + +> The T-Display-S3 uses ST7789 at 320×170. The T-PicoC3 is smaller (240×135). +> The `UMPDisplay.h` driver will need adaptation for the lower resolution. +> **The display is the primary debug interface** — it shows alt setting, received UMP +> packets, and counters, exactly as on the T-Display-S3. No USB flip needed for debug. + +--- + +## USB Behavior + +The USB-C connector is **physically shared** between both chips via orientation switch: + +| USB-C orientation | Connected chip | Use | +|-------------------|----------------|-----| +| Normal | RP2040 | USB MIDI 2.0 device → ping-pong with T-Display-S3 | +| Flipped 180° | ESP32-C3 | Serial debug fallback (if display is insufficient) | + +LED indicators on the board show which chip is currently connected. + +**In normal operation the display (ST7789V) is used for debug** — it shows alt setting, +UMP message type, and packet counters. The USB-C flip to ESP32-C3 is available as a +fallback for deeper Serial debugging only. + +--- + +## RP2040 GPIO Pinout + +| Signal | GPIO | +|--------|------| +| TFT_MOSI | 3 | +| TFT_SCLK | 2 | +| TFT_CS | 5 | +| TFT_DC | 1 | +| TFT_RST | 0 | +| TFT_BL | 4 | +| PWR_ON | 22 | +| BUTTON1 | 6 | +| BUTTON2 | 7 | +| Red LED | 25 | + +> `PWR_ON` (GPIO 22) must be driven HIGH to power the display and peripherals. + +--- + +## Images + +- [pinout.png](pinout.png) — GPIO pinout diagram +- [specs.png](specs.png) — board specifications +- [switch.png](switch.png) — USB switch / connector orientation diagram + +--- + +## References + +- **Board repository:** https://github.com/Xinyuan-LilyGO/T-PicoC3 +- **Schematic (PDF):** https://github.com/Xinyuan-LilyGO/T-PicoC3/blob/main/Schematic/T-PicoC3.pdf +- **RP2040 datasheet:** https://datasheets.raspberrypi.com/rp2040/rp2040-datasheet.pdf +- **ESP32-C3 TRM (CN):** https://github.com/Xinyuan-LilyGO/T-PicoC3/blob/main/doc/esp32-c3_technical_reference_manual_cn.pdf +- **arduino-pico (Earle Philhower):** https://github.com/earlephilhower/arduino-pico +- **TinyUSB:** https://github.com/hathach/tinyusb + +--- + +## Development Notes + +- Framework target: **arduino-pico** (not arduino-esp32) +- The RP2040 TinyUSB in arduino-pico is the **upstream** version — no IDF quirks +- `__attribute__((used))` workaround from T-Display-S3 may **not** be needed +- `build_unflags` for `--gc-sections` should be reviewed against arduino-pico defaults +- Display library: **TFT_eSPI** (already in T-PicoC3 repo `/lib/`) configured for ST7789V +- `UMPDisplay.h` adaptation needed: change `TFT_WIDTH`/`TFT_HEIGHT` from 320×170 → 240×135 +- **Ping-pong target:** T-Display-S3 (ESP32-S3) — same VID/PID `0x1209:0x0001`, same UMP + protocol contract, same Endpoint Discovery. Any two boards can exchange UMP packets. diff --git a/examples/T-PicoC3-MIDI2-PingPong/files/pinout.png b/examples/T-PicoC3-MIDI2-PingPong/files/pinout.png new file mode 100644 index 0000000..3ae175c Binary files /dev/null and b/examples/T-PicoC3-MIDI2-PingPong/files/pinout.png differ diff --git a/examples/T-PicoC3-MIDI2-PingPong/files/specs.png b/examples/T-PicoC3-MIDI2-PingPong/files/specs.png new file mode 100644 index 0000000..31476e6 Binary files /dev/null and b/examples/T-PicoC3-MIDI2-PingPong/files/specs.png differ diff --git a/examples/T-PicoC3-MIDI2-PingPong/files/switch.png b/examples/T-PicoC3-MIDI2-PingPong/files/switch.png new file mode 100644 index 0000000..dbc6f31 Binary files /dev/null and b/examples/T-PicoC3-MIDI2-PingPong/files/switch.png differ diff --git a/examples/T-PicoC3-MIDI2-PingPong/images/T-PicoC3-RP2040.jpeg b/examples/T-PicoC3-MIDI2-PingPong/images/T-PicoC3-RP2040.jpeg new file mode 100644 index 0000000..d727945 Binary files /dev/null and b/examples/T-PicoC3-MIDI2-PingPong/images/T-PicoC3-RP2040.jpeg differ diff --git a/examples/T-PicoC3-MIDI2-PingPong/platformio/tpicoc3_midi2/platformio.ini b/examples/T-PicoC3-MIDI2-PingPong/platformio/tpicoc3_midi2/platformio.ini new file mode 100644 index 0000000..adec360 --- /dev/null +++ b/examples/T-PicoC3-MIDI2-PingPong/platformio/tpicoc3_midi2/platformio.ini @@ -0,0 +1,60 @@ +; tusb_ump — USB MIDI 2.0 device example for RP2040 / RP2350 +; Boards: LilyGO T-PicoC3 (display), bare RP2040 Pico, bare RP2350 Pico 2 +; +; Build: pio run -e T-PicoC3-MIDI2 +; Flash: pio run -e T-PicoC3-MIDI2 -t upload +; Serial: pio device monitor +; +; All environments implement the same UMP protocol contract +; as the T-Display-S3 example — any two boards can ping-pong. + +[platformio] +default_envs = T-PicoC3-MIDI2 + +; ── Common base for all RP2040/RP2350 environments ─────────────────────────── +[rp2040_base] +platform = https://github.com/maxgerhardt/platform-raspberrypi.git +framework = arduino +board_build.core = earlephilhower +monitor_speed = 115200 +build_flags = + -DUSE_TINYUSB + -DCFG_TUD_UMP=1 + -DCFG_TUD_UMP_RX_BUFSIZE=512 + -DCFG_TUD_UMP_TX_BUFSIZE=512 + -DCFG_TUD_MIDI=0 + ; Keep USB descriptor callback symbols if --gc-sections is active + -Wl,-u,tud_descriptor_device_cb + -Wl,-u,tud_descriptor_configuration_cb + -Wl,-u,tud_descriptor_string_cb +lib_deps = + ; tusb_ump library (4 levels up from platformio.ini → repo root) + symlink://../../../../ + +; ── T-PicoC3: RP2040 + ESP32-C3, ST7789V 240×135 display ──────────────────── +[env:T-PicoC3-MIDI2] +extends = rp2040_base +board = rpipico +build_flags = + ${rp2040_base.build_flags} + -DHAS_DISPLAY + -DBOARD_T_PICOC3 +lib_deps = + lovyan03/LovyanGFX @ 1.2.19 + symlink://../../../../ + +; ── Bare RP2040 Raspberry Pi Pico (no display) ─────────────────────────────── +[env:RP-Pico-MIDI2] +extends = rp2040_base +board = rpipico +build_flags = + ${rp2040_base.build_flags} + -DBOARD_RP_PICO + +; ── Bare RP2350 Raspberry Pi Pico 2 (no display) ───────────────────────────── +[env:RP-Pico2-RP2350-MIDI2] +extends = rp2040_base +board = rpipico2 +build_flags = + ${rp2040_base.build_flags} + -DBOARD_RP_PICO2 diff --git a/examples/T-PicoC3-MIDI2-PingPong/platformio/tpicoc3_midi2/src/UMPDisplay.h b/examples/T-PicoC3-MIDI2-PingPong/platformio/tpicoc3_midi2/src/UMPDisplay.h new file mode 100644 index 0000000..480bdbc --- /dev/null +++ b/examples/T-PicoC3-MIDI2-PingPong/platformio/tpicoc3_midi2/src/UMPDisplay.h @@ -0,0 +1,517 @@ +// UMPDisplay.h — Visual UMP monitor (T-PicoC3 ST7789V 240×135 / Serial stub) +// +// Design mirrors T-Display-S3: header + protocol tabs + hex view + +// velocity bar + event log + status bar. +// Protocol tabs light up orange (MIDI 1.0) or green (MIDI 2.0/UMP). +// +// IMPORTANT: memory_width=240, memory_height=320 must be the full ST7789V RAM +// dimensions, NOT the panel size — required for correct rotation offset math. +// +// Copyright (c) 2026 Saulo Verissimo +// SPDX-License-Identifier: MIT + +#ifndef UMP_DISPLAY_H +#define UMP_DISPLAY_H + +#include +#include "mapping.h" +#include "ump_stream_handler.h" + +// ═══════════════════════════════════════════════════════════════ +// DISPLAY IMPLEMENTATION — T-PicoC3 (ST7789V 240×135, SPI) +// ═══════════════════════════════════════════════════════════════ +#ifdef HAS_DISPLAY + +#include +#include +#include + +class UMPDisplay { +public: + + void init() { + pinMode(PIN_POWER_ON, OUTPUT); + digitalWrite(PIN_POWER_ON, HIGH); + + _tft.init(); + _tft.setRotation(1); + _tft.setBrightness(255); + _tft.setTextDatum(lgfx::top_left); + _tft.fillScreen(C_BG); + + _proto = 0; _conn = false; + _rx = 0; _tx = 0; _vel = 0; _nw = 0; + memset(_w, 0, sizeof(_w)); + memset(_log, 0, sizeof(_log)); + _logN = 0; + + _drawAll(); + } + + void setConnected(bool c) { _conn = c; _drawHeader(); } + + void setProtocol(uint8_t alt) { + _proto = alt; + _drawProto(); + _drawStatus(); + } + + void pushRxUMP(const uint32_t* words, uint8_t nw) { + _rx++; + _storeWords(words, nw); + + uint8_t b0 = _w[0] & 0xFF; + uint8_t b1 = (_w[0] >> 8) & 0xFF; + uint8_t b2 = (_w[0] >> 16) & 0xFF; + uint8_t b3 = (_w[0] >> 24) & 0xFF; + uint8_t mt = (b0 >> 4) & 0xF; + + if (mt == 0x2 || mt == 0x4) { + uint8_t st = b1 & 0xF0; + if (st == 0x90 && b3 > 0) { + _vel = (mt == 0x4 && nw >= 2) + ? (uint8_t)((_w[1] >> 17) & 0x7F) + : b3; + } else if (st == 0x80 || (st == 0x90 && b3 == 0)) { + _vel = 0; + } + } + + _drawHex(); + _drawVel(); + _addLog(b0, b1, b2, b3, mt, false); + _drawStatus(); + } + + void pushTxUMP(const uint32_t* words, uint8_t nw) { + _tx++; + uint8_t b0 = words[0] & 0xFF; + uint8_t b1 = (words[0] >> 8) & 0xFF; + uint8_t b2 = (words[0] >> 16) & 0xFF; + uint8_t b3 = (words[0] >> 24) & 0xFF; + uint8_t mt = (b0 >> 4) & 0xF; + _addLog(b0, b1, b2, b3, mt, true); + _drawStatus(); + } + + void pulseRx() { + static bool s = false; s = !s; + _tft.fillCircle(SCR_W - MARGIN - 4, Y_HEADER + H_HEADER / 2, + 4, s ? C_CYAN : C_HEADER); + } + +private: + // ── LGFX driver — T-PicoC3 (ST7789V, SPI) ─────────────── + // NOTE: memory_width/height = full ST7789V RAM (240×320), NOT panel size. + // LovyanGFX uses these to compute rotation offsets: mh-ph-oy, etc. + // With memory=panel (135×240), rotation=1 gives offset=240-240-40=-40 (wrong). + // With memory=full (240×320), rotation=1 gives offset=320-240-40=40 (correct). + class LGFX : public lgfx::LGFX_Device { + public: + LGFX() { + { auto c = _bus.config(); + c.spi_host = 0; + c.pin_sclk = 2; + c.pin_mosi = 3; + c.pin_miso = -1; + c.pin_dc = 1; + c.freq_write = 40000000; + _bus.config(c); _panel.setBus(&_bus); } + + { auto c = _panel.config(); + c.pin_cs = 5; + c.pin_rst = 0; + c.pin_busy = -1; + c.memory_width = 240; // full ST7789V RAM width (NOT panel size) + c.memory_height = 320; // full ST7789V RAM height (NOT panel size) + c.panel_width = 135; // actual visible panel + c.panel_height = 240; + c.offset_x = 52; + c.offset_y = 40; + c.offset_rotation = 0; + c.readable = false; + c.invert = true; + c.rgb_order = false; + c.dlen_16bit = false; + c.bus_shared = false; + _panel.config(c); } + + setPanel(&_panel); + + { auto c = _bl.config(); + c.pin_bl = 4; + c.invert = false; + c.freq = 44100; + c.pwm_channel = 0; + _bl.config(c); _panel.setLight(&_bl); } + } + private: + lgfx::Bus_SPI _bus; + lgfx::Panel_ST7789 _panel; + lgfx::Light_PWM _bl; + }; + + // ── State ─────────────────────────────────────────────── + LGFX _tft; + uint8_t _proto = 0; + bool _conn = false; + uint32_t _rx = 0, _tx = 0; + uint8_t _vel = 0, _nw = 0; + uint32_t _w[4] = {}; + + struct LogLine { char t[48]; uint32_t c; }; + LogLine _log[N_LOG]; + int _logN = 0; + + // ── Helpers ───────────────────────────────────────────── + + void _storeWords(const uint32_t* words, uint8_t nw) { + _w[0] = words[0]; + _w[1] = (nw >= 2) ? words[1] : 0; + _w[2] = (nw >= 3) ? words[2] : 0; + _w[3] = (nw >= 4) ? words[3] : 0; + _nw = nw; + } + + void _fill(int y, int h, uint32_t col) { + _tft.fillRect(0, y, SCR_W, h, col); + } + + void _hline(int y) { + _tft.drawFastHLine(0, y, SCR_W, C_DIV); + } + + void _txt(int x, int y, uint32_t fg, uint32_t bg, + const lgfx::IFont* f, const char* s) { + _tft.setFont(f); + _tft.setTextColor(fg, bg); + _tft.drawString(s, x, y); + } + + static void _noteName(uint8_t n, char* buf, size_t sz) { + static const char* const names[] = { + "C","C#","D","D#","E","F","F#","G","G#","A","A#","B" + }; + snprintf(buf, sz, "%s%d", names[n % 12], (int)(n / 12) - 1); + } + + // ── Full redraw ───────────────────────────────────────── + + void _drawAll() { + _drawHeader(); + _drawProto(); + _drawHex(); + _drawVel(); + _drawLog(); + _drawStatus(); + } + + // ── HEADER ────────────────────────────────────────────── + // │ USB MIDI T-PicoC3 - RP2040 ● │ + void _drawHeader() { + _fill(Y_HEADER, H_HEADER, C_HEADER); + _txt(MARGIN + 2, Y_HEADER + 2, C_WHITE, C_HEADER, + &lgfx::fonts::Font2, "USB MIDI"); + _txt(MARGIN + 2 + 88, Y_HEADER + 2, C_CYAN, C_HEADER, + &lgfx::fonts::Font2, "T-PicoC3 - RP2040"); + uint32_t dot = _conn ? C_GREEN : C_ORANGE; + _tft.fillCircle(SCR_W - MARGIN - 4, Y_HEADER + H_HEADER / 2, 4, dot); + } + + // ── PROTO: color tabs ──────────────────────────────────── + // │ MIDI 1.0 (orange=active) │ MIDI 2.0 / UMP (green=active) │ + void _drawProto() { + int half = SCR_W / 2; + uint32_t l_bg = (_proto == 0) ? C_ORANGE : C_DIV; + uint32_t l_fg = (_proto == 0) ? C_BG : C_GRAY; + uint32_t r_bg = (_proto == 1) ? C_GREEN : C_DIV; + uint32_t r_fg = (_proto == 1) ? C_BG : C_GRAY; + + _tft.fillRect(0, Y_PROTO, half, H_PROTO, l_bg); + _tft.fillRect(half, Y_PROTO, half, H_PROTO, r_bg); + + _txt(MARGIN + 4, Y_PROTO + 1, l_fg, l_bg, + &lgfx::fonts::Font2, "MIDI 1.0"); + _txt(half + MARGIN + 4, Y_PROTO + 1, r_fg, r_bg, + &lgfx::fonts::Font2, "MIDI 2.0 / UMP"); + + _hline(Y_PROTO + H_PROTO); + } + + // ── HEX VIEW ───────────────────────────────────────────── + // Line 1: color-coded hex bytes (Font2) + // Line 2: decoded description (Font2) + void _drawHex() { + _fill(Y_HEX, H_HEX, C_BG); + _hline(Y_HEX + H_HEX); + + if (_nw == 0) { + _txt(MARGIN, Y_HEX + 7, C_DIV, C_BG, + &lgfx::fonts::Font2, "waiting for data..."); + return; + } + + uint8_t b0 = _w[0] & 0xFF; + uint8_t b1 = (_w[0] >> 8) & 0xFF; + uint8_t b2 = (_w[0] >> 16) & 0xFF; + uint8_t b3 = (_w[0] >> 24) & 0xFF; + uint8_t mt = (b0 >> 4) & 0xF; + uint8_t grp = b0 & 0xF; + uint8_t st = b1 & 0xF0; + uint8_t ch = b1 & 0x0F; + + // Line 1: color-coded hex bytes + _tft.setFont(&lgfx::fonts::Font2); + char hex[10]; int cx = MARGIN; + + _tft.setTextColor(C_CYAN, C_BG); + snprintf(hex, sizeof(hex), "%02X ", b0); + _tft.drawString(hex, cx, Y_HEX + 2); cx += 24; + + _tft.setTextColor(C_ORANGE, C_BG); + snprintf(hex, sizeof(hex), "%02X ", b1); + _tft.drawString(hex, cx, Y_HEX + 2); cx += 24; + + _tft.setTextColor(C_DIV, C_BG); + snprintf(hex, sizeof(hex), "%02X %02X", b2, b3); + _tft.drawString(hex, cx, Y_HEX + 2); + + if (_nw >= 2) { + _tft.setTextColor(C_DIV, C_BG); + char w2[12]; + snprintf(w2, sizeof(w2), " %08lX", (unsigned long)_w[1]); + _tft.drawString(w2, cx + 48, Y_HEX + 2); + } + + // Line 2: decoded description + char desc[48]; uint32_t col = C_DIV; + const char* tag = (mt == 0x4) ? "M2" : "M1"; + + if (mt == 0x2 || mt == 0x4) { + if (st == 0x90 && b3 > 0) { + char nn[6]; _noteName(b2, nn, sizeof(nn)); + snprintf(desc, sizeof(desc), "%s NoteOn G%u Ch%u %s V=%u", + tag, grp, ch, nn, b3); + col = C_GREEN; + } + else if (st == 0x80 || (st == 0x90 && b3 == 0)) { + char nn[6]; _noteName(b2, nn, sizeof(nn)); + snprintf(desc, sizeof(desc), "%s NoteOff G%u Ch%u %s", + tag, grp, ch, nn); + col = C_GRAY; + } + else if (st == 0xB0) { + snprintf(desc, sizeof(desc), "%s CC G%u Ch%u CC=%u V=%u", + tag, grp, ch, b2, b3); + col = C_CYAN; + } + else if (st == 0xE0) { + snprintf(desc, sizeof(desc), "%s PBend G%u Ch%u %02X%02X", + tag, grp, ch, b2, b3); + col = C_MAGENTA; + } + else { + snprintf(desc, sizeof(desc), "%s Msg G%u Ch%u %02X %02X", + tag, grp, ch, b2, b3); + } + } + else if (mt == 0x3 || mt == 0x5) { + snprintf(desc, sizeof(desc), "SysEx G%u [%u words]", grp, _nw); + col = C_YELLOW; + } + else if (mt == 0xF) { + uint16_t sts = streamStatus(_w[0]); + snprintf(desc, sizeof(desc), "<< %s", umpStreamLabel(sts)); + col = C_CYAN; + } + else { + snprintf(desc, sizeof(desc), "MT=%01X G%u %02X %02X %02X", + mt, grp, b1, b2, b3); + } + + _txt(MARGIN, Y_HEX + 17, col, C_BG, &lgfx::fonts::Font2, desc); + } + + // ── VEL BAR ───────────────────────────────────────────── + void _drawVel() { + _fill(Y_VEL, H_VEL, C_BG); + _hline(Y_VEL + H_VEL); + + int bx = MARGIN, bw = SCR_W - 72, bh = H_VEL - 4, by = Y_VEL + 2; + int fill = (_vel * bw) / 127; + _tft.fillRect(bx, by, fill, bh, C_CYAN); + _tft.fillRect(bx + fill, by, bw - fill, bh, C_DIV); + + char buf[10]; snprintf(buf, sizeof(buf), "V:%3u", _vel); + _txt(bx + bw + 6, Y_VEL + 1, C_DIV, C_BG, &lgfx::fonts::Font2, buf); + } + + // ── LOG ───────────────────────────────────────────────── + + void _addLog(uint8_t b0, uint8_t b1, uint8_t b2, uint8_t b3, + uint8_t mt, bool isTx) + { + if (mt == 0x0) return; // skip utility/timing/NOOP + + LogLine& e = _log[_logN % N_LOG]; + uint8_t st = b1 & 0xF0; + uint8_t grp = b0 & 0xF; + uint8_t ch = b1 & 0x0F; + const char* dir = isTx ? "T:" : "R:"; + e.c = C_DIV; + + if (mt == 0x2 || mt == 0x4) { + if (st == 0x90 && b3 > 0) { + snprintf(e.t, sizeof(e.t), "%sNoteOn G%u Ch%u N%u V%u", + dir, grp, ch, b2, b3); + e.c = C_GREEN; + } + else if (st == 0x80 || (st == 0x90 && b3 == 0)) { + snprintf(e.t, sizeof(e.t), "%sNoteOff G%u Ch%u N%u", + dir, grp, ch, b2); + e.c = C_GRAY; + } + else if (st == 0xB0) { + snprintf(e.t, sizeof(e.t), "%sCC G%u Ch%u CC%u V%u", + dir, grp, ch, b2, b3); + e.c = C_CYAN; + } + else if (st == 0xE0) { + snprintf(e.t, sizeof(e.t), "%sPBend G%u Ch%u %02X%02X", + dir, grp, ch, b2, b3); + e.c = C_MAGENTA; + } + else { + snprintf(e.t, sizeof(e.t), "%s%02X %02X %02X %02X", + dir, b0, b1, b2, b3); + } + } + else if (mt == 0x3 || mt == 0x5) { + snprintf(e.t, sizeof(e.t), "%sSysEx G%u %02X", dir, grp, b1); + e.c = C_YELLOW; + } + else if (mt == 0xF) { + uint16_t sts = ((uint16_t)(b0 & 0x03) << 8) | b1; + snprintf(e.t, sizeof(e.t), "%s%s", dir, umpStreamLabel(sts)); + e.c = isTx ? C_GREEN : C_CYAN; + } + else { + snprintf(e.t, sizeof(e.t), "%sMT%01X %02X %02X %02X %02X", + dir, mt, b0, b1, b2, b3); + } + + _logN++; + _drawLog(); + } + + void _drawLog() { + _fill(Y_LOG, H_LOG, C_BG); + _hline(Y_LOG + H_LOG); + + for (int i = 0; i < N_LOG; i++) { + if (_logN < N_LOG - i) continue; + int idx = (_logN - N_LOG + i) % N_LOG; + if (idx < 0) idx += N_LOG; + _txt(MARGIN, Y_LOG + 1 + i * H_LLINE, _log[idx].c, C_BG, + &lgfx::fonts::Font2, _log[idx].t); + } + } + + // ── STATUS BAR ────────────────────────────────────────── + // │ RX: xxxx TX: xxxx MIDI 2.0 │ + void _drawStatus() { + _fill(Y_STATUS, H_STATUS, C_HEADER); + + char buf[40]; + snprintf(buf, sizeof(buf), "RX: %-5lu TX: %-5lu", + (unsigned long)_rx, (unsigned long)_tx); + _txt(MARGIN, Y_STATUS + 4, C_WHITE, C_HEADER, + &lgfx::fonts::Font2, buf); + + const char* ps = (_proto == 1) ? "MIDI 2.0" : "MIDI 1.0"; + uint32_t pc = (_proto == 1) ? C_GREEN : C_ORANGE; + int pw = strlen(ps) * 12; + _txt(SCR_W - MARGIN - pw, Y_STATUS + 4, pc, C_HEADER, + &lgfx::fonts::Font2, ps); + } +}; + +// ═══════════════════════════════════════════════════════════════ +// SERIAL STUB — bare RP2040/RP2350 Pico (no display) +// ═══════════════════════════════════════════════════════════════ +#else // !HAS_DISPLAY + +class UMPDisplay { +public: + void init() { + pinMode(LED_PIN, OUTPUT); + digitalWrite(LED_PIN, LOW); + Serial.println("[MIDI2] USB MIDI 2.0 — RP2040/RP2350"); + Serial.println("[MIDI2] No display: using Serial monitor"); + } + + void setConnected(bool c) { + Serial.printf("[MIDI2] USB %s\n", c ? "mounted" : "disconnected"); + digitalWrite(LED_PIN, c ? HIGH : LOW); + } + + void setProtocol(uint8_t alt) { + Serial.printf("[MIDI2] Alt %d — %s\n", alt, + alt ? "MIDI 2.0 / UMP" : "MIDI 1.0"); + } + + void pushRxUMP(const uint32_t* words, uint8_t nw) { + _rx++; + char desc[48]; + _decode(words[0], words, nw, desc, sizeof(desc)); + Serial.printf("RX #%-4lu %08lX %s\n", + (unsigned long)_rx, (unsigned long)words[0], desc); + } + + void pushTxUMP(const uint32_t* words, uint8_t nw) { + _tx++; + char desc[48]; + _decode(words[0], words, nw, desc, sizeof(desc)); + Serial.printf("TX #%-4lu %08lX %s\n", + (unsigned long)_tx, (unsigned long)words[0], desc); + } + + void pulseRx() { + static bool s = false; s = !s; + digitalWrite(LED_PIN, s ? HIGH : LOW); + } + +private: + uint32_t _rx = 0, _tx = 0; + + static void _decode(uint32_t w0, const uint32_t* words, uint8_t nw, + char* buf, size_t sz) + { + uint8_t b0 = w0 & 0xFF, b1 = (w0 >> 8) & 0xFF; + uint8_t b2 = (w0 >> 16) & 0xFF, b3 = (w0 >> 24) & 0xFF; + uint8_t mt = (b0 >> 4) & 0xF, grp = b0 & 0xF; + uint8_t st = b1 & 0xF0, ch = b1 & 0x0F; + + if (mt == 0x2 || mt == 0x4) { + if (st == 0x90 && b3 > 0) + snprintf(buf, sz, "NoteOn G%u Ch%u N%u V%u", grp, ch, b2, b3); + else if (st == 0x80 || (st == 0x90 && b3 == 0)) + snprintf(buf, sz, "NoteOff G%u Ch%u N%u", grp, ch, b2); + else if (st == 0xB0) + snprintf(buf, sz, "CC G%u Ch%u CC%u V%u", grp, ch, b2, b3); + else + snprintf(buf, sz, "MT%01X G%u %02X %02X", mt, grp, b2, b3); + } + else if (mt == 0xF) { + uint16_t sts = streamStatus(words[0]); + snprintf(buf, sz, "%s", umpStreamLabel(sts)); + } + else { + snprintf(buf, sz, "MT%01X G%u %02X %02X %02X", mt, grp, b1, b2, b3); + } + } +}; + +#endif // HAS_DISPLAY + +#endif // UMP_DISPLAY_H diff --git a/examples/T-PicoC3-MIDI2-PingPong/platformio/tpicoc3_midi2/src/main.cpp b/examples/T-PicoC3-MIDI2-PingPong/platformio/tpicoc3_midi2/src/main.cpp new file mode 100644 index 0000000..6858ca2 --- /dev/null +++ b/examples/T-PicoC3-MIDI2-PingPong/platformio/tpicoc3_midi2/src/main.cpp @@ -0,0 +1,305 @@ +// ============================================================ +// USB MIDI 2.0 Device — RP2040 / RP2350 (PlatformIO) +// ============================================================ +// +// Boards: +// T-PicoC3 (-DHAS_DISPLAY -DBOARD_T_PICOC3) ← ST7789V 240×135 +// RP-Pico (-DBOARD_RP_PICO) ← Serial + LED + auto-send +// RP-Pico2 (-DBOARD_RP_PICO2) ← Serial + LED + auto-send +// +// Build: pio run -e T-PicoC3-MIDI2 +// Flash: pio run -e T-PicoC3-MIDI2 -t upload +// Serial: pio device monitor +// +// Controls (T-PicoC3 / boards with buttons): +// BTN1 (GPIO6) — toggle NoteOn / NoteOff (C5) +// BTN2 (GPIO7) — cycle test velocity: 32 → 64 → 96 → 127 +// +// No-display boards: +// Auto-sends NoteOn every 2 s, NoteOff every 2.5 s. +// Serial monitor shows RX/TX events and protocol negotiation. +// LED (GPIO 25) blinks on USB mount and pulses on RX. +// +// Protocol contract (same as T-Display-S3 example): +// • VID/PID 0x1209:0x0001 +// • Alt 0 = MIDI 1.0, Alt 1 = UMP +// • Full Endpoint Discovery (MT=0xF Stream messages) +// • Echo-back of all non-Stream UMP messages +// +// Copyright (c) 2026 Saulo Verissimo +// SPDX-License-Identifier: MIT +// ============================================================ + +#include +#include "ump_device.h" +#include "ump_stream_handler.h" +#include "UMPDisplay.h" +#include "mapping.h" + +// Injects MIDI 2.0 config descriptor and strings into TinyUSBDevice. +// Must be called at the top of setup() on RP2040 (arduino-pico). +extern void usb_descriptors_begin(void); + +// ── Device identity — board-specific ───────────────────────── +#ifdef BOARD_RP_PICO2 +# define BOARD_MODEL_ID 0x0003 +# define BOARD_ENDPOINT_NAME "RP2350 MIDI2" +# define BOARD_PRODUCT_ID "RP2350MIDI001" +#else +# define BOARD_MODEL_ID 0x0002 +# define BOARD_ENDPOINT_NAME "RP2040 MIDI2" +# define BOARD_PRODUCT_ID "RP2040MIDI001" +#endif + +// ── Device configuration ───────────────────────────────────── + +static const UMPStreamConfig streamCfg = { + .umpVersionMajor = 1, + .umpVersionMinor = 1, + .numFunctionBlocks = 1, + .staticFunctionBlocks = true, + .protocolCaps = UMP_PROTO_CAP_MIDI1 | UMP_PROTO_CAP_MIDI2, + .manufacturerId = { 0x00, 0x00, 0x7D }, // 0x7D = educational/dev + .familyId = 0x0001, + .modelId = BOARD_MODEL_ID, // 0x0002 RP2040, 0x0003 RP2350 + .swRevision = { '0', '1', '0', '0' }, + .endpointName = BOARD_ENDPOINT_NAME, // ≤14 chars → 1 packet + .productInstanceId = BOARD_PRODUCT_ID, // ≤14 chars → 1 packet + .fbName = "Group 0", + .fbDirection = UMP_FB_DIR_BIDIRECTIONAL, + .fbFirstGroup = 0, + .fbNumGroups = 1, + .fbUIHint = 0x00, + .fbMidi10 = 0x00, + .fbMidiCIVer = 0x00, + .fbSysEx8 = 0x00, +}; + +// ── State ──────────────────────────────────────────────────── + +static UMPDisplay display; +static bool usbWasConnected = false; +static uint8_t currentAlt = 0xFF; + +static uint32_t btn1_last = 0; +static uint32_t btn2_last = 0; +static const uint32_t DEBOUNCE_MS = 80; + +static bool noteIsOn = false; +static uint8_t testNote = 72; +static uint8_t testVelIdx = 1; +static const uint8_t velocities[] = { 32, 64, 96, 127 }; +static uint8_t testVel = 64; + +#ifndef HAS_DISPLAY +// Auto-send state for no-display boards +static uint32_t autoLast = 0; +static bool autoOn = false; +#endif + +// ── tusb_ump callbacks ─────────────────────────────────────── + +extern "C" void tud_ump_set_itf_cb(uint8_t itf, uint8_t alt) { + (void)itf; + currentAlt = alt; + display.setProtocol(alt); +} + +extern "C" void tud_ump_rx_cb(uint8_t itf) { + (void)itf; +} + +// ── Stream handler TX callback ─────────────────────────────── + +static void onStreamTx(const uint32_t* words, uint8_t nw, + const char* /* label */) +{ + display.pushTxUMP(words, nw); +} + +// ── UMP helpers ────────────────────────────────────────────── + +static inline uint8_t umpWordsForMT(uint8_t mt) { + switch (mt) { + case 0x0: case 0x1: case 0x2: case 0x6: case 0x7: return 1; + case 0x3: case 0x4: case 0x8: case 0x9: case 0xA: case 0xD: return 2; + case 0xB: case 0xC: return 3; + case 0x5: case 0xE: case 0xF: return 4; + default: return 1; + } +} + +static inline uint32_t makeUMP_MIDI1CV(uint8_t group, uint8_t status, + uint8_t ch, uint8_t d0, uint8_t d1) +{ + return (uint32_t)d1 << 24 | + (uint32_t)d0 << 16 | + (uint32_t)((status & 0xF0) | (ch & 0x0F)) << 8 | + (uint32_t)(0x20 | (group & 0x0F)); +} + +// ── Send ───────────────────────────────────────────────────── + +static void sendNoteOn(uint8_t group, uint8_t ch, + uint8_t note, uint8_t vel) +{ + if (!tud_ump_n_mounted(0)) return; + uint32_t w = makeUMP_MIDI1CV(group, 0x90, ch, note, vel); + tud_ump_write(0, &w, 1); + display.pushTxUMP(&w, 1); +} + +static void sendNoteOff(uint8_t group, uint8_t ch, uint8_t note) +{ + if (!tud_ump_n_mounted(0)) return; + uint32_t w = makeUMP_MIDI1CV(group, 0x80, ch, note, 0); + tud_ump_write(0, &w, 1); + display.pushTxUMP(&w, 1); +} + +static void sendCC(uint8_t group, uint8_t ch, uint8_t cc, uint8_t val) +{ + if (!tud_ump_n_mounted(0)) return; + uint32_t w = makeUMP_MIDI1CV(group, 0xB0, ch, cc, val); + tud_ump_write(0, &w, 1); + display.pushTxUMP(&w, 1); +} + +// ── RX ─────────────────────────────────────────────────────── + +static void processRxUMP() +{ + for (int pass = 0; pass < 16; pass++) { + if (tud_ump_n_available(0) == 0) break; + + uint32_t firstWord = 0; + if (tud_ump_read(0, &firstWord, 1) == 0) break; + + uint8_t mt = (uint8_t)((firstWord & 0xF0) >> 4); + uint8_t numWords = umpWordsForMT(mt); + uint32_t words[4] = { firstWord, 0, 0, 0 }; + + // Wait for the remaining words before processing. + // Avoids passing a truncated packet to umpStreamHandleRx(). + if (numWords > 1 && tud_ump_n_available(0) < (numWords - 1)) break; + + for (uint8_t w = 1; w < numWords; w++) { + tud_ump_read(0, &words[w], 1); + } + + display.pushRxUMP(words, numWords); + display.pulseRx(); + + if (mt == 0xF) { + umpStreamHandleRx(0, words, streamCfg, onStreamTx); + } else { + if (tud_ump_n_writeable(0) >= numWords) { + tud_ump_write(0, words, numWords); + display.pushTxUMP(words, numWords); + } + } + } +} + +// ── Buttons ────────────────────────────────────────────────── + +static void handleButtons() +{ + uint32_t now = millis(); + + if (digitalRead(BTN1) == LOW && (now - btn1_last) > DEBOUNCE_MS) { + btn1_last = now; + if (!noteIsOn) { sendNoteOn(0, 0, testNote, testVel); noteIsOn = true; } + else { sendNoteOff(0, 0, testNote); noteIsOn = false; } + } + + if (digitalRead(BTN2) == LOW && (now - btn2_last) > DEBOUNCE_MS) { + btn2_last = now; + testVelIdx = (testVelIdx + 1) % (sizeof(velocities) / sizeof(velocities[0])); + testVel = velocities[testVelIdx]; + sendCC(0, 0, 7, testVel); + } +} + +#ifndef HAS_DISPLAY +// ── Auto-send (no-display boards) ──────────────────────────── +// Sends NoteOn every 2 s, NoteOff 500 ms later. Provides +// visible USB traffic without requiring physical buttons. + +static void handleAutoSend() +{ + if (!tud_ump_n_mounted(0)) return; + uint32_t now = millis(); + if (!autoOn && (now - autoLast) > 2000) { + sendNoteOn(0, 0, testNote, testVel); + noteIsOn = true; + autoOn = true; + autoLast = now; + } else if (autoOn && (now - autoLast) > 500) { + sendNoteOff(0, 0, testNote); + noteIsOn = false; + autoOn = false; + autoLast = now; + } +} +#endif + +// ── Arduino ────────────────────────────────────────────────── + +void setup() +{ + // Inject our MIDI 2.0 descriptor into Adafruit TinyUSBDevice. + // Must be first — before Serial, display, or any other init. + usb_descriptors_begin(); + + // arduino-pico initializes TinyUSB automatically — no USB.begin() needed + Serial.begin(115200); + + pinMode(LED_PIN, OUTPUT); + digitalWrite(LED_PIN, LOW); + + pinMode(BTN1, INPUT_PULLUP); + pinMode(BTN2, INPUT_PULLUP); + testVel = velocities[testVelIdx]; + + display.init(); + display.setConnected(false); + display.setProtocol(0); + + Serial.println("[MIDI2] USB MIDI 2.0 device started"); + Serial.printf("[MIDI2] EP name: %s\n", streamCfg.endpointName); + Serial.printf("[MIDI2] Product: %s\n", streamCfg.productInstanceId); +} + +void loop() +{ + bool mounted = tud_ump_n_mounted(0); + + if (mounted && !usbWasConnected) { + usbWasConnected = true; + digitalWrite(LED_PIN, HIGH); + display.setConnected(true); + } else if (!mounted && usbWasConnected) { + usbWasConnected = false; + currentAlt = 0xFF; + digitalWrite(LED_PIN, LOW); + display.setConnected(false); + } + + if (mounted) { + uint8_t alt = tud_alt_setting(0); + if (alt != currentAlt) { + currentAlt = alt; + display.setProtocol(alt); + } + processRxUMP(); + } + + handleButtons(); + +#ifndef HAS_DISPLAY + handleAutoSend(); +#endif + + delay(2); +} diff --git a/examples/T-PicoC3-MIDI2-PingPong/platformio/tpicoc3_midi2/src/mapping.h b/examples/T-PicoC3-MIDI2-PingPong/platformio/tpicoc3_midi2/src/mapping.h new file mode 100644 index 0000000..2dfe468 --- /dev/null +++ b/examples/T-PicoC3-MIDI2-PingPong/platformio/tpicoc3_midi2/src/mapping.h @@ -0,0 +1,82 @@ +#ifndef MAPPING_H +#define MAPPING_H + +// ── Board selection ─────────────────────────────────────────────────────────── +// -DBOARD_T_PICOC3 → LilyGO T-PicoC3 (RP2040 + ESP32-C3, ST7789V 240×135) +// -DBOARD_RP_PICO → Raspberry Pi Pico (RP2040, no display) +// -DBOARD_RP_PICO2 → Raspberry Pi Pico 2 (RP2350, no display) + +// ── Color palette ───────────────────────────────────────────────────────────── +#define C_BG 0x0000 // Black +#define C_HEADER 0x0821 // Very dark blue — header/status bg +#define C_DIV 0x7BEF // Light gray (~48%) — dividers, dim text, inactive +#define C_WHITE 0xFFFF +#define C_GREEN 0x07E0 // Bright green — MIDI 2.0 active, NoteOn +#define C_CYAN 0x07FF // Cyan — CC, stream messages, hex byte 0 +#define C_ORANGE 0xFD20 // Orange — MIDI 1.0 active, disconnected +#define C_YELLOW 0xFFE0 // Yellow — SysEx +#define C_GRAY 0x8C71 // Medium gray (~55%) — NoteOff (was 0x4208, too dark) +#define C_MAGENTA 0xF81F // Magenta — PitchBend + +#ifdef BOARD_T_PICOC3 + +// ── T-PicoC3 hardware ──────────────────────────────────────────────────────── +#define TFT_BL_PIN 4 +#define PIN_POWER_ON 22 +#define BTN1 6 +#define BTN2 7 +#define LED_PIN 25 + +// ── Screen (landscape 240 × 135) ───────────────────────────────────────────── +#define SCR_W 240 +#define SCR_H 135 +#define MARGIN 4 + +// ── Layout — T-Display-S3 style, scaled for 240×135 ────────────────────────── +// +// Y= 0 ┌──────────────────────────────────────────┐ +// │ USB MIDI 2.0 • T-PicoC3 ● │ 20 HEADER +// Y= 20 ├─────────────────────┬────────────────────┤ +// │ MIDI 1.0 │ MIDI 2.0 / UMP │ 18 PROTO (color tabs) +// Y= 38 ├─────────────────────┴────────────────────┤ +// │ 20 90 3C 60 NoteOn G0 Ch0 C#4 V96 │ 30 HEX (2 lines) +// Y= 68 ├──────────────────────────────────────────┤ +// │ ████████████████░░░░░ V: 96 │ 14 VEL bar +// Y= 82 ├──────────────────────────────────────────┤ +// │ R:NoteOn G0 Ch0 N60 V96 │ +// │ T:EndpointReply │ 28 LOG (2 × 14px, Font2) +// Y=110 ├──────────────────────────────────────────┤ +// │ RX: 1234 TX: 5678 MIDI 2.0 │ 25 STATUS +// Y=135 └──────────────────────────────────────────┘ +// +// Totals: 20+18+30+14+28+25 = 135 ✓ + +#define Y_HEADER 0 +#define H_HEADER 20 + +#define Y_PROTO 20 +#define H_PROTO 18 + +#define Y_HEX 38 +#define H_HEX 30 + +#define Y_VEL 68 +#define H_VEL 14 + +#define Y_LOG 82 +#define H_LOG 28 +#define N_LOG 2 +#define H_LLINE 14 + +#define Y_STATUS 110 +#define H_STATUS 25 + +#else // BOARD_RP_PICO or BOARD_RP_PICO2 + +#define BTN1 15 +#define BTN2 16 +#define LED_PIN 25 + +#endif // board selection + +#endif // MAPPING_H diff --git a/examples/T-PicoC3-MIDI2-PingPong/platformio/tpicoc3_midi2/src/ump_stream_handler.h b/examples/T-PicoC3-MIDI2-PingPong/platformio/tpicoc3_midi2/src/ump_stream_handler.h new file mode 100644 index 0000000..63f64b0 --- /dev/null +++ b/examples/T-PicoC3-MIDI2-PingPong/platformio/tpicoc3_midi2/src/ump_stream_handler.h @@ -0,0 +1,384 @@ +// ump_stream_handler.h — UMP Stream Message (MT=0xF) handler +// +// Implements the device-side UMP Endpoint Discovery protocol per the +// UMP and MIDI 2.0 Protocol Specification v1.1 (M2-104-UM). +// +// When Windows MIDI Services (or any UMP-capable host) switches to +// Alternate Setting 1 (MIDI Streaming 2.0), it sends a series of +// MT=0xF Stream Messages to discover the device's capabilities: +// +// Host → Device EP Discovery (status 0x000) +// Device → Host EP Info Notification (0x001) +// Device → Host Device Identity Notification (0x002) +// Device → Host Endpoint Name Notification (0x003) +// Device → Host Product Instance ID Notification (0x004) +// +// Host → Device Stream Configuration Request (0x005) +// Device → Host Stream Configuration Notification (0x006) +// +// Host → Device Function Block Discovery (0x010) +// Device → Host Function Block Info Notification (0x011) +// Device → Host Function Block Name Notification (0x012) +// +// This handler parses incoming requests and generates the correct +// responses. All responses are sent via tud_ump_write(). +// +// Copyright (c) 2026 Saulo Verissimo +// SPDX-License-Identifier: MIT + +#ifndef UMP_STREAM_HANDLER_H +#define UMP_STREAM_HANDLER_H + +#include +#include +#include "ump_device.h" + +// ───────────────────────────────────────────────────────────── +// UMP Stream Message status codes (10-bit) +// Reference: M2-104-UM v1.1, Section 7.1 +// ───────────────────────────────────────────────────────────── + +#define UMP_STREAM_STATUS_EP_DISCOVERY 0x000 +#define UMP_STREAM_STATUS_EP_INFO 0x001 +#define UMP_STREAM_STATUS_DEVICE_INFO 0x002 +#define UMP_STREAM_STATUS_EP_NAME 0x003 +#define UMP_STREAM_STATUS_PRODUCT_ID 0x004 +#define UMP_STREAM_STATUS_STREAM_CFG_REQ 0x005 +#define UMP_STREAM_STATUS_STREAM_CFG 0x006 +#define UMP_STREAM_STATUS_FB_DISCOVERY 0x010 +#define UMP_STREAM_STATUS_FB_INFO 0x011 +#define UMP_STREAM_STATUS_FB_NAME 0x012 +#define UMP_STREAM_STATUS_START_CLIP 0x020 +#define UMP_STREAM_STATUS_END_CLIP 0x021 + +// ── Format field (bits [3:2] of wire byte 0) ───────────────── +#define UMP_STREAM_FMT_COMPLETE 0 +#define UMP_STREAM_FMT_START 1 +#define UMP_STREAM_FMT_CONTINUE 2 +#define UMP_STREAM_FMT_END 3 + +// ── Endpoint Discovery filter bitmap (wire byte 4) ─────────── +#define UMP_FILTER_EP_INFO 0x01 +#define UMP_FILTER_DEVICE_ID 0x02 +#define UMP_FILTER_EP_NAME 0x04 +#define UMP_FILTER_PRODUCT_ID 0x08 +#define UMP_FILTER_STREAM_CFG 0x10 + +// ── Protocol capabilities (EP Info byte 6) ─────────────────── +#define UMP_PROTO_CAP_MIDI2 0x01 // supports MIDI 2.0 Protocol +#define UMP_PROTO_CAP_MIDI1 0x02 // supports MIDI 1.0 Protocol + +// ── Protocol selection (Stream Configuration) ──────────────── +#define UMP_PROTO_MIDI1 0x01 +#define UMP_PROTO_MIDI2 0x02 + +// ── Function Block direction ───────────────────────────────── +#define UMP_FB_DIR_INPUT 0x01 // receives from host +#define UMP_FB_DIR_OUTPUT 0x02 // sends to host +#define UMP_FB_DIR_BIDIRECTIONAL 0x03 + +// ───────────────────────────────────────────────────────────── +// Wire byte packing — little-endian ↔ UMP wire order +// +// UMP words are transmitted MSB-first on USB. On RP2040 (LE), +// the LSB of a uint32_t occupies the lowest memory address, +// which is the first byte sent by the USB bulk endpoint. +// +// packWord(b0, b1, b2, b3) places wire byte 0 at the LSB: +// address+0 = b0 (first on wire — contains MT nibble) +// address+1 = b1 +// address+2 = b2 +// address+3 = b3 (last on wire) +// ───────────────────────────────────────────────────────────── + +static inline uint32_t packWord(uint8_t b0, uint8_t b1, + uint8_t b2, uint8_t b3) +{ + return (uint32_t)b0 + | ((uint32_t)b1 << 8) + | ((uint32_t)b2 << 16) + | ((uint32_t)b3 << 24); +} + +static inline uint8_t wireB0(uint32_t w) { return (uint8_t)(w); } +static inline uint8_t wireB1(uint32_t w) { return (uint8_t)(w >> 8); } +static inline uint8_t wireB2(uint32_t w) { return (uint8_t)(w >> 16); } +static inline uint8_t wireB3(uint32_t w) { return (uint8_t)(w >> 24); } + +// ── Stream header helpers ──────────────────────────────────── + +static inline uint32_t streamWord0(uint8_t format, uint16_t status, + uint8_t d2, uint8_t d3) +{ + uint8_t b0 = 0xF0 | ((format & 0x03) << 2) | ((status >> 8) & 0x03); + uint8_t b1 = status & 0xFF; + return packWord(b0, b1, d2, d3); +} + +static inline uint16_t streamStatus(uint32_t w0) +{ + return ((uint16_t)(wireB0(w0) & 0x03) << 8) | wireB1(w0); +} + +static inline uint8_t streamFormat(uint32_t w0) +{ + return (wireB0(w0) >> 2) & 0x03; +} + +// ───────────────────────────────────────────────────────────── +// Device configuration — all fields the host may discover +// ───────────────────────────────────────────────────────────── + +struct UMPStreamConfig { + // UMP protocol version + uint8_t umpVersionMajor = 1; + uint8_t umpVersionMinor = 1; + + // Function blocks + uint8_t numFunctionBlocks = 1; + bool staticFunctionBlocks = true; + + // Protocol capabilities + uint8_t protocolCaps = UMP_PROTO_CAP_MIDI1 | UMP_PROTO_CAP_MIDI2; + + // SysEx Manufacturer ID (3 bytes — 1-byte IDs use 0x00 0x00 ID) + uint8_t manufacturerId[3] = { 0x00, 0x00, 0x7D }; // 0x7D = educational + uint16_t familyId = 0x0001; + uint16_t modelId = 0x0001; + char swRevision[4] = { '0', '1', '0', '0' }; + + // Endpoint strings (UTF-8, null-terminated) + const char* endpointName = "MIDI 2.0 Device"; + const char* productInstanceId = "MIDI2-001"; + + // Function Block 0 + const char* fbName = "Group 0"; + uint8_t fbDirection = UMP_FB_DIR_BIDIRECTIONAL; + uint8_t fbFirstGroup = 0; + uint8_t fbNumGroups = 1; + uint8_t fbUIHint = 0x00; // 0=unknown + uint8_t fbMidi10 = 0x00; // 0=not a MIDI 1.0 stream + uint8_t fbMidiCIVer = 0x00; + uint8_t fbSysEx8 = 0x00; +}; + +// ───────────────────────────────────────────────────────────── +// TX callback — invoked after each response is sent +// ───────────────────────────────────────────────────────────── + +typedef void (*UMPStreamTxCb)(const uint32_t* words, uint8_t nw, + const char* label); + +// ───────────────────────────────────────────────────────────── +// Human-readable label for any Stream message status +// ───────────────────────────────────────────────────────────── + +__attribute__((unused)) +static const char* umpStreamLabel(uint16_t status) +{ + switch (status) { + case UMP_STREAM_STATUS_EP_DISCOVERY: return "EP Discovery"; + case UMP_STREAM_STATUS_EP_INFO: return "EP Info"; + case UMP_STREAM_STATUS_DEVICE_INFO: return "Device ID"; + case UMP_STREAM_STATUS_EP_NAME: return "EP Name"; + case UMP_STREAM_STATUS_PRODUCT_ID: return "Product ID"; + case UMP_STREAM_STATUS_STREAM_CFG_REQ: return "StreamCfg Req"; + case UMP_STREAM_STATUS_STREAM_CFG: return "StreamCfg OK"; + case UMP_STREAM_STATUS_FB_DISCOVERY: return "FB Discovery"; + case UMP_STREAM_STATUS_FB_INFO: return "FB Info"; + case UMP_STREAM_STATUS_FB_NAME: return "FB Name"; + case UMP_STREAM_STATUS_START_CLIP: return "Start Clip"; + case UMP_STREAM_STATUS_END_CLIP: return "End Clip"; + default: return "Stream ???"; + } +} + +// ───────────────────────────────────────────────────────────── +// Internal — send a 4-word Stream message +// ───────────────────────────────────────────────────────────── + +static void _streamSend(uint8_t itf, uint32_t w[4], + UMPStreamTxCb onTx, const char* label) +{ + tud_ump_write(itf, w, 4); + if (onTx) onTx(w, 4, label); +} + +// ───────────────────────────────────────────────────────────── +// Internal — send a UTF-8 string as one or more Stream packets +// ───────────────────────────────────────────────────────────── + +static void _streamSendString(uint8_t itf, uint16_t status, + const char* str, int prefixByte, + UMPStreamTxCb onTx, const char* label) +{ + const size_t len = strlen(str); + const size_t charsPerPkt = (prefixByte >= 0) ? 13 : 14; + const size_t nPkts = (len == 0) ? 1 : (len + charsPerPkt - 1) / charsPerPkt; + + size_t pos = 0; + for (size_t pkt = 0; pkt < nPkts; pkt++) { + + uint8_t fmt; + if (nPkts == 1) fmt = UMP_STREAM_FMT_COMPLETE; + else if (pkt == 0) fmt = UMP_STREAM_FMT_START; + else if (pkt == nPkts - 1) fmt = UMP_STREAM_FMT_END; + else fmt = UMP_STREAM_FMT_CONTINUE; + + uint8_t buf[16]; + memset(buf, 0, sizeof(buf)); + + buf[0] = 0xF0 | ((fmt & 0x03) << 2) | ((status >> 8) & 0x03); + buf[1] = status & 0xFF; + + size_t off = 2; + if (prefixByte >= 0) + buf[off++] = (uint8_t)prefixByte; + + while (off < 16 && pos < len) + buf[off++] = (uint8_t)str[pos++]; + + uint32_t w[4]; + w[0] = packWord(buf[0], buf[1], buf[2], buf[3]); + w[1] = packWord(buf[4], buf[5], buf[6], buf[7]); + w[2] = packWord(buf[8], buf[9], buf[10], buf[11]); + w[3] = packWord(buf[12], buf[13], buf[14], buf[15]); + + _streamSend(itf, w, onTx, label); + } +} + +// ───────────────────────────────────────────────────────────── +// Response generators +// ───────────────────────────────────────────────────────────── + +static void _replyEndpointInfo(uint8_t itf, const UMPStreamConfig& cfg, + UMPStreamTxCb onTx) +{ + uint32_t w[4]; + w[0] = streamWord0(UMP_STREAM_FMT_COMPLETE, UMP_STREAM_STATUS_EP_INFO, + cfg.umpVersionMajor, cfg.umpVersionMinor); + w[1] = packWord( + (cfg.staticFunctionBlocks ? 0x80 : 0x00) | (cfg.numFunctionBlocks & 0x7F), + 0x00, cfg.protocolCaps, 0x00 + ); + w[2] = 0; w[3] = 0; + _streamSend(itf, w, onTx, "EP Info"); +} + +static void _replyDeviceIdentity(uint8_t itf, const UMPStreamConfig& cfg, + UMPStreamTxCb onTx) +{ + uint32_t w[4]; + w[0] = streamWord0(UMP_STREAM_FMT_COMPLETE, UMP_STREAM_STATUS_DEVICE_INFO, + 0x00, 0x00); + w[1] = packWord(0x00, cfg.manufacturerId[0], + cfg.manufacturerId[1], cfg.manufacturerId[2]); + w[2] = packWord((cfg.familyId >> 8) & 0xFF, cfg.familyId & 0xFF, + (cfg.modelId >> 8) & 0xFF, cfg.modelId & 0xFF); + w[3] = packWord(cfg.swRevision[0], cfg.swRevision[1], + cfg.swRevision[2], cfg.swRevision[3]); + _streamSend(itf, w, onTx, "Device ID"); +} + +static void _replyEndpointName(uint8_t itf, const UMPStreamConfig& cfg, + UMPStreamTxCb onTx) +{ + _streamSendString(itf, UMP_STREAM_STATUS_EP_NAME, + cfg.endpointName, -1, onTx, "EP Name"); +} + +static void _replyProductInstanceId(uint8_t itf, const UMPStreamConfig& cfg, + UMPStreamTxCb onTx) +{ + _streamSendString(itf, UMP_STREAM_STATUS_PRODUCT_ID, + cfg.productInstanceId, -1, onTx, "Product ID"); +} + +static void _replyStreamConfig(uint8_t itf, uint8_t requestedProto, + uint8_t jrts, const UMPStreamConfig& cfg, + UMPStreamTxCb onTx) +{ + uint8_t proto = requestedProto; + if (proto == UMP_PROTO_MIDI2 && !(cfg.protocolCaps & UMP_PROTO_CAP_MIDI2)) + proto = UMP_PROTO_MIDI1; + else if (proto == UMP_PROTO_MIDI1 && !(cfg.protocolCaps & UMP_PROTO_CAP_MIDI1)) + proto = UMP_PROTO_MIDI2; + + uint32_t w[4]; + w[0] = streamWord0(UMP_STREAM_FMT_COMPLETE, UMP_STREAM_STATUS_STREAM_CFG, + proto, 0x00); + w[1] = 0; w[2] = 0; w[3] = 0; + _streamSend(itf, w, onTx, "StreamCfg OK"); +} + +static void _replyFBInfo(uint8_t itf, uint8_t fbIdx, + const UMPStreamConfig& cfg, UMPStreamTxCb onTx) +{ + uint32_t w[4]; + w[0] = streamWord0(UMP_STREAM_FMT_COMPLETE, UMP_STREAM_STATUS_FB_INFO, + 0x80 | (fbIdx & 0x7F), 0x00); + w[1] = packWord( + (cfg.fbUIHint << 6) | (cfg.fbMidi10 << 4) | (cfg.fbDirection << 2), + cfg.fbFirstGroup, cfg.fbNumGroups, cfg.fbMidiCIVer + ); + w[2] = packWord(cfg.fbSysEx8, 0x00, 0x00, 0x00); + w[3] = 0; + _streamSend(itf, w, onTx, "FB Info"); +} + +static void _replyFBName(uint8_t itf, uint8_t fbIdx, + const UMPStreamConfig& cfg, UMPStreamTxCb onTx) +{ + _streamSendString(itf, UMP_STREAM_STATUS_FB_NAME, + cfg.fbName, (int)fbIdx, onTx, "FB Name"); +} + +// ───────────────────────────────────────────────────────────── +// Main entry point — process a received MT=0xF Stream message +// ───────────────────────────────────────────────────────────── + +static const char* umpStreamHandleRx(uint8_t itf, const uint32_t* words, + const UMPStreamConfig& cfg, + UMPStreamTxCb onTx) +{ + const uint16_t status = streamStatus(words[0]); + + switch (status) { + + case UMP_STREAM_STATUS_EP_DISCOVERY: { + const uint8_t filter = wireB0(words[1]); + if (filter & UMP_FILTER_EP_INFO) _replyEndpointInfo(itf, cfg, onTx); + if (filter & UMP_FILTER_DEVICE_ID) _replyDeviceIdentity(itf, cfg, onTx); + if (filter & UMP_FILTER_EP_NAME) _replyEndpointName(itf, cfg, onTx); + if (filter & UMP_FILTER_PRODUCT_ID) _replyProductInstanceId(itf, cfg, onTx); + if (filter & UMP_FILTER_STREAM_CFG) _replyStreamConfig(itf, UMP_PROTO_MIDI2, 0x00, cfg, onTx); + return "EP Discovery"; + } + + case UMP_STREAM_STATUS_STREAM_CFG_REQ: { + const uint8_t protocol = wireB2(words[0]); + const uint8_t jrts = wireB3(words[0]); + _replyStreamConfig(itf, protocol, jrts, cfg, onTx); + return "StreamCfg Req"; + } + + case UMP_STREAM_STATUS_FB_DISCOVERY: { + const uint8_t fbNum = wireB2(words[0]); + const uint8_t filter = wireB3(words[0]); + uint8_t first = (fbNum == 0xFF) ? 0 : fbNum; + uint8_t last = (fbNum == 0xFF) ? cfg.numFunctionBlocks : (fbNum + 1); + if (last > cfg.numFunctionBlocks) last = cfg.numFunctionBlocks; + for (uint8_t i = first; i < last; i++) { + if (filter & 0x01) _replyFBInfo(itf, i, cfg, onTx); + if (filter & 0x02) _replyFBName(itf, i, cfg, onTx); + } + return "FB Discovery"; + } + + default: + return nullptr; + } +} + +#endif // UMP_STREAM_HANDLER_H diff --git a/examples/T-PicoC3-MIDI2-PingPong/platformio/tpicoc3_midi2/src/usb_descriptors.cpp b/examples/T-PicoC3-MIDI2-PingPong/platformio/tpicoc3_midi2/src/usb_descriptors.cpp new file mode 100644 index 0000000..2afcca5 --- /dev/null +++ b/examples/T-PicoC3-MIDI2-PingPong/platformio/tpicoc3_midi2/src/usb_descriptors.cpp @@ -0,0 +1,183 @@ +// usb_descriptors.cpp — USB MIDI 2.0 descriptors for tusb_ump (RP2040) +// +// On RP2040 + arduino-pico, tud_descriptor_*_cb() are already defined +// as strong symbols in Adafruit_USBD_Device.cpp (from Adafruit TinyUSB +// Arduino). We therefore do NOT define those callbacks here. +// +// Instead, usb_descriptors_begin() injects our raw 153-byte MIDI 2.0 +// configuration descriptor directly into TinyUSBDevice after it has been +// initialised by the framework (before setup()), and configures the device +// identity strings via the Adafruit API. +// +// Call from setup() BEFORE Serial or any other TinyUSB initialisation. +// +// USB MIDI 2.0 descriptor structure — same as T-Display-S3: +// VID=0x1209, PID=0x0001 +// Alt 0 — MIDI Streaming 1.0 (legacy hosts) +// Alt 1 — MIDI Streaming 2.0 (UMP, modern hosts) +// Same VID/PID and GTB ensure ping-pong compatibility with T-Display-S3. + +#include +#include "tusb.h" +#include "ump.h" +#include + +// ───────────────────────────────────────────────────────────── +// Interface and endpoint numbering +// ───────────────────────────────────────────────────────────── +#define ITF_NUM_AUDIO_CONTROL 0 +#define ITF_NUM_MIDI_STREAMING 1 +#define ITF_NUM_TOTAL 2 + +#define EPNUM_MIDI_OUT 0x01 +#define EPNUM_MIDI_IN 0x81 + +#define EP_SIZE_FS 64 + +// ───────────────────────────────────────────────────────────── +// String descriptor indices +// Adafruit TinyUSBDevice always reserves 0-3: +// 0 = Language ID, 1 = Manufacturer, 2 = Product, 3 = Serial +// addStringDescriptor() appends from index 4 onwards. +// ───────────────────────────────────────────────────────────── +enum { + STR_IDX_LANGID = 0, + STR_IDX_MANUFACTURER = 1, + STR_IDX_PRODUCT = 2, + STR_IDX_SERIAL = 3, + STR_IDX_MIDI_STREAMING = 4, + STR_IDX_BLOCK_1 = 5, +}; + +// ───────────────────────────────────────────────────────────── +// Configuration descriptor (153 bytes) +// +// Config(9) + IAD(8) + ITF0_AC(9) + CS_AC(9) +// + ITF1_Alt0(9) + CS_MS(7) + 2xJackIN(12) + 2xJackOUT(18) +// + 2xEP(18) + 2xCS_EP(10) +// + ITF1_Alt1(9) + CS_MS(7) + 2xEP(18) + 2xCS_EP(10) +// = 153 +// ───────────────────────────────────────────────────────────── + +#define CONFIG_TOTAL_LEN 153 +#define AC_CS_HEADER_LEN 9 + +static const uint8_t desc_configuration[CONFIG_TOTAL_LEN] = { + + // ── Configuration ───────────────────────────────────────── + 9, TUSB_DESC_CONFIGURATION, + U16_TO_U8S_LE(CONFIG_TOTAL_LEN), + ITF_NUM_TOTAL, 1, 0, + TU_BIT(7) | TUSB_DESC_CONFIG_ATT_SELF_POWERED, 50, + + // ── IAD ─────────────────────────────────────────────────── + 8, TUSB_DESC_INTERFACE_ASSOCIATION, + ITF_NUM_AUDIO_CONTROL, 2, + TUSB_CLASS_AUDIO, 0x03, 0x00, 0, + + // ── Interface 0: AudioControl ───────────────────────────── + 9, TUSB_DESC_INTERFACE, + ITF_NUM_AUDIO_CONTROL, 0, 0, + TUSB_CLASS_AUDIO, 0x01, 0x00, 0, + + // ── CS AC Header (UAC2) ─────────────────────────────────── + AC_CS_HEADER_LEN, TUSB_DESC_CS_INTERFACE, 0x01, + U16_TO_U8S_LE(0x0200), 0x03, + U16_TO_U8S_LE(AC_CS_HEADER_LEN), 0x00, + + // ─── Interface 1, Alt 0: MIDI Streaming 1.0 ─────────────── + 9, TUSB_DESC_INTERFACE, + ITF_NUM_MIDI_STREAMING, 0, 2, + TUSB_CLASS_AUDIO, 0x03, 0x00, STR_IDX_MIDI_STREAMING, + + // CS MS Header (bcdMSC=1.00, wTotalLength=37) + 7, TUSB_DESC_CS_INTERFACE, MIDI_1_CS_INTERFACE_HEADER, + U16_TO_U8S_LE(0x0100), U16_TO_U8S_LE(37), + + // Embedded IN Jack (ID=1) + 6, TUSB_DESC_CS_INTERFACE, MIDI_1_CS_INTERFACE_IN_JACK, + MIDI_1_JACK_EMBEDDED, 1, 0, + + // External IN Jack (ID=3) + 6, TUSB_DESC_CS_INTERFACE, MIDI_1_CS_INTERFACE_IN_JACK, + MIDI_1_JACK_EXTERNAL, 3, 0, + + // Embedded OUT Jack (ID=2, src=ExtIN 3) + 9, TUSB_DESC_CS_INTERFACE, MIDI_1_CS_INTERFACE_OUT_JACK, + MIDI_1_JACK_EMBEDDED, 2, 1, 3, 1, 0, + + // External OUT Jack (ID=4, src=EmbIN 1) + 9, TUSB_DESC_CS_INTERFACE, MIDI_1_CS_INTERFACE_OUT_JACK, + MIDI_1_JACK_EXTERNAL, 4, 1, 1, 1, 0, + + // Bulk OUT endpoint Alt0 + 9, TUSB_DESC_ENDPOINT, EPNUM_MIDI_OUT, TUSB_XFER_BULK, + U16_TO_U8S_LE(EP_SIZE_FS), 0, 0, 0, + + // CS Endpoint (assoc Embedded OUT jack 2) + 5, TUSB_DESC_CS_ENDPOINT, MIDI_1_CS_ENDPOINT_GENERAL, 1, 2, + + // Bulk IN endpoint Alt0 + 9, TUSB_DESC_ENDPOINT, EPNUM_MIDI_IN, TUSB_XFER_BULK, + U16_TO_U8S_LE(EP_SIZE_FS), 0, 0, 0, + + // CS Endpoint (assoc Embedded IN jack 1) + 5, TUSB_DESC_CS_ENDPOINT, MIDI_1_CS_ENDPOINT_GENERAL, 1, 1, + + // ─── Interface 1, Alt 1: MIDI Streaming 2.0 (UMP) ───────── + 9, TUSB_DESC_INTERFACE, + ITF_NUM_MIDI_STREAMING, 1, 2, + TUSB_CLASS_AUDIO, 0x03, 0x00, STR_IDX_MIDI_STREAMING, + + // CS MS Header (bcdMSC=2.00, wTotalLength=7) + 7, TUSB_DESC_CS_INTERFACE, MIDI_1_CS_INTERFACE_HEADER, + U16_TO_U8S_LE(0x0200), U16_TO_U8S_LE(7), + + // Bulk OUT endpoint Alt1 + 9, TUSB_DESC_ENDPOINT, EPNUM_MIDI_OUT, TUSB_XFER_BULK, + U16_TO_U8S_LE(EP_SIZE_FS), 0, 0, 0, + + // CS Endpoint MIDI 2.0 (assoc GTB ID=1) + 5, TUSB_DESC_CS_ENDPOINT, MIDI20_CS_ENDPOINT_GENERAL, 1, 1, + + // Bulk IN endpoint Alt1 + 9, TUSB_DESC_ENDPOINT, EPNUM_MIDI_IN, TUSB_XFER_BULK, + U16_TO_U8S_LE(EP_SIZE_FS), 0, 0, 0, + + // CS Endpoint MIDI 2.0 (assoc GTB ID=1) + 5, TUSB_DESC_CS_ENDPOINT, MIDI20_CS_ENDPOINT_GENERAL, 1, 1, +}; + +static_assert(sizeof(desc_configuration) == CONFIG_TOTAL_LEN, + "CONFIG_TOTAL_LEN mismatch"); + +// ───────────────────────────────────────────────────────────── +// RP2040 descriptor setup +// +// TinyUSBDevice.begin() is called by the framework before setup(). +// We call this function at the top of setup() to: +// 1. Set VID/PID and identity strings via the Adafruit API +// 2. Inject our raw 153-byte MIDI 2.0 config descriptor +// +// The persistent _cfg_buf holds the config descriptor for the lifetime +// of the firmware — it must NOT be a local variable. +// +// Note: Adafruit begin() sets bDeviceClass=0xEF/SubClass=2/Protocol=1 +// (IAD) automatically, which is exactly what USB MIDI 2.0 requires. +// ───────────────────────────────────────────────────────────── +static uint8_t _cfg_buf[256]; + +void usb_descriptors_begin(void) { + TinyUSBDevice.setID(0x1209, 0x0001); + TinyUSBDevice.setManufacturerDescriptor("sauloverissimo"); + TinyUSBDevice.setProductDescriptor("RP2040 MIDI 2.0 Device"); + TinyUSBDevice.setSerialDescriptor("RP2040-MIDI2-001"); + TinyUSBDevice.addStringDescriptor("MIDI Streaming"); // index 4 + TinyUSBDevice.addStringDescriptor("Group 0"); // index 5 + + // Replace Adafruit's default config descriptor with our raw MIDI 2.0 one. + // setConfigurationBuffer() points _desc_cfg at _cfg_buf. + // The subsequent memcpy overwrites it with our full 153-byte descriptor. + TinyUSBDevice.setConfigurationBuffer(_cfg_buf, sizeof(_cfg_buf)); + memcpy(_cfg_buf, desc_configuration, CONFIG_TOTAL_LEN); +} diff --git a/examples/T-PicoC3-MIDI2-PingPong/platformio/tpicoc3_midi2/tools/ump_ping_pong.sh b/examples/T-PicoC3-MIDI2-PingPong/platformio/tpicoc3_midi2/tools/ump_ping_pong.sh new file mode 100644 index 0000000..d444e97 --- /dev/null +++ b/examples/T-PicoC3-MIDI2-PingPong/platformio/tpicoc3_midi2/tools/ump_ping_pong.sh @@ -0,0 +1,72 @@ +#!/bin/bash +# ump_ping_pong.sh — Bidirectional UMP message exchange: T-PicoC3 ↔ ESP32-S3 +# +# Creates a visual ping-pong effect on both displays: +# T-PicoC3 → ascending arpeggio (N 60..84) +# ESP32-S3 → descending mirror (N 72..48) +# +# Endpoint IDs are machine-specific. Update EP_C and EP_B if the devices +# change USB port or hub. Run to find current IDs: +# midi enumerate midi-services-endpoints --verbose +# +# Usage: +# bash ump_ping_pong.sh # 20 rounds +# bash ump_ping_pong.sh loop # infinite loop (Ctrl+C to stop) +# +# Copyright (c) 2026 Saulo Verissimo +# SPDX-License-Identifier: MIT + +# ── Endpoint IDs (update if devices change port/hub) ────────────────────────── +EP_C='\\?\swd#midisrv#midiu_ks_404377584258157766_outpin.0_inpin.2#{e7cce071-3c03-423f-88d3-f1045d02552b}' +EP_B='\\?\swd#midisrv#midiu_ks_13319646191189016750_outpin.0_inpin.2#{e7cce071-3c03-423f-88d3-f1045d02552b}' + +# ── Config ──────────────────────────────────────────────────────────────────── +MODE=${1:-20} +NOTE_HOLD=0.20 +NOTE_GAP=0.08 + +NOTES=(60 64 67 72 76 79 84 79 76 72 67 64) +VELS=( 55 70 85 100 110 120 127 120 110 100 85 70) + +# ── Helpers ─────────────────────────────────────────────────────────────────── +send() { midi endpoint "$1" send-message "$2" >/dev/null 2>&1; } + +note_on() { send "$1" "$(printf '0x%02X%02X%02X%02X' 0x20 0x90 "$2" "$3")"; } +note_off() { send "$1" "$(printf '0x%02X%02X%02X%02X' 0x20 0x80 "$2" 0)"; } + +run_round() { + local r=$1 + local idx=$(( r % ${#NOTES[@]} )) + local note=${NOTES[$idx]} + local vel=${VELS[$idx]} + local note_b=$(( 132 - note )) + + note_on "$EP_C" $note $vel & + note_on "$EP_B" $note_b $vel & + wait + + printf "\r [%4d] T-PicoC3→N=%-3d ESP32→N=%-3d V=%-3d" \ + $((r+1)) $note $note_b $vel + + sleep $NOTE_HOLD + + note_off "$EP_C" $note & + note_off "$EP_B" $note_b & + wait + + sleep $NOTE_GAP +} + +# ── Main ────────────────────────────────────────────────────────────────────── +echo "=== UMP Ping-Pong: T-PicoC3 (RP2040) ↔ ESP32-S3 (T-Display-S3) ===" + +if [[ "$MODE" == "loop" ]]; then + echo " Infinite loop — Ctrl+C to stop" + echo "" + r=0 + while true; do run_round $r; (( r++ )); done +else + for (( r=0; r/dev/null 2>&1; } +note_on() { send "$1" "$(printf '0x%02X%02X%02X%02X' 0x20 0x90 "$2" "$3")"; } +note_off() { send "$1" "$(printf '0x%02X%02X%02X%02X' 0x20 0x80 "$2" 0)"; } + +vel_bar() { + local v=$1 + local filled=$(( v * 20 / 127 )) + local bar="" + for (( i=0; i"], + "includeDir": "." + } +} diff --git a/library.properties b/library.properties new file mode 100644 index 0000000..4d14795 --- /dev/null +++ b/library.properties @@ -0,0 +1,10 @@ +name=tusb_ump +version=0.1.0 +author= +maintainer= +sentence=USB MIDI 2.0 UMP device driver for TinyUSB +paragraph=Adds USB MIDI 2.0 device support with automatic MIDI 1.0 backwards compatibility via dual alternate settings. Includes UMP packet processing, SysEx handling, and Group Terminal Block descriptors. +category=Communication +url=https://github.com/midi2-dev/tusb_ump +architectures=esp32,rp2040 +includes=ump_device.h diff --git a/ump.h b/ump.h index 761935b..05235a6 100644 --- a/ump.h +++ b/ump.h @@ -27,8 +27,7 @@ */ /** \ingroup group_class - * \defgroup ClassDriver_CDC Communication Device Class (CDC) - * Currently only Abstract Control Model subclass is supported + * \defgroup ClassDriver_UMP MIDI/UMP Device Class * @{ */ #ifndef _TUSB_UMP_H__ @@ -95,24 +94,24 @@ typedef enum { - MIDI_CS_INTERFACE_HEADER = 0x01, - MIDI_CS_INTERFACE_IN_JACK = 0x02, - MIDI_CS_INTERFACE_OUT_JACK = 0x03, - MIDI_CS_INTERFACE_ELEMENT = 0x04, - MIDI_CS_INTERFACE_GR_TRM_BLOCK = 0x26, -} midi_cs_interface_subtype_t; + MIDI_1_CS_INTERFACE_HEADER = 0x01, + MIDI_1_CS_INTERFACE_IN_JACK = 0x02, + MIDI_1_CS_INTERFACE_OUT_JACK = 0x03, + MIDI_1_CS_INTERFACE_ELEMENT = 0x04, + MIDI_1_CS_INTERFACE_GR_TRM_BLOCK = 0x26, +} midi_1_cs_interface_subtype_t; typedef enum { - MIDI_CS_ENDPOINT_GENERAL = 0x01, + MIDI_1_CS_ENDPOINT_GENERAL = 0x01, MIDI20_CS_ENDPOINT_GENERAL = 0x02, -} midi_cs_endpoint_subtype_t; +} midi_1_cs_endpoint_subtype_t; typedef enum { - MIDI_JACK_EMBEDDED = 0x01, - MIDI_JACK_EXTERNAL = 0x02 -} midi_jack_type_t; + MIDI_1_JACK_EMBEDDED = 0x01, + MIDI_1_JACK_EXTERNAL = 0x02 +} midi_1_jack_type_t; typedef enum { @@ -122,47 +121,47 @@ typedef enum typedef enum { - MIDI_CIN_MISC = 0, - MIDI_CIN_CABLE_EVENT = 1, - MIDI_CIN_SYSCOM_2BYTE = 2, // 2 byte system common message e.g MTC, SongSelect - MIDI_CIN_SYSCOM_3BYTE = 3, // 3 byte system common message e.g SPP - MIDI_CIN_SYSEX_START = 4, // SysEx starts or continue - MIDI_CIN_SYSEX_END_1BYTE = 5, // SysEx ends with 1 data, or 1 byte system common message - MIDI_CIN_SYSEX_END_2BYTE = 6, // SysEx ends with 2 data - MIDI_CIN_SYSEX_END_3BYTE = 7, // SysEx ends with 3 data - MIDI_CIN_NOTE_ON = 8, - MIDI_CIN_NOTE_OFF = 9, - MIDI_CIN_POLY_KEYPRESS = 10, - MIDI_CIN_CONTROL_CHANGE = 11, - MIDI_CIN_PROGRAM_CHANGE = 12, - MIDI_CIN_CHANNEL_PRESSURE = 13, - MIDI_CIN_PITCH_BEND_CHANGE = 14, - MIDI_CIN_1BYTE_DATA = 15 -} midi_code_index_number_t; + MIDI_1_CIN_MISC = 0, + MIDI_1_CIN_CABLE_EVENT = 1, + MIDI_1_CIN_SYSCOM_2BYTE = 2, // 2 byte system common message e.g MTC, SongSelect + MIDI_1_CIN_SYSCOM_3BYTE = 3, // 3 byte system common message e.g SPP + MIDI_1_CIN_SYSEX_START = 4, // SysEx starts or continue + MIDI_1_CIN_SYSEX_END_1BYTE = 5, // SysEx ends with 1 data, or 1 byte system common message + MIDI_1_CIN_SYSEX_END_2BYTE = 6, // SysEx ends with 2 data + MIDI_1_CIN_SYSEX_END_3BYTE = 7, // SysEx ends with 3 data + MIDI_1_CIN_NOTE_ON = 8, + MIDI_1_CIN_NOTE_OFF = 9, + MIDI_1_CIN_POLY_KEYPRESS = 10, + MIDI_1_CIN_CONTROL_CHANGE = 11, + MIDI_1_CIN_PROGRAM_CHANGE = 12, + MIDI_1_CIN_CHANNEL_PRESSURE = 13, + MIDI_1_CIN_PITCH_BEND_CHANGE = 14, + MIDI_1_CIN_1BYTE_DATA = 15 +} midi_1_code_index_number_t; // MIDI 1.0 status byte enum { //------------- System Exclusive -------------// - MIDI_STATUS_SYSEX_START = 0xF0, - MIDI_STATUS_SYSEX_END = 0xF7, + MIDI_1_STATUS_SYSEX_START = 0xF0, + MIDI_1_STATUS_SYSEX_END = 0xF7, //------------- System Common -------------// - MIDI_STATUS_SYSCOM_TIME_CODE_QUARTER_FRAME = 0xF1, - MIDI_STATUS_SYSCOM_SONG_POSITION_POINTER = 0xF2, - MIDI_STATUS_SYSCOM_SONG_SELECT = 0xF3, + MIDI_1_STATUS_SYSCOM_TIME_CODE_QUARTER_FRAME = 0xF1, + MIDI_1_STATUS_SYSCOM_SONG_POSITION_POINTER = 0xF2, + MIDI_1_STATUS_SYSCOM_SONG_SELECT = 0xF3, // F4, F5 is undefined - MIDI_STATUS_SYSCOM_TUNE_REQUEST = 0xF6, + MIDI_1_STATUS_SYSCOM_TUNE_REQUEST = 0xF6, //------------- System RealTime -------------// - MIDI_STATUS_SYSREAL_TIMING_CLOCK = 0xF8, + MIDI_1_STATUS_SYSREAL_TIMING_CLOCK = 0xF8, // 0xF9 is undefined - MIDI_STATUS_SYSREAL_START = 0xFA, - MIDI_STATUS_SYSREAL_CONTINUE = 0xFB, - MIDI_STATUS_SYSREAL_STOP = 0xFC, + MIDI_1_STATUS_SYSREAL_START = 0xFA, + MIDI_1_STATUS_SYSREAL_CONTINUE = 0xFB, + MIDI_1_STATUS_SYSREAL_STOP = 0xFC, // 0xFD is undefined - MIDI_STATUS_SYSREAL_ACTIVE_SENSING = 0xFE, - MIDI_STATUS_SYSREAL_SYSTEM_RESET = 0xFF, + MIDI_1_STATUS_SYSREAL_ACTIVE_SENSING = 0xFE, + MIDI_1_STATUS_SYSREAL_SYSTEM_RESET = 0xFF, }; /// MIDI Interface Header Descriptor @@ -173,7 +172,7 @@ typedef struct TU_ATTR_PACKED uint8_t bDescriptorSubType ; ///< Descriptor SubType uint16_t bcdMSC ; ///< MidiStreaming SubClass release number in Binary-Coded Decimal uint16_t wTotalLength ; -} midi_desc_header_t; +} midi_1_desc_header_t; /// MIDI In Jack Descriptor typedef struct TU_ATTR_PACKED @@ -184,7 +183,7 @@ typedef struct TU_ATTR_PACKED uint8_t bJackType ; ///< Embedded or External uint8_t bJackID ; ///< Unique ID for MIDI IN Jack uint8_t iJack ; ///< string descriptor -} midi_desc_in_jack_t; +} midi_1_desc_in_jack_t; /// MIDI Out Jack Descriptor with single pin @@ -201,7 +200,7 @@ typedef struct TU_ATTR_PACKED uint8_t baSourcePin; uint8_t iJack ; ///< string descriptor -} midi_desc_out_jack_t ; +} midi_1_desc_out_jack_t ; /// MIDI Out Jack Descriptor with multiple pins #define midi_desc_out_jack_n_t(input_num) \ @@ -238,7 +237,7 @@ typedef struct TU_ATTR_PACKED uint16_t bmElementCaps; uint8_t iElement; -} midi_desc_element_t; +} midi_1_desc_element_t; /// MIDI Element Descriptor with multiple pins #define midi_desc_element_n_t(input_num) \ @@ -285,7 +284,7 @@ typedef struct TU_ATTR_PACKED typedef struct TU_ATTR_PACKED { uint8_t bLength ; ///< Size of this descriptor in bytes: 5 - uint8_t bDescriptorType ; ///< Descriptor Type: MIDI_CS_INTERFACE_GR_TRM_BLOCK + uint8_t bDescriptorType ; ///< Descriptor Type: MIDI_1_CS_INTERFACE_GR_TRM_BLOCK uint8_t bDescriptorSubType ; ///< Descriptor SubType: MIDI_GR_TRM_BLOCK_HEADER uint16_t wTotalLength ; ///< Total number of bytes returned for the class-specific Group Terminal Block descriptors. Includes the combined length of this header descriptor and all Group Terminal Block descriptors. } midi2_desc_group_terminal_block_header_t; @@ -294,7 +293,7 @@ typedef struct TU_ATTR_PACKED typedef struct TU_ATTR_PACKED { uint8_t bLength ; ///< Size of this descriptor in bytes: 13 - uint8_t bDescriptorType ; ///< Descriptor Type: MIDI_CS_INTERFACE_GR_TRM_BLOCK + uint8_t bDescriptorType ; ///< Descriptor Type: MIDI_1_CS_INTERFACE_GR_TRM_BLOCK uint8_t bDescriptorSubType ; ///< Descriptor SubType: MIDI_GR_TRM_BLOCK uint8_t bGrpTrmBlkID ; ///< ID of this Group Terminal Block uint8_t bGrpTrmBlkType ; ///< Group Terminal Block Type diff --git a/ump_device.cpp b/ump_device.cpp index cd84be5..58792a2 100644 --- a/ump_device.cpp +++ b/ump_device.cpp @@ -60,8 +60,47 @@ #include "device/usbd.h" #include "device/usbd_pvt.h" +#include "tusb.h" #include "ump_device.h" +//--------------------------------------------------------------------+ +// TinyUSB API compatibility +// +// The ESP-IDF TinyUSB fork (shipped by arduino-esp32 3.x) is versioned +// 0.18 (TUSB_VERSION_NUMBER = 1800) but retains the PRE-0.16 upstream +// signatures: +// usbd_edpt_xfer — 4 args, no is_isr +// tu_fifo_config — 5 args, with item_size +// +// The upstream TinyUSB 0.16 briefly added `is_isr` and removed +// `item_size`, then 0.17+ reverted `is_isr`. The IDF fork never +// adopted those transient changes. +// +// Detection: sdkconfig.h is always present in ESP-IDF builds and +// never in standalone TinyUSB — use it to identify the IDF fork. +//--------------------------------------------------------------------+ +#if defined(TUSB_VERSION_NUMBER) && TUSB_VERSION_NUMBER >= 1600 && \ + TUSB_VERSION_NUMBER < 1700 && !__has_include() + // Upstream TinyUSB 0.16 only (transient: added is_isr, removed item_size) + #define _usbd_edpt_xfer(rh, ep, buf, len) \ + usbd_edpt_xfer((rh), (ep), (buf), (len), false) + #define _tu_fifo_cfg(f, buf, depth, item_sz, ow) \ + tu_fifo_config((f), (buf), (depth), (ow)) +#else + // ESP-IDF TinyUSB fork OR upstream TinyUSB < 0.16 / >= 0.17 + #define _usbd_edpt_xfer(rh, ep, buf, len) \ + usbd_edpt_xfer((rh), (ep), (buf), (len)) + #define _tu_fifo_cfg(f, buf, depth, item_sz, ow) \ + tu_fifo_config((f), (buf), (depth), (item_sz), (ow)) +#endif + +// TUD_OPT_RHPORT: defined by the old CFG_TUSB_RHPORT*_MODE config style. +// TinyUSB 0.18+ on platforms like RP2040 use CFG_TUD_ENABLED instead, +// leaving TUD_OPT_RHPORT undefined. Single-port USB hardware is always port 0. +#ifndef TUD_OPT_RHPORT + #define TUD_OPT_RHPORT 0 +#endif + //--------------------------------------------------------------------+ // APP SPECIFIC DRIVERS //--------------------------------------------------------------------+ @@ -167,13 +206,13 @@ static uint8_t default_ump_group_terminal_blk_desc[] = { // header 5, // bLength - MIDI_CS_INTERFACE_GR_TRM_BLOCK, + MIDI_1_CS_INTERFACE_GR_TRM_BLOCK, MIDI_GR_TRM_BLOCK_HEADER, U16_TO_U8S_LE(sizeof(midi2_cs_interface_desc_group_terminal_blocks_t)), // wTotalLength // block 13, // bLength - MIDI_CS_INTERFACE_GR_TRM_BLOCK, + MIDI_1_CS_INTERFACE_GR_TRM_BLOCK, MIDI_GR_TRM_BLOCK, 1, // bGrpTrmBlkID 0x00, // bGrpTrmBlkType: bi-directional @@ -220,7 +259,7 @@ static void _prep_out_transaction (umpd_interface_t* p_ump) available = tu_fifo_remaining(&p_ump->rx_ff); if ( available >= sizeof(p_ump->epout_buf) ) { - usbd_edpt_xfer(rhport, p_ump->ep_out, p_ump->epout_buf, sizeof(p_ump->epout_buf)); + _usbd_edpt_xfer(rhport, p_ump->ep_out, p_ump->epout_buf, sizeof(p_ump->epout_buf)); }else { // Release endpoint since we don't make any transfer @@ -345,7 +384,7 @@ static uint32_t write_flush(umpd_interface_t* ump) if (count) { - TU_ASSERT( usbd_edpt_xfer(rhport, ump->ep_in, ump->epin_buf, count), 0 ); + TU_ASSERT( _usbd_edpt_xfer(rhport, ump->ep_in, ump->epin_buf, count), 0 ); return count; }else { @@ -466,16 +505,16 @@ uint16_t tud_ump_write( uint8_t itf, uint32_t *words, uint16_t numWords ) case UMP_SYSTEM_UNDEFINED_F5: case UMP_SYSTEM_UNDEFINED_F9: case UMP_SYSTEM_UNDEFINED_FD: - umpWritePacket.umpData.umpBytes[0] = (cbl_num << 4) | MIDI_CIN_SYSEX_END_1BYTE; + umpWritePacket.umpData.umpBytes[0] = (cbl_num << 4) | MIDI_1_CIN_SYSEX_END_1BYTE; break; case UMP_SYSTEM_MTC: case UMP_SYSTEM_SONG_SELECT: - umpWritePacket.umpData.umpBytes[0] = (cbl_num << 4) | MIDI_CIN_SYSCOM_2BYTE; + umpWritePacket.umpData.umpBytes[0] = (cbl_num << 4) | MIDI_1_CIN_SYSCOM_2BYTE; break; case UMP_SYSTEM_SONG_POS_PTR: - umpWritePacket.umpData.umpBytes[0] = (cbl_num << 4) | MIDI_CIN_SYSCOM_3BYTE; + umpWritePacket.umpData.umpBytes[0] = (cbl_num << 4) | MIDI_1_CIN_SYSCOM_3BYTE; break; default: @@ -543,7 +582,7 @@ uint16_t tud_ump_write( uint8_t itf, uint32_t *words, uint16_t numWords ) if (sysexStatus <= 1 && numberBytes < SYSEX_BS_RB_SIZE) { - byteStream[numberBytes++] = MIDI_STATUS_SYSEX_START; + byteStream[numberBytes++] = MIDI_1_STATUS_SYSEX_START; } for (uint8_t count = 0; count < (umpPacket.umpData.umpBytes[1] & 0xf); count++) { @@ -554,7 +593,7 @@ uint16_t tud_ump_write( uint8_t itf, uint32_t *words, uint16_t numWords ) } if ((sysexStatus == 0 || sysexStatus == 3) && numberBytes < SYSEX_BS_RB_SIZE) { - byteStream[numberBytes++] = MIDI_STATUS_SYSEX_END; + byteStream[numberBytes++] = MIDI_1_STATUS_SYSEX_END; } // Move into sysex circular buffer queue @@ -587,11 +626,11 @@ uint16_t tud_ump_write( uint8_t itf, uint32_t *words, uint16_t numWords ) // Mark cable number and CIN for start / continue SYSEX in USB MIDI 1.0 format if (bEndSysex && !numberBytes) { - pumpBytes[0] = (uint8_t)(cbl_num << 4) | MIDI_CIN_SYSEX_END_3BYTE; + pumpBytes[0] = (uint8_t)(cbl_num << 4) | MIDI_1_CIN_SYSEX_END_3BYTE; } else { - pumpBytes[0] = (uint8_t)(cbl_num << 4) | MIDI_CIN_SYSEX_START; + pumpBytes[0] = (uint8_t)(cbl_num << 4) | MIDI_1_CIN_SYSEX_START; } } else @@ -613,12 +652,12 @@ uint16_t tud_ump_write( uint8_t itf, uint32_t *words, uint16_t numWords ) switch (count) { case 1: - pumpBytes[0] = (uint8_t)(cbl_num << 4) | MIDI_CIN_SYSEX_END_1BYTE; + pumpBytes[0] = (uint8_t)(cbl_num << 4) | MIDI_1_CIN_SYSEX_END_1BYTE; break; case 2: default: - pumpBytes[0] = (uint8_t)(cbl_num << 4) | MIDI_CIN_SYSEX_END_2BYTE; + pumpBytes[0] = (uint8_t)(cbl_num << 4) | MIDI_1_CIN_SYSEX_END_2BYTE; break; } } @@ -675,8 +714,8 @@ void umpd_init(void) umpd_interface_t* ump = &_umpd_itf[i]; // config fifo - tu_fifo_config(&ump->rx_ff, ump->rx_ff_buf, CFG_TUD_UMP_RX_BUFSIZE, 1, false); - tu_fifo_config(&ump->tx_ff, ump->tx_ff_buf, CFG_TUD_UMP_TX_BUFSIZE, 1, false); + _tu_fifo_cfg(&ump->rx_ff, ump->rx_ff_buf, CFG_TUD_UMP_RX_BUFSIZE, 1, false); + _tu_fifo_cfg(&ump->tx_ff, ump->tx_ff_buf, CFG_TUD_UMP_TX_BUFSIZE, 1, false); // Default select the first interface ump->ump_interface_selected = 0; @@ -896,7 +935,7 @@ bool umpd_xfer_cb(uint8_t rhport, uint8_t ep_addr, xfer_result_t result, uint32_ { if ( usbd_edpt_claim(rhport, p_ump->ep_in) ) { - usbd_edpt_xfer(rhport, p_ump->ep_in, NULL, 0); + _usbd_edpt_xfer(rhport, p_ump->ep_in, NULL, 0); } } } @@ -979,7 +1018,7 @@ Return Value: uint8_t code_index = pBuffer[0] & 0x0f; // Handle special case of single byte data - if (code_index == MIDI_CIN_1BYTE_DATA && (pBuffer[1] & 0x80)) + if (code_index == MIDI_1_CIN_1BYTE_DATA && (pBuffer[1] & 0x80)) { switch (pBuffer[1]) { @@ -994,7 +1033,7 @@ Return Value: case UMP_SYSTEM_UNDEFINED_F5: case UMP_SYSTEM_UNDEFINED_F9: case UMP_SYSTEM_UNDEFINED_FD: - code_index = MIDI_CIN_SYSEX_END_1BYTE; + code_index = MIDI_1_CIN_SYSEX_END_1BYTE; break; default: @@ -1008,17 +1047,17 @@ Return Value: switch (code_index) { - case MIDI_CIN_SYSEX_START: // or continue + case MIDI_1_CIN_SYSEX_START: // or continue if (!*pbIsInSysex) { // SYSEX Start means first byte should be SYSEX start - if (pBuffer[1] != MIDI_STATUS_SYSEX_START) return false; + if (pBuffer[1] != MIDI_1_STATUS_SYSEX_START) return false; firstByte = 2; lastByte = 4; // As this is start of SYSEX, need to set status to indicate so and copy 2 bytes of data - // as first byte of MIDI_STATUS_SYSEX_START + // as first byte of MIDI_1_STATUS_SYSEX_START umpPkt->umpData.umpBytes[1] = UMP_SYSEX7_START | 2; // Set that in SYSEX @@ -1046,10 +1085,10 @@ Return Value: } break; - case MIDI_CIN_SYSEX_END_1BYTE: // or single byte System Common + case MIDI_1_CIN_SYSEX_END_1BYTE: // or single byte System Common // Determine if a system common if ( (pBuffer[1] & 0x80) // most significant bit set and not sysex ending - && (pBuffer[1] != MIDI_STATUS_SYSEX_END)) + && (pBuffer[1] != MIDI_1_STATUS_SYSEX_END)) { umpPkt->umpData.umpBytes[0] = UMP_MT_SYSTEM | cbl_num; umpPkt->umpData.umpBytes[1] = pBuffer[1]; @@ -1063,7 +1102,7 @@ Return Value: // Determine if complete based on if currently in SYSEX if (*pbIsInSysex) { - if (pBuffer[1] != MIDI_STATUS_SYSEX_END) return false; + if (pBuffer[1] != MIDI_1_STATUS_SYSEX_END) return false; umpPkt->umpData.umpBytes[1] = UMP_SYSEX7_END | 0; *pbIsInSysex = false; // we are done with SYSEX firstByte = 1; @@ -1086,13 +1125,13 @@ Return Value: } break; - case MIDI_CIN_SYSEX_END_2BYTE: + case MIDI_1_CIN_SYSEX_END_2BYTE: umpPkt->umpData.umpBytes[0] = UMP_MT_DATA_64 | cbl_num; // Determine if complete based on if currently in SYSEX if (*pbIsInSysex) { - if (pBuffer[2] != MIDI_STATUS_SYSEX_END) return false; + if (pBuffer[2] != MIDI_1_STATUS_SYSEX_END) return false; umpPkt->umpData.umpBytes[1] = UMP_SYSEX7_END | 1; *pbIsInSysex = false; // we are done with SYSEX firstByte = 1; @@ -1116,13 +1155,13 @@ Return Value: } break; - case MIDI_CIN_SYSEX_END_3BYTE: + case MIDI_1_CIN_SYSEX_END_3BYTE: umpPkt->umpData.umpBytes[0] = UMP_MT_DATA_64 | cbl_num; // Determine if complete based on if currently in SYSEX if (*pbIsInSysex) { - if (pBuffer[3] != MIDI_STATUS_SYSEX_END) return false; + if (pBuffer[3] != MIDI_1_STATUS_SYSEX_END) return false; umpPkt->umpData.umpBytes[1] = UMP_SYSEX7_END | 2; *pbIsInSysex = false; // we are done with SYSEX firstByte = 1; @@ -1130,7 +1169,7 @@ Return Value: } else { - if (pBuffer[1] != MIDI_STATUS_SYSEX_START || pBuffer[3] != MIDI_STATUS_SYSEX_END) return false; + if (pBuffer[1] != MIDI_1_STATUS_SYSEX_START || pBuffer[3] != MIDI_1_STATUS_SYSEX_END) return false; umpPkt->umpData.umpBytes[1] = UMP_SYSEX7_COMPLETE | 1; *pbIsInSysex = false; // we are done with SYSEX firstByte = 2; @@ -1148,13 +1187,13 @@ Return Value: break; // MIDI1 Channel Voice Messages - case MIDI_CIN_NOTE_ON: - case MIDI_CIN_NOTE_OFF: - case MIDI_CIN_POLY_KEYPRESS: - case MIDI_CIN_CONTROL_CHANGE: - case MIDI_CIN_PROGRAM_CHANGE: - case MIDI_CIN_CHANNEL_PRESSURE: - case MIDI_CIN_PITCH_BEND_CHANGE: + case MIDI_1_CIN_NOTE_ON: + case MIDI_1_CIN_NOTE_OFF: + case MIDI_1_CIN_POLY_KEYPRESS: + case MIDI_1_CIN_CONTROL_CHANGE: + case MIDI_1_CIN_PROGRAM_CHANGE: + case MIDI_1_CIN_CHANNEL_PRESSURE: + case MIDI_1_CIN_PITCH_BEND_CHANGE: umpPkt->umpData.umpBytes[0] = UMP_MT_MIDI1_CV | cbl_num; // message type 2 *pbIsInSysex = false; // ensure we end any current sysex packets, other layers need to handle error @@ -1167,8 +1206,8 @@ Return Value: umpPkt->wordCount = 1; break; - case MIDI_CIN_SYSCOM_2BYTE: - case MIDI_CIN_SYSCOM_3BYTE: + case MIDI_1_CIN_SYSCOM_2BYTE: + case MIDI_1_CIN_SYSCOM_3BYTE: umpPkt->umpData.umpBytes[0] = UMP_MT_SYSTEM | cbl_num; for (int count = 1; count < 4; count++) { @@ -1177,8 +1216,8 @@ Return Value: umpPkt->wordCount = 1; break; - case MIDI_CIN_MISC: - case MIDI_CIN_CABLE_EVENT: + case MIDI_1_CIN_MISC: + case MIDI_1_CIN_CABLE_EVENT: // These are reserved for future use and will not be translated, drop data with no processing default: // Not valid USB MIDI 1.0 transfer or NULL, skip