500720dadf
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.
382 lines
14 KiB
C++
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);
|
|
}
|