Last active
October 24, 2025 04:49
-
-
Save acidsound/045f23d73d4fca28533a5e880fc6d931 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #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