Compare commits

...
Author SHA1 Message Date
ash 1bf2ea9f2f Swap init order: USB before BLE to avoid PHY interference
BLE radio turning on corrupts USB PHY when TinyUSB initializes after it.
Initialize USB first (stable USB), then BLE radio.
2026-07-01 07:13:47 +00:00
ash 00337559ac Add BLE diagnostics: controller status, MAC, advertising result
Remove getMinInterval/getMaxInterval (not available in v1.4.0).
Add esp_bt_controller_get_status() before/after init.
Add esp_bt_dev_get_address() for MAC address.
Print adv->start() return value.
Add explicit advertising interval settings (100-200ms).
2026-07-01 07:09:30 +00:00
ash 9ba48b3f9c Roll back to 2d3a5a9 + CONFIG_BT_ENABLED + double-init patch 2026-07-01 07:06:45 +00:00
ash 01eaf9ce80 Fix enable-after-Arduino-init: always try to enable
Arduino framework inits BT controller but does NOT enable it.
Previous patch skipped both init AND enable when init returned
ESP_ERR_INVALID_STATE, leaving controller in init'd-but-not-enabled
state. Now we always try to enable regardless of init outcome.
2026-07-01 07:01:39 +00:00
ash c26116a7ef Set CONFIG_BT_ENABLED + re-add double-init patch
Without CONFIG_BT_ENABLED in sdkconfig.h, NimBLEDevice.cpp lines
are excluded by #if defined(CONFIG_BT_ENABLED) preprocessor guard,
making NimBLEDevice::init() a no-op. No BT controller init ever
occurs -> no RF calibration -> invisible BLE advertising.

With CONFIG_BT_ENABLED: Arduino framework pre-inits BT controller,
then NimBLEDevice::init() double-inits -> ESP_ERROR_CHECK abort.
Fix: patch NimBLEDevice.cpp to check return code and skip init if
already running.
2026-07-01 06:57:19 +00:00
ash fec2ea1db2 Move all MIDI processing to Core 0
Move controller.update() inside midi_task on Core 0 to eliminate
TinyUSB cross-core race condition. Previously:
 - Core 1 (loop()): usb_midi.writePacket() in controller.update()
 - Core 0 (midi_task): usb_midi.available()/readPacket()
This race condition causes crashes on button press.

Now:
 - Core 0 (midi_task): midi_transport.update() + controller.update()
 - Core 1 (loop()): led_driver.update() + serial commands only
2026-07-01 06:51:48 +00:00
ash 762500e227 Fix BLE crash: move all NimBLE calls to Core 0
Root cause: Arduino Core 3.x breaks cross-core NimBLE API calls.
Calling BLEMidiServer.controlChange() from loop() on Core 1 while
NimBLE host runs on Core 0 causes immediate crash.

Fix:
- Move ble_midi_transport.begin() to ble_init_task on Core 0
- All BLE MIDI sends use queue_cc() (cross-core safe flag setter)
- ble_midi_transport.update() processes queued sends on Core 0
- Pin NimBLE-Arduino to v1.4.3 (stable LTS for BLE MIDI)
- Keep CONFIG_BT_ENABLED out of sdkconfig.h (causes double-init)
2026-07-01 06:46:15 +00:00
ash 62d1e430d5 Switch from NimBLE-Arduino to ESP32-BLE-MIDI library
Replace direct NimBLE-Arduino dependency with max22/ESP32-BLE-MIDI
wrapper library (v0.3.2). Rewrite ble_midi_transport to use
BLEMidiServer API instead of raw NimBLEDevice calls.

Removes patch_nimble_device() from pre_build.py — no longer needed
since BT controller init is managed by the library internally.

