Compare commits

..
Author SHA1 Message Date
ash 0d22769cf6 Tweak cal_max: 1239 -> 1220 2026-07-02 04:24:29 +00:00
ash 9197294365 Increase pedal read rate: 20ms -> 5ms 2026-07-02 04:21:53 +00:00
ash 2cf49b8deb Calibrate expression pedal: cal_min=36, cal_max=1239
Raw ADC observed range: heel=36, toe=1239.
Maps full pedal travel to MIDI 0-127.
2026-07-02 04:19:30 +00:00
ash f6c0a01a14 Fix expression pedal pin: 5 -> 4
Constructor was still GPIO5 despite physical move to GPIO4.
The pedal was reading the wrong (unconnected) pin, so no ADC changes.
2026-07-02 04:17:30 +00:00
ash f0b6df1477 Remove serial cal commands, just log raw ADC on each value change
- Print raw ADC + mapped MIDI value on every change
- Remove cal min/cal max serial commands (broken with multi-word)
- Keep cal_min=0, cal_max=4095 defaults
- User moves pedal full range and reports min/max raw values
2026-07-02 04:14:39 +00:00
ash 8bde8efaff Add expression pedal calibration: cal min/cal max
- Remove fixed 0-4095 mapping, use cal_min/cal_max instead
- Auto-calibrate minimum at boot (assumes pedal at heel)
- 'cal min' sets current position as heel (0)
- 'cal max' sets current position as toe (127)
- Map cal_min->0, cal_max->127 with clamping
- Update 'exp' command to show raw ADC + cal values
2026-07-02 04:09:22 +00:00
ash 08d6fc8701 Add expression pedal support on GPIO5 (CC 4) 2026-07-02 04:00:06 +00:00
11 changed files with 137 additions and 504 deletions
+2 -9
View File
@@ -2,7 +2,6 @@
#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"
@@ -15,7 +14,7 @@ struct PadMapping {
class AppTask { class AppTask {
public: public:
AppTask(LedStub* led, SwitchStub* sw, UsbMidiTransport* usb_midi, BleMidiTransport* ble_midi = nullptr); AppTask(LedStub* led, SwitchStub* sw, UsbMidiTransport* midi);
void begin(); void begin();
void update(); void update();
@@ -24,8 +23,7 @@ public:
private: private:
LedStub* led_driver; LedStub* led_driver;
SwitchStub* switch_driver; SwitchStub* switch_driver;
UsbMidiTransport* usb_midi; UsbMidiTransport* midi_transport;
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];
@@ -38,11 +36,6 @@ 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
@@ -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;
};
+36
View File
@@ -0,0 +1,36 @@
#pragma once
#include <cstdint>
class UsbMidiTransport;
class ExpressionPedal {
public:
ExpressionPedal(uint8_t adc_pin = 5);
void begin();
void update(UsbMidiTransport& midi);
uint8_t get_value() const { return current_value; }
uint16_t get_raw() const { return current_raw; }
void set_cal_min();
void set_cal_max();
uint16_t get_cal_min() const { return cal_min; }
uint16_t get_cal_max() const { return cal_max; }
private:
uint8_t adc_pin;
uint8_t current_value;
uint8_t last_sent_value;
uint8_t midi_channel;
uint8_t midi_cc;
static const uint8_t HYSTERESIS = 3;
static const uint16_t READ_INTERVAL_MS = 5;
uint32_t last_read_time;
uint16_t current_raw;
uint16_t cal_min;
uint16_t cal_max;
uint8_t adc_to_midi(uint16_t adc_value);
};
-3
View File
@@ -8,7 +8,6 @@ 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
@@ -17,8 +16,6 @@ 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
+3 -169
View File
@@ -1,6 +1,5 @@
import os import os
import re import fileinput
def patch_usb_ids(): def patch_usb_ids():
# Find the core's pins_arduino.h for ESP32-S3 DevKitC-1 # Find the core's pins_arduino.h for ESP32-S3 DevKitC-1
@@ -16,7 +15,7 @@ def patch_usb_ids():
pins_file = os.path.join(core_variants, "esp32s3", "pins_arduino.h") pins_file = os.path.join(core_variants, "esp32s3", "pins_arduino.h")
if os.path.exists(pins_file): if os.path.exists(pins_file):
print(f"Patching {pins_file} with JOC Midi USB descriptors") print(f"Patching {pins_file} with Launchpad X VID/PID")
# Read and replace # Read and replace
with open(pins_file, 'r') as f: with open(pins_file, 'r') as f:
@@ -47,169 +46,4 @@ def patch_usb_ids():
else: else:
print(f"WARNING: Could not find pins_arduino.h at {pins_file}") print(f"WARNING: Could not find pins_arduino.h at {pins_file}")
patch_usb_ids()
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
@@ -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
+11 -26
View File
@@ -1,8 +1,8 @@
#include "app_task.h" #include "app_task.h"
#include <Arduino.h> #include <Arduino.h>
AppTask::AppTask(LedStub* led, SwitchStub* sw, UsbMidiTransport* usb_midi, BleMidiTransport* ble_midi) AppTask::AppTask(LedStub* led, SwitchStub* sw, UsbMidiTransport* midi)
: led_driver(led), switch_driver(sw), usb_midi(usb_midi), ble_midi(ble_midi) { : led_driver(led), switch_driver(sw), midi_transport(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};
@@ -20,15 +20,11 @@ AppTask::AppTask(LedStub* led, SwitchStub* sw, UsbMidiTransport* usb_midi, BleMi
void AppTask::begin() { void AppTask::begin() {
Serial.println("[APP] Registering MIDI callbacks..."); 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); 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"); Serial.println("[APP] Controller ready - CC mode");
for (uint8_t i = 0; i < NUM_PADS; i++) { 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.printf("[APP] Pad %d -> CC%d -> LED%d\n", i + 1, cc_map[i], i);
@@ -49,13 +45,6 @@ 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) {
@@ -141,20 +130,16 @@ void AppTask::process_switch_event(uint8_t switch_id, bool pressed) {
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;
uint8_t cc_num = cc_map[i]; uint8_t cc_num = cc_map[i];
// Use palette index 127 (magenta) for visible feedback
uint8_t value = pressed ? 127 : 0; uint8_t value = pressed ? 127 : 0;
if (pressed) {
midi_transport->send_cc(channel, cc_num, value);
}
Serial.printf("[APP] Switch %d -> Ch%d CC%d Val%d (%s)\n", Serial.printf("[APP] Switch %d -> Ch%d CC%d Val%d (%s)\n",
switch_id, channel, cc_num, value, switch_id, channel, cc_num, value,
pressed ? "PRESS" : "RELEASE"); pressed ? "PRESS" : "RELEASE");
if (pressed) {
// Send MIDI via a flag that loop() processes
pending_cc = true;
pending_channel = channel;
pending_cc_num = cc_num;
pending_value = value;
}
break; break;
} }
} }
-247
View File
@@ -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;
}
+70
View File
@@ -0,0 +1,70 @@
#include "expression_pedal.h"
#include "midi_transport.h"
#include <Arduino.h>
ExpressionPedal::ExpressionPedal(uint8_t adc_pin)
: adc_pin(adc_pin)
, current_value(0)
, last_sent_value(255)
, midi_channel(1)
, midi_cc(4)
, last_read_time(0)
, current_raw(0)
, cal_min(36)
, cal_max(1220)
{
}
void ExpressionPedal::begin() {
Serial.printf("[EXP] Expression pedal on ADC1_CH3 (GPIO%d)\n", adc_pin);
analogReadResolution(12);
pinMode(adc_pin, INPUT);
delay(10);
current_raw = analogRead(adc_pin);
current_value = adc_to_midi(current_raw);
last_sent_value = current_value;
Serial.printf("[EXP] Starting: raw ADC=%d -> MIDI=%d (cal: %d-%d)\n",
current_raw, current_value, cal_min, cal_max);
Serial.printf("[EXP] Move pedal full range to see raw values\n");
}
void ExpressionPedal::update(UsbMidiTransport& midi) {
uint32_t now = millis();
if (now - last_read_time < READ_INTERVAL_MS) return;
last_read_time = now;
current_raw = analogRead(adc_pin);
uint8_t new_value = adc_to_midi(current_raw);
if (new_value > current_value + HYSTERESIS ||
new_value + HYSTERESIS < current_value ||
(new_value != current_value && (new_value == 0 || new_value == 127))) {
current_value = new_value;
}
if (current_value != last_sent_value) {
last_sent_value = current_value;
midi.send_cc(midi_channel, midi_cc, current_value);
Serial.printf("[EXP] CC%d: %d (raw ADC: %d)\n", midi_cc, current_value, current_raw);
}
}
void ExpressionPedal::set_cal_min() {
cal_min = current_raw;
if (cal_min >= cal_max) cal_max = cal_min + 1;
Serial.printf("[EXP] cal_min set to %d (ADC raw)\n", cal_min);
}
void ExpressionPedal::set_cal_max() {
cal_max = current_raw;
if (cal_max <= cal_min) cal_min = cal_max - 1;
Serial.printf("[EXP] cal_max set to %d (ADC raw)\n", cal_max);
}
uint8_t ExpressionPedal::adc_to_midi(uint16_t adc_value) {
if (adc_value <= cal_min) return 0;
if (adc_value >= cal_max) return 127;
uint32_t span = cal_max - cal_min;
uint32_t offset = adc_value - cal_min;
return (uint8_t)(offset * 127 / span);
}
+14 -11
View File
@@ -5,21 +5,21 @@
#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"
#include "switch_stub.h" #include "switch_stub.h"
#include "app_task.h" #include "app_task.h"
#include "expression_pedal.h"
PixelStompMux mux(12, 10, 11, 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, &ble_midi_transport); AppTask controller(&led_driver, &switch_driver, &midi_transport);
ExpressionPedal exp_pedal(4);
TaskHandle_t midi_task_handle = NULL; TaskHandle_t midi_task_handle = NULL;
@@ -28,7 +28,7 @@ void midi_task(void* parameter) {
while (true) { while (true) {
midi_transport.update(); midi_transport.update();
ble_midi_transport.update(); exp_pedal.update(midi_transport);
vTaskDelay(1); vTaskDelay(1);
} }
} }
@@ -86,6 +86,9 @@ void handle_serial_command(const String& cmd) {
mux.set_led_color(1, 255, 255, 255); mux.set_led_color(1, 255, 255, 255);
mux.show(); mux.show();
Serial.println("[CMD] Pixel 1 WHITE (max brightness)"); Serial.println("[CMD] Pixel 1 WHITE (max brightness)");
} else if (cmd == "exp") {
Serial.printf("[CMD] EXP ADC=%d MIDI=%d\n",
exp_pedal.get_raw(), exp_pedal.get_value());
} else if (cmd == "usb") { } else if (cmd == "usb") {
Serial.printf("[CMD] USB mounted: %s\n", TinyUSBDevice.mounted() ? "YES" : "NO"); Serial.printf("[CMD] USB mounted: %s\n", TinyUSBDevice.mounted() ? "YES" : "NO");
Serial.printf("[CMD] USB ready: %s\n", TinyUSBDevice.ready() ? "YES" : "NO"); Serial.printf("[CMD] USB ready: %s\n", TinyUSBDevice.ready() ? "YES" : "NO");
@@ -129,6 +132,7 @@ void handle_serial_command(const String& cmd) {
Serial.println(" read - raw button read"); Serial.println(" read - raw button read");
Serial.println(" red/green/blue - solid colour"); Serial.println(" red/green/blue - solid colour");
Serial.println(" pixel0/pixel1 - single pixel test"); Serial.println(" pixel0/pixel1 - single pixel test");
Serial.println(" exp - expression pedal ADC/MIDI value");
Serial.println(" usb - USB connection status and descriptor info"); Serial.println(" usb - USB connection status and descriptor info");
Serial.println(" gpiotest - raw GPIO pin diagnostic"); Serial.println(" gpiotest - raw GPIO pin diagnostic");
Serial.println(" rawled - bit-bang WS2812 (no library)"); Serial.println(" rawled - bit-bang WS2812 (no library)");
@@ -331,7 +335,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.5: USB + BLE MIDI"); Serial.println(" Phase 1: USB MIDI + Expression Pedal");
Serial.println(" Board: ESP32-S3-WROOM-1"); Serial.println(" Board: ESP32-S3-WROOM-1");
Serial.println("================================="); Serial.println("=================================");
@@ -346,13 +350,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 first (before BLE)..."); Serial.println("[INIT] Initializing USB MIDI...");
midi_transport.begin(); midi_transport.begin();
delay(1000);
Serial.println("[INIT] Initializing Expression Pedal...");
Serial.println("[INIT] Initializing BLE MIDI..."); exp_pedal.begin();
ble_midi_transport.begin();
Serial.println("[INIT] Registering MIDI callbacks..."); Serial.println("[INIT] Registering MIDI callbacks...");
controller.begin(); controller.begin();
+1 -1
View File
@@ -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() {