Add BLE MIDI transport (feature_bluetooth)

This commit is contained in:
ash
2026-06-30 06:13:36 +00:00
parent ae2891fca8
commit aea3a28206
5 changed files with 256 additions and 12 deletions
+4 -2
View File
@@ -2,6 +2,7 @@
#include <cstdint>
#include "midi_transport.h"
#include "ble_midi_transport.h"
#include "led_stub.h"
#include "switch_stub.h"
@@ -14,7 +15,7 @@ struct PadMapping {
class AppTask {
public:
AppTask(LedStub* led, SwitchStub* sw, UsbMidiTransport* midi);
AppTask(LedStub* led, SwitchStub* sw, UsbMidiTransport* usb_midi, BleMidiTransport* ble_midi = nullptr);
void begin();
void update();
@@ -23,7 +24,8 @@ public:
private:
LedStub* led_driver;
SwitchStub* switch_driver;
UsbMidiTransport* midi_transport;
UsbMidiTransport* usb_midi;
BleMidiTransport* ble_midi;
static const uint8_t NUM_PADS = 10;
PadMapping pad_mapping[NUM_PADS];
+31
View File
@@ -0,0 +1,31 @@
#pragma once
#include <cstdint>
#include <functional>
#include "midi_transport.h"
class BleMidiTransport {
public:
BleMidiTransport();
~BleMidiTransport();
bool begin();
void update();
void on_midi_receive(std::function<void(const MidiEvent&)> callback);
void send_note_on(uint8_t channel, uint8_t note, uint8_t velocity);
void send_note_off(uint8_t channel, uint8_t note, uint8_t velocity);
void send_cc(uint8_t channel, uint8_t cc, uint8_t value);
bool is_connected();
private:
std::function<void(const MidiEvent&)> receive_callback;
bool initialized;
bool client_connected;
void send_midi_packet(const uint8_t* data, uint8_t len);
void on_receive(const uint8_t* data, size_t len);
size_t parse_ble_midi(uint8_t status, const uint8_t* data, size_t len, size_t offset, MidiEvent& event);
};
+12 -7
View File
@@ -1,8 +1,8 @@
#include "app_task.h"
#include <Arduino.h>
AppTask::AppTask(LedStub* led, SwitchStub* sw, UsbMidiTransport* midi)
: led_driver(led), switch_driver(sw), midi_transport(midi) {
AppTask::AppTask(LedStub* led, SwitchStub* sw, UsbMidiTransport* usb_midi, BleMidiTransport* ble_midi)
: led_driver(led), switch_driver(sw), usb_midi(usb_midi), ble_midi(ble_midi) {
// Launchpad X standard: bottom row = notes 36-45 (C2 to A2) on channel 1
const uint8_t launchpad_notes[10] = {36, 37, 38, 39, 40, 41, 42, 43, 44, 45};
@@ -20,11 +20,15 @@ AppTask::AppTask(LedStub* led, SwitchStub* sw, UsbMidiTransport* midi)
void AppTask::begin() {
Serial.println("[APP] Registering MIDI callbacks...");
midi_transport->on_midi_receive([this](const MidiEvent& event) {
auto handler = [this](const MidiEvent& event) {
process_midi_event(event);
});
};
usb_midi->on_midi_receive(handler);
if (ble_midi) {
ble_midi->on_midi_receive(handler);
}
Serial.println("[APP] Controller ready - CC mode");
for (uint8_t i = 0; i < NUM_PADS; i++) {
Serial.printf("[APP] Pad %d -> CC%d -> LED%d\n", i + 1, cc_map[i], i);
@@ -134,7 +138,8 @@ void AppTask::process_switch_event(uint8_t switch_id, bool pressed) {
uint8_t value = pressed ? 127 : 0;
if (pressed) {
midi_transport->send_cc(channel, cc_num, value);
usb_midi->send_cc(channel, cc_num, value);
if (ble_midi) ble_midi->send_cc(channel, cc_num, value);
}
Serial.printf("[APP] Switch %d -> Ch%d CC%d Val%d (%s)\n",
+200
View File
@@ -0,0 +1,200 @@
#include "ble_midi_transport.h"
#include <Arduino.h>
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <BLE2902.h>
#define BLE_MIDI_SERVICE_UUID "03B80E5A-EDE8-4B33-A751-6CE34EC4C700"
#define BLE_MIDI_CHAR_UUID "7772E5DB-3868-4112-A1A9-F2669D106BF3"
static BleMidiTransport* instance = nullptr;
static BLEServer* ble_server = nullptr;
static BLEService* ble_service = nullptr;
static BLECharacteristic* ble_char = nullptr;
static bool device_connected = false;
class ServerCallbacks : public BLEServerCallbacks {
void onConnect(BLEServer* server) override {
device_connected = true;
Serial.println("[BLE] Client connected");
}
void onDisconnect(BLEServer* server) override {
device_connected = false;
Serial.println("[BLE] Client disconnected");
BLEDevice::startAdvertising();
}
};
class CharCallbacks : public BLECharacteristicCallbacks {
void onWrite(BLECharacteristic* characteristic) override {
std::string value = characteristic->getValue();
if (value.length() > 0 && instance) {
instance->on_receive((const uint8_t*)value.data(), value.length());
}
}
};
BleMidiTransport::BleMidiTransport() : initialized(false), client_connected(false) {
instance = this;
}
BleMidiTransport::~BleMidiTransport() {
if (instance == this) instance = nullptr;
}
bool BleMidiTransport::begin() {
Serial.println("[BLE] Initializing BLE MIDI...");
BLEDevice::init("JOC Midi");
BLEDevice::setPower(ESP_PWR_LVL_P9);
ble_server = BLEDevice::createServer();
ble_server->setCallbacks(new ServerCallbacks());
ble_service = ble_server->createService(BLE_MIDI_SERVICE_UUID);
ble_char = ble_service->createCharacteristic(
BLE_MIDI_CHAR_UUID,
BLECharacteristic::PROPERTY_READ |
BLECharacteristic::PROPERTY_WRITE_NR |
BLECharacteristic::PROPERTY_NOTIFY
);
ble_char->addDescriptor(new BLE2902());
ble_char->setCallbacks(new CharCallbacks());
ble_service->start();
BLEAdvertising* adv = BLEDevice::getAdvertising();
adv->addServiceUUID(BLE_MIDI_SERVICE_UUID);
adv->setScanResponse(true);
adv->setMinPreferred(0x06);
adv->setMinPreferred(0x12);
BLEDevice::startAdvertising();
initialized = true;
Serial.println("[BLE] BLE MIDI advertising as 'JOC Midi'");
return true;
}
void BleMidiTransport::update() {
client_connected = device_connected;
}
void BleMidiTransport::on_midi_receive(std::function<void(const MidiEvent&)> callback) {
receive_callback = callback;
}
void BleMidiTransport::send_midi_packet(const uint8_t* data, uint8_t len) {
if (!initialized || !client_connected || !ble_char) return;
// BLE MIDI format: timestamp header + raw MIDI message
uint16_t timestamp = micros() & 0x3FFF;
uint8_t packet[16];
uint8_t idx = 0;
packet[idx++] = 0x80 | (timestamp >> 7);
packet[idx++] = timestamp & 0x7F;
for (uint8_t i = 0; i < len && idx < 16; i++) {
packet[idx++] = data[i];
}
ble_char->setValue(packet, idx);
ble_char->notify();
}
void BleMidiTransport::send_note_on(uint8_t channel, uint8_t note, uint8_t velocity) {
uint8_t packet[3] = {0x90 | (channel - 1), note, velocity};
send_midi_packet(packet, 3);
Serial.printf("[BLE OUT] Ch:%d NOTE_ON:%d:%d\n", channel, note, velocity);
}
void BleMidiTransport::send_note_off(uint8_t channel, uint8_t note, uint8_t velocity) {
uint8_t packet[3] = {0x80 | (channel - 1), note, velocity};
send_midi_packet(packet, 3);
Serial.printf("[BLE OUT] Ch:%d NOTE_OFF:%d:%d\n", channel, note, velocity);
}
void BleMidiTransport::send_cc(uint8_t channel, uint8_t cc, uint8_t value) {
uint8_t packet[3] = {0xB0 | (channel - 1), cc, value};
send_midi_packet(packet, 3);
Serial.printf("[BLE OUT] Ch:%d CC:%d:%d\n", channel, cc, value);
}
bool BleMidiTransport::is_connected() {
return initialized && client_connected;
}
void BleMidiTransport::on_receive(const uint8_t* data, size_t len) {
if (len < 3 || !receive_callback) return; // need timestamp(2) + status(1)
// BLE MIDI format: [ts_hi|0x80] [ts_lo] [status ...]
size_t offset = 2; // skip timestamp header
while (offset < len) {
uint8_t status = data[offset++];
if (status < 0x80) continue; // skip non-status bytes
uint8_t type = status & 0xF0;
uint8_t channel = (status & 0x0F) + 1;
MidiEvent event;
event.channel = channel;
event.timestamp = millis();
event.type = MidiEvent::NOTE_ON; // default
offset = parse_ble_midi(status, data, len, offset, event);
if (event.type == MidiEvent::SYSEX) {
Serial.printf("[BLE IN] Sysex len=%zu\n", len);
} else {
Serial.printf("[BLE IN] Ch:%d %s:%d:%d\n",
event.channel,
event.type == MidiEvent::NOTE_ON ? "NOTE_ON" :
event.type == MidiEvent::NOTE_OFF ? "NOTE_OFF" :
event.type == MidiEvent::CONTROL_CHANGE ? "CC" : "OTHER",
event.data1, event.data2);
}
if (receive_callback) {
receive_callback(event);
}
}
}
size_t BleMidiTransport::parse_ble_midi(uint8_t status, const uint8_t* data, size_t len, size_t offset, MidiEvent& event) {
uint8_t type = status & 0xF0;
event.data1 = 0;
event.data2 = 0;
if (offset >= len) return offset;
event.data1 = data[offset++];
switch (type) {
case 0x80:
event.type = MidiEvent::NOTE_OFF;
if (offset < len) event.data2 = data[offset++];
break;
case 0x90:
event.type = MidiEvent::NOTE_ON;
if (offset < len) event.data2 = data[offset++];
if (event.data2 == 0) event.type = MidiEvent::NOTE_OFF;
break;
case 0xB0:
event.type = MidiEvent::CONTROL_CHANGE;
if (offset < len) event.data2 = data[offset++];
break;
case 0xC0:
event.type = MidiEvent::PROGRAM_CHANGE;
break;
case 0xE0:
event.type = MidiEvent::PITCH_BEND;
if (offset < len) event.data2 = data[offset++];
break;
default:
break;
}
return offset;
}
+9 -3
View File
@@ -5,6 +5,7 @@
#include <driver/gpio.h>
#include <esp_rom_sys.h>
#include "midi_transport.h"
#include "ble_midi_transport.h"
#include "Adafruit_TinyUSB.h"
#include "pixel_stomp_mux.h"
#include "led_stub.h"
@@ -16,8 +17,9 @@ PixelStompMux mux(12, 10, 11, 9);
DefaultLedStub led_driver;
DefaultSwitchStub switch_driver;
UsbMidiTransport midi_transport;
BleMidiTransport ble_midi_transport;
AppTask controller(&led_driver, &switch_driver, &midi_transport);
AppTask controller(&led_driver, &switch_driver, &midi_transport, &ble_midi_transport);
TaskHandle_t midi_task_handle = NULL;
@@ -26,6 +28,7 @@ void midi_task(void* parameter) {
while (true) {
midi_transport.update();
ble_midi_transport.update();
vTaskDelay(1);
}
}
@@ -328,7 +331,7 @@ void setup() {
Serial.println("=================================");
Serial.println(" Loopy MIDI Controller v0.1");
Serial.println(" Phase 1: USB MIDI");
Serial.println(" Phase 1.5: USB + BLE MIDI");
Serial.println(" Board: ESP32-S3-WROOM-1");
Serial.println("=================================");
@@ -345,7 +348,10 @@ void setup() {
Serial.println("[INIT] Initializing USB MIDI...");
midi_transport.begin();
Serial.println("[INIT] Initializing BLE MIDI...");
ble_midi_transport.begin();
Serial.println("[INIT] Registering MIDI callbacks...");
controller.begin();