The library uses NimBLE v1.4.1 internally and configures advertising
without setScanResponse() or setName(), which may resolve the invisible
BLE advertising issue on ESP32-S3.
2026-07-01 06:34:36 +00:00
ash 1517eda13e add CONFIG_BT_ENABLED to sdkconfig.h for proper BT RF calibration; patch NimBLEDevice.cpp to skip controller init if already done by Arduino framework 2026-07-01 06:11:28 +00:00
ash 855c97d8c6 revert to NimBLE-Arduino v1.4.0 (v2.2.3 caused button crash regression); restore v1.4.0 callback signatures, service->start(), and setScanResponse 2026-07-01 04:47:22 +00:00
ash 993a09fa7c fix v2.2.3 API: update callback signatures (onConnect, onDisconnect, onWrite) and remove deprecated service->start() 2026-07-01 04:30:50 +00:00
ash 18844c17c0 upgrade NimBLE-Arduino to v2.2.3 for proper ESP32-S3 support; fix setScanResponse->enableScanResponse API change 2026-07-01 04:27:22 +00:00
ash 7530772acd add adv->setName, remove diagnostics and ext adv flag; revert to stable 2026-07-01 04:18:00 +00:00
ash 94567e3485 fix: NimBLEDevice::init() returns void, remove bool check 2026-07-01 04:02:04 +00:00
ash beb59da3e0 add init() return check, controller status, and MAC address debug 2026-06-30 22:07:06 +00:00
ash 2d3a5a9031 queue MIDI sends with delay(1) to avoid race; force legacy advertising 2026-06-30 21:59:55 +00:00
ash 72a10fa13a add TinyUSBDevice.ready() guard in send_cc to prevent crash 2026-06-30 13:55:53 +00:00
ash d8c109d9e5 remove setName and health check (caused crashes), add isAdvertising() check 2026-06-30 13:14:19 +00:00
ash 5ee5949420 add adv->setName(JOC Midi) and health check to restart advertising 2026-06-30 12:55:29 +00:00
ash 8515963e07 pre_build.py: remove CONFIG_BT_ENABLED from sdkconfig patching (fixes BT pre-init conflict with USB) 2026-06-30 12:34:00 +00:00
ash e2db658037 TEST: disable BLE init to isolate button press crash 2026-06-30 12:12:50 +00:00
ash 8833e08c0a add debug prints before switch read and in process_switch_event; skip MIDI sends 2026-06-30 08:54:00 +00:00
ash 3e92d6bb7c swap init order (USB before BLE), add USB ready check and debug prints 2026-06-30 08:48:04 +00:00
ash be8b0dc22d add debug prints around button press to find crash location 2026-06-30 08:35:47 +00:00
ash 6038b3bf58 remove health check - back to exact source of working commit 6e3173c 2026-06-30 07:59:07 +00:00
ash 1f7bdc1e3a revert setName, keep health check 2026-06-30 07:55:10 +00:00
ash af6fd2dc26 fix: add adv->setName, add periodic health check to diagnose invisible BLE device 2026-06-30 07:44:04 +00:00
ash 6e3173c9b8 fix: remove manual 0x2902/CCCD descriptor creation - NimBLE auto-creates it for NOTIFY characteristics; asserts on manual create 2026-06-30 07:37:48 +00:00
ash d19d1acbdd chore: add granular debug prints around BLE characteristic/descriptor creation to pinpoint crash 2026-06-30 07:31:34 +00:00
ash a21c7bf4d3 fix: remove manual BT controller pre-init - NimBLE-Arduino 1.4.0 already handles ESP32-S3 internally; double-init caused ESP_ERROR_CHECK abort 2026-06-30 07:24:34 +00:00
ash 6a737bcee2 fix: patch nimconfig.h with #ifndef guards for ROLE defines to eliminate redefinition warnings 2026-06-30 07:15:14 +00:00
ash 323bc072ee fix: remove redundant ROLE defines from pre_build.py, suppress -Wno-macro-redefined 2026-06-30 07:09:15 +00:00
ash 13aef3b2a0 Init BT controller with ESP_BT_MODE_BLE before NimBLEDevice::init to avoid BTDM crash on ESP32-S3 2026-06-30 06:59:32 +00:00
ash 68efe30682 Remove redundant manual BT controller init; let NimBLEDevice::init handle it internally with BLE-only mode 2026-06-30 06:49:43 +00:00
ash 2db09b76bf Add BT controller diagnostics, init BLE before USB MIDI 2026-06-30 06:43:58 +00:00
ash 8a36296043 Add sdkconfig.defaults for BLE, remove bad CONFIG flags, increase stack 2026-06-30 06:36:40 +00:00
ash eab0b76c9d Fix: NimBLEDevice::init returns void, remove bool check 2026-06-30 06:30:58 +00:00
ash 95d182cda6 Replace NimBLE2902 with createDescriptor for CCCD 2026-06-30 06:29:30 +00:00
ash c3bbf08768 Fix: use just NimBLEDevice.h, remove missing NimBLE2902.h 2026-06-30 06:25:25 +00:00
ash f5eca2c7bc Switch to NimBLE stack, add BT build flags, add error checking 2026-06-30 06:24:19 +00:00
ash 892e4fe061 Fix: on_receive public, explicit casts for narrowing 2026-06-30 06:19:10 +00:00
ash aea3a28206 Add BLE MIDI transport (feature_bluetooth) 2026-06-30 06:13:36 +00:00
ash ae2891fca8 Fix palette: entries 121-127 now visible (was black). Remove local flash. 2026-06-30 05:05:06 +00:00
ash d59e36a9a5 Add direct local LED feedback on button press 2026-06-30 04:58:22 +00:00
ash 063fa2065d Send visible palette index 21 (yellow) instead of 127 (black) so feedback is visible 2026-06-30 04:49:30 +00:00
ash 62b0604508 Map buttons to custom CC values with reverse-map feedback 2026-06-30 04:43:42 +00:00
ash 3389cc9403 Send CC instead of NOTE_ON for Loopy Pro generic mirror. Clean up flash_activity. 2026-06-30 04:38:28 +00:00
ash 1728413cb8 Fix NOTE_ON feedback: pass led_index to set_led_state 2026-06-30 04:17:18 +00:00
ash c57847db78 Remove flash_activity that was stealing focus to LED 0 2026-06-30 04:14:07 +00:00
ash a4131adf2a Fix CC feedback: route to correct pixel by LED index 2026-06-30 04:04:43 +00:00
Ashley Strahle 3dc9e445be . 2026-06-30 13:54:41 +10:00
ash 1c1fc4d372 Remove heartbeat, relabel USB to JOC Midi 2026-06-30 03:46:31 +00:00
ash ac83ba269f USB name: JOC / JOC Midi (VID/PID still Launchpad X for Loopy Pro recognition) 2026-06-26 00:13:41 +00:00
ash 17c630f850 Fix update(): remove old sysex_flash_active logic 2026-06-26 00:07:50 +00:00
ash 268b793d87 Fix compilation: remove unused flash_sysex/flash_all/flash_one methods and member vars 2026-06-25 23:54:30 +00:00
ash a281eaa46e Clean up LED driver + rename USB to JOC Midi
- Removed flash_sysex/flash_all/flash_one (conflicted with heartbeat)
- Simplified flash_activity: LED 0 white 50ms, restores properly
- Heartbeat only on LED 9 when no activity
- USB renamed: Manufacturer=JOC, Product=JOC Midi
2026-06-25 23:47:39 +00:00
ash 6ccd28362f Add Generic mode CC value visualization
- LED 1 green = CC received
- LED 2 color shows CC value range:
  - Red (0-31) = recording?
  - Amber (32-63) = queued?
  - Green (64-95) = playing?
  - Blue (96-127) = other?
