Files
loopy_midi_controller/src/main.cpp
T
ash 500720dadf Add visual MIDI activity indicator
Flash LED 0 white for 30ms on any MIDI input.
Visible without serial - helps diagnose whether MIDI
is arriving when connected to iPad with Loopy Pro.
2026-06-25 06:43:00 +00:00

382 lines
14 KiB
C++

// Loopy MIDI Controller - Phase 1
// ESP32-S3 USB MIDI Foot Controller
#include <Arduino.h>
#include <driver/gpio.h>
#include <esp_rom_sys.h>
#include "midi_transport.h"
#include "Adafruit_TinyUSB.h"
#include "pixel_stomp_mux.h"
#include "led_stub.h"
#include "switch_stub.h"
#include "app_task.h"
PixelStompMux mux(12, 10, 11, 9);
DefaultLedStub led_driver;
DefaultSwitchStub switch_driver;
UsbMidiTransport midi_transport;
AppTask controller(&led_driver, &switch_driver, &midi_transport);
TaskHandle_t midi_task_handle = NULL;
void midi_task(void* parameter) {
Serial.println("[TASK] MIDI task started on core 0");
while (true) {
midi_transport.update();
vTaskDelay(1);
}
}
void handle_serial_command(const String& cmd) {
if (cmd == "dump" || cmd == "d") {
mux.dump();
} else if (cmd == "probe" || cmd == "p") {
mux.probe();
} else if (cmd == "ledon") {
for (int i = 0; i < PixelStompMux::NUM_LEDS; i++) {
mux.set_led_color(i, 255, 255, 255);
}
mux.show();
Serial.println("[CMD] All LEDs WHITE");
} else if (cmd == "ledoff") {
mux.clear_all();
} else if (cmd == "ledtest") {
uint32_t colors[] = {0xFF0000, 0x00FF00, 0x0000FF, 0xFFFF00, 0xFF00FF, 0x00FFFF};
for (int c = 0; c < 6; c++) {
uint8_t r = (colors[c] >> 16) & 0xFF;
uint8_t g = (colors[c] >> 8) & 0xFF;
uint8_t b = colors[c] & 0xFF;
for (int i = 0; i < PixelStompMux::NUM_LEDS; i++) {
mux.set_led_color(i, r, g, b);
}
mux.show();
delay(300);
}
mux.clear_all();
Serial.println("[CMD] LED test complete");
} else if (cmd == "read") {
uint16_t raw = mux.read_buttons();
Serial.printf("[CMD] Raw: 0x%04X (", raw);
for (int i = 15; i >= 0; i--) {
Serial.print((raw >> i) & 1);
}
Serial.println(")");
} else if (cmd == "red") {
for (int i = 0; i < PixelStompMux::NUM_LEDS; i++) mux.set_led_color(i, 255, 0, 0);
mux.show();
} else if (cmd == "green") {
for (int i = 0; i < PixelStompMux::NUM_LEDS; i++) mux.set_led_color(i, 0, 255, 0);
mux.show();
} else if (cmd == "blue") {
for (int i = 0; i < PixelStompMux::NUM_LEDS; i++) mux.set_led_color(i, 0, 0, 255);
mux.show();
} else if (cmd == "pixel0") {
mux.set_led_brightness(255);
mux.set_led_color(0, 255, 255, 255);
mux.show();
Serial.println("[CMD] Pixel 0 WHITE (max brightness)");
} else if (cmd == "pixel1") {
mux.set_led_brightness(255);
mux.set_led_color(1, 255, 255, 255);
mux.show();
Serial.println("[CMD] Pixel 1 WHITE (max brightness)");
} else if (cmd == "usb") {
Serial.printf("[CMD] USB mounted: %s\n", TinyUSBDevice.mounted() ? "YES" : "NO");
Serial.printf("[CMD] USB ready: %s\n", TinyUSBDevice.ready() ? "YES" : "NO");
} else if (cmd == "gpiotest") {
Serial.println("[CMD] === Raw GPIO Test ===");
uint8_t pins[] = {9, 10, 11, 12};
const char* names[] = {"DAT(9)", "LD(10)", "CLK(11)", "DI(12)"};
for (int p = 0; p < 4; p++) {
uint8_t pin = pins[p];
pinMode(pin, INPUT_PULLUP);
delay(10);
int hi = digitalRead(pin);
pinMode(pin, INPUT_PULLDOWN);
delay(10);
int lo = digitalRead(pin);
pinMode(pin, INPUT);
delay(10);
int fl = digitalRead(pin);
Serial.printf(" %s: PULLUP=%d PULLDOWN=%d FLOAT=%d\n", names[p], hi, lo, fl);
}
Serial.println(" Testing WS2812(12) output toggle...");
pinMode(12, OUTPUT);
for (int i = 0; i < 5; i++) {
digitalWrite(12, HIGH);
delayMicroseconds(100);
int readback = digitalRead(9);
Serial.printf(" HIGH -> readback=%d\n", readback);
digitalWrite(9, LOW);
delayMicroseconds(100);
readback = digitalRead(9);
Serial.printf(" LOW -> readback=%d\n", readback);
}
Serial.println("[CMD] === GPIO Test Complete ===");
} else if (cmd == "help") {
Serial.println("[CMD] Commands:");
Serial.println(" dump - show button states");
Serial.println(" probe - hardware diagnostic");
Serial.println(" ledon - all LEDs white");
Serial.println(" ledoff - all LEDs off");
Serial.println(" ledtest - colour cycle");
Serial.println(" read - raw button read");
Serial.println(" red/green/blue - solid colour");
Serial.println(" pixel0/pixel1 - single pixel test");
Serial.println(" usb - USB connection status and descriptor info");
Serial.println(" gpiotest - raw GPIO pin diagnostic");
Serial.println(" rawled - bit-bang WS2812 (no library)");
Serial.println(" anim - re-run startup animation from loop");
Serial.println(" miditest - simulate MIDI IN for common Launchpad layouts (fast)");
Serial.println(" padtest - test each pad individually (3s per pad, slow)");
Serial.println(" mapping - show current pad->note mapping");
} else if (cmd == "anim") {
Serial.println("[CMD] Running animation...");
uint32_t colors[] = {0xFF0000, 0x00FF00, 0x0000FF, 0xFFFF00, 0xFF00FF, 0x00FFFF, 0xFFFFFF};
for (int c = 0; c < 7; c++) {
uint8_t r = (colors[c] >> 16) & 0xFF;
uint8_t g = (colors[c] >> 8) & 0xFF;
uint8_t b = colors[c] & 0xFF;
Serial.printf(" Colour %d: #%06X\n", c, colors[c]);
for (int i = 0; i < PixelStompMux::NUM_LEDS; i++) mux.set_led_color(i, r, g, b);
mux.show();
delay(500);
}
mux.clear_all();
Serial.println("[CMD] Animation done");
} else if (cmd == "rawled") {
uint8_t pin = 12;
Serial.println("[CMD] Bit-bang WS2812: pixel 0 = RED on GPIO 9");
uint8_t colour[] = {0x00, 0xFF, 0x00};
noInterrupts();
for (int byte_idx = 0; byte_idx < 3; byte_idx++) {
uint8_t val = colour[byte_idx];
for (int bit = 7; bit >= 0; bit--) {
gpio_set_level((gpio_num_t)pin, 1);
if (val & (1 << bit)) {
esp_rom_delay_us(1);
}
gpio_set_level((gpio_num_t)pin, 0);
if (!(val & (1 << bit))) {
esp_rom_delay_us(1);
}
}
}
gpio_set_level((gpio_num_t)pin, 0);
esp_rom_delay_us(50);
interrupts();
Serial.println("[CMD] Sent. Wait 2 sec...");
delay(2000);
Serial.println("[CMD] Done");
} else if (cmd == "miditest") {
Serial.println("[CMD] Simulating MIDI IN for common Launchpad layouts...");
// Test 1: Launchpad X bottom row (notes 36-45, channel 1)
Serial.println("[CMD] Test 1: Launchpad X bottom row (notes 36-45, ch1)");
for (int i = 0; i < 10; i++) {
MidiEvent event;
event.type = MidiEvent::NOTE_ON;
event.channel = 1;
event.data1 = 36 + i;
event.data2 = 64; // Medium velocity (yellow)
controller.process_midi_event(event);
vTaskDelay(pdMS_TO_TICKS(200));
}
for (int i = 0; i < 10; i++) {
MidiEvent event;
event.type = MidiEvent::NOTE_OFF;
event.channel = 1;
event.data1 = 36 + i;
event.data2 = 0;
controller.process_midi_event(event);
}
// Test 2: Launchpad Mini Mk3 (notes 0-9, channel 1)
Serial.println("[CMD] Test 2: Launchpad Mini Mk3 (notes 0-9, ch1)");
for (int i = 0; i < 10; i++) {
MidiEvent event;
event.type = MidiEvent::NOTE_ON;
event.channel = 1;
event.data1 = i;
event.data2 = 64;
controller.process_midi_event(event);
vTaskDelay(pdMS_TO_TICKS(200));
}
for (int i = 0; i < 10; i++) {
MidiEvent event;
event.type = MidiEvent::NOTE_OFF;
event.channel = 1;
event.data1 = i;
event.data2 = 0;
controller.process_midi_event(event);
}
// Test 3: Channel 2 (flashing) notes 36-45
Serial.println("[CMD] Test 3: Channel 2 flashing (notes 36-45, ch2)");
for (int i = 0; i < 10; i++) {
MidiEvent event;
event.type = MidiEvent::NOTE_ON;
event.channel = 2;
event.data1 = 36 + i;
event.data2 = 64;
controller.process_midi_event(event);
vTaskDelay(pdMS_TO_TICKS(200));
}
for (int i = 0; i < 10; i++) {
MidiEvent event;
event.type = MidiEvent::NOTE_OFF;
event.channel = 2;
event.data1 = 36 + i;
event.data2 = 0;
controller.process_midi_event(event);
}
// Test 4: Channel 3 (pulsing) notes 36-45
Serial.println("[CMD] Test 4: Channel 3 pulsing (notes 36-45, ch3)");
for (int i = 0; i < 10; i++) {
MidiEvent event;
event.type = MidiEvent::NOTE_ON;
event.channel = 3;
event.data1 = 36 + i;
event.data2 = 64;
controller.process_midi_event(event);
vTaskDelay(pdMS_TO_TICKS(200));
}
for (int i = 0; i < 10; i++) {
MidiEvent event;
event.type = MidiEvent::NOTE_OFF;
event.channel = 3;
event.data1 = 36 + i;
event.data2 = 0;
controller.process_midi_event(event);
}
// Test 5: Full velocity sweep on pad 0
Serial.println("[CMD] Test 5: Velocity sweep on pad 0 (note 36)");
for (int v = 1; v <= 127; v += 8) {
MidiEvent event;
event.type = MidiEvent::NOTE_ON;
event.channel = 1;
event.data1 = 36;
event.data2 = v;
controller.process_midi_event(event);
vTaskDelay(pdMS_TO_TICKS(100));
}
MidiEvent event;
event.type = MidiEvent::NOTE_OFF;
event.channel = 1;
event.data1 = 36;
event.data2 = 0;
controller.process_midi_event(event);
Serial.println("[CMD] MIDI test complete");
} else if (cmd == "padtest") {
Serial.println("[CMD] Testing each pad individually (3s each)...");
// Test Launchpad X mapping (notes 36-45, ch1)
Serial.println("[CMD] Layout: Launchpad X (notes 36-45, ch1)");
for (int i = 0; i < 10; i++) {
Serial.printf("[CMD] Pad %d: note=%d ch=1\n", i, 36+i);
MidiEvent event;
event.type = MidiEvent::NOTE_ON;
event.channel = 1;
event.data1 = 36 + i;
event.data2 = 64;
controller.process_midi_event(event);
vTaskDelay(pdMS_TO_TICKS(3000));
event.type = MidiEvent::NOTE_OFF;
event.data2 = 0;
controller.process_midi_event(event);
vTaskDelay(pdMS_TO_TICKS(500));
}
// Test Launchpad Mini mapping (notes 0-9, ch1)
Serial.println("[CMD] Layout: Launchpad Mini (notes 0-9, ch1)");
for (int i = 0; i < 10; i++) {
Serial.printf("[CMD] Pad %d: note=%d ch=1\n", i, i);
MidiEvent event;
event.type = MidiEvent::NOTE_ON;
event.channel = 1;
event.data1 = i;
event.data2 = 64;
controller.process_midi_event(event);
vTaskDelay(pdMS_TO_TICKS(3000));
event.type = MidiEvent::NOTE_OFF;
event.data2 = 0;
controller.process_midi_event(event);
vTaskDelay(pdMS_TO_TICKS(500));
}
Serial.println("[CMD] Pad test complete");
} else if (cmd == "mapping") {
Serial.println("[CMD] Current pad mapping:");
for (int i = 0; i < 10; i++) {
// Can't access private pad_mapping directly, so just show defaults
Serial.printf(" Pad %d: note=%d ch=%d\n", i, 36+i, 1);
}
} else {
Serial.println("[CMD] Unknown command. Type 'help'");
}
}
void setup() {
Serial.begin(115200);
delay(2000);
Serial.println("=================================");
Serial.println(" Loopy MIDI Controller v0.1");
Serial.println(" Phase 1: USB MIDI");
Serial.println(" Board: ESP32-S3-WROOM-1");
Serial.println("=================================");
Serial.println("[INIT] Initializing PixelStomp MUX...");
mux.begin();
Serial.println("[INIT] Starting LED startup animation...");
led_driver.set_mux(&mux);
led_driver.begin();
Serial.println("[INIT] Initializing switches via MUX...");
switch_driver.set_mux(&mux);
switch_driver.begin();
Serial.println("[INIT] Initializing USB MIDI...");
midi_transport.begin();
Serial.println("[INIT] Registering MIDI callbacks...");
controller.begin();
xTaskCreatePinnedToCore(
midi_task,
"midi_task",
4096,
NULL,
3,
&midi_task_handle,
0
);
Serial.println("=================================");
Serial.println(" All systems ready");
Serial.println(" Type 'help' for diagnostics");
Serial.println("=================================");
Serial.flush();
}
void loop() {
led_driver.update();
controller.update();
if (Serial.available()) {
String cmd = Serial.readStringUntil('\n');
cmd.trim();
cmd.toLowerCase();
handle_serial_command(cmd);
}
delay(10);
}