Add SysEx handling for Launchpad X Programmer mode

- Parse all SysEx CIN types (start, continue, end)
- Reassemble multi-packet SysEx messages
- Handle layout select (Programmer mode = 0x7F)
- Handle Programmer/Live mode switch
This commit is contained in:
2026-06-25 07:20:47 +00:00
parent 8d859ce0af
commit b5bbe24977
3 changed files with 71 additions and 0 deletions
+8
View File
@@ -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);
};
+59
View File
@@ -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);
}
}
+4
View File
@@ -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;