MIDI over physical DIN5

This commit is contained in:
Dejvino 2026-02-16 01:48:03 +01:00
parent fcd4396d4a
commit 4c6a4bfbc1
2 changed files with 18 additions and 331 deletions

View File

@ -1,6 +1,6 @@
# RP2040 MIDI Tracker # RP2040 MIDI Tracker
A simple MIDI step sequencer using a Raspberry Pi Pico W, an OLED display, a rotary encoder, and an 8x8 NeoPixel matrix. A simple MIDI step sequencer using a Raspberry Pi Pico (or Pico W), an OLED display, a rotary encoder, and an 8x8 NeoPixel matrix.
## Software Requirements ## Software Requirements
@ -8,10 +8,9 @@ A simple MIDI step sequencer using a Raspberry Pi Pico W, an OLED display, a rot
1. Add this URL to your Additional Boards Manager URLs in Preferences: 1. Add this URL to your Additional Boards Manager URLs in Preferences:
`https://github.com/earlephilhower/arduino-pico/releases/download/global/package_rp2040_index.json` `https://github.com/earlephilhower/arduino-pico/releases/download/global/package_rp2040_index.json`
2. Go to **Tools > Board > Boards Manager**, search for **"pico"**, and install **Raspberry Pi Pico/RP2040**. 2. Go to **Tools > Board > Boards Manager**, search for **"pico"**, and install **Raspberry Pi Pico/RP2040**.
3. Select **Raspberry Pi Pico W** from the board menu. 3. Select **Raspberry Pi Pico** (or **Pico W**) from the board menu.
* **Libraries**: Install via Library Manager: * **Libraries**: Install via Library Manager:
* `Adafruit GFX Library`, `Adafruit SSD1306`, `Adafruit NeoPixel` * `Adafruit GFX Library`, `Adafruit SSD1306`, `Adafruit NeoPixel`
* *(Note: `ArduinoBLE` is NO LONGER required. We use the built-in BTStack)*
## Connections & Wiring ## Connections & Wiring
@ -47,6 +46,11 @@ Make sure to establish a **common ground** by connecting the ground from your ex
| 5V / VCC | External 5V Supply `+` | **External 5V Power** | | 5V / VCC | External 5V Supply `+` | **External 5V Power** |
| GND | External 5V Supply `-` | **External Power Ground** | | GND | External 5V Supply `-` | **External Power Ground** |
| | GND (Pin 18) | **Common Ground with Pico** | | | GND (Pin 18) | **Common Ground with Pico** |
| **MIDI DIN (Serial)** | | |
| TX (MIDI OUT) | GP0 (Pin 1) | To DIN Pin 5 (via 220Ω) |
**MIDI Hardware Note**:
* **MIDI OUT**: Connect **GP0** to DIN Pin 5 via a 220Ω resistor. Connect DIN Pin 4 to +5V via a 220Ω resistor. Connect DIN Pin 2 to GND.
Once everything is wired up, you can upload the code and your tracker should be ready to go! Once everything is wired up, you can upload the code and your tracker should be ready to go!
@ -87,4 +91,4 @@ For a more integrated build that can fit inside an enclosure, you can use a Lith
* **Navigation**: Rotate the encoder to move between steps. * **Navigation**: Rotate the encoder to move between steps.
* **Edit Mode**: Short press the encoder button to toggle Edit Mode. Rotate to change the note. * **Edit Mode**: Short press the encoder button to toggle Edit Mode. Rotate to change the note.
* **Playback**: Long press the encoder button (> 0.6s) to Start/Stop playback. * **Playback**: Long press the encoder button (> 0.6s) to Start/Stop playback.
* **Bluetooth**: The device advertises as "RP2040 Tracker". Connect to it from your computer or MIDI Bluetooth dongle. * **MIDI**: The device sends MIDI notes via the Hardware Serial (DIN) port on GP0.

View File

