From db4b63c7558b3afb78492f447a5e4689de7a8de9 Mon Sep 17 00:00:00 2001 From: Ashley Strahle Date: Tue, 23 Jun 2026 08:55:42 +0000 Subject: [PATCH] Initial commit: Phase 1 skeleton --- components/controller/app_task.cpp | 110 ++++++++++++++++++++++++++ components/controller/app_task.h | 22 ++++++ components/hal/led_stub.cpp | 62 +++++++++++++++ components/hal/led_stub.h | 27 +++++++ components/hal/switch_stub.h | 24 ++++++ components/midi/midi_transport.cpp | 121 +++++++++++++++++++++++++++++ components/midi/midi_transport.h | 46 +++++++++++ main.cpp | 76 ++++++++++++++++++ 8 files changed, 488 insertions(+) create mode 100644 components/controller/app_task.cpp create mode 100644 components/controller/app_task.h create mode 100644 components/hal/led_stub.cpp create mode 100644 components/hal/led_stub.h create mode 100644 components/hal/switch_stub.h create mode 100644 components/midi/midi_transport.cpp create mode 100644 components/midi/midi_transport.h create mode 100644 main.cpp diff --git a/components/controller/app_task.cpp b/components/controller/app_task.cpp new file mode 100644 index 0000000..1f76ca6 --- /dev/null +++ b/components/controller/app_task.cpp @@ -0,0 +1,110 @@ +// components/controller/app_task.cpp +#include "controller/app_task.h" +#include "midi/midi_transport.h" +#include "esp_log.h" + +static const char* TAG = "app_task"; + +// Simple pad mapping table (Phase 1 - modifiable) +struct PadMapping { + uint8_t physical_switch; // 0-9 + uint8_t midi_channel; // 1-3 + uint8_t midi_note; // Note number (configurable) + uint8_t led_index; // LED index (0-9) +}; + +static PadMapping pad_mapping[] = { + {0, 1, 0, 0}, // Switch 0 -> Channel 1, Note 0, LED 0 + {1, 1, 1, 1}, // Switch 1 -> Channel 1, Note 1, LED 1 + {2, 1, 2, 2}, // Switch 2 -> Channel 1, Note 2, LED 2 + {3, 1, 3, 3}, // Switch 3 -> Channel 1, Note 3, LED 3 + {4, 1, 4, 4}, // Switch 4 -> Channel 1, Note 4, LED 4 + {5, 1, 5, 5}, // Switch 5 -> Channel 1, Note 5, LED 5 + {6, 1, 6, 6}, // Switch 6 -> Channel 1, Note 6, LED 6 + {7, 1, 7, 7}, // Switch 7 -> Channel 1, Note 7, LED 7 + {8, 1, 8, 8}, // Switch 8 -> Channel 1, Note 8, LED 8 + {9, 1, 9, 9}, // Switch 9 -> Channel 1, Note 9, LED 9 +}; + +static const uint8_t NUM_PADS = sizeof(pad_mapping) / sizeof(pad_mapping[0]); + +BaseType_t app_task(void* parameters) { + AppTaskParams* params = (AppTaskParams*)parameters; + + ESP_LOGI(TAG, "Controller task started"); + + while (true) { + // Check for MIDI events + MidiEvent midi_event; + if (xQueueReceive(params->midi_queue, &midi_event, 0) == pdPASS) { + app_process_midi_event(midi_event, params->led_driver); + } + + // Check for switch events (Phase 1 stub) + for (uint8_t i = 0; i < NUM_PADS; i++) { + bool is_pressed = params->switch_driver->is_pressed(i); + static bool last_state[10] = {false}; + + if (is_pressed && !last_state[i]) { + // Switch press detected + app_process_switch_event(i, true); + last_state[i] = true; + } else if (!is_pressed && last_state[i]) { + // Switch release detected + app_process_switch_event(i, false); + last_state[i] = false; + } + } + + vTaskDelay(pdMS_TO_TICKS(10)); // 10ms task period + } +} + +void app_process_midi_event(const MidiEvent& event, LedStub* led_driver) { + // Convert MIDI event to LED command + // This is where we map MIDI to LED state + uint8_t led_index = -1; + uint8_t midi_channel = event.channel; + uint8_t midi_note = event.data1; + uint8_t midi_velocity = event.data2; + + // Find matching LED index from pad mapping + for (uint8_t i = 0; i < NUM_PADS; i++) { + if (pad_mapping[i].midi_channel == midi_channel && + pad_mapping[i].midi_note == midi_note) { + led_index = pad_mapping[i].led_index; + break; + } + } + + if (led_index != 255) { + // Trigger LED state change + led_driver->set_led_state( + pad_mapping[led_index].midi_note, + pad_mapping[led_index].midi_channel, + event.type == MidiEvent::NOTE_ON ? midi_velocity : 0 + ); + + ESP_LOGI(TAG, "MIDI PROCESSED: Channel %d Note %d Velocity %d -> LED %d", + midi_channel, midi_note, midi_velocity, led_index); + } +} + +void app_process_switch_event(uint8_t switch_id, bool pressed) { + // Convert switch event to MIDI event + MidiEvent midi_event; + + // Find mapping for this switch + for (uint8_t i = 0; i < NUM_PADS; i++) { + if (pad_mapping[i].physical_switch == switch_id) { + midi_event.channel = pad_mapping[i].midi_channel; + midi_event.data1 = pad_mapping[i].midi_note; + midi_event.data2 = pressed ? 127 : 0; // Full velocity for press + midi_event.type = pressed ? MidiEvent::NOTE_ON : MidiEvent::NOTE_OFF; + + ESP_LOGI(TAG, "SWITCH EVENT: Switch %d -> Channel %d Note %d Velocity %d", + switch_id, midi_event.channel, midi_event.data1, midi_event.data2); + break; + } + } +} \ No newline at end of file diff --git a/components/controller/app_task.h b/components/controller/app_task.h new file mode 100644 index 0000000..4944447 --- /dev/null +++ b/components/controller/app_task.h @@ -0,0 +1,22 @@ +// components/controller/app_task.h +#pragma once + +#include +#include +#include "hal/led_stub.h" +#include "hal/switch_stub.h" + +// Application task parameters +struct AppTaskParams { + LedStub* led_driver; + SwitchStub* switch_driver; + QueueHandle_t midi_queue; +}; + +// Application task function +BaseType_t app_task(void* parameters); + +// Application state management +void app_process_midi_event(const MidiEvent& event); +void app_process_switch_event(uint8_t switch_id, bool pressed); +void app_initialize_config(); \ No newline at end of file diff --git a/components/hal/led_stub.cpp b/components/hal/led_stub.cpp new file mode 100644 index 0000000..0ac9408 --- /dev/null +++ b/components/hal/led_stub.cpp @@ -0,0 +1,62 @@ +// hal/led_stub.cpp +#include "hal/led_stub.h" +#include "esp_log.h" + +static const char* TAG = "led_stub"; + +class DefaultLedStub : public LedStub { +private: + LedState led_states[10]; // Support up to 10 LEDs + bool initialized; + +public: + DefaultLedStub() : initialized(false) { + // Initialize all LEDs to off state + for (int i = 0; i < 10; i++) { + led_states[i].active = false; + led_states[i].velocity = 0; + } + } + + void begin() override { + // GPIO initialization would go here + // For Phase 1, this is a stub + initialized = true; + ESP_LOGI(TAG, "LED stub initialized (GPIO pins not configured yet)"); + } + + void set_led_state(uint8_t note, uint8_t channel, uint8_t velocity) override { + if (!initialized) return; + + // For Phase 1, we assume note 0-9 maps directly to LED 0-9 + // This is configurable in the PadMapping + uint8_t led_index = note_to_index(note); + + if (led_index < 10) { + led_states[led_index].note = note; + led_states[led_index].channel = channel; + led_states[led_index].velocity = velocity; + led_states[led_index].active = (velocity > 0); + led_states[led_index].timestamp = 0; // TODO: Add proper timestamp + + ESP_LOGI(TAG, "LED STATE: Note %d -> LED %d Channel %d Velocity %d (%s)", + note, led_index, channel, velocity, + velocity > 0 ? "ON" : "OFF"); + } else { + ESP_LOGW(TAG, "LED index out of range: %d (Note: %d)", led_index, note); + } + } + + void clear_all() override { + for (int i = 0; i < 10; i++) { + led_states[i].active = false; + led_states[i].velocity = 0; + } + ESP_LOGI(TAG, "All LEDs cleared"); + } +}; + +// Factory function to create the default LED stub +LedStub* create_led_stub() { + return new DefaultLedStub(); +} \ No newline at end of file diff --git a/components/hal/led_stub.h b/components/hal/led_stub.h new file mode 100644 index 0000000..d82dd70 --- /dev/null +++ b/components/hal/led_stub.h @@ -0,0 +1,27 @@ +// hal/led_stub.h +#pragma once + +class LedStub { +public: + virtual ~LedStub() {} + + virtual void begin() = 0; + virtual void set_led_state(uint8_t note, uint8_t channel, uint8_t velocity) = 0; + virtual void clear_all() = 0; + + // Helper function to map MIDI note to LED index + virtual uint8_t note_to_index(uint8_t note) { + // Default implementation - direct mapping + // Can be overridden by specific implementations + return note; + } +}; + +// LED state structure +struct LedState { + uint8_t note; // Launchpad note + uint8_t channel; // LED channel (1-3) + uint8_t velocity; // Color/brightness (0-127) + uint32_t timestamp; // When state was set + bool active; // Current on/off state +}; \ No newline at end of file diff --git a/components/hal/switch_stub.h b/components/hal/switch_stub.h new file mode 100644 index 0000000..93daefd --- /dev/null +++ b/components/hal/switch_stub.h @@ -0,0 +1,24 @@ +// hal/switch_stub.h +#pragma once + +class SwitchStub { +public: + virtual ~SwitchStub() {} + + virtual void begin() = 0; + virtual bool is_pressed(uint8_t switch_id) = 0; + + // Configuration methods + virtual void configure_switch(uint8_t switch_id, uint8_t gpio_pin) = 0; + virtual void set_debounce_time(uint32_t time_ms) = 0; +}; + +// Switch state structure +struct SwitchState { + uint8_t id; // Switch identifier + uint8_t gpio_pin; // GPIO pin (if applicable) + bool current_state; // Current pressed state + bool previous_state; // Previous state (for debounce) + uint32_t last_change_time; // Timestamp of last state change + uint32_t debounce_time; // Debounce time in ms +}; \ No newline at end of file diff --git a/components/midi/midi_transport.cpp b/components/midi/midi_transport.cpp new file mode 100644 index 0000000..9ad7d8e --- /dev/null +++ b/components/midi/midi_transport.cpp @@ -0,0 +1,121 @@ +// midi/midi_transport.cpp +#include "midi/midi_transport.h" +#include "esp_log.h" +#include "tusb.h" +#include "class/midi/midi.h" + +static const char* TAG = "midi_transport"; + +UsbMidiTransport::UsbMidiTransport() : event_queue(nullptr), initialized(false) { +} + +UsbMidiTransport::~UsbMidiTransport() { + if (event_queue != NULL) { + vQueueDelete(event_queue); + } +} + +bool UsbMidiTransport::begin() { + // Create event queue + event_queue = xQueueCreate(32, sizeof(MidiEvent)); + if (event_queue == NULL) { + ESP_LOGE(TAG, "Failed to create event queue"); + return false; + } + + // Initialize TinyUSB MIDI + tusb_init(); + + // Configure USB device descriptors + tusb_device_set_string(1, "Loopy Foot Controller"); + + // Register MIDI callback + tuh_midi_set_cb(usb_midi_callback); + + initialized = true; + ESP_LOGI(TAG, "USB MIDI transport initialized"); + return true; +} + +void UsbMidiTransport::task() { + if (!initialized) return; + + // Process USB MIDI events + while (tuh_uart_read_available()) { + uint8_t buffer[128]; + uint32_t bytes_read = tuh_midi_read_packet(buffer, sizeof(buffer)); + + if (bytes_read > 0) { + MidiEvent event; + parse_midi_packet(buffer, bytes_read, event); + + // Log incoming event + log_incoming("USB", event); + + // Send to event queue + if (xQueueSend(event_queue, &event, portMAX_DELAY) != pdPASS) { + ESP_LOGW(TAG, "Failed to queue MIDI event"); + } + } + } +} + +void usb_midi_callback(const uint8_t* event, uint32_t size) { + // This callback is called by TinyUSB when MIDI data is received + // For now, we'll implement a simple version + // In a full implementation, this would parse the MIDI packet + + MidiEvent midi_event; + // TODO: Implement actual MIDI parsing based on event type + // For Phase 1, we'll handle basic Note On/Off messages +} + +void UsbMidiTransport::log_incoming(const char* source, const MidiEvent& event) { + const char* type_str; + switch (event.type) { + case MidiEvent::NOTE_ON: type_str = "NOTE_ON"; break; + case MidiEvent::NOTE_OFF: type_str = "NOTE_OFF"; break; + case MidiEvent::CONTROL_CHANGE: type_str = "CONTROL_CHANGE"; break; + default: type_str = "UNKNOWN"; break; + } + + ESP_LOGI(TAG, "MIDI IN: %s Channel: %d Type: %s Note: %d Velocity: %d", + source, event.channel, type_str, event.data1, event.data2); +} + +void UsbMidiTransport::parse_midi_packet(const uint8_t* buffer, uint32_t size, MidiEvent& event) { + // Simple MIDI parser for basic messages + // This is a simplified version for Phase 1 + + if (size < 2) return; + + uint8_t status = buffer[0]; + uint8_t type = status & 0xF0; // Message type + uint8_t channel = status & 0x0F; // Channel (0-15, but MIDI uses 1-16) + + event.channel = channel + 1; // Convert to 1-16 range + + switch (type) { + case 0x90: // Note On + event.type = MidiEvent::NOTE_ON; + event.data1 = buffer[1]; + event.data2 = buffer[2]; + break; + + case 0x80: // Note Off + event.type = MidiEvent::NOTE_OFF; + event.data1 = buffer[1]; + event.data2 = buffer[2]; + break; + + case 0xB0: // Control Change + event.type = MidiEvent::CONTROL_CHANGE; + event.data1 = buffer[1]; + event.data2 = buffer[2]; + break; + + default: + // Unknown message type - ignore for now + return; + } +} \ No newline at end of file diff --git a/components/midi/midi_transport.h b/components/midi/midi_transport.h new file mode 100644 index 0000000..ab2df82 --- /dev/null +++ b/components/midi/midi_transport.h @@ -0,0 +1,46 @@ +// midi/midi_transport.h +#pragma once + +#include +#include +#include + +struct MidiEvent { + enum Type { + NOTE_ON, + NOTE_OFF, + CONTROL_CHANGE, + PROGRAM_CHANGE, + PITCH_BEND, + AFTERTOUCH_POLY, + AFTERTOUCH_CHAN, + SYSEX + } type; + + uint8_t channel; // MIDI channel (1-16) + uint8_t data1; // Note number or CC number + uint8_t data2; // Velocity or CC value + uint32_t timestamp; // Event timestamp +}; + +class UsbMidiTransport { +public: + UsbMidiTransport(); + ~UsbMidiTransport(); + + bool begin(); + void task(); + + // Event queue for communication with controller task + QueueHandle_t get_event_queue() const { return event_queue; } + + // Diagnostic logging + void log_incoming(const char* source, const MidiEvent& event); + +private: + QueueHandle_t event_queue; + bool initialized; +}; + +// Forward declaration for USB callback +void usb_midi_callback(const uint8_t* event, uint32_t size); \ No newline at end of file diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..62e91a1 --- /dev/null +++ b/main.cpp @@ -0,0 +1,76 @@ +// main.cpp - Entry point for ESP32-S3 FreeRTOS application +// Phase 1: USB MIDI + Basic Event Processing + +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "esp_system.h" +#include "esp_log.h" +#include "driver/gpio.h" + +// Component includes +#include "midi/midi_transport.h" +#include "controller/app_task.h" +#include "hal/led_stub.h" +#include "hal/switch_stub.h" + +// Logging tag +static const char *TAG = "loopy_midi_controller"; + +// FreeRTOS task handles +static TaskHandle_t usb_midi_task_handle = NULL; +static TaskHandle_t controller_task_handle = NULL; + +extern "C" void app_main(void) { + ESP_LOGI(TAG, "Starting Loopy MIDI Controller (Phase 1)"); + ESP_LOGI(TAG, "Device name: Loopy Foot Controller"); + + // Initialize hardware stubs (Phase 1 - no real hardware yet) + LedStub led_driver; + SwitchStub switch_driver; + + led_driver.begin(); + switch_driver.begin(); + + // Initialize MIDI transport (USB) + UsbMidiTransport midi_transport; + midi_transport.begin(); + + // Create USB MIDI task (High priority) + BaseType_t usb_midi_result = xTaskCreate( + usb_midi_task, + "usb_midi_task", + 4096, + (void*)&midi_transport, + tskIDLE_PRIORITY + 3, + &usb_midi_task_handle + ); + + if (usb_midi_result != pdPASS) { + ESP_LOGE(TAG, "Failed to create USB MIDI task"); + return; + } + + // Create Controller task (Lower priority) + AppTaskParams app_params; + app_params.led_driver = &led_driver; + app_params.switch_driver = &switch_driver; + app_params.midi_queue = midi_transport.get_event_queue(); + + BaseType_t controller_result = xTaskCreate( + app_task, + "controller_task", + 4096, + (void*)&app_params, + tskIDLE_PRIORITY + 1, + &controller_task_handle + ); + + if (controller_result != pdPASS) { + ESP_LOGE(TAG, "Failed to create Controller task"); + return; + } + + ESP_LOGI(TAG, "Loopy MIDI Controller initialized successfully"); + ESP_LOGI(TAG, "Phase 1 complete: USB MIDI device ready"); +} \ No newline at end of file