Skip to content

Instantly share code, notes, and snippets.

@acidsound
Last active October 24, 2025 04:49
Show Gist options
  • Select an option

  • Save acidsound/045f23d73d4fca28533a5e880fc6d931 to your computer and use it in GitHub Desktop.

Select an option

Save acidsound/045f23d73d4fca28533a5e880fc6d931 to your computer and use it in GitHub Desktop.
#include <Arduino.h>
#include <NimBLEDevice.h>
#include <U8g2lib.h>
// BLE MIDI 서비스 고유 UUID
static NimBLEUUID midiServiceUUID("03B80E5A-EDE8-4B33-A751-6CE34EC4C700");
// BLE MIDI characteristic UUID - used for both TX (device to central) and RX (central to device)
static NimBLEUUID midiCharacteristicUUID("7772E5DB-3868-4112-A1A9-F2669D106BF3");
// 스캔 후 찾은 장치 포인터
NimBLEAdvertisedDevice* myDevice = nullptr;
bool connected = false;
std::string connectedDeviceName;
// BLE Client and related objects
NimBLEClient* pClient = nullptr;
NimBLERemoteService* pRemoteService = nullptr;
NimBLERemoteCharacteristic* pMidiTXCharacteristic = nullptr;
// there is no 72x40 constructor in u8g2 hence the 72x40 screen is mapped in the middle of the 132x64 pixel buffer of the SSD1306 controller
U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE, 6, 5);
int width = 72;
int height = 40;
int xOffset = 30; // = (132-w)/2
int yOffset = 12; // = (64-h)/2
unsigned long lastMarqMillis = 0;
int marqPos = 0;
// MIDI activity tracking
unsigned long lastMidiActivity = 0;
bool midiActivity = false;
unsigned long midiActivityTimeout = 1000; // 1 second timeout for MIDI activity indicator
// Variables to store latest MIDI data for display
int lastMidiChannel = 0;
int lastMidiCommand = 0;
int lastMidiData1 = 0;
int lastMidiData2 = 0;
bool hasMidiData = false;
void updateMidiActivity() {
// Turn off MIDI activity indicator if timeout has passed
if (midiActivity && (millis() - lastMidiActivity) > midiActivityTimeout) {
midiActivity = false;
}
}
void updateU8G2(const char* name) {
u8g2.clearBuffer();
u8g2.drawFrame(xOffset - 2, yOffset + 12, width, height);
if (connected && hasMidiData) {
// When connected and MIDI data has been received, show MIDI info
// Show MIDI activity indicator
if (midiActivity) {
// Draw a small filled circle as MIDI activity indicator
u8g2.drawDisc(xOffset + width - 5, yOffset + 15, 3); // Small filled circle
} else {
// Draw an outline circle when there's no MIDI activity
u8g2.drawCircle(xOffset + width - 5, yOffset + 15, 3); // Small outline circle
}
// Show MIDI data header
u8g2.setCursor(xOffset, yOffset + 23);
u8g2.printf("ChTy D1 D2");
// Show the latest MIDI data
u8g2.setCursor(xOffset, yOffset + 35);
u8g2.printf("%02d%02X %03d%03d", lastMidiChannel, lastMidiCommand, lastMidiData1, lastMidiData2);
} else {
// When not connected or no MIDI data received yet, show connection info
u8g2.setCursor(xOffset, yOffset + 48);
u8g2.printf("%s", "@acidsound");
u8g2.setCursor(xOffset, yOffset + 23);
u8g2.println("::MIDI FROM");
size_t nameLen = strlen(name);
const int displayLen = 11;
char displayStr[displayLen + 1]; // +1 for null terminator
if (nameLen <= displayLen) {
// 문자열이 11자 이하면 그대로 표시
strncpy(displayStr, name, displayLen);
displayStr[nameLen] = '\0';
} else {
// marquee 효과: 맨 끝과 맨 앞 사이 공백 1칸 삽입
int scrollLen = nameLen + 1; // 공백 한 칸 추가한 길이
for (int i = 0; i < displayLen; i++) {
int idx = (marqPos + i) % scrollLen;
if (idx == nameLen) {
displayStr[i] = ' '; // 공백 삽입
} else {
displayStr[i] = name[idx];
}
}
displayStr[displayLen] = '\0';
}
u8g2.setCursor(xOffset, yOffset + 35);
u8g2.printf("%s", displayStr);
// Draw MIDI activity indicator (a small dot or rectangle that shows when MIDI is active)
if (midiActivity) {
// Draw a small filled circle as MIDI activity indicator
u8g2.drawDisc(xOffset + width - 5, yOffset + 15, 3); // Small filled circle
} else {
// Draw an outline circle when there's no MIDI activity
u8g2.drawCircle(xOffset + width - 5, yOffset + 15, 3); // Small outline circle
}
}
u8g2.sendBuffer();
}
// 스캔 결과를 처리할 콜백 클래스
class MyAdvertisedDeviceCallbacks : public NimBLEAdvertisedDeviceCallbacks {
void onResult(NimBLEAdvertisedDevice* advertisedDevice) override {
if (advertisedDevice->isAdvertisingService(midiServiceUUID)) {
Serial.print("Found BLE MIDI Device: ");
Serial.print(advertisedDevice->getName().c_str());
Serial.print(" (Address: ");
Serial.print(advertisedDevice->getAddress().toString().c_str());
Serial.println(")");
connectedDeviceName = advertisedDevice->getName();
Serial.printf("update device: %s", connectedDeviceName.c_str());
Serial.println("");
if (myDevice == nullptr) {
Serial.println("Connecting device");
myDevice = new NimBLEAdvertisedDevice(*advertisedDevice);
NimBLEDevice::getScan()->stop();
}
}
}
};
class MyClientCallback : public NimBLEClientCallbacks {
void onConnect(NimBLEClient* pclient) override {
Serial.println("Connected to BLE MIDI device");
connected = true;
}
void onDisconnect(NimBLEClient* pclient) override {
Serial.println("Disconnected from BLE MIDI device");
connected = false;
// Clean up - don't delete pClient as NimBLE manages it
pRemoteService = nullptr;
pMidiTXCharacteristic = nullptr;
}
};
void connectToDevice() {
if (myDevice == nullptr) {
Serial.println("No device to connect to");
return;
}
Serial.println("Creating BLE Client...");
pClient = NimBLEDevice::createClient();
Serial.println("Setting client callbacks...");
pClient->setClientCallbacks(new MyClientCallback());
Serial.print("Connecting to ");
Serial.println(myDevice->getAddress().toString().c_str());
// Connect to the device
if (!pClient->connect(myDevice)) {
Serial.println("Failed to connect to device");
connected = false;
// Don't delete pClient - NimBLE manages it
pClient = nullptr;
return;
}
Serial.println("Connected, discovering services...");
// Get the service
pRemoteService = pClient->getService(midiServiceUUID);
if (pRemoteService == nullptr) {
Serial.print("Failed to find MIDI service UUID: ");
Serial.println(midiServiceUUID.toString().c_str());
pClient->disconnect();
connected = false;
return;
}
Serial.println("Found MIDI service");
// Get the MIDI characteristic (used for both TX and RX)
pMidiTXCharacteristic = pRemoteService->getCharacteristic(midiCharacteristicUUID);
if (pMidiTXCharacteristic == nullptr) {
Serial.print("Failed to find MIDI characteristic UUID: ");
Serial.println(midiCharacteristicUUID.toString().c_str());
pClient->disconnect();
connected = false;
return;
}
Serial.println("Found MIDI characteristic");
// Subscribe to notifications from the characteristic to receive MIDI data
if (pMidiTXCharacteristic->canNotify()) {
if (!pMidiTXCharacteristic->subscribe(true, [](NimBLERemoteCharacteristic* pNimBLERemoteCharacteristic,
uint8_t* pData,
size_t length,
bool isNotify) {
Serial.print("MIDI Data received: ");
for (int i = 0; i < length; i++) {
Serial.printf("0x%02X ", pData[i]);
}
Serial.println();
// Process MIDI data here
processMIDIData(pData, length);
})) { // true to enable notifications
Serial.println("Failed to subscribe for notifications");
} else {
Serial.println("Successfully subscribed for MIDI notifications");
}
} else {
Serial.println("MIDI characteristic cannot notify");
}
connected = true;
Serial.println("Successfully connected to MIDI device and subscribed for notifications");
}
void listenBLEMIDI() {
Serial.println("Starting BLE Scan for MIDI Controllers...");
// Initialize NimBLE device
NimBLEDevice::init("");
NimBLEScan* pBLEScan = NimBLEDevice::getScan();
// 디바이스 콜백 등록
pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks());
pBLEScan->setActiveScan(true);
pBLEScan->start(10);
// 스캔 완료 후 myDevice가 설정되었는지 확인
if (myDevice == nullptr) {
Serial.println("No MIDI device found. Restart and try again.");
return;
}
Serial.println("Attempting to connect to the device...");
connectToDevice();
}
void processMIDIData(uint8_t* pData, size_t length) {
// Process incoming MIDI data according to MIDI 1.0 and BLE MIDI specifications
Serial.print("MIDI Data received: ");
for (int i = 0; i < length; i++) {
Serial.printf("0x%02X ", pData[i]);
}
Serial.println();
// Update MIDI activity indicator
lastMidiActivity = millis();
midiActivity = true;
// BLE MIDI packets may include headers (timestamp, etc.), so we need to parse them
size_t idx = 0;
while (idx < length) {
uint8_t byte = pData[idx+2];
// Check for BLE MIDI header (upper nibble indicates packet type)
// 0x80-0xBF: MIDI data with 13-bit timestamp
// 0xC0-0xDF: MIDI data with 10-bit timestamp
// 0xE0-0xEF: MIDI data with 7-bit timestamp
// 0xF0-0xFF: MIDI data with 0-bit timestamp (no timestamp)
// For simplicity, we'll parse the MIDI message directly
if ((byte & 0x80) == 0x80) { // MIDI status byte
uint8_t status = byte;
uint8_t command = status & 0xF0; // Upper 4 bits: command
uint8_t channel = status & 0x0F; // Lower 4 bits: channel
Serial.printf(" Status: 0x%02X (Command: 0x%02X, Channel: %d)\n", status, command, channel + 1);
switch (command) {
case 0x80: // Note Off
case 0x90: // Note On
case 0xA0: // Polyphonic Aftertouch
case 0xB0: // Control Change
case 0xE0: // Pitch Bend
if (idx + 2 < length) {
uint8_t data1 = pData[idx + 3];
uint8_t data2 = pData[idx + 4];
// Store for display
lastMidiChannel = channel + 1;
lastMidiCommand = command;
lastMidiData1 = data1;
lastMidiData2 = data2;
hasMidiData = true;
switch (command) {
case 0x80: // Note Off
Serial.printf(" Note Off: Note=%d, Velocity=%d\n", data1, data2);
handleNoteEvent(false, data1, data2, channel);
break;
case 0x90: // Note On
Serial.printf(" Note On: Note=%d, Velocity=%d\n", data1, data2);
handleNoteEvent(true, data1, data2, channel);
break;
case 0xA0: // Polyphonic Aftertouch
Serial.printf(" Poly Aftertouch: Note=%d, Pressure=%d\n", data1, data2);
break;
case 0xB0: // Control Change
Serial.printf(" Control Change: Controller=%d, Value=%d\n", data1, data2);
handleControlChange(data1, data2, channel);
break;
case 0xE0: // Pitch Bend
uint16_t pitchValue = (data2 << 7) | data1; // Pitch bend combines both data bytes
Serial.printf(" Pitch Bend: Value=%d (LSB=%d, MSB=%d)\n", pitchValue, data1, data2);
lastMidiData1 = pitchValue/1000;
lastMidiData2 = pitchValue%1000;
handlePitchBend(pitchValue, channel);
break;
}
idx += 3; // Move past status + 2 data bytes
} else {
idx++;
}
break;
case 0xC0: // Program Change
case 0xD0: // Channel Aftertouch
if (idx + 1 < length) {
uint8_t data1 = pData[idx + 1];
// Store for display
lastMidiChannel = channel + 1;
lastMidiCommand = command;
lastMidiData1 = data1;
lastMidiData2 = 0; // No second data byte
hasMidiData = true;
if (command == 0xC0) {
Serial.printf(" Program Change: Program=%d\n", data1);
handleProgramChange(data1, channel);
} else {
Serial.printf(" Channel Aftertouch: Pressure=%d\n", data1);
}
idx += 2; // Move past status + 1 data byte
} else {
idx++;
}
break;
default:
Serial.printf(" Unknown MIDI command: 0x%02X\n", command);
idx++;
break;
}
} else {
// Data byte without status - this should not occur in proper MIDI but might in BLE MIDI
Serial.printf(" Unexpected data byte: 0x%02X at position %d\n", byte, idx);
idx++;
}
}
}
// Functions to handle specific MIDI events
void handleNoteEvent(bool isNoteOn, uint8_t note, uint8_t velocity, uint8_t channel) {
Serial.printf(" Output: %s - Note: %d, Velocity: %d, Channel: %d\n",
isNoteOn ? "NOTE ON" : "NOTE OFF", note, velocity, channel + 1);
// Here you would implement the actual signal output
// For example: send to external MIDI interface, control LEDs, etc.
// Example: Control an LED based on note events
// digitalWrite(ledPin, isNoteOn ? HIGH : LOW);
// Example: Send to hardware serial MIDI (5-pin DIN)
// if (isNoteOn) {
// Serial1.write(0x90 | channel); // Note On for this channel
// Serial1.write(note);
// Serial1.write(velocity);
// } else {
// Serial1.write(0x80 | channel); // Note Off for this channel
// Serial1.write(note);
// Serial1.write(velocity);
// }
}
void handleControlChange(uint8_t controller, uint8_t value, uint8_t channel) {
Serial.printf(" Output: Control Change - Controller: %d, Value: %d, Channel: %d\n",
controller, value, channel + 1);
// Implement your control change handling here
// For example, control parameter values, light effects, etc.
}
void handleProgramChange(uint8_t program, uint8_t channel) {
Serial.printf(" Output: Program Change - Program: %d, Channel: %d\n",
program, channel + 1);
// Implement your program change handling here
}
void handlePitchBend(uint16_t value, uint8_t channel) {
Serial.printf(" Output: Pitch Bend - Value: %d, Channel: %d\n",
value, channel + 1);
// Implement your pitch bend handling here
}
void setup() {
Serial.begin(115200);
u8g2.begin();
u8g2.setContrast(255); // set contrast to maximum
u8g2.setBusClock(400000); //400kHz I2C
u8g2.setFont(u8g2_font_6x10_mr);
// Initialize NimBLE
NimBLEDevice::init("");
// Initialize MIDI data display variables
hasMidiData = false;
}
void loop() {
unsigned long currentMillis = millis();
// marquee 0.4초 간격 타이머
if (currentMillis - lastMarqMillis >= 400) {
lastMarqMillis = currentMillis;
if (connected && connectedDeviceName.length() > 11) {
marqPos++;
if (marqPos >= connectedDeviceName.length()) {
marqPos = 0;
}
}
}
// Update MIDI activity state
updateMidiActivity();
// 연결이 끊어지면 재연결 시도
if (myDevice == nullptr || pClient == nullptr) {
updateU8G2("no device"); // 기본 문자열 표시
} else {
updateU8G2(connectedDeviceName.c_str());
}
if (!connected) {
Serial.println("Disconnected. Scanning for a new device to connect...");
// Clean up any existing client object reference (NimBLE manages the object)
pClient = nullptr;
// Restart scanning for devices
listenBLEMIDI(); // 스캔 및 재연결 재시도
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment