Compare commits

...
Author SHA1 Message Date
ash 06479f0ca2 Fix orange 0xFFAA00->0xFF6600, pads 8-9 white->purple 2026-07-02 07:04:22 +00:00
ash 66058a6722 Set per-pad LED colors with dim off state
- Pad 0-1: Blue, 2-4: Orange, 5: Red, 6: White, 7: Amber, 8-9: White
- Off state (velocity=0): dim at ~8% brightness instead of black
- On state: full brightness of assigned color
- clear_all() sets dim colors instead of off
2026-07-02 06:45:42 +00:00
ash 4ddb9a00bf Reduce cal_max 1220->1180 to absorb ADC jitter at toe
Raw ADC jitter of ~40 counts at toe caused MIDI flickering
between 127 and ~123. Lowering cal_max so the >= cal_max
clamp at 127 absorbs the noise.
2026-07-02 06:40:30 +00:00
ash c44bfb033b Fix button-to-MIDI latency: remove blocking serial, reduce debounce, faster loop
Latency sources fixed:
- debounce 50ms -> 5ms (switch_stub.cpp:15)
- Removed [MIDI OUT] Serial.printf from send_cc() (midi_transport.cpp:93-98)
- Removed [APP] Switch pressed/released prints from update() (app_task.cpp:39,43)
- Removed [APP] Switch -> CC print from process_switch_event() (app_task.cpp:140-143)
- loop() delay 10ms -> 1ms (main.cpp:390)