@ -4,18 +4,6 @@
#include <Adafruit_SSD1306.h> #include <Adafruit_SSD1306.h>
#include <Adafruit_NeoPixel.h> #include <Adafruit_NeoPixel.h>
// --- BLUETOOTH CONFIGURATION (Earle Philhower Core) ---
#if defined(ARDUINO_ARCH_RP2040) && defined(ARDUINO_RASPBERRY_PI_PICO_W)
#define ENABLE_BTSTACK
#include <btstack.h>
// BTStack System Locks (provided by the core)
extern "C" {
void __lockBluetooth();
void __unlockBluetooth();
}
#endif
// --- HARDWARE CONFIGURATION --- // --- HARDWARE CONFIGURATION ---
#define SCREEN_WIDTH 128 #define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64 #define SCREEN_HEIGHT 64
@ -30,6 +18,9 @@
#define ENC_DT 13 #define ENC_DT 13
#define ENC_SW 14 #define ENC_SW 14
// MIDI UART Pins (GP0/GP1) -- OUT only so far
#define PIN_MIDI_TX 0
// NeoPixel Pin (any GPIO is fine, I've chosen 16) // NeoPixel Pin (any GPIO is fine, I've chosen 16)
#define PIN_NEOPIXEL 16 #define PIN_NEOPIXEL 16
#define NUM_PIXELS 64 // For 8x8 WS2812B matrix #define NUM_PIXELS 64 // For 8x8 WS2812B matrix
@ -47,44 +38,6 @@ Step sequence[NUM_STEPS];
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET); Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
Adafruit_NeoPixel pixels(NUM_PIXELS, PIN_NEOPIXEL, NEO_GRB + NEO_KHZ800); Adafruit_NeoPixel pixels(NUM_PIXELS, PIN_NEOPIXEL, NEO_GRB + NEO_KHZ800);
#ifdef ENABLE_BTSTACK
// BLE State
#define TARGET_DEVICE_NAME "WIDI Thru6"
static hci_con_handle_t con_handle = HCI_CON_HANDLE_INVALID;
static uint16_t midi_char_value_handle = 0; // Handle to write to on remote device
static btstack_packet_callback_registration_t hci_event_callback_registration;
bool wasConnected = false;
// UUIDs (Big Endian to match stack return values)
// Service: 03B80E5A-EDE8-4B33-A751-6CE34EC4C700
static const uint8_t midi_service_uuid[] = {0x03, 0xB8, 0x0E, 0x5A, 0xED, 0xE8, 0x4B, 0x33, 0xA7, 0x51, 0x6C, 0xE3, 0x4E, 0xC4, 0xC7, 0x00};
// Char: 7772E5DB-3868-4112-A1A9-F2669D106BF3 (Little Endian for packet search)
static const uint8_t midi_char_uuid[] = {0xF3, 0x6B, 0x10, 0x9D, 0x66, 0xF2, 0xA9, 0xA1, 0x12, 0x41, 0x68, 0x38, 0xDB, 0xE5, 0x72, 0x77};
// Char: 7772E5DB-3868-4112-A1A9-F2669D106BF3 (Big Endian fallback)
static const uint8_t midi_char_uuid_be[] = {0x77, 0x72, 0xE5, 0xDB, 0x38, 0x68, 0x41, 0x12, 0xA1, 0xA9, 0xF2, 0x66, 0x9D, 0x10, 0x6B, 0xF3};
static gatt_client_service_t found_service;
static gatt_client_service_t target_service;
static gatt_client_characteristic_t target_characteristic;
static uint16_t midi_cccd_handle = 0;
enum DiscoveryState {
DISCOVERY_IDLE,
DISCOVERY_FINDING_SERVICE,
DISCOVERY_FINDING_CHARACTERISTIC,
DISCOVERY_FINDING_CCCD,
DISCOVERY_ENABLING_NOTIFICATIONS,
DISCOVERY_COMPLETE
};
static DiscoveryState discovery_state = DISCOVERY_IDLE;
static bool service_found = false;
static bool midi_ready = false;
// Uncomment to use Write Request (slower, reliable) instead of Write Command (faster, fire-and-forget)
// #define BLE_USE_WRITE_RESPONSE
#endif
int currentStep = 0; int currentStep = 0;
bool isEditing = false; bool isEditing = false;
int scrollOffset = 0; int scrollOffset = 0;
@ -105,18 +58,6 @@ bool buttonActive = false;
bool buttonConsumed = false; bool buttonConsumed = false;
unsigned long buttonPressTime = 0; unsigned long buttonPressTime = 0;
// Bluetooth Icon (8x8)
const unsigned char bluetooth_icon[] PROGMEM = {
0x10, // ...1....
0x18, // ...11...
0x14, // ...1.1..
0x52, // .1.1..1.
0x38, // ..111...
0x52, // .1.1..1.
0x14, // ...1.1..
0x18 // ...11...
};
// --- ENCODER INTERRUPT --- // --- ENCODER INTERRUPT ---
// Robust Rotary Encoder reading // Robust Rotary Encoder reading
void readEncoder() { void readEncoder() {
@ -136,215 +77,6 @@ void readEncoder() {
} }
} }
#ifdef ENABLE_BTSTACK
// GATT Client Event Handler
void handle_gatt_client_event(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size) {
switch(hci_event_packet_get_type(packet)) {
case GATT_EVENT_SERVICE_QUERY_RESULT:
gatt_event_service_query_result_get_service(packet, &found_service);
Serial.print(F("Svc UUID: "));
if (found_service.uuid16) {
Serial.println(found_service.uuid16, HEX);
} else {
for(int i=0; i<16; i++) { Serial.print(found_service.uuid128[i], HEX); Serial.print(" "); }
Serial.println();
if (memcmp(found_service.uuid128, midi_service_uuid, 16) == 0) {
target_service = found_service;
service_found = true;
}
}
break;
case GATT_EVENT_CHARACTERISTIC_QUERY_RESULT:
{
Serial.print(F("Char Packet: "));
for(int i=0; i<size; i++) {
Serial.print(packet[i], HEX); Serial.print(" ");
}
Serial.println();
// Scan packet for the MIDI Characteristic UUID
// The packet structure varies, so we search for the 128-bit UUID sequence.
for(int i=0; i <= size - 16; i++) {
if (memcmp(&packet[i], midi_char_uuid, 16) == 0 || memcmp(&packet[i], midi_char_uuid_be, 16) == 0) {
Serial.println(F("Found MIDI Char UUID!"));
// The Value Handle is typically located 2 bytes before the UUID (in this specific event format)
midi_char_value_handle = little_endian_read_16(packet, i - 2);
Serial.print(F("Handle: 0x")); Serial.println(midi_char_value_handle, HEX);
// Capture the characteristic struct ONLY if this is the correct one
gatt_event_characteristic_query_result_get_characteristic(packet, &target_characteristic);
}
}
}
break;
case GATT_EVENT_ALL_CHARACTERISTIC_DESCRIPTORS_QUERY_RESULT:
{
Serial.print(F("Desc Packet: "));
for(int i=0; i<size; i++) {
Serial.print(packet[i], HEX); Serial.print(" ");
}
Serial.println();
uint16_t handle = little_endian_read_16(packet, 8);
const uint8_t *uuid = &packet[10];
Serial.print(F("Desc Handle: 0x")); Serial.print(handle, HEX);
// Check for CCCD (0x2902) in standard 128-bit base UUID (Little Endian)
uint8_t cccd_uuid128[] = {0xFB, 0x34, 0x9B, 0x5F, 0x80, 0x00, 0x00, 0x80, 0x00, 0x10, 0x00, 0x00, 0x02, 0x29, 0x00, 0x00};
if (memcmp(uuid, cccd_uuid128, 16) == 0) {
midi_cccd_handle = handle;
Serial.println(F(" -> Found CCCD!"));
} else {
Serial.println();
}
}
break;
case GATT_EVENT_NOTIFICATION:
{
uint16_t value_length = gatt_event_notification_get_value_length(packet);
const uint8_t *value = gatt_event_notification_get_value(packet);
Serial.print(F("RX MIDI: "));
for (int i = 0; i < value_length; i++) {
Serial.print(value[i], HEX); Serial.print(" ");
}
Serial.println();
}
break;
case GATT_EVENT_QUERY_COMPLETE:
if (discovery_state == DISCOVERY_FINDING_SERVICE) {
if (service_found) {
Serial.println(F("Service found, searching for characteristic..."));
discovery_state = DISCOVERY_FINDING_CHARACTERISTIC;
// Discover ALL characteristics to avoid endianness issues in filters
gatt_client_discover_characteristics_for_service(handle_gatt_client_event, con_handle, &target_service);
} else {
Serial.println(F("MIDI Service not found on device."));
discovery_state = DISCOVERY_IDLE;
}
} else if (discovery_state == DISCOVERY_FINDING_CHARACTERISTIC) {
if (midi_char_value_handle != 0) {
Serial.println(F("MIDI Char found. Searching for CCCD..."));
discovery_state = DISCOVERY_FINDING_CCCD;
gatt_client_discover_characteristic_descriptors(handle_gatt_client_event, con_handle, &target_characteristic);
} else {
Serial.println(F("MIDI Characteristic not found."));
discovery_state = DISCOVERY_IDLE;
}
} else if (discovery_state == DISCOVERY_FINDING_CCCD) {
if (midi_cccd_handle != 0) {
Serial.println(F("CCCD found. Enabling Notifications..."));
discovery_state = DISCOVERY_ENABLING_NOTIFICATIONS;
uint8_t data[] = {0x01, 0x00}; // Enable Notification (Little Endian)
gatt_client_write_characteristic_descriptor_using_descriptor_handle(handle_gatt_client_event, con_handle, midi_cccd_handle, 2, data);
} else {
Serial.println(F("CCCD not found. Sending without notifications."));
discovery_state = DISCOVERY_COMPLETE;
midi_ready = true;
}
} else if (discovery_state == DISCOVERY_ENABLING_NOTIFICATIONS) {
Serial.println(F("Notifications Enabled. BLE MIDI Ready!"));
discovery_state = DISCOVERY_COMPLETE;
midi_ready = true;
}
break;
}
}
// BTStack Packet Handler
void packet_handler(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size) {
if (packet_type != HCI_EVENT_PACKET) return;
switch (hci_event_packet_get_type(packet)) {
case GAP_EVENT_ADVERTISING_REPORT: {
// Check if this is the target device
uint8_t event_type = gap_event_advertising_report_get_advertising_event_type(packet);
uint8_t length = gap_event_advertising_report_get_data_length(packet);
const uint8_t *data = gap_event_advertising_report_get_data(packet);
bd_addr_t addr;
gap_event_advertising_report_get_address(packet, addr);
Serial.print(F("Adv [Type ")); Serial.print(event_type);
Serial.print(F("] from ")); Serial.print(bd_addr_to_str(addr));
Serial.print(F(" len=")); Serial.println(length);
// Parser to find Local Name (0x09 Complete, 0x08 Shortened)
int i = 0;
while (i < length) {
uint8_t len = data[i];
if (len == 0) break;
uint8_t type = data[i+1];
if (type == 0x09 || type == 0x08) { // Complete or Shortened Local Name
Serial.print(F(" Name: "));
Serial.write(&data[i+2], len - 1);
Serial.println();
// Check for match (ensure length matches to avoid partial matches)
if ((len - 1) == strlen(TARGET_DEVICE_NAME) &&
memcmp(&data[i+2], TARGET_DEVICE_NAME, len - 1) == 0) {
Serial.println(F("Found WIDI Thru6! Connecting..."));
gap_stop_scan();
gap_connect(addr, (bd_addr_type_t)gap_event_advertising_report_get_address_type(packet));
return;
}
}
i += len + 1;
}
break;
}
case HCI_EVENT_LE_META:
if (hci_event_le_meta_get_subevent_code(packet) == HCI_SUBEVENT_LE_CONNECTION_COMPLETE) {
con_handle = hci_subevent_le_connection_complete_get_connection_handle(packet);
Serial.println(F("Connected. Searching for MIDI Service..."));
discovery_state = DISCOVERY_FINDING_SERVICE;
service_found = false;
midi_char_value_handle = 0;
midi_cccd_handle = 0;
midi_ready = false;
// Discover ALL Services to debug/find MIDI
gatt_client_discover_primary_services(handle_gatt_client_event, con_handle);
// Optimize Connection
gatt_client_send_mtu_negotiation(handle_gatt_client_event, con_handle);
gap_request_connection_parameter_update(con_handle, 12, 24, 0, 3000); // 15ms - 30ms interval
}
break;
case HCI_EVENT_DISCONNECTION_COMPLETE:
con_handle = HCI_CON_HANDLE_INVALID;
midi_char_value_handle = 0;
discovery_state = DISCOVERY_IDLE;
midi_ready = false;
Serial.println(F("Disconnected. Restarting scan..."));
gap_start_scan();
break;
}
}
void setupBluetooth() {
__lockBluetooth();
l2cap_init();
gatt_client_init();
sm_init();
sm_set_io_capabilities(IO_CAPABILITY_NO_INPUT_NO_OUTPUT);
// Register Callback & Start
hci_event_callback_registration.callback = &packet_handler;
hci_add_event_handler(&hci_event_callback_registration);
// Start Scanning
gap_set_scan_parameters(1, 0x0030, 0x0030); // 1 = Active Scanning (requests Scan Response)
gap_start_scan();
Serial.println(F("Scanning started..."));
hci_power_control(HCI_POWER_ON);
__unlockBluetooth();
}
#endif
void setup() { void setup() {
Serial.begin(115200); Serial.begin(115200);
delay(5000); delay(5000);
@ -390,46 +122,18 @@ void setup() {
display.clearDisplay(); display.clearDisplay();
display.display(); display.display();
// 5. Setup BLE // 5. Setup MIDI Serial
#ifdef ENABLE_BTSTACK Serial1.setTX(PIN_MIDI_TX);
setupBluetooth(); Serial1.begin(31250);
Serial.println(F("BTStack initialized.")); Serial.println(F("MIDI Serial initialized on GP0/GP1"));
#else
Serial.println(F("BLE Disabled (Requires Earle Philhower Core + Pico W)"));
#endif
Serial.println(F("Started.")); Serial.println(F("Started."));
} }
void sendMidi(uint8_t status, uint8_t note, uint8_t velocity) { void sendMidi(uint8_t status, uint8_t note, uint8_t velocity) {
#ifdef ENABLE_BTSTACK Serial1.write(status);
if (con_handle != HCI_CON_HANDLE_INVALID && midi_char_value_handle != 0) { Serial1.write(note);
// Use 0 timestamp for "immediate" execution to rule out timing/sync issues Serial1.write(velocity);
// Header: 10xxxxxx (Bit 7=1, Bit 6=0 for MIDI, Bits 5-0 = high 6 bits of timestamp)
// Timestamp: 1xxxxxxx (Bit 7=1, Bits 6-0 = low 7 bits of timestamp)
uint8_t header = 0x80;
uint8_t timestamp = 0x80;
uint8_t midiPacket[] = { header, timestamp, status, note, velocity };
__lockBluetooth();
#ifdef BLE_USE_WRITE_RESPONSE
uint8_t err = gatt_client_write_value_of_characteristic(handle_gatt_client_event, con_handle, midi_char_value_handle, sizeof(midiPacket), midiPacket);
#else
uint8_t err = gatt_client_write_value_of_characteristic_without_response(con_handle, midi_char_value_handle, sizeof(midiPacket), midiPacket);
#endif
__unlockBluetooth();
if (err) {
Serial.print(F("BLE Write Error: 0x")); Serial.println(err, HEX);
} else {
Serial.print(F("MIDI TX: "));
for(int i=0; i<sizeof(midiPacket); i++) {
Serial.print(midiPacket[i], HEX); Serial.print(" ");
}
Serial.println();
}
}
#endif
} }
void handleInput() { void handleInput() {
@ -538,16 +242,6 @@ void drawUI() {
display.println(); display.println();
display.drawLine(0, 8, 128, 8, SSD1306_WHITE); display.drawLine(0, 8, 128, 8, SSD1306_WHITE);
// Bluetooth Icon
#ifdef ENABLE_BTSTACK
if (con_handle != HCI_CON_HANDLE_INVALID) {
display.drawBitmap(120, 0, bluetooth_icon, 8, 8, SSD1306_WHITE);
if (midi_ready) {
display.fillCircle(114, 4, 2, SSD1306_WHITE); // Small dot to indicate MIDI Ready
}
}
#endif
// Steps // Steps
int y = 10; int y = 10;
for (int i = scrollOffset; i < min(scrollOffset + 6, NUM_STEPS); i++) { for (int i = scrollOffset; i < min(scrollOffset + 6, NUM_STEPS); i++) {
@ -625,17 +319,6 @@ void updateLeds() {
} }
void loop() { void loop() {
// BTStack runs in background, no poll needed.
#ifdef ENABLE_BTSTACK
bool connected = (con_handle != HCI_CON_HANDLE_INVALID);
if (connected != wasConnected) {
wasConnected = connected;
if (connected) Serial.println(F("BLE: Connected"));
else Serial.println(F("BLE: Disconnected"));
}
#endif
handleInput(); handleInput();
handlePlayback(); handlePlayback();
drawUI(); drawUI();