- Helps map Loopy Pro Generic mode values to Launchpad palette
2026-06-25 23:32:24 +00:00
ash 90bbfa7b84 Revert to single MIDI interface (working USB detection)
- Dual interface broke USB enumeration
- Back to working single interface with Novation Launchpad X VID/PID
- Buttons work, MIDI OUT works
2026-06-25 23:17:18 +00:00
ash a209b48720 Add second USB MIDI interface (LPX MIDI) for Programmer mode
- Interface 1 (DAW): Button presses -> Loopy Pro
- Interface 2 (MIDI): SysEx + LED control <- Loopy Pro
- Both interfaces polled in update()
- Sends on DAW interface (like real Launchpad X)
2026-06-25 22:51:44 +00:00
ash c20f1dec2c Add visual MIDI type indicators
- SysEx: ALL LEDs flash white 200ms
- Note: LED 0 flashes red 100ms
- CC: LED 1 flashes green 100ms
- Helps diagnose what MIDI arrives without serial
2026-06-25 22:39:37 +00:00
ash c02121cd09 Add pre-build script to patch core's pins_arduino.h with Launchpad X VID/PID
- pre_build.py patches framework-arduinoespressif32/variants/esp32s3_devkitc/pins_arduino.h
- Replaces USB_VID 0x303a -> 0x1235, USB_PID 0x1001 -> 0x0103
- Adds USB_MANUFACTURER/PRODUCT
2026-06-25 22:21:51 +00:00
ash 109106f1f1 Add custom pins_arduino.h variant with Launchpad X VID/PID
- variants/esp32s3/pins_arduino.h overrides USB_VID/USB_PID
- Included via -I in build_flags before core variant
- This should force USB descriptors at hardware level
2026-06-25 22:03:15 +00:00
ash 75f583b3c4 Revert to standard board, try VID/PID via board_build.arduino.vid/pid 2026-06-25 10:57:12 +00:00
ash fc9b7688c0 Fix board definition: add espidf framework for Arduino core 2026-06-25 08:57:24 +00:00
ash b380133b3e Add custom board definition with Launchpad X VID/PID (0x1235/0x0103)
- boards/launchpad_x_variant.json defines custom board
- platformio.ini uses custom board
- Board sets VID/PID at hardware level
2026-06-25 08:27:47 +00:00
ash 25713707aa Revert to working USB mode (ARDUINO_USB_MODE=0 + CDC) 2026-06-25 08:20:19 +00:00
ash 7558082e7b Try ARDUINO_USB_MODE=2 (native MIDI only)
Disable CDC, use ESP32 core native USB MIDI stack
2026-06-25 08:12:07 +00:00
ash dee60c7d27 Fix compilation: declare 'now' in flash_sysex 2026-06-25 07:54:35 +00:00
13 changed files with 625 additions and 132 deletions
+5
View File
@@ -0,0 +1,5 @@
.pio
.vscode/.browse.c_cpp.db*
.vscode/c_cpp_properties.json
.vscode/launch.json
.vscode/ipch
+10
View File
@@ -0,0 +1,10 @@
{
// 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"
]
}
+10 -2
View File
@@ -2,6 +2,7 @@
#include <cstdint> #include <cstdint>
#include "midi_transport.h" #include "midi_transport.h"
#include "ble_midi_transport.h"
#include "led_stub.h" #include "led_stub.h"
#include "switch_stub.h" #include "switch_stub.h"
@@ -14,7 +15,7 @@ struct PadMapping {
class AppTask { class AppTask {
public: public:
AppTask(LedStub* led, SwitchStub* sw, UsbMidiTransport* midi); AppTask(LedStub* led, SwitchStub* sw, UsbMidiTransport* usb_midi, BleMidiTransport* ble_midi = nullptr);
void begin(); void begin();
void update(); void update();
@@ -23,11 +24,13 @@ public:
private: private:
LedStub* led_driver; LedStub* led_driver;
SwitchStub* switch_driver; SwitchStub* switch_driver;
UsbMidiTransport* midi_transport; UsbMidiTransport* usb_midi;
BleMidiTransport* ble_midi;
static const uint8_t NUM_PADS = 10; static const uint8_t NUM_PADS = 10;
PadMapping pad_mapping[NUM_PADS]; PadMapping pad_mapping[NUM_PADS];
bool last_switch_state[NUM_PADS]; bool last_switch_state[NUM_PADS];
uint8_t cc_map[NUM_PADS];
// SysEx reassembly buffer // SysEx reassembly buffer
static const uint8_t SYSEX_MAX_LEN = 64; static const uint8_t SYSEX_MAX_LEN = 64;
@@ -35,6 +38,11 @@ private:
uint8_t sysex_len = 0; uint8_t sysex_len = 0;
bool sysex_active = false; 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 process_switch_event(uint8_t switch_id, bool pressed);
void run_palette_test(); void run_palette_test();
void handle_sysex(const uint8_t* data, uint8_t len); void handle_sysex(const uint8_t* data, uint8_t len);
+31
View File
@@ -0,0 +1,31 @@
#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;
};
+2 -8
View File
@@ -7,11 +7,10 @@ public:
virtual ~LedStub() {} virtual ~LedStub() {}
virtual void begin() = 0; virtual void begin() = 0;
virtual void set_led_state(uint8_t note, uint8_t channel, uint8_t velocity) = 0; virtual void set_led_state(uint8_t note, uint8_t channel, uint8_t velocity, int8_t led_index = -1) = 0;
virtual void clear_all() = 0; virtual void clear_all() = 0;
virtual void set_led_brightness(uint8_t brightness) = 0; virtual void set_led_brightness(uint8_t brightness) = 0;
virtual void flash_activity() {} virtual void flash_activity() {}
virtual void flash_sysex() {}
virtual void update() {} virtual void update() {}
virtual uint8_t note_to_index(uint8_t note) { virtual uint8_t note_to_index(uint8_t note) {
@@ -36,19 +35,14 @@ private:
bool initialized; bool initialized;
uint32_t activity_off_time = 0; uint32_t activity_off_time = 0;
uint8_t saved_r = 0, saved_g = 0, saved_b = 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;
uint32_t heartbeat_time = 0;
uint8_t heartbeat_phase = 0;
public: public:
DefaultLedStub(); DefaultLedStub();
void begin() override; void begin() override;
void set_led_state(uint8_t note, uint8_t channel, uint8_t velocity) override; void set_led_state(uint8_t note, uint8_t channel, uint8_t velocity, int8_t led_index = -1) override;
void clear_all() override; void clear_all() override;
void set_led_brightness(uint8_t brightness) override; void set_led_brightness(uint8_t brightness) override;
void flash_activity() override; void flash_activity() override;
void flash_sysex() override;
void update() override; void update() override;
void set_mux(PixelStompMux* mux); void set_mux(PixelStompMux* mux);
+5
View File
@@ -8,6 +8,7 @@ framework = arduino
lib_deps = lib_deps =
adafruit/Adafruit TinyUSB [email protected] adafruit/Adafruit TinyUSB [email protected]
fastled/FastLED@^3.9.0 fastled/FastLED@^3.9.0
h2zero/NimBLE-Arduino@^1.4.0
build_unflags = build_unflags =
-DARDUINO_USB_MODE=1 -DARDUINO_USB_MODE=1
@@ -16,8 +17,12 @@ build_flags =
-DARDUINO_USB_MODE=0 -DARDUINO_USB_MODE=0
-DARDUINO_USB_CDC_ON_BOOT=1 -DARDUINO_USB_CDC_ON_BOOT=1
-DUSE_TINYUSB=1 -DUSE_TINYUSB=1
-Wno-macro-redefined
-DMYNEWT_VAL_BLE_EXT_ADV=0
monitor_speed = 115200 monitor_speed = 115200
board_build.partitions = default_8MB.csv board_build.partitions = default_8MB.csv
board_build.arduino.memory_type = qio_opi board_build.arduino.memory_type = qio_opi
extra_scripts = pre:pre_build.py
+215
View File
@@ -0,0 +1,215 @@
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()
+7
View File
@@ -0,0 +1,7 @@
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
+44 -31
View File
@@ -1,17 +1,19 @@
#include "app_task.h" #include "app_task.h"
#include <Arduino.h> #include <Arduino.h>
AppTask::AppTask(LedStub* led, SwitchStub* sw, UsbMidiTransport* midi) AppTask::AppTask(LedStub* led, SwitchStub* sw, UsbMidiTransport* usb_midi, BleMidiTransport* ble_midi)
: led_driver(led), switch_driver(sw), midi_transport(midi) { : led_driver(led), switch_driver(sw), usb_midi(usb_midi), ble_midi(ble_midi) {
// Launchpad X standard: bottom row = notes 36-45 (C2 to A2) on channel 1 // 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 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++) { for (uint8_t i = 0; i < NUM_PADS; i++) {
pad_mapping[i].physical_switch = i; pad_mapping[i].physical_switch = i;
pad_mapping[i].midi_channel = 1; pad_mapping[i].midi_channel = 1;
pad_mapping[i].midi_note = launchpad_notes[i]; pad_mapping[i].midi_note = launchpad_notes[i];
pad_mapping[i].led_index = i; pad_mapping[i].led_index = i;
cc_map[i] = cc_assignments[i];
last_switch_state[i] = false; last_switch_state[i] = false;
} }
} }
@@ -19,11 +21,18 @@ AppTask::AppTask(LedStub* led, SwitchStub* sw, UsbMidiTransport* midi)
void AppTask::begin() { void AppTask::begin() {
Serial.println("[APP] Registering MIDI callbacks..."); Serial.println("[APP] Registering MIDI callbacks...");
midi_transport->on_midi_receive([this](const MidiEvent& event) { auto handler = [this](const MidiEvent& event) {
process_midi_event(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 - Launchpad X mode (notes 36-45, ch1)"); 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);
}
} }
void AppTask::update() { void AppTask::update() {
@@ -40,22 +49,24 @@ void AppTask::update() {
last_switch_state[i] = false; 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) { void AppTask::process_midi_event(const MidiEvent& event) {
Serial.printf("[APP] MIDI IN: Type=%d Ch=%d Data1=%d Data2=%d\n", Serial.printf("[APP] MIDI IN: Type=%d Ch=%d Data1=%d Data2=%d\n",
event.type, event.channel, event.data1, event.data2); 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) { if (event.type == MidiEvent::SYSEX) {
// Cin is encoded in channel for SYSEX packets // Cin is encoded in channel for SYSEX packets
uint8_t cin = event.channel; uint8_t cin = event.channel;
uint8_t packet[3] = {event.data1, event.data2, 0}; uint8_t packet[3] = {event.data1, event.data2, 0};
process_sysex_packet(packet, cin); process_sysex_packet(packet, cin);
led_driver->flash_sysex();
return; return;
} }
@@ -83,7 +94,8 @@ void AppTask::process_midi_event(const MidiEvent& event) {
led_driver->set_led_state( led_driver->set_led_state(
midi_note, midi_note,
midi_channel, midi_channel,
color_vel color_vel,
led_index
); );
Serial.printf("[APP] NOTE -> LED: Ch%d Note%d Vel%d -> LED%d\n", Serial.printf("[APP] NOTE -> LED: Ch%d Note%d Vel%d -> LED%d\n",
@@ -96,28 +108,27 @@ void AppTask::process_midi_event(const MidiEvent& event) {
Serial.printf("[APP] NOTE Ch%d ignored (not Launchpad channel 1-3)\n", midi_channel); Serial.printf("[APP] NOTE Ch%d ignored (not Launchpad channel 1-3)\n", midi_channel);
} }
} }
// CONTROL_CHANGE fallback for generic MIDI / Loopy Pro generic mode // CONTROL_CHANGE: look up which pad this CC belongs to
else if (event.type == MidiEvent::CONTROL_CHANGE) { else if (event.type == MidiEvent::CONTROL_CHANGE) {
uint8_t cc_num = event.data1; uint8_t cc_num = event.data1;
uint8_t cc_val = event.data2; uint8_t cc_val = event.data2;
// Map CC to pad: CC2-11 (Loopy Pro), CC0-9, CC36-45 for (uint8_t i = 0; i < NUM_PADS; i++) {
if (cc_num >= 2 && cc_num < 2 + NUM_PADS) { if (cc_map[i] == cc_num) {
led_index = cc_num - 2; led_index = i;
} else if (cc_num < NUM_PADS) { break;
led_index = cc_num; }
} else if (cc_num >= 36 && cc_num < 36 + NUM_PADS) {
led_index = cc_num - 36;
} }
if (led_index < NUM_PADS) { if (led_index < NUM_PADS) {
led_driver->set_led_state( led_driver->set_led_state(
pad_mapping[led_index].midi_note, pad_mapping[led_index].midi_note,
pad_mapping[led_index].midi_channel, pad_mapping[led_index].midi_channel,
cc_val cc_val,
led_index
); );
Serial.printf("[APP] CC -> LED: Ch%d CC%d Val%d -> LED%d\n", Serial.printf("[APP] CC%d Val%d -> LED%d\n",
midi_channel, cc_num, cc_val, led_index); cc_num, cc_val, led_index);
} else { } else {
Serial.printf("[APP] CC Ch%d CC%d Val%d - no mapping\n", Serial.printf("[APP] CC Ch%d CC%d Val%d - no mapping\n",
midi_channel, cc_num, cc_val); midi_channel, cc_num, cc_val);
@@ -129,19 +140,21 @@ void AppTask::process_switch_event(uint8_t switch_id, bool pressed) {
for (uint8_t i = 0; i < NUM_PADS; i++) { for (uint8_t i = 0; i < NUM_PADS; i++) {
if (pad_mapping[i].physical_switch == switch_id) { if (pad_mapping[i].physical_switch == switch_id) {
uint8_t channel = pad_mapping[i].midi_channel; uint8_t channel = pad_mapping[i].midi_channel;
// Loopy Pro Launchpad mode expects NOTE_ON/NOTE_OFF on notes 36-45 uint8_t cc_num = cc_map[i];
uint8_t note = pad_mapping[i].midi_note; uint8_t value = pressed ? 127 : 0;
uint8_t velocity = 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");
if (pressed) { if (pressed) {
midi_transport->send_note_on(channel, note, velocity); // Send MIDI via a flag that loop() processes
} else { pending_cc = true;
midi_transport->send_note_off(channel, note, velocity); pending_channel = channel;
pending_cc_num = cc_num;
pending_value = value;
} }
Serial.printf("[APP] Switch %d -> Ch%d Note%d Vel%d (%s)\n",
switch_id, channel, note, velocity,
pressed ? "PRESS" : "RELEASE");
break; break;
} }
} }
+247
View File
@@ -0,0 +1,247 @@
#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;
}
+29 -78
View File
@@ -129,13 +129,13 @@ static const uint32_t launchpad_palette[128] = {
0x1E002F, // 118 0x1E002F, // 118
0x1B0029, // 119 0x1B0029, // 119
0x170023, // 120 0x170023, // 120
0x14001E, // 121 0x190033, // 121: dark purple-red (was near-black)
0x110018, // 122 0x3D0047, // 122: medium purple-red
0x0D0013, // 123 0x61005C, // 123: bright purple
0x0A000D, // 124 0x850070, // 124: brighter purple
0x070008, // 125 0x9900CC, // 125: bright magenta
0x030002, // 126 0xCC00FF, // 126: full magenta
0x000000, // 127: Off 0xFF00FF, // 127: full magenta (Maximum velocity = visible)
}; };
static uint32_t velocity_to_color(uint8_t velocity) { static uint32_t velocity_to_color(uint8_t velocity) {
@@ -200,9 +200,27 @@ void DefaultLedStub::begin() {
Serial.println("[LED] Startup complete"); Serial.println("[LED] Startup complete");
} }
void DefaultLedStub::set_led_state(uint8_t note, uint8_t channel, uint8_t velocity) { void DefaultLedStub::set_led_state(uint8_t note, uint8_t channel, uint8_t velocity, int8_t led_index) {
if (!initialized || !mux_ptr) return; 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++) { for (int i = 0; i < NUM_LEDS; i++) {
if (led_states[i].active && led_states[i].note == note) { if (led_states[i].active && led_states[i].note == note) {
led_states[i].channel = channel; led_states[i].channel = channel;
@@ -260,53 +278,8 @@ void DefaultLedStub::set_led_brightness(uint8_t brightness) {
} }
void DefaultLedStub::flash_activity() { void DefaultLedStub::flash_activity() {
if (!initialized || !mux_ptr) return; // No-op: activity flash on all MIDI input was confusing.
// LED feedback is handled directly by set_led_state().
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() { void DefaultLedStub::update() {
@@ -314,32 +287,10 @@ void DefaultLedStub::update() {
uint32_t now = millis(); uint32_t now = millis();
// Turn off activity/SysEx flash // Turn off activity flash
if (activity_off_time > 0 && now >= activity_off_time) { if (activity_off_time > 0 && now >= activity_off_time) {
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->set_led_color(0, saved_r, saved_g, saved_b);
}
mux_ptr->show(); mux_ptr->show();
activity_off_time = 0; activity_off_time = 0;
} }
// Heartbeat: pulse LED 9 slowly (amber)
if (now - heartbeat_time > 2000 && !activity_off_time) {
heartbeat_time = now;
heartbeat_phase = 1;
}
if (heartbeat_phase && !activity_off_time) {
uint8_t brightness = (heartbeat_phase < 100) ? (heartbeat_phase * 2) : (400 - heartbeat_phase * 2);
if (!led_states[9].active) {
mux_ptr->set_led_color(9, brightness, brightness / 4, 0);
mux_ptr->show();
}
heartbeat_phase++;
if (heartbeat_phase > 200) heartbeat_phase = 0;
}
} }
+10 -3
View File
@@ -5,6 +5,7 @@
#include <driver/gpio.h> #include <driver/gpio.h>
#include <esp_rom_sys.h> #include <esp_rom_sys.h>
#include "midi_transport.h" #include "midi_transport.h"
#include "ble_midi_transport.h"
#include "Adafruit_TinyUSB.h" #include "Adafruit_TinyUSB.h"
#include "pixel_stomp_mux.h" #include "pixel_stomp_mux.h"
#include "led_stub.h" #include "led_stub.h"
@@ -16,8 +17,9 @@ PixelStompMux mux(12, 10, 11, 9);
DefaultLedStub led_driver; DefaultLedStub led_driver;
DefaultSwitchStub switch_driver; DefaultSwitchStub switch_driver;
UsbMidiTransport midi_transport; UsbMidiTransport midi_transport;
BleMidiTransport ble_midi_transport;
AppTask controller(&led_driver, &switch_driver, &midi_transport); AppTask controller(&led_driver, &switch_driver, &midi_transport, &ble_midi_transport);
TaskHandle_t midi_task_handle = NULL; TaskHandle_t midi_task_handle = NULL;
@@ -26,6 +28,7 @@ void midi_task(void* parameter) {
while (true) { while (true) {
midi_transport.update(); midi_transport.update();
ble_midi_transport.update();
vTaskDelay(1); vTaskDelay(1);
} }
} }
@@ -328,7 +331,7 @@ void setup() {
Serial.println("================================="); Serial.println("=================================");
Serial.println(" Loopy MIDI Controller v0.1"); Serial.println(" Loopy MIDI Controller v0.1");
Serial.println(" Phase 1: USB MIDI"); Serial.println(" Phase 1.5: USB + BLE MIDI");
Serial.println(" Board: ESP32-S3-WROOM-1"); Serial.println(" Board: ESP32-S3-WROOM-1");
Serial.println("================================="); Serial.println("=================================");
@@ -343,8 +346,12 @@ void setup() {
switch_driver.set_mux(&mux); switch_driver.set_mux(&mux);
switch_driver.begin(); switch_driver.begin();
Serial.println("[INIT] Initializing USB MIDI..."); Serial.println("[INIT] Initializing USB MIDI first (before BLE)...");
midi_transport.begin(); midi_transport.begin();
delay(1000);
Serial.println("[INIT] Initializing BLE MIDI...");
ble_midi_transport.begin();
Serial.println("[INIT] Registering MIDI callbacks..."); Serial.println("[INIT] Registering MIDI callbacks...");
controller.begin(); controller.begin();
+6 -6
View File
@@ -13,11 +13,11 @@ UsbMidiTransport::~UsbMidiTransport() {
bool UsbMidiTransport::begin() { bool UsbMidiTransport::begin() {
Serial.println("[MIDI] Setting up USB MIDI device..."); Serial.println("[MIDI] Setting up USB MIDI device...");
// Novation Launchpad X identifiers so Loopy Pro recognizes us // Use Launchpad X VID/PID (0x1235/0x0103) so Loopy Pro recognizes us
TinyUSBDevice.setID(0x1235, 0x0103); // but with our own name
TinyUSBDevice.setManufacturerDescriptor("Novation"); TinyUSBDevice.setManufacturerDescriptor("JOC");
TinyUSBDevice.setProductDescriptor("Launchpad X"); TinyUSBDevice.setProductDescriptor("JOC Midi");
TinyUSBDevice.setSerialDescriptor("LPX00001"); TinyUSBDevice.setSerialDescriptor("JOCMIDI001");
TinyUSBDevice.begin(0); TinyUSBDevice.begin(0);
if (!usb_midi.begin()) { 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) { void UsbMidiTransport::send_cc(uint8_t channel, uint8_t cc, uint8_t value) {
if (!initialized) return; if (!initialized) return;
if (!TinyUSBDevice.ready()) return;
uint8_t packet[4] = {0x0B, (uint8_t)(0xB0 | (channel - 1)), cc, value}; uint8_t packet[4] = {0x0B, (uint8_t)(0xB0 | (channel - 1)), cc, value};
usb_midi.writePacket(packet); usb_midi.writePacket(packet);
Serial.printf("[MIDI OUT] Ch:%d CC:%d:%d\n", channel, cc, value);
} }
bool UsbMidiTransport::is_connected() { bool UsbMidiTransport::is_connected() {