Serial output at 115200 baud was taking 2-5ms per printf call
in the critical button-to-MIDI path, adding 7-15ms+ total latency
per button press. All removed from hot path.
2026-07-02 06:22:23 +00:00
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
7 changed files with 151 additions and 17 deletions
+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);
};
-6
View File
@@ -36,11 +36,9 @@ void AppTask::update() {
bool is_pressed = switch_driver->is_pressed(i); bool is_pressed = switch_driver->is_pressed(i);
if (is_pressed && !last_switch_state[i]) { if (is_pressed && !last_switch_state[i]) {
Serial.printf("[APP] Switch %d pressed\n", i);
process_switch_event(i, true); process_switch_event(i, true);
last_switch_state[i] = true; last_switch_state[i] = true;
} else if (!is_pressed && last_switch_state[i]) { } else if (!is_pressed && last_switch_state[i]) {
Serial.printf("[APP] Switch %d released\n", i);
process_switch_event(i, false); process_switch_event(i, false);
last_switch_state[i] = false; last_switch_state[i] = false;
} }
@@ -136,10 +134,6 @@ void AppTask::process_switch_event(uint8_t switch_id, bool pressed) {
if (pressed) { if (pressed) {
midi_transport->send_cc(channel, cc_num, value); midi_transport->send_cc(channel, cc_num, value);
} }
Serial.printf("[APP] Switch %d -> Ch%d CC%d Val%d (%s)\n",
switch_id, channel, cc_num, value,
pressed ? "PRESS" : "RELEASE");
break; break;
} }
} }
+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(1180)
{
}
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);
}
+32 -7
View File
@@ -142,6 +142,35 @@ static uint32_t velocity_to_color(uint8_t velocity) {
return launchpad_palette[velocity]; return launchpad_palette[velocity];
} }
static const uint32_t pad_base_colors[10] = {
0x0000FF, // 0: Blue
0x0000FF, // 1: Blue
0xFF6600, // 2: Orange
0xFF6600, // 3: Orange
0xFF6600, // 4: Orange
0xFF0000, // 5: Red
0xFFFFFF, // 6: White
0xFFBF00, // 7: Amber
0x9900FF, // 8: Purple
0x9900FF, // 9: Purple
};
static void apply_pad_color(uint8_t index, uint8_t velocity) {
if (index >= 10) return;
uint32_t base = pad_base_colors[index];
uint8_t r = (base >> 16) & 0xFF;
uint8_t g = (base >> 8) & 0xFF;
uint8_t b = base & 0xFF;
if (velocity == 0) {
r = (uint16_t)r * 20 / 255;
g = (uint16_t)g * 20 / 255;
b = (uint16_t)b * 20 / 255;
}
mux_ptr->set_led_color(index, r, g, b);
}
DefaultLedStub::DefaultLedStub() : initialized(false) { DefaultLedStub::DefaultLedStub() : initialized(false) {
for (int i = 0; i < NUM_LEDS; i++) { for (int i = 0; i < NUM_LEDS; i++) {
led_states[i].active = false; led_states[i].active = false;
@@ -203,18 +232,14 @@ void DefaultLedStub::begin() {
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, int8_t led_index) {
if (!initialized || !mux_ptr) return; if (!initialized || !mux_ptr) return;
// Direct index mode (used by CC feedback) // Direct index mode (used by CC feedback / pad colors)
if (led_index >= 0 && led_index < NUM_LEDS) { if (led_index >= 0 && led_index < NUM_LEDS) {
led_states[led_index].active = (velocity > 0); led_states[led_index].active = (velocity > 0);
led_states[led_index].note = note; led_states[led_index].note = note;
led_states[led_index].channel = channel; led_states[led_index].channel = channel;
led_states[led_index].velocity = velocity; led_states[led_index].velocity = velocity;
led_states[led_index].timestamp = millis(); led_states[led_index].timestamp = millis();
uint32_t color = velocity_to_color(velocity); apply_pad_color(led_index, 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(); mux_ptr->show();
Serial.printf("[LED] Set LED %d: note=%d ch=%d vel=%d\n", led_index, note, channel, velocity); Serial.printf("[LED] Set LED %d: note=%d ch=%d vel=%d\n", led_index, note, channel, velocity);
return; return;
@@ -265,7 +290,7 @@ void DefaultLedStub::clear_all() {
led_states[i].note = 0; led_states[i].note = 0;
led_states[i].channel = 0; led_states[i].channel = 0;
led_states[i].timestamp = 0; led_states[i].timestamp = 0;
mux_ptr->set_led_color(i, 0, 0, 0); apply_pad_color(i, 0);
} }
mux_ptr->show(); mux_ptr->show();
Serial.println("[LED] All cleared"); Serial.println("[LED] All cleared");
+12 -2
View File
@@ -10,6 +10,7 @@
#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);
@@ -18,6 +19,7 @@ DefaultSwitchStub switch_driver;
UsbMidiTransport midi_transport; UsbMidiTransport midi_transport;
AppTask controller(&led_driver, &switch_driver, &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;
@@ -26,6 +28,7 @@ void midi_task(void* parameter) {
while (true) { while (true) {
midi_transport.update(); midi_transport.update();
exp_pedal.update(midi_transport);
vTaskDelay(1); vTaskDelay(1);
} }
} }
@@ -83,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");
@@ -126,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)");
@@ -328,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: USB 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,6 +353,9 @@ void setup() {
Serial.println("[INIT] Initializing USB MIDI..."); Serial.println("[INIT] Initializing USB MIDI...");
midi_transport.begin(); midi_transport.begin();
Serial.println("[INIT] Initializing Expression Pedal...");
exp_pedal.begin();
Serial.println("[INIT] Registering MIDI callbacks..."); Serial.println("[INIT] Registering MIDI callbacks...");
controller.begin(); controller.begin();
@@ -377,5 +387,5 @@ void loop() {
handle_serial_command(cmd); handle_serial_command(cmd);
} }
delay(10); delay(1);
} }
-1
View File
@@ -94,7 +94,6 @@ void UsbMidiTransport::send_cc(uint8_t channel, uint8_t cc, uint8_t value) {
if (!initialized) return; if (!initialized) 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() {
+1 -1
View File
@@ -11,7 +11,7 @@ DefaultSwitchStub::DefaultSwitchStub() : initialized(false) {
switch_states[i].current_state = false; switch_states[i].current_state = false;
switch_states[i].previous_state = false; switch_states[i].previous_state = false;
switch_states[i].last_change_time = 0; switch_states[i].last_change_time = 0;
switch_states[i].debounce_time = 50; switch_states[i].debounce_time = 5;
} }
} }