Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
31cd488488 |
@@ -1,5 +0,0 @@
|
||||
.pio
|
||||
.vscode/.browse.c_cpp.db*
|
||||
.vscode/c_cpp_properties.json
|
||||
.vscode/launch.json
|
||||
.vscode/ipch
|
||||
Vendored
-10
@@ -1,10 +0,0 @@
|
||||
{
|
||||
// See http://go.microsoft.com/fwlink/?LinkId=827846
|
||||
// for the documentation about the extensions.json format
|
||||
"recommendations": [
|
||||
"platformio.platformio-ide"
|
||||
],
|
||||
"unwantedRecommendations": [
|
||||
"ms-vscode.cpptools-extension-pack"
|
||||
]
|
||||
}
|
||||
+2
-10
@@ -2,7 +2,6 @@
|
||||
|
||||
#include <cstdint>
|
||||
#include "midi_transport.h"
|
||||
#include "ble_midi_transport.h"
|
||||
#include "led_stub.h"
|
||||
#include "switch_stub.h"
|
||||
|
||||
@@ -15,7 +14,7 @@ struct PadMapping {
|
||||
|
||||
class AppTask {
|
||||
public:
|
||||
AppTask(LedStub* led, SwitchStub* sw, UsbMidiTransport* usb_midi, BleMidiTransport* ble_midi = nullptr);
|
||||
AppTask(LedStub* led, SwitchStub* sw, UsbMidiTransport* midi);
|
||||
|
||||
void begin();
|
||||
void update();
|
||||
@@ -24,13 +23,11 @@ public:
|
||||
private:
|
||||
LedStub* led_driver;
|
||||
SwitchStub* switch_driver;
|
||||
UsbMidiTransport* usb_midi;
|
||||
BleMidiTransport* ble_midi;
|
||||
UsbMidiTransport* midi_transport;
|
||||
|
||||
static const uint8_t NUM_PADS = 10;
|
||||
PadMapping pad_mapping[NUM_PADS];
|
||||
bool last_switch_state[NUM_PADS];
|
||||
uint8_t cc_map[NUM_PADS];
|
||||
|
||||
// SysEx reassembly buffer
|
||||
static const uint8_t SYSEX_MAX_LEN = 64;
|
||||
@@ -38,11 +35,6 @@ private:
|
||||
uint8_t sysex_len = 0;
|
||||
bool sysex_active = false;
|
||||
|
||||
bool pending_cc = false;
|
||||
uint8_t pending_channel = 0;
|
||||
uint8_t pending_cc_num = 0;
|
||||
uint8_t pending_value = 0;
|
||||
|
||||
void process_switch_event(uint8_t switch_id, bool pressed);
|
||||
void run_palette_test();
|
||||
void handle_sysex(const uint8_t* data, uint8_t len);
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include "midi_transport.h"
|
||||
|
||||
class BleMidiTransport {
|
||||
public:
|
||||
BleMidiTransport();
|
||||
~BleMidiTransport();
|
||||
|
||||
bool begin();
|
||||
void update();
|
||||
|
||||
void on_midi_receive(std::function<void(const MidiEvent&)> callback);
|
||||
|
||||
void send_note_on(uint8_t channel, uint8_t note, uint8_t velocity);
|
||||
void send_note_off(uint8_t channel, uint8_t note, uint8_t velocity);
|
||||
void send_cc(uint8_t channel, uint8_t cc, uint8_t value);
|
||||
|
||||
bool is_connected();
|
||||
|
||||
void send_midi_packet(const uint8_t* data, uint8_t len);
|
||||
void on_receive(const uint8_t* data, size_t len);
|
||||
size_t parse_ble_midi(uint8_t status, const uint8_t* data, size_t len, size_t offset, MidiEvent& event);
|
||||
|
||||
private:
|
||||
std::function<void(const MidiEvent&)> receive_callback;
|
||||
bool initialized;
|
||||
bool client_connected;
|
||||
};
|
||||
+6
-2
@@ -7,10 +7,11 @@ public:
|
||||
virtual ~LedStub() {}
|
||||
|
||||
virtual void begin() = 0;
|
||||
virtual void set_led_state(uint8_t note, uint8_t channel, uint8_t velocity, int8_t led_index = -1) = 0;
|
||||
virtual void set_led_state(uint8_t note, uint8_t channel, uint8_t velocity) = 0;
|
||||
virtual void clear_all() = 0;
|
||||
virtual void set_led_brightness(uint8_t brightness) = 0;
|
||||
virtual void flash_activity() {}
|
||||
virtual void flash_sysex() {}
|
||||
virtual void update() {}
|
||||
|
||||
virtual uint8_t note_to_index(uint8_t note) {
|
||||
@@ -35,14 +36,17 @@ private:
|
||||
bool initialized;
|
||||
uint32_t activity_off_time = 0;
|
||||
uint8_t saved_r = 0, saved_g = 0, saved_b = 0;
|
||||
uint8_t sysex_saved_r[10] = {0}, sysex_saved_g[10] = {0}, sysex_saved_b[10] = {0};
|
||||
bool sysex_flash_active = false;
|
||||
|
||||
public:
|
||||
DefaultLedStub();
|
||||
void begin() override;
|
||||
void set_led_state(uint8_t note, uint8_t channel, uint8_t velocity, int8_t led_index = -1) override;
|
||||
void set_led_state(uint8_t note, uint8_t channel, uint8_t velocity) override;
|
||||
void clear_all() override;
|
||||
void set_led_brightness(uint8_t brightness) override;
|
||||
void flash_activity() override;
|
||||
void flash_sysex() override;
|
||||
void update() override;
|
||||
|
||||
void set_mux(PixelStompMux* mux);
|
||||
|
||||
@@ -8,7 +8,6 @@ framework = arduino
|
||||
lib_deps =
|
||||
adafruit/Adafruit TinyUSB [email protected]
|
||||
fastled/FastLED@^3.9.0
|
||||
h2zero/NimBLE-Arduino@^1.4.0
|
||||
|
||||
build_unflags =
|
||||
-DARDUINO_USB_MODE=1
|
||||
@@ -17,12 +16,8 @@ build_flags =
|
||||
-DARDUINO_USB_MODE=0
|
||||
-DARDUINO_USB_CDC_ON_BOOT=1
|
||||
-DUSE_TINYUSB=1
|
||||
-Wno-macro-redefined
|
||||
-DMYNEWT_VAL_BLE_EXT_ADV=0
|
||||
|
||||
monitor_speed = 115200
|
||||
|
||||
board_build.partitions = default_8MB.csv
|
||||
board_build.arduino.memory_type = qio_opi
|
||||
|
||||
extra_scripts = pre:pre_build.py
|
||||
|
||||
-215
@@ -1,215 +0,0 @@
|
||||
import os
|
||||
import re
|
||||
|
||||
|
||||
def patch_usb_ids():
|
||||
# Find the core's pins_arduino.h for ESP32-S3 DevKitC-1
|
||||
core_variants = os.path.expanduser("~/.platformio/packages/framework-arduinoespressif32/variants")
|
||||
if not os.path.exists(core_variants):
|
||||
# Try PlatformIO default location
|
||||
core_variants = os.path.join(os.environ.get("PLATFORMIO_PACKAGES_DIR", ""), "framework-arduinoespressif32", "variants")
|
||||
|
||||
pins_file = os.path.join(core_variants, "esp32s3_devkitc", "pins_arduino.h")
|
||||
|
||||
if not os.path.exists(pins_file):
|
||||
# Try alternative variant name
|
||||
pins_file = os.path.join(core_variants, "esp32s3", "pins_arduino.h")
|
||||
|
||||
if os.path.exists(pins_file):
|
||||
print(f"Patching {pins_file} with JOC Midi USB descriptors")
|
||||
|
||||
# Read and replace
|
||||
with open(pins_file, 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
# Replace USB_VID and USB_PID
|
||||
content = content.replace(
|
||||
'#define USB_VID 0x303a',
|
||||
'#define USB_VID 0x1235'
|
||||
)
|
||||
content = content.replace(
|
||||
'#define USB_PID 0x1001',
|
||||
'#define USB_PID 0x0103'
|
||||
)
|
||||
# Add or replace manufacturer/product
|
||||
if 'USB_MANUFACTURER' not in content:
|
||||
content = content.replace(
|
||||
'#define USB_PID 0x0103',
|
||||
'#define USB_PID 0x0103\n#define USB_MANUFACTURER "JOC"\n#define USB_PRODUCT "JOC Midi"'
|
||||
)
|
||||
else:
|
||||
content = content.replace('USB_MANUFACTURER "Novation"', 'USB_MANUFACTURER "JOC"')
|
||||
content = content.replace('USB_PRODUCT "Launchpad X"', 'USB_PRODUCT "JOC Midi"')
|
||||
|
||||
with open(pins_file, 'w') as f:
|
||||
f.write(content)
|
||||
print("USB VID/PID patched successfully")
|
||||
else:
|
||||
print(f"WARNING: Could not find pins_arduino.h at {pins_file}")
|
||||
|
||||
|
||||
def patch_sdkconfig_bt():
|
||||
# Find the precompiled sdkconfig.h for this board variant
|
||||
sdk_dir = os.path.expanduser(
|
||||
"~/.platformio/packages/framework-arduinoespressif32/tools/sdk/esp32s3/qio_opi/include"
|
||||
)
|
||||
alt_name = os.path.join(os.environ.get("PLATFORMIO_PACKAGES_DIR", ""),
|
||||
"framework-arduinoespressif32/tools/sdk/esp32s3/qio_opi/include")
|
||||
if not os.path.exists(sdk_dir):
|
||||
sdk_dir = alt_name
|
||||
|
||||
sdkconfig_file = os.path.join(sdk_dir, "sdkconfig.h")
|
||||
|
||||
if not os.path.exists(sdkconfig_file):
|
||||
print(f"WARNING: sdkconfig.h not found at {sdkconfig_file}")
|
||||
return
|
||||
|
||||
print(f"Patching {sdkconfig_file} with BLE/NimBLE support")
|
||||
|
||||
with open(sdkconfig_file, 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
defines = {
|
||||
"CONFIG_BT_ENABLED": 1,
|
||||
"CONFIG_BTDM_CTRL_MODE_BLE_ONLY": 1,
|
||||
"CONFIG_BT_NIMBLE_MAX_CONNECTIONS": 1,
|
||||
"CONFIG_BT_NIMBLE_TASK_STACK_SIZE": 6144,
|
||||
}
|
||||
|
||||
changed = False
|
||||
for name, value in defines.items():
|
||||
pattern = re.compile(
|
||||
r'^[#/]*\s*#?\s*(define\s+' + re.escape(name) + r'\b).*$',
|
||||
re.MULTILINE
|
||||
)
|
||||
if pattern.search(content):
|
||||
# Already defined, replace the line
|
||||
content = pattern.sub(r'#define ' + name + ' ' + str(value), content)
|
||||
changed = True
|
||||
elif f'#define {name}' not in content:
|
||||
# Not present at all, add it
|
||||
content += f'\n#define {name} {value}'
|
||||
changed = True
|
||||
|
||||
if changed:
|
||||
with open(sdkconfig_file, 'w') as f:
|
||||
f.write(content)
|
||||
print("BLE/NimBLE sdkconfig patched successfully")
|
||||
else:
|
||||
print("BLE/NimBLE already configured in sdkconfig")
|
||||
|
||||
|
||||
def patch_nimconfig():
|
||||
# Find nimconfig.h in the NimBLE-Arduino library
|
||||
search_dirs = [
|
||||
os.path.join(os.getcwd(), ".pio", "libdeps"),
|
||||
os.path.expanduser("~/.platformio/lib"),
|
||||
]
|
||||
for env_dir in ["esp32s3", ""]:
|
||||
candidate = os.path.join(
|
||||
os.getcwd(), ".pio", "libdeps",
|
||||
env_dir, "NimBLE-Arduino", "src", "nimconfig.h"
|
||||
) if env_dir else ""
|
||||
if candidate and os.path.exists(candidate):
|
||||
nimconfig_path = candidate
|
||||
break
|
||||
else:
|
||||
# Walk search dirs as fallback
|
||||
nimconfig_path = None
|
||||
for base in search_dirs:
|
||||
if not os.path.exists(base):
|
||||
continue
|
||||
for root, _dirs, files in os.walk(base):
|
||||
if "nimconfig.h" in files:
|
||||
nimconfig_path = os.path.join(root, "nimconfig.h")
|
||||
break
|
||||
if nimconfig_path:
|
||||
break
|
||||
|
||||
if not nimconfig_path:
|
||||
print("WARNING: nimconfig.h not found, skipping ROLE define patch")
|
||||
return
|
||||
|
||||
print(f"Patching {nimconfig_path} with #ifndef guards for ROLE defines")
|
||||
|
||||
with open(nimconfig_path, 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
for role in ["CENTRAL", "OBSERVER", "PERIPHERAL", "BROADCASTER"]:
|
||||
pattern = re.compile(
|
||||
'#ifndef CONFIG_BT_NIMBLE_ROLE_' + role + '_DISABLED\n'
|
||||
'#define CONFIG_BT_NIMBLE_ROLE_' + role + '\n'
|
||||
'#endif'
|
||||
)
|
||||
replacement = (
|
||||
'#ifndef CONFIG_BT_NIMBLE_ROLE_' + role + '_DISABLED\n'
|
||||
'#ifndef CONFIG_BT_NIMBLE_ROLE_' + role + '\n'
|
||||
'#define CONFIG_BT_NIMBLE_ROLE_' + role + '\n'
|
||||
'#endif\n'
|
||||
'#endif'
|
||||
)
|
||||
if pattern.search(content):
|
||||
content = pattern.sub(replacement, content)
|
||||
print(f" Guarded CONFIG_BT_NIMBLE_ROLE_{role}")
|
||||
else:
|
||||
print(f" NOTE: CONFIG_BT_NIMBLE_ROLE_{role} pattern not found (may already be guarded)")
|
||||
|
||||
with open(nimconfig_path, 'w') as f:
|
||||
f.write(content)
|
||||
print("nimconfig.h patched successfully")
|
||||
|
||||
|
||||
def patch_nimble_device():
|
||||
search_dirs = [
|
||||
os.path.join(os.getcwd(), ".pio", "libdeps"),
|
||||
os.path.expanduser("~/.platformio/lib"),
|
||||
]
|
||||
dev_path = None
|
||||
for base in search_dirs:
|
||||
if not os.path.exists(base):
|
||||
continue
|
||||
for root, _dirs, files in os.walk(base):
|
||||
if "NimBLEDevice.cpp" in files:
|
||||
dev_path = os.path.join(root, "NimBLEDevice.cpp")
|
||||
break
|
||||
if dev_path:
|
||||
break
|
||||
|
||||
if not dev_path:
|
||||
print("WARNING: NimBLEDevice.cpp not found, skipping controller init patch")
|
||||
return
|
||||
|
||||
print(f"Patching {dev_path} to handle double BT controller init")
|
||||
|
||||
with open(dev_path, 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
# Arduino framework inits controller but does NOT enable it.
|
||||
# Handle both init outcomes: skip if already initialized, but always try to enable.
|
||||
patterns = [
|
||||
(r'(\s*)ESP_ERROR_CHECK\(esp_bt_controller_init\(&bt_cfg\)\);\s*'
|
||||
r'\1ESP_ERROR_CHECK\(esp_bt_controller_enable\(ESP_BT_MODE_BLE\)\);\s*'
|
||||
r'\1ESP_ERROR_CHECK\(esp_nimble_hci_init\(\)\);',
|
||||
r'\1esp_err_t __bt_err = esp_bt_controller_init(&bt_cfg);\n'
|
||||
r'\1if (__bt_err == ESP_OK || __bt_err == ESP_ERR_INVALID_STATE) {\n'
|
||||
r'\1 ESP_ERROR_CHECK(esp_bt_controller_enable(ESP_BT_MODE_BLE));\n'
|
||||
r'\1}\n'
|
||||
r'\1ESP_ERROR_CHECK(esp_nimble_hci_init());')
|
||||
]
|
||||
|
||||
for pattern, replacement in patterns:
|
||||
new_content = re.sub(pattern, replacement, content)
|
||||
if new_content != content:
|
||||
content = new_content
|
||||
with open(dev_path, 'w') as f:
|
||||
f.write(content)
|
||||
print(" Patched esp_bt_controller_init to skip if already initialized")
|
||||
return
|
||||
|
||||
print(" WARNING: Could not find init pattern in NimBLEDevice.cpp")
|
||||
|
||||
|
||||
patch_usb_ids()
|
||||
patch_sdkconfig_bt()
|
||||
patch_nimconfig()
|
||||
patch_nimble_device()
|
||||
@@ -1,7 +0,0 @@
|
||||
CONFIG_BT_ENABLED=y
|
||||
CONFIG_BT_NIMBLE_ENABLED=y
|
||||
CONFIG_BTDM_CTRL_MODE_BLE_ONLY=y
|
||||
CONFIG_BT_NIMBLE_MAX_CONNECTIONS=1
|
||||
CONFIG_BT_NIMBLE_TASK_STACK_SIZE=6144
|
||||
CONFIG_BT_NIMBLE_ROLE_PERIPHERAL=y
|
||||
CONFIG_BT_NIMBLE_ROLE_BROADCASTER=y
|
||||
+31
-44
@@ -1,19 +1,17 @@
|
||||
#include "app_task.h"
|
||||
#include <Arduino.h>
|
||||
|
||||
AppTask::AppTask(LedStub* led, SwitchStub* sw, UsbMidiTransport* usb_midi, BleMidiTransport* ble_midi)
|
||||
: led_driver(led), switch_driver(sw), usb_midi(usb_midi), ble_midi(ble_midi) {
|
||||
AppTask::AppTask(LedStub* led, SwitchStub* sw, UsbMidiTransport* midi)
|
||||
: led_driver(led), switch_driver(sw), midi_transport(midi) {
|
||||
|
||||
// Launchpad X standard: bottom row = notes 36-45 (C2 to A2) on channel 1
|
||||
const uint8_t launchpad_notes[10] = {36, 37, 38, 39, 40, 41, 42, 43, 44, 45};
|
||||
const uint8_t cc_assignments[10] = {112, 22, 113, 25, 114, 24, 115, 26, 116, 117};
|
||||
|
||||
for (uint8_t i = 0; i < NUM_PADS; i++) {
|
||||
pad_mapping[i].physical_switch = i;
|
||||
pad_mapping[i].midi_channel = 1;
|
||||
pad_mapping[i].midi_note = launchpad_notes[i];
|
||||
pad_mapping[i].led_index = i;
|
||||
cc_map[i] = cc_assignments[i];
|
||||
last_switch_state[i] = false;
|
||||
}
|
||||
}
|
||||
@@ -21,18 +19,11 @@ AppTask::AppTask(LedStub* led, SwitchStub* sw, UsbMidiTransport* usb_midi, BleMi
|
||||
void AppTask::begin() {
|
||||
Serial.println("[APP] Registering MIDI callbacks...");
|
||||
|
||||
auto handler = [this](const MidiEvent& event) {
|
||||
midi_transport->on_midi_receive([this](const MidiEvent& event) {
|
||||
process_midi_event(event);
|
||||
};
|
||||
usb_midi->on_midi_receive(handler);
|
||||
if (ble_midi) {
|
||||
ble_midi->on_midi_receive(handler);
|
||||
}
|
||||
});
|
||||
|
||||
Serial.println("[APP] Controller ready - CC mode");
|
||||
for (uint8_t i = 0; i < NUM_PADS; i++) {
|
||||
Serial.printf("[APP] Pad %d -> CC%d -> LED%d\n", i + 1, cc_map[i], i);
|
||||
}
|
||||
Serial.println("[APP] Controller ready - Launchpad X mode (notes 36-45, ch1)");
|
||||
}
|
||||
|
||||
void AppTask::update() {
|
||||
@@ -49,24 +40,22 @@ void AppTask::update() {
|
||||
last_switch_state[i] = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (pending_cc) {
|
||||
pending_cc = false;
|
||||
delay(1);
|
||||
usb_midi->send_cc(pending_channel, pending_cc_num, pending_value);
|
||||
if (ble_midi) ble_midi->send_cc(pending_channel, pending_cc_num, pending_value);
|
||||
}
|
||||
}
|
||||
|
||||
void AppTask::process_midi_event(const MidiEvent& event) {
|
||||
Serial.printf("[APP] MIDI IN: Type=%d Ch=%d Data1=%d Data2=%d\n",
|
||||
event.type, event.channel, event.data1, event.data2);
|
||||
|
||||
// Flash LED 0 white briefly on ANY MIDI input - visual activity indicator
|
||||
// (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);
|
||||
led_driver->flash_sysex();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -94,8 +83,7 @@ void AppTask::process_midi_event(const MidiEvent& event) {
|
||||
led_driver->set_led_state(
|
||||
midi_note,
|
||||
midi_channel,
|
||||
color_vel,
|
||||
led_index
|
||||
color_vel
|
||||
);
|
||||
|
||||
Serial.printf("[APP] NOTE -> LED: Ch%d Note%d Vel%d -> LED%d\n",
|
||||
@@ -108,27 +96,28 @@ void AppTask::process_midi_event(const MidiEvent& event) {
|
||||
Serial.printf("[APP] NOTE Ch%d ignored (not Launchpad channel 1-3)\n", midi_channel);
|
||||
}
|
||||
}
|
||||
// CONTROL_CHANGE: look up which pad this CC belongs to
|
||||
// CONTROL_CHANGE fallback for generic MIDI / Loopy Pro generic mode
|
||||
else if (event.type == MidiEvent::CONTROL_CHANGE) {
|
||||
uint8_t cc_num = event.data1;
|
||||
uint8_t cc_val = event.data2;
|
||||
|
||||
for (uint8_t i = 0; i < NUM_PADS; i++) {
|
||||
if (cc_map[i] == cc_num) {
|
||||
led_index = i;
|
||||
break;
|
||||
}
|
||||
// Map CC to pad: CC2-11 (Loopy Pro), CC0-9, CC36-45
|
||||
if (cc_num >= 2 && cc_num < 2 + NUM_PADS) {
|
||||
led_index = cc_num - 2;
|
||||
} else if (cc_num < NUM_PADS) {
|
||||
led_index = cc_num;
|
||||
} else if (cc_num >= 36 && cc_num < 36 + NUM_PADS) {
|
||||
led_index = cc_num - 36;
|
||||
}
|
||||
|
||||
if (led_index < NUM_PADS) {
|
||||
led_driver->set_led_state(
|
||||
pad_mapping[led_index].midi_note,
|
||||
pad_mapping[led_index].midi_channel,
|
||||
cc_val,
|
||||
led_index
|
||||
cc_val
|
||||
);
|
||||
Serial.printf("[APP] CC%d Val%d -> LED%d\n",
|
||||
cc_num, cc_val, led_index);
|
||||
Serial.printf("[APP] CC -> LED: Ch%d CC%d Val%d -> LED%d\n",
|
||||
midi_channel, cc_num, cc_val, led_index);
|
||||
} else {
|
||||
Serial.printf("[APP] CC Ch%d CC%d Val%d - no mapping\n",
|
||||
midi_channel, cc_num, cc_val);
|
||||
@@ -140,21 +129,19 @@ void AppTask::process_switch_event(uint8_t switch_id, bool pressed) {
|
||||
for (uint8_t i = 0; i < NUM_PADS; i++) {
|
||||
if (pad_mapping[i].physical_switch == switch_id) {
|
||||
uint8_t channel = pad_mapping[i].midi_channel;
|
||||
uint8_t cc_num = cc_map[i];
|
||||
uint8_t value = pressed ? 127 : 0;
|
||||
|
||||
Serial.printf("[APP] Switch %d -> Ch%d CC%d Val%d (%s)\n",
|
||||
switch_id, channel, cc_num, value,
|
||||
pressed ? "PRESS" : "RELEASE");
|
||||
// Loopy Pro Launchpad mode expects NOTE_ON/NOTE_OFF on notes 36-45
|
||||
uint8_t note = pad_mapping[i].midi_note;
|
||||
uint8_t velocity = pressed ? 127 : 0;
|
||||
|
||||
if (pressed) {
|
||||
// Send MIDI via a flag that loop() processes
|
||||
pending_cc = true;
|
||||
pending_channel = channel;
|
||||
pending_cc_num = cc_num;
|
||||
pending_value = value;
|
||||
midi_transport->send_note_on(channel, note, velocity);
|
||||
} else {
|
||||
midi_transport->send_note_off(channel, note, velocity);
|
||||
}
|
||||
|
||||
Serial.printf("[APP] Switch %d -> Ch%d Note%d Vel%d (%s)\n",
|
||||
switch_id, channel, note, velocity,
|
||||
pressed ? "PRESS" : "RELEASE");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,247 +0,0 @@
|
||||
#include "ble_midi_transport.h"
|
||||
#include <Arduino.h>
|
||||
#include <NimBLEDevice.h>
|
||||
#include <esp_bt.h>
|
||||
#include <esp_bt_main.h>
|
||||
#include <esp_bt_device.h>
|
||||
|
||||
#define BLE_MIDI_SERVICE_UUID "03B80E5A-EDE8-4B33-A751-6CE34EC4C700"
|
||||
#define BLE_MIDI_CHAR_UUID "7772E5DB-3868-4112-A1A9-F2669D106BF3"
|
||||
|
||||
static BleMidiTransport* instance = nullptr;
|
||||
|
||||
static NimBLEServer* ble_server = nullptr;
|
||||
static NimBLEService* ble_service = nullptr;
|
||||
static NimBLECharacteristic* ble_char = nullptr;
|
||||
static bool device_connected = false;
|
||||
|
||||
class ServerCallbacks : public NimBLEServerCallbacks {
|
||||
void onConnect(NimBLEServer* server) override {
|
||||
device_connected = true;
|
||||
Serial.println("[BLE] Client connected");
|
||||
}
|
||||
void onDisconnect(NimBLEServer* server) override {
|
||||
device_connected = false;
|
||||
Serial.println("[BLE] Client disconnected, restarting advertising");
|
||||
NimBLEDevice::startAdvertising();
|
||||
}
|
||||
};
|
||||
|
||||
class CharCallbacks : public NimBLECharacteristicCallbacks {
|
||||
void onWrite(NimBLECharacteristic* characteristic) override {
|
||||
std::string value = characteristic->getValue();
|
||||
if (value.length() > 0 && instance) {
|
||||
instance->on_receive((const uint8_t*)value.data(), value.length());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
BleMidiTransport::BleMidiTransport() : initialized(false), client_connected(false) {
|
||||
instance = this;
|
||||
}
|
||||
|
||||
BleMidiTransport::~BleMidiTransport() {
|
||||
if (instance == this) instance = nullptr;
|
||||
}
|
||||
|
||||
bool BleMidiTransport::begin() {
|
||||
Serial.println("[BLE] Initializing BLE MIDI...");
|
||||
|
||||
Serial.println("[BLE] NimBLEDevice init...");
|
||||
esp_bt_controller_status_t ctrl_status = esp_bt_controller_get_status();
|
||||
Serial.printf("[BLE] BT controller status before init: %d (0=IDLE,1=INITED,2=ENABLED)\n", ctrl_status);
|
||||
|
||||
NimBLEDevice::init("JOC Midi");
|
||||
Serial.println("[BLE] NimBLEDevice initialized");
|
||||
|
||||
ctrl_status = esp_bt_controller_get_status();
|
||||
Serial.printf("[BLE] BT controller status after init: %d\n", ctrl_status);
|
||||
|
||||
const uint8_t* mac = esp_bt_dev_get_address();
|
||||
if (mac) {
|
||||
Serial.printf("[BLE] Device MAC: %02X:%02X:%02X:%02X:%02X:%02X\n",
|
||||
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
|
||||
} else {
|
||||
Serial.println("[BLE] Device MAC: NOT AVAILABLE");
|
||||
}
|
||||
|
||||
Serial.println("[BLE] Creating server...");
|
||||
ble_server = NimBLEDevice::createServer();
|
||||
if (!ble_server) {
|
||||
Serial.println("[BLE] FAILED to create server");
|
||||
return false;
|
||||
}
|
||||
Serial.println("[BLE] Server created, setting callbacks...");
|
||||
ble_server->setCallbacks(new ServerCallbacks());
|
||||
|
||||
Serial.println("[BLE] Creating service...");
|
||||
ble_service = ble_server->createService(BLE_MIDI_SERVICE_UUID);
|
||||
if (!ble_service) {
|
||||
Serial.println("[BLE] FAILED to create service");
|
||||
return false;
|
||||
}
|
||||
|
||||
Serial.println("[BLE] Creating characteristic...");
|
||||
Serial.flush();
|
||||
ble_char = ble_service->createCharacteristic(
|
||||
BLE_MIDI_CHAR_UUID,
|
||||
NIMBLE_PROPERTY::READ |
|
||||
NIMBLE_PROPERTY::WRITE_NR |
|
||||
NIMBLE_PROPERTY::NOTIFY
|
||||
);
|
||||
Serial.println("[BLE] characteristic pointer ok");
|
||||
if (!ble_char) {
|
||||
Serial.println("[BLE] FAILED to create characteristic");
|
||||
return false;
|
||||
}
|
||||
// 0x2902 (CCCD) is auto-created by NimBLE stack when characteristic
|
||||
// has NOTIFY or INDICATE property - do NOT manually create it.
|
||||
Serial.println("[BLE] Setting callbacks...");
|
||||
ble_char->setCallbacks(new CharCallbacks());
|
||||
Serial.println("[BLE] Callbacks set");
|
||||
|
||||
Serial.println("[BLE] Starting service...");
|
||||
ble_service->start();
|
||||
Serial.println("[BLE] Service started");
|
||||
|
||||
Serial.println("[BLE] Starting advertising...");
|
||||
NimBLEAdvertising* adv = NimBLEDevice::getAdvertising();
|
||||
adv->addServiceUUID(BLE_MIDI_SERVICE_UUID);
|
||||
adv->setScanResponse(true);
|
||||
adv->setMinInterval(160); // 100ms (160 * 0.625ms)
|
||||
adv->setMaxInterval(320); // 200ms (320 * 0.625ms)
|
||||
bool adv_started = adv->start();
|
||||
Serial.printf("[BLE] adv->start() returned: %d\n", adv_started);
|
||||
|
||||
vTaskDelay(pdMS_TO_TICKS(500));
|
||||
|
||||
if (adv->isAdvertising()) {
|
||||
Serial.println("[BLE] Advertising confirmed started");
|
||||
} else {
|
||||
Serial.println("[BLE] WARNING: isAdvertising() reports false!");
|
||||
}
|
||||
|
||||
|
||||
initialized = true;
|
||||
Serial.println("[BLE] BLE MIDI advertising as 'JOC Midi'");
|
||||
return true;
|
||||
}
|
||||
|
||||
void BleMidiTransport::update() {
|
||||
client_connected = device_connected;
|
||||
}
|
||||
|
||||
void BleMidiTransport::on_midi_receive(std::function<void(const MidiEvent&)> callback) {
|
||||
receive_callback = callback;
|
||||
}
|
||||
|
||||
void BleMidiTransport::send_midi_packet(const uint8_t* data, uint8_t len) {
|
||||
if (!initialized || !client_connected || !ble_char) return;
|
||||
|
||||
uint16_t timestamp = micros() & 0x3FFF;
|
||||
uint8_t packet[16];
|
||||
uint8_t idx = 0;
|
||||
packet[idx++] = 0x80 | (timestamp >> 7);
|
||||
packet[idx++] = timestamp & 0x7F;
|
||||
for (uint8_t i = 0; i < len && idx < 16; i++) {
|
||||
packet[idx++] = data[i];
|
||||
}
|
||||
|
||||
ble_char->setValue(packet, idx);
|
||||
ble_char->notify();
|
||||
}
|
||||
|
||||
void BleMidiTransport::send_note_on(uint8_t channel, uint8_t note, uint8_t velocity) {
|
||||
uint8_t packet[3] = {(uint8_t)(0x90 | (channel - 1)), note, velocity};
|
||||
send_midi_packet(packet, 3);
|
||||
Serial.printf("[BLE OUT] Ch:%d NOTE_ON:%d:%d\n", channel, note, velocity);
|
||||
}
|
||||
|
||||
void BleMidiTransport::send_note_off(uint8_t channel, uint8_t note, uint8_t velocity) {
|
||||
uint8_t packet[3] = {(uint8_t)(0x80 | (channel - 1)), note, velocity};
|
||||
send_midi_packet(packet, 3);
|
||||
Serial.printf("[BLE OUT] Ch:%d NOTE_OFF:%d:%d\n", channel, note, velocity);
|
||||
}
|
||||
|
||||
void BleMidiTransport::send_cc(uint8_t channel, uint8_t cc, uint8_t value) {
|
||||
uint8_t packet[3] = {(uint8_t)(0xB0 | (channel - 1)), cc, value};
|
||||
send_midi_packet(packet, 3);
|
||||
}
|
||||
|
||||
bool BleMidiTransport::is_connected() {
|
||||
return initialized && client_connected;
|
||||
}
|
||||
|
||||
void BleMidiTransport::on_receive(const uint8_t* data, size_t len) {
|
||||
if (len < 3 || !receive_callback) return; // need timestamp(2) + status(1)
|
||||
|
||||
// BLE MIDI format: [ts_hi|0x80] [ts_lo] [status ...]
|
||||
size_t offset = 2; // skip timestamp header
|
||||
|
||||
while (offset < len) {
|
||||
uint8_t status = data[offset++];
|
||||
if (status < 0x80) continue; // skip non-status bytes
|
||||
|
||||
uint8_t type = status & 0xF0;
|
||||
uint8_t channel = (status & 0x0F) + 1;
|
||||
|
||||
MidiEvent event;
|
||||
event.channel = channel;
|
||||
event.timestamp = millis();
|
||||
event.type = MidiEvent::NOTE_ON; // default
|
||||
|
||||
offset = parse_ble_midi(status, data, len, offset, event);
|
||||
|
||||
if (event.type == MidiEvent::SYSEX) {
|
||||
Serial.printf("[BLE IN] Sysex len=%zu\n", len);
|
||||
} else {
|
||||
Serial.printf("[BLE IN] Ch:%d %s:%d:%d\n",
|
||||
event.channel,
|
||||
event.type == MidiEvent::NOTE_ON ? "NOTE_ON" :
|
||||
event.type == MidiEvent::NOTE_OFF ? "NOTE_OFF" :
|
||||
event.type == MidiEvent::CONTROL_CHANGE ? "CC" : "OTHER",
|
||||
event.data1, event.data2);
|
||||
}
|
||||
|
||||
if (receive_callback) {
|
||||
receive_callback(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
size_t BleMidiTransport::parse_ble_midi(uint8_t status, const uint8_t* data, size_t len, size_t offset, MidiEvent& event) {
|
||||
uint8_t type = status & 0xF0;
|
||||
event.data1 = 0;
|
||||
event.data2 = 0;
|
||||
|
||||
if (offset >= len) return offset;
|
||||
|
||||
event.data1 = data[offset++];
|
||||
|
||||
switch (type) {
|
||||
case 0x80:
|
||||
event.type = MidiEvent::NOTE_OFF;
|
||||
if (offset < len) event.data2 = data[offset++];
|
||||
break;
|
||||
case 0x90:
|
||||
event.type = MidiEvent::NOTE_ON;
|
||||
if (offset < len) event.data2 = data[offset++];
|
||||
if (event.data2 == 0) event.type = MidiEvent::NOTE_OFF;
|
||||
break;
|
||||
case 0xB0:
|
||||
event.type = MidiEvent::CONTROL_CHANGE;
|
||||
if (offset < len) event.data2 = data[offset++];
|
||||
break;
|
||||
case 0xC0:
|
||||
event.type = MidiEvent::PROGRAM_CHANGE;
|
||||
break;
|
||||
case 0xE0:
|
||||
event.type = MidiEvent::PITCH_BEND;
|
||||
if (offset < len) event.data2 = data[offset++];
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return offset;
|
||||
}
|
||||
+66
-30
@@ -129,13 +129,13 @@ static const uint32_t launchpad_palette[128] = {
|
||||
0x1E002F, // 118
|
||||
0x1B0029, // 119
|
||||
0x170023, // 120
|
||||
0x190033, // 121: dark purple-red (was near-black)
|
||||
0x3D0047, // 122: medium purple-red
|
||||
0x61005C, // 123: bright purple
|
||||
0x850070, // 124: brighter purple
|
||||
0x9900CC, // 125: bright magenta
|
||||
0xCC00FF, // 126: full magenta
|
||||
0xFF00FF, // 127: full magenta (Maximum velocity = visible)
|
||||
0x14001E, // 121
|
||||
0x110018, // 122
|
||||
0x0D0013, // 123
|
||||
0x0A000D, // 124
|
||||
0x070008, // 125
|
||||
0x030002, // 126
|
||||
0x000000, // 127: Off
|
||||
};
|
||||
|
||||
static uint32_t velocity_to_color(uint8_t velocity) {
|
||||
@@ -200,27 +200,9 @@ void DefaultLedStub::begin() {
|
||||
Serial.println("[LED] Startup complete");
|
||||
}
|
||||
|
||||
void DefaultLedStub::set_led_state(uint8_t note, uint8_t channel, uint8_t velocity, int8_t led_index) {
|
||||
void DefaultLedStub::set_led_state(uint8_t note, uint8_t channel, uint8_t velocity) {
|
||||
if (!initialized || !mux_ptr) return;
|
||||
|
||||
// Direct index mode (used by CC feedback)
|
||||
if (led_index >= 0 && led_index < NUM_LEDS) {
|
||||
led_states[led_index].active = (velocity > 0);
|
||||
led_states[led_index].note = note;
|
||||
led_states[led_index].channel = channel;
|
||||
led_states[led_index].velocity = velocity;
|
||||
led_states[led_index].timestamp = millis();
|
||||
uint32_t color = velocity_to_color(velocity);
|
||||
uint8_t r = (color >> 16) & 0xFF;
|
||||
uint8_t g = (color >> 8) & 0xFF;
|
||||
uint8_t b = color & 0xFF;
|
||||
mux_ptr->set_led_color(led_index, r, g, b);
|
||||
mux_ptr->show();
|
||||
Serial.printf("[LED] Set LED %d: note=%d ch=%d vel=%d\n", led_index, note, channel, velocity);
|
||||
return;
|
||||
}
|
||||
|
||||
// Note-match mode (used by NOTE_ON/OFF)
|
||||
for (int i = 0; i < NUM_LEDS; i++) {
|
||||
if (led_states[i].active && led_states[i].note == note) {
|
||||
led_states[i].channel = channel;
|
||||
@@ -278,8 +260,53 @@ void DefaultLedStub::set_led_brightness(uint8_t brightness) {
|
||||
}
|
||||
|
||||
void DefaultLedStub::flash_activity() {
|
||||
// No-op: activity flash on all MIDI input was confusing.
|
||||
// LED feedback is handled directly by set_led_state().
|
||||
if (!initialized || !mux_ptr) return;
|
||||
|
||||
uint32_t now = millis();
|
||||
|
||||
// Save current LED 0 color if we just started flashing
|
||||
if (now >= activity_off_time) {
|
||||
saved_r = 0;
|
||||
saved_g = 0;
|
||||
saved_b = 0;
|
||||
if (led_states[0].active) {
|
||||
uint32_t color = launchpad_palette[led_states[0].velocity];
|
||||
saved_r = (color >> 16) & 0xFF;
|
||||
saved_g = (color >> 8) & 0xFF;
|
||||
saved_b = color & 0xFF;
|
||||
}
|
||||
}
|
||||
|
||||
// Flash LED 0 white
|
||||
mux_ptr->set_led_color(0, 255, 255, 255);
|
||||
mux_ptr->show();
|
||||
activity_off_time = now + 50;
|
||||
}
|
||||
|
||||
void DefaultLedStub::flash_sysex() {
|
||||
if (!initialized || !mux_ptr) return;
|
||||
|
||||
// Save all LED states
|
||||
for (int i = 0; i < NUM_LEDS; i++) {
|
||||
if (led_states[i].active) {
|
||||
uint32_t color = launchpad_palette[led_states[i].velocity];
|
||||
sysex_saved_r[i] = (color >> 16) & 0xFF;
|
||||
sysex_saved_g[i] = (color >> 8) & 0xFF;
|
||||
sysex_saved_b[i] = color & 0xFF;
|
||||
} else {
|
||||
sysex_saved_r[i] = 0;
|
||||
sysex_saved_g[i] = 0;
|
||||
sysex_saved_b[i] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Flash ALL LEDs white
|
||||
for (int i = 0; i < NUM_LEDS; i++) {
|
||||
mux_ptr->set_led_color(i, 255, 255, 255);
|
||||
}
|
||||
mux_ptr->show();
|
||||
activity_off_time = now + 200;
|
||||
sysex_flash_active = true;
|
||||
}
|
||||
|
||||
void DefaultLedStub::update() {
|
||||
@@ -287,10 +314,19 @@ void DefaultLedStub::update() {
|
||||
|
||||
uint32_t now = millis();
|
||||
|
||||
// Turn off activity flash
|
||||
// Turn off activity/SysEx flash
|
||||
if (activity_off_time > 0 && now >= activity_off_time) {
|
||||
mux_ptr->set_led_color(0, saved_r, saved_g, saved_b);
|
||||
if (sysex_flash_active) {
|
||||
for (int i = 0; i < NUM_LEDS; i++) {
|
||||
mux_ptr->set_led_color(i, sysex_saved_r[i], sysex_saved_g[i], sysex_saved_b[i]);
|
||||
}
|
||||
sysex_flash_active = false;
|
||||
} else {
|
||||
mux_ptr->set_led_color(0, saved_r, saved_g, saved_b);
|
||||
}
|
||||
mux_ptr->show();
|
||||
activity_off_time = 0;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
+3
-10
@@ -5,7 +5,6 @@
|
||||
#include <driver/gpio.h>
|
||||
#include <esp_rom_sys.h>
|
||||
#include "midi_transport.h"
|
||||
#include "ble_midi_transport.h"
|
||||
#include "Adafruit_TinyUSB.h"
|
||||
#include "pixel_stomp_mux.h"
|
||||
#include "led_stub.h"
|
||||
@@ -17,9 +16,8 @@ PixelStompMux mux(12, 10, 11, 9);
|
||||
DefaultLedStub led_driver;
|
||||
DefaultSwitchStub switch_driver;
|
||||
UsbMidiTransport midi_transport;
|
||||
BleMidiTransport ble_midi_transport;
|
||||
|
||||
AppTask controller(&led_driver, &switch_driver, &midi_transport, &ble_midi_transport);
|
||||
AppTask controller(&led_driver, &switch_driver, &midi_transport);
|
||||
|
||||
TaskHandle_t midi_task_handle = NULL;
|
||||
|
||||
@@ -28,7 +26,6 @@ void midi_task(void* parameter) {
|
||||
|
||||
while (true) {
|
||||
midi_transport.update();
|
||||
ble_midi_transport.update();
|
||||
vTaskDelay(1);
|
||||
}
|
||||
}
|
||||
@@ -331,7 +328,7 @@ void setup() {
|
||||
|
||||
Serial.println("=================================");
|
||||
Serial.println(" Loopy MIDI Controller v0.1");
|
||||
Serial.println(" Phase 1.5: USB + BLE MIDI");
|
||||
Serial.println(" Phase 1: USB MIDI");
|
||||
Serial.println(" Board: ESP32-S3-WROOM-1");
|
||||
Serial.println("=================================");
|
||||
|
||||
@@ -346,12 +343,8 @@ void setup() {
|
||||
switch_driver.set_mux(&mux);
|
||||
switch_driver.begin();
|
||||
|
||||
Serial.println("[INIT] Initializing USB MIDI first (before BLE)...");
|
||||
Serial.println("[INIT] Initializing USB MIDI...");
|
||||
midi_transport.begin();
|
||||
delay(1000);
|
||||
|
||||
Serial.println("[INIT] Initializing BLE MIDI...");
|
||||
ble_midi_transport.begin();
|
||||
|
||||
Serial.println("[INIT] Registering MIDI callbacks...");
|
||||
controller.begin();
|
||||
|
||||
@@ -13,11 +13,11 @@ UsbMidiTransport::~UsbMidiTransport() {
|
||||
bool UsbMidiTransport::begin() {
|
||||
Serial.println("[MIDI] Setting up USB MIDI device...");
|
||||
|
||||
// Use Launchpad X VID/PID (0x1235/0x0103) so Loopy Pro recognizes us
|
||||
// but with our own name
|
||||
TinyUSBDevice.setManufacturerDescriptor("JOC");
|
||||
TinyUSBDevice.setProductDescriptor("JOC Midi");
|
||||
TinyUSBDevice.setSerialDescriptor("JOCMIDI001");
|
||||
// Novation Launchpad X identifiers so Loopy Pro recognizes us
|
||||
TinyUSBDevice.setID(0x1235, 0x0103);
|
||||
TinyUSBDevice.setManufacturerDescriptor("Novation");
|
||||
TinyUSBDevice.setProductDescriptor("Launchpad X");
|
||||
TinyUSBDevice.setSerialDescriptor("LPX00001");
|
||||
TinyUSBDevice.begin(0);
|
||||
|
||||
if (!usb_midi.begin()) {
|
||||
@@ -92,9 +92,9 @@ void UsbMidiTransport::send_note_off(uint8_t channel, uint8_t note, uint8_t velo
|
||||
|
||||
void UsbMidiTransport::send_cc(uint8_t channel, uint8_t cc, uint8_t value) {
|
||||
if (!initialized) return;
|
||||
if (!TinyUSBDevice.ready()) return;
|
||||
uint8_t packet[4] = {0x0B, (uint8_t)(0xB0 | (channel - 1)), cc, value};
|
||||
usb_midi.writePacket(packet);
|
||||
Serial.printf("[MIDI OUT] Ch:%d CC:%d:%d\n", channel, cc, value);
|
||||
}
|
||||
|
||||
bool UsbMidiTransport::is_connected() {
|
||||
|
||||
Reference in New Issue
Block a user