diff --git a/include/app_task.h b/include/app_task.h index 2169b3e..19fdebe 100644 --- a/include/app_task.h +++ b/include/app_task.h @@ -29,6 +29,14 @@ private: PadMapping pad_mapping[NUM_PADS]; bool last_switch_state[NUM_PADS]; + // SysEx reassembly buffer + static const uint8_t SYSEX_MAX_LEN = 64; + uint8_t sysex_buffer[SYSEX_MAX_LEN]; + uint8_t sysex_len = 0; + bool sysex_active = false; + void process_switch_event(uint8_t switch_id, bool pressed); void run_palette_test(); + void handle_sysex(const uint8_t* data, uint8_t len); + void process_sysex_packet(const uint8_t* packet, uint8_t cin); }; diff --git a/src/app_task.cpp b/src/app_task.cpp index b87c448..d2739d4 100644 --- a/src/app_task.cpp +++ b/src/app_task.cpp @@ -50,6 +50,14 @@ void AppTask::process_midi_event(const MidiEvent& event) { // (visible without serial when connected to iPad) led_driver->flash_activity(); + if (event.type == MidiEvent::SYSEX) { + // Cin is encoded in channel for SYSEX packets + uint8_t cin = event.channel; + uint8_t packet[3] = {event.data1, event.data2, 0}; + process_sysex_packet(packet, cin); + return; + } + uint8_t led_index = 0xFF; uint8_t midi_channel = event.channel; uint8_t midi_note = event.data1; @@ -137,3 +145,54 @@ void AppTask::process_switch_event(uint8_t switch_id, bool pressed) { } } } + +void AppTask::process_sysex_packet(const uint8_t* packet, uint8_t cin) { + // Cin values: 0x4=start/short, 0x5=continue, 0x6=end (2 bytes), 0x7=end (1 byte/3 bytes) + + if (cin == 0x4) { // SysEx start + sysex_active = true; + sysex_len = 0; + } + + if (!sysex_active || sysex_len >= SYSEX_MAX_LEN) return; + + // Add data bytes (skip F0/F7 which are handled by Cin) + sysex_buffer[sysex_len++] = packet[0]; + if (cin == 0x4 || cin == 0x5 || cin == 0x6) { + sysex_buffer[sysex_len++] = packet[1]; + } + + if (cin == 0x6 || cin == 0x7) { // SysEx end + sysex_active = false; + handle_sysex(sysex_buffer, sysex_len); + sysex_len = 0; + } +} + +void AppTask::handle_sysex(const uint8_t* data, uint8_t len) { + if (len < 7) return; + + // Check Novation SysEx header: F0 00 20 29 02 0C/0D ... + if (data[0] != 0x00 || data[1] != 0x20 || data[2] != 0x29 || data[3] != 0x02) { + return; + } + + uint8_t sub_id = data[4]; + uint8_t command = data[5]; + + Serial.printf("[APP] SysEx: sub=%02X cmd=%02X len=%d\n", sub_id, command, len); + + // Command 0x00 = Layout select, 0x0E = Programmer/Live mode + if (command == 0x00 && len >= 7) { // Layout select + uint8_t layout = data[6]; + Serial.printf("[APP] Layout select: %02X\n", layout); + + // 0x7F = Programmer mode + if (layout == 0x7F) { + Serial.println("[APP] Entered Programmer mode"); + } + } else if (command == 0x0E && len >= 7) { // Programmer/Live mode + uint8_t mode = data[6]; + Serial.printf("[APP] Programmer mode: %02X\n", mode); + } +} diff --git a/src/midi_transport.cpp b/src/midi_transport.cpp index c8639d5..81e9c7c 100644 --- a/src/midi_transport.cpp +++ b/src/midi_transport.cpp @@ -114,6 +114,10 @@ void UsbMidiTransport::parse_midi_packet(const uint8_t* buffer, uint32_t size, M event.data2 = buffer[3]; switch (cin) { + case 0x4: event.type = MidiEvent::SYSEX; break; // SysEx start/short + case 0x5: event.type = MidiEvent::SYSEX; break; // SysEx continue + case 0x6: event.type = MidiEvent::SYSEX; break; // SysEx end + case 0x7: event.type = MidiEvent::SYSEX; break; // SysEx end (single byte) case 0x8: event.type = MidiEvent::NOTE_OFF; break; case 0x9: event.type = (event.data2 > 0) ? MidiEvent::NOTE_ON : MidiEvent::NOTE_OFF;