BT connection WIP

This commit is contained in:
Dejvino 2026-02-16 00:48:05 +01:00
parent 2ed1467122
commit 7c9df501a7
2 changed files with 384 additions and 10 deletions

View File

@ -1,6 +1,17 @@
# RP2040 MIDI Tracker # RP2040 MIDI Tracker
A simple MIDI step sequencer using a Raspberry Pi Pico, an OLED display, a rotary encoder, and an 8x8 NeoPixel matrix. A simple MIDI step sequencer using a Raspberry Pi Pico W, an OLED display, a rotary encoder, and an 8x8 NeoPixel matrix.
## Software Requirements
* **Board Core**: This project uses the **Raspberry Pi Pico/RP2040** core by **Earle F. Philhower, III**.
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`
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.
* **Libraries**: Install via Library Manager:
* `Adafruit GFX Library`, `Adafruit SSD1306`, `Adafruit NeoPixel`
* *(Note: `ArduinoBLE` is NO LONGER required. We use the built-in BTStack)*
## Connections & Wiring ## Connections & Wiring
@ -9,10 +20,10 @@ Properly wiring the components is crucial, especially for power.
### Power ### Power
The project is best powered in two parts: The project is best powered in two parts:
1. **Raspberry Pi Pico**: Power the Pico via its Micro-USB port from a computer or a USB wall adapter. 1. **Raspberry Pi Pico W**: Power the Pico via its Micro-USB port from a computer or a USB wall adapter.
2. **NeoPixel 8x8 Matrix**: This component is power-hungry and **requires a separate, external 5V power supply**. A power supply capable of delivering at least 2A is recommended. 2. **NeoPixel 8x8 Matrix**: This component is power-hungry and **requires a separate, external 5V power supply**. A power supply capable of delivering at least 2A is recommended.
> **WARNING**: Do NOT power the NeoPixel matrix from the Pico's 3.3V or VBUS pins. Doing so can draw too much current and damage your Pico and/or the host computer's USB port. > **WARNING**: Do NOT power the NeoPixel matrix from the Pico's 3.3V or VBUS pins. VBUS is connected directly to the USB port, which is typically limited to 500mA. The matrix can draw over 2A, which could overload and damage your host computer's USB port.
### Component Wiring ### Component Wiring
@ -38,3 +49,42 @@ Make sure to establish a **common ground** by connecting the ground from your ex
| | GND (Pin 18) | **Common Ground with Pico** | | | GND (Pin 18) | **Common Ground with Pico** |
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!
## Making it Portable
To run this project without being tethered to a computer, you'll need a portable power source that can supply both the Pico and the power-hungry NeoPixel matrix.
### Option 1: Using a USB Power Bank (Easiest)
This is the simplest and safest method.
* **What you'll need:** A standard USB power bank with at least two outputs, capable of supplying a total of 2.5A or more.
* **Pico Power:** Connect a standard Micro-USB cable from one of the power bank's outputs to the Pico's USB port.
* **Matrix Power:** Use a second USB cable to power the NeoPixel matrix. You will likely need to cut the end of a USB cable and connect the 5V (usually red) and GND (usually black) wires to the matrix's power input.
* **Common Ground:** The common ground is handled automatically through the USB connections to the same power bank. However, it is still best practice to run a dedicated wire from the matrix's GND to one of the Pico's GND pins.
### Option 2: LiPo Battery with a 5V Booster (More Compact)
For a more integrated build that can fit inside an enclosure, you can use a Lithium Polymer (LiPo) battery and a voltage-boosting board.
* **What you'll need:**
* A single-cell (3.7V) LiPo battery.
* A 5V booster board, such as the Adafruit PowerBoost 1000C. These boards can charge the LiPo battery and provide a stable 5V output.
> **LIPO BATTERY WARNING**: LiPo batteries are powerful but require careful handling.
> * **Never** use a LiPo battery without a dedicated protection circuit, which prevents over-charge, over-discharge, and short-circuits. Boards like the PowerBoost series have this built-in.
> * Do not puncture, bend, or short-circuit a LiPo battery.
> * Always charge them with a proper LiPo charger.
* **Wiring:**
1. Connect the **LiPo battery** to the battery input terminals on the **PowerBoost board**.
2. Connect the **PowerBoost's 5V output** to the **NeoPixel Matrix's 5V/VCC input**.
3. Connect the **PowerBoost's 5V output** to the **Pico's VBUS pin (Pin 40)**. This will power the Pico.
4. Connect the **PowerBoost's GND** to **both** the **NeoPixel Matrix's GND** and one of the **Pico's GND pins**. This creates the essential common ground.
## Usage
* **Navigation**: Rotate the encoder to move between steps.
* **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.
* **Bluetooth**: The device advertises as "RP2040 Tracker". Connect to it from your computer or MIDI Bluetooth dongle.

View File

@ -4,6 +4,18 @@
#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
@ -35,9 +47,47 @@ 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;
enum DiscoveryState {
DISCOVERY_IDLE,
DISCOVERY_FINDING_SERVICE,
DISCOVERY_FINDING_CHARACTERISTIC,
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;
bool isPlaying = false;
unsigned long lastStepTime = 0;
int tempo = 120; // BPM
// Encoder State // Encoder State
volatile int encoderDelta = 0; volatile int encoderDelta = 0;
@ -47,6 +97,21 @@ static uint16_t store = 0;
// Button State // Button State
bool lastButtonState = HIGH; bool lastButtonState = HIGH;
unsigned long lastDebounceTime = 0; unsigned long lastDebounceTime = 0;
bool buttonActive = false;
bool buttonConsumed = false;
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
@ -67,8 +132,162 @@ 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);
}
}
}
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("BLE MIDI Ready to send!"));
discovery_state = DISCOVERY_COMPLETE;
midi_ready = true;
} else {
Serial.println(F("MIDI Characteristic not found."));
discovery_state = DISCOVERY_IDLE;
}
}
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_ready = false;
// Discover ALL Services to debug/find MIDI
gatt_client_discover_primary_services(handle_gatt_client_event, con_handle);
}
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);
Serial.println(F("Starting.")); Serial.println(F("Starting."));
// 1. Setup Encoder // 1. Setup Encoder
@ -80,8 +299,7 @@ void setup() {
attachInterrupt(digitalPinToInterrupt(ENC_DT), readEncoder, CHANGE); attachInterrupt(digitalPinToInterrupt(ENC_DT), readEncoder, CHANGE);
// 2. Setup Display // 2. Setup Display
Wire.setSDA(PIN_SDA); // Note: Using default I2C pins (SDA=GP4, SCL=GP5) which works on both cores.
Wire.setSCL(PIN_SCL);
Wire.begin(); Wire.begin();
// SSD1306_SWITCHCAPVCC = generate display voltage from 3.3V internally // SSD1306_SWITCHCAPVCC = generate display voltage from 3.3V internally
@ -109,9 +327,42 @@ void setup() {
display.clearDisplay(); display.clearDisplay();
display.display(); display.display();
// 5. Setup BLE
#ifdef ENABLE_BTSTACK
setupBluetooth();
Serial.println(F("BTStack initialized."));
#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) {
#ifdef ENABLE_BTSTACK
if (con_handle != HCI_CON_HANDLE_INVALID && midi_char_value_handle != 0) {
unsigned long t = millis();
uint8_t header = 0x80 | ((t >> 7) & 0x3F);
uint8_t timestamp = 0x80 | (t & 0x7F);
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: ")); Serial.println(note);
}
}
#endif
}
void handleInput() { void handleInput() {
// Handle Encoder Rotation // Handle Encoder Rotation
int delta = 0; int delta = 0;
@ -127,6 +378,7 @@ void handleInput() {
if (newNote < -1) newNote = -1; if (newNote < -1) newNote = -1;
if (newNote > 127) newNote = 127; if (newNote > 127) newNote = 127;
sequence[currentStep].note = newNote; sequence[currentStep].note = newNote;
Serial.print(F("Note changed: ")); Serial.println(newNote);
} else { } else {
// Move Cursor // Move Cursor
currentStep += (delta > 0 ? 1 : -1); currentStep += (delta > 0 ? 1 : -1);
@ -141,20 +393,70 @@ void handleInput() {
// Handle Button // Handle Button
int reading = digitalRead(ENC_SW); int reading = digitalRead(ENC_SW);
if (reading != lastButtonState) { if (reading != lastButtonState) {
lastDebounceTime = millis(); lastDebounceTime = millis();
} }
if ((millis() - lastDebounceTime) > 50) { if ((millis() - lastDebounceTime) > 50) {
if (reading == LOW) { // Button Pressed if (reading == LOW && !buttonActive) {
// Wait for release to toggle mode // Button Pressed
while(digitalRead(ENC_SW) == LOW); buttonActive = true;
buttonPressTime = millis();
buttonConsumed = false;
Serial.println(F("Button Down"));
}
if (reading == HIGH && buttonActive) {
// Button Released
buttonActive = false;
if (!buttonConsumed) {
isEditing = !isEditing; isEditing = !isEditing;
Serial.print(F("Mode toggled: ")); Serial.println(isEditing ? F("EDIT") : F("NAV"));
} }
} }
}
// Check for Long Press (Start/Stop Playback)
if (buttonActive && !buttonConsumed && (millis() - buttonPressTime > 600)) {
isPlaying = !isPlaying;
buttonConsumed = true; // Prevent short press action
Serial.print(F("Playback: ")); Serial.println(isPlaying ? F("ON") : F("OFF"));
if (!isPlaying) {
// Send All Notes Off on stop (CC 123)
sendMidi(0xB0, 123, 0);
}
}
lastButtonState = reading; lastButtonState = reading;
} }
void handlePlayback() {
if (!isPlaying) return;
unsigned long interval = 15000 / tempo; // 16th notes (60000 / tempo / 4)
if (millis() - lastStepTime > interval) {
lastStepTime = millis();
// Note Off for current step (before advancing)
if (sequence[currentStep].note != -1) {
sendMidi(0x80, sequence[currentStep].note, 0);
}
currentStep++;
if (currentStep >= NUM_STEPS) currentStep = 0;
// Note On for new step
if (sequence[currentStep].note != -1) {
sendMidi(0x90, sequence[currentStep].note, 100);
}
// Auto-scroll logic is handled in drawUI based on currentStep
if (currentStep < scrollOffset) scrollOffset = currentStep;
if (currentStep >= scrollOffset + 6) scrollOffset = currentStep - 5;
}
}
void drawUI() { void drawUI() {
display.clearDisplay(); display.clearDisplay();
display.setTextSize(1); display.setTextSize(1);
@ -167,6 +469,16 @@ 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++) {
@ -244,7 +556,19 @@ 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();
drawUI(); drawUI();
updateLeds(); updateLeds();
delay(10); // Small delay to prevent screen tearing/excessive refresh delay(10); // Small delay to prevent screen tearing/excessive refresh