Compare commits
42
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1bf2ea9f2f | ||
|
|
00337559ac | ||
|
|
9ba48b3f9c | ||
|
|
01eaf9ce80 | ||
|
|
c26116a7ef | ||
|
|
fec2ea1db2 | ||
|
|
762500e227 | ||
|
|
62d1e430d5 | ||
|
|
1517eda13e | ||
|
|
855c97d8c6 | ||
|
|
993a09fa7c | ||
|
|
18844c17c0 | ||
|
|
7530772acd | ||
|
|
94567e3485 | ||
|
|
beb59da3e0 | ||
|
|
2d3a5a9031 | ||
|
|
72a10fa13a | ||
|
|
d8c109d9e5 | ||
|
|
5ee5949420 | ||
|
|
8515963e07 | ||
|
|
e2db658037 | ||
|
|
8833e08c0a | ||
|
|
3e92d6bb7c | ||
|
|
be8b0dc22d | ||
|
|
6038b3bf58 | ||
|
|
1f7bdc1e3a | ||
|
|
af6fd2dc26 | ||
|
|
6e3173c9b8 | ||
|
|
d19d1acbdd | ||
|
|
a21c7bf4d3 | ||
|
|
6a737bcee2 | ||
|
|
323bc072ee | ||
|
|
13aef3b2a0 | ||
|
|
68efe30682 | ||
|
|
2db09b76bf | ||
|
|
8a36296043 | ||
|
|
eab0b76c9d | ||
|
|
95d182cda6 | ||
|
|
c3bbf08768 | ||
|
|
f5eca2c7bc | ||
|
|
892e4fe061 | ||
|
|
aea3a28206 |
@@ -1,467 +0,0 @@
|
||||
# Loopy MIDI Controller - USB MIDI Foot Controller
|
||||
|
||||
A compact ESP32-S3 based USB MIDI foot controller with hardware acceleration, designed to work seamlessly with Loopy Pro.
|
||||
|
||||
## Overview
|
||||
|
||||
This project implements a USB MIDI device that accepts foot pedal inputs and converts them to MIDI Continuous Controller (CC) messages. It's designed to be a reliable replacement for the MIDI-to-USB adapters used with Loopy Pro, offering low latency (1-5ms) and USB compatibility without Bluetooth interference.
|
||||
|
||||
**Key Features:**
|
||||
- **USB MIDI Interface**: Uses ESP32-S3's native USB MIDI support for stable, reliable connectivity
|
||||
- **Expression Pedal Support**: Read analog input with configurable calibration
|
||||
- **Launchpad X Compatible**: Per-pad LED colors matching Novation Launchpad hardware
|
||||
- **Low Latency Design**: 1-5ms response time for responsive control
|
||||
- **MIDI Clock Sync**: Pixel 6 pulses in time with Loopy Pro tempo
|
||||
- **Hardware Acceleration**: Uses multiple cores efficiently without cross-core LED issues
|
||||
|
||||
## Hardware Requirements
|
||||
|
||||
### PCB/Layout Notes
|
||||
- **ESP32-S3-WROOM-1** microcontroller
|
||||
- **Daisy-chained 74HC165** shift registers for button inputs (16 buttons total)
|
||||
- **2x RMT WS2812C** LED drivers for 10 programmable RGB pixels
|
||||
- **ADC on GPIO4** for expression pedal input
|
||||
- **WS2812 external LED** for boot animation (GPIO12)
|
||||
|
||||
### Required Components
|
||||
- ESP32-S3 development board (ESP32-S3-WROOM-1 preferred)
|
||||
- 74HC165 shift register (2x for 16 buttons)
|
||||
- WS2812C LED strip (10 pixels)
|
||||
- PWM/ DAC capable for expression pedal
|
||||
- Level shifters if needed for input voltages
|
||||
|
||||
## Software Architecture
|
||||
|
||||
### Core Design
|
||||
```
|
||||
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
|
||||
│ USB MIDI │ │ MIDI Task │ │ LED Driver │
|
||||
│ (Core 1) │ │ (Core 0) │ │ (Core 0) │
|
||||
│ - Loopy Pro │◄──►│ - Clock Parity │◄──►│ - Pixel Pulse │
|
||||
│ recognition │ │ Detection │ │ on beats │
|
||||
│ - Note/CC │ │ - MIDI update │◄──►│ - Button LED │
|
||||
│ processing │ │ processing │ │ feedback │
|
||||
└─────────────────┘ └──────────────────┘ └─────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ Expression │
|
||||
│ Pedal (Core 0)│
|
||||
│ - ADC Read │
|
||||
│ - CC 4 output │
|
||||
└─────────────────┘
|
||||
```
|
||||
|
||||
### Key Subsystems
|
||||
|
||||
#### 1. USB MIDI Transport (Core 1)
|
||||
- Adafruit TinyUSB library integration
|
||||
- Proper VID/PID spoofing (Novation Launchpad X: 0x1235/0x0103)
|
||||
- MIDI event parsing and routing to Core 0
|
||||
- Re-enumeration support for stable device connection
|
||||
|
||||
#### 2. MIDI Task (Core 0)
|
||||
- Core 0 exclusive for all FastLED operations (prevents crashes)
|
||||
- MIDI clock (0xF8) detection and counting
|
||||
- Beat detection using tick count / 24 (24 PPQN)
|
||||
- Pixel 6 pulsing on every MIDI beat
|
||||
- Button-to-CC message conversion
|
||||
|
||||
#### 3. LED Driver (Core 0)
|
||||
- Per-pad color configuration (Launchpad X compatibility)
|
||||
- Velocity-based color mapping
|
||||
- Dim off-state for power efficiency
|
||||
- Launchpad-style startup animation
|
||||
|
||||
#### 4. Expression Pedal (Core 0)
|
||||
- ADC input on GPIO4 (calibrated min/max)
|
||||
- CC 4 output on MIDI channel 1
|
||||
- Hysteresis filtering for stable readings
|
||||
- 5ms update interval for responsive control
|
||||
|
||||
#### 5. Switch Driver (Core 0)
|
||||
- 16 buttons via daisy-chained 74HC165
|
||||
- 5ms debounce for reliable input
|
||||
- Direct button-to-CC mapping
|
||||
- Per-pad LED feedback
|
||||
|
||||
## Configuration Options
|
||||
|
||||
### MIDI Mapping
|
||||
|
||||
The controller uses a Launchpad X-style mapping for intuitive control:
|
||||
|
||||
| Pad | Hardware Button | MIDI Channel | Note/Pitch | CC | Default Color |
|
||||
|-----|-----------------|--------------|------------|----|---------------|
|
||||
| 0-1 | 0-1 | 1 | 36-45 | - | Blue (0x0000FF) |
|
||||
| 2-4 | 2-4 | 1 | 36-45 | - | Orange (0xFF6600) |
|
||||
| 5 | 5 | 1 | 36 | - | Red (0xFF0000) |
|
||||
| 6 | 6 | 1 | 36 | - | White (0xFFFFFF) |
|
||||
| 7 | 7 | 1 | 36 | - | Amber (0xFFBF00) |
|
||||
| 8-9 | 8-9 | 1 | 36-37 | - | Purple (0x9900FF) |
|
||||
|
||||
**For Loopy Pro Users:**
|
||||
- Buttons generate CC messages (not Note On/Off)
|
||||
- CC values: 2-11 for buttons 0-9
|
||||
- CC 4 for expression pedal
|
||||
- Pixel colors provide visual feedback
|
||||
|
||||
### MIDI Clock Synchronization
|
||||
|
||||
Pixel 6 provides visual feedback synchronized to Loopy Pro's tempo:
|
||||
|
||||
1. **Configure Loopy Pro**: Enable MIDI Clock output targeting JOC Midi
|
||||
2. **Device Recognition**: Loopy Pro detects the VID/PID (0x1235/0x0103)
|
||||
3. **Visual Feedback**: Pixel 6 pulses white on each MIDI Clock beat (0xF8)
|
||||
4. **Pulse Animation**: Quadratic fade (80ms duration) between beats
|
||||
|
||||
**Troubleshooting**: If pixel 6 doesn't pulse:
|
||||
- Verify Loopy Pro has MIDI Clock enabled
|
||||
- Ensure JOC Midi is the clock target
|
||||
- Check MIDI device permissions
|
||||
- Verify MIDI input in Loopy Pro shows "JOC Midi"
|
||||
|
||||
### PlatformIO Configuration
|
||||
|
||||
#### Build Options (`platformio.ini`)
|
||||
|
||||
```ini
|
||||
[env]
|
||||
platform = espressif32
|
||||
board = esp32-s3-devkitc-1
|
||||
framework = arduino
|
||||
build_type = release
|
||||
monitor_speed = 115200
|
||||
upload_speed = 921600
|
||||
|
||||
build_flags =
|
||||
-DCORE_DEBUG_LEVEL=0
|
||||
-DARDUINO_USB_MODE=0
|
||||
-DARDUINO_USB_LAUNCHER_MODE=0
|
||||
|
||||
lib_deps =
|
||||
adafruit/[email protected]
|
||||
https://github.com/FastLED/FastLED/archive/refs/tags/3.6.0.zip
|
||||
|
||||
extra_scripts =
|
||||
pre_build.py
|
||||
extra_script.py
|
||||
```
|
||||
|
||||
#### Build Flags Explanation
|
||||
|
||||
- `-DARDUINO_USB_MODE=0`: USB in CDC/MIDI mode (no virtual serial)
|
||||
- `-DARDUINO_USB_LAUNCHER_MODE=0`: No USB launcher mode
|
||||
- `-DCORE_DEBUG_LEVEL=0`: Disable debug output
|
||||
- `--allow-multiple-definition`: Required for Adafruit TinyUSB compatibility
|
||||
|
||||
#### Build Scripts
|
||||
|
||||
**`pre_build.py`**: Patches board definitions with Launchpad X VID/PID
|
||||
**`extra_script.py`**: Ensures proper TinyUSB linking order
|
||||
|
||||
## Usage
|
||||
|
||||
### Initial Setup
|
||||
|
||||
1. **Flash Firmware**
|
||||
```bash
|
||||
platformio run --target upload
|
||||
```
|
||||
|
||||
2. **Open Serial Monitor** (115200 baud)
|
||||
- Shows startup sequence
|
||||
- MIDI activity diagnostics
|
||||
- System status
|
||||
|
||||
3. **Available Commands** (type in serial)
|
||||
- `help` - Show all commands
|
||||
- `dump` - Display button states
|
||||
- `probe` - Hardware diagnostics
|
||||
- `ledon`/`ledoff` - Turn all LEDs on/off
|
||||
- `ledtest` - Color cycle test
|
||||
- `exp` - Show expression pedal ADC/value
|
||||
- `usb` - USB status
|
||||
- `gpiotest` - Raw GPIO diagnostics
|
||||
- `miditest` - Simulate MIDI input
|
||||
- `padtest` - Test individual pads
|
||||
- `mapping` - Show current pad mapping
|
||||
|
||||
### MIDI Configuration in Loopy Pro
|
||||
|
||||
1. **System Settings**
|
||||
- Name: "JOC Midi"
|
||||
- Manufacturer: "JOC"
|
||||
- Model: "JOC Midi"
|
||||
|
||||
2. **MIDI Setup**
|
||||
- Port 1: Enabled
|
||||
- Input Channel: All
|
||||
- Output Channel: 1 (or preferred)
|
||||
|
||||
3. **Sync Configuration**
|
||||
- Sync Master: LOOPY (if Loopy Pro is master)
|
||||
- Clock Output: Enabled
|
||||
- Clock Targets: JOC Midi
|
||||
|
||||
### Operation
|
||||
|
||||
1. **Button Presses**
|
||||
- Press any button to send corresponding CC message
|
||||
- Pixel color changes to match button state
|
||||
|
||||
2. **Expression Pedal**
|
||||
- Connect foot pedal to ADC input
|
||||
- Calibrate min (heel) and max (toe) positions
|
||||
- Watch pixel 5 for pedal value feedback
|
||||
|
||||
3. **Visual Feedback**
|
||||
- Buttons: Color indicates CC state
|
||||
- Expression pedal: Pixel 5 brightness reflects CC value
|
||||
- Sync: Pixel 6 pulses with MIDI Clock
|
||||
|
||||
## Customization
|
||||
|
||||
### Adding New MIDI Functions
|
||||
|
||||
1. **Add New CC Mappings**
|
||||
```cpp
|
||||
// In switch_stub.h or app_task.cpp
|
||||
static const uint8_t BUTTONS_TO_CC[] = {2, 3, 4, 5, 6, 7, 8, 9, 10, 11};
|
||||
```
|
||||
|
||||
2. **Custom LED Patterns**
|
||||
```cpp
|
||||
// In led_stub.cpp
|
||||
void apply_custom_color(uint8_t index, uint8_t velocity) {
|
||||
if (index == 6) { // Example: Custom color for expression pedal
|
||||
mux_ptr->set_led_color(index, 0, 255, 0); // Green
|
||||
return;
|
||||
}
|
||||
apply_pad_color(index, velocity);
|
||||
}
|
||||
```
|
||||
|
||||
### Changing MIDI Clock Behavior
|
||||
|
||||
1. **Modify Beat Detection**
|
||||
```cpp
|
||||
// In main.cpp, midi_task
|
||||
uint32_t beat_interval = 24; // PPQN for MIDI Clock
|
||||
|
||||
if (tick / beat_interval != last_beat) {
|
||||
last_beat = tick / beat_interval;
|
||||
// Trigger pulse
|
||||
}
|
||||
```
|
||||
|
||||
2. **Change Pulse Animation**
|
||||
```cpp
|
||||
// In main.cpp, midi_task
|
||||
float pulse_speed = 1.0f / 1.0; // Adjust pulse speed
|
||||
mux.set_led_color(6, v * pulse_speed, v * pulse_speed, v * pulse_speed);
|
||||
```
|
||||
|
||||
### Modifying Expression Pedal
|
||||
|
||||
1. **Change ADC Pin**
|
||||
```cpp
|
||||
// In expression_pedal.h/expression_pedal.cpp
|
||||
static const uint8_t EXP_PEDAL_PIN = 4; // or another GPIO
|
||||
```
|
||||
|
||||
2. **Adjust Calibration**
|
||||
```cpp
|
||||
// In main.cpp
|
||||
default exp_pedal.cal_min(0); // Set based on testing
|
||||
default exp_pedal.cal_max(1023); // Set based on testing
|
||||
```
|
||||
|
||||
3. **Change MIDI CC**
|
||||
```cpp
|
||||
// In expression_pedal.cpp
|
||||
send_cc(1, 4, value); // Keep existing
|
||||
// Or change:
|
||||
send_cc(1, 7, value); // Volume instead of expression
|
||||
```
|
||||
|
||||
### Custom Pad Layout
|
||||
|
||||
1. **Map Pads Differently**
|
||||
```cpp
|
||||
// In app_task.h/app_task.cpp
|
||||
#define PAD_NOTE_MAPPING {36, 37, 38, 39, 40, 41, 42, 43, 44, 45}
|
||||
#define PAD_CC_MAPPING {2, 3, 4, 5, 6, 7, 8, 9, 10, 11}
|
||||
```
|
||||
|
||||
2. **Add Special Functions**
|
||||
```cpp
|
||||
// In app_task.cpp
|
||||
void process_special_button(uint8_t led_index) {
|
||||
if (led_index == 5) { // Expression pedal button
|
||||
// Toggle pedal mode
|
||||
}
|
||||
if (led_index == 6) { // MIDI Clock toggle
|
||||
// Toggle clock visualization
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Testing and Diagnostics
|
||||
|
||||
### Serial Commands for Debugging
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `probe` | Test all buttons and LEDs |
|
||||
| `gpiotest` | Check GPIO pin states |
|
||||
| `ledtest` | Cycle through all LED colors |
|
||||
| `miditest` | Simulate MIDI input for testing |
|
||||
| `dump` | Show current button states |
|
||||
|
||||
### Troubleshooting Common Issues
|
||||
|
||||
#### Issue: Pixel 6 doesn't pulse
|
||||
**Cause**: MIDI Clock not arriving
|
||||
**Solution**:
|
||||
1. Check Loopy Pro sync settings
|
||||
2. Verify MIDI Clock target is JOC Midi
|
||||
3. Examine `[CLK] !! NO MIDI CLOCK RECEIVED !!` in serial
|
||||
|
||||
#### Issue: Buttons not sending MIDI
|
||||
**Cause**: Shift register issues
|
||||
**Solution**:
|
||||
1. Run `probe` command
|
||||
2. Check wiring and pin connections
|
||||
3. Verify shift register operation
|
||||
|
||||
#### Issue: Expression pedal unresponsive
|
||||
**Cause**: ADC calibration incorrect
|
||||
**Solution**:
|
||||
1. Use `exp` command to see raw ADC values
|
||||
2. Adjust calibration min/max values
|
||||
3. Check voltage range and connection
|
||||
|
||||
#### Issue: USB connection drops
|
||||
**Cause**: Re-enumeration issues
|
||||
**Solution**:
|
||||
1. Check mounted() status in `[MIDI] USB mounted:` messages
|
||||
2. Ensure proper VID/PID values in build
|
||||
3. Verify TinyUSB initialization
|
||||
|
||||
## Build Instructions
|
||||
|
||||
### Prerequisites
|
||||
|
||||
1. **PlatformIO IDE** or
|
||||
2. **Arduino CLI** with ESP32 core support
|
||||
|
||||
### Quick Build
|
||||
|
||||
```bash
|
||||
# Using PlatformIO
|
||||
platformio run
|
||||
|
||||
# Using Arduino CLI
|
||||
cd your-project
|
||||
arduino-cli compile --builder chitrak/micropython-builder --fqbn esp32-s3-devkitc-1 .
|
||||
```
|
||||
|
||||
### Upload
|
||||
|
||||
```bash
|
||||
# Using PlatformIO
|
||||
platformio upload
|
||||
|
||||
# Using Arduino CLI
|
||||
arduino-cli upload -p /dev/ttyUSB0 --fqbn esp32-s3-devkitc-1 .
|
||||
```
|
||||
|
||||
### Troubleshooting Build Issues
|
||||
|
||||
#### Common Build Errors
|
||||
|
||||
1. **Adafruit TinyUSB conflicts**
|
||||
- Solution: Use the provided `extra_script.py` with `--allow-multiple-definition`
|
||||
- Ensure build_flags match exactly
|
||||
|
||||
2. **Memory Issues**
|
||||
- Solution: Reduce debug output, use release build type
|
||||
- Check stack sizes in platformio.ini
|
||||
|
||||
3. **Pin Conflicts**
|
||||
- Solution: Adjust GPIO pins in configuration files
|
||||
- Verify all components use different pins
|
||||
|
||||
#### Hardware Issues
|
||||
|
||||
1. **LED Driver Problems**
|
||||
- Solution: Test with simple color output commands
|
||||
- Check wiring and power supply
|
||||
|
||||
2. **Button Issues**
|
||||
- Solution: Use `probe` command regularly
|
||||
- Ensure proper pull-up/pull-down configurations
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
### Planned Features
|
||||
|
||||
1. **USB MIDI Sysex Support**
|
||||
- Launchpad X programmer mode
|
||||
- Bank select and patch change
|
||||
|
||||
2. **Advanced Expression Pedal**
|
||||
- Rotary encoding support
|
||||
- Multiple pedal modes (CC1/CC4)
|
||||
|
||||
3. **Enhanced LED Effects**
|
||||
- Breathing animations
|
||||
- SOS patterns for diagnostics
|
||||
- Battery level indicator
|
||||
|
||||
4. **Additional MIDI Functions**
|
||||
- Pitch Bend support
|
||||
- Aftertouch
|
||||
- Poly Pressure
|
||||
|
||||
### Custom Configuration Examples
|
||||
|
||||
#### For Loopy Pro DJs
|
||||
```cpp
|
||||
// Loopy-specific CC mappings
|
||||
static const uint8_t LOOPY_CC_MAP[10] = {2, 3, 4, 5, 6, 7, 8, 9, 10, 11};
|
||||
```
|
||||
|
||||
#### For External Controllers
|
||||
```cpp
|
||||
// CC-based interaction
|
||||
static const uint8_t EXTERNAL_CC_TARGET = 1;
|
||||
send_cc(EXTERNAL_CC_TARGET, 7, value); // Volume
|
||||
```
|
||||
|
||||
#### For Recording Studios
|
||||
```cpp
|
||||
// Note-based for drum triggers
|
||||
static const uint8_t DRUM_NOTES[10] = {36, 38, 40, 42, 44, 45, 47, 48, 50, 52};
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
This project is provided as-is with no explicit license. The code is intended for educational and personal use. Modifications and distributions should respect original authors' intentions where specified.
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
- **Novation Launchpad**: Inspiration for layout and color scheme
|
||||
- **ESP32-S3**: Powerful microcontroller with native USB MIDI
|
||||
- **Adafruit TinyUSB**: Reliable USB MIDI stack
|
||||
- **FastLED**: Efficient LED control library
|
||||
- **Contributors**: All who tested and provided feedback
|
||||
|
||||
## Contact
|
||||
|
||||
For issues or questions:
|
||||
1. Check the project documentation
|
||||
2. Review serial output for diagnostic messages
|
||||
3. Use available testing commands
|
||||
4. Submit issues with complete build logs if encountering problems
|
||||
+9
-2
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <cstdint>
|
||||
#include "midi_transport.h"
|
||||
#include "ble_midi_transport.h"
|
||||
#include "led_stub.h"
|
||||
#include "switch_stub.h"
|
||||
|
||||
@@ -14,7 +15,7 @@ struct PadMapping {
|
||||
|
||||
class AppTask {
|
||||
public:
|
||||
AppTask(LedStub* led, SwitchStub* sw, UsbMidiTransport* midi);
|
||||
AppTask(LedStub* led, SwitchStub* sw, UsbMidiTransport* usb_midi, BleMidiTransport* ble_midi = nullptr);
|
||||
|
||||
void begin();
|
||||
void update();
|
||||
@@ -23,7 +24,8 @@ public:
|
||||
private:
|
||||
LedStub* led_driver;
|
||||
SwitchStub* switch_driver;
|
||||
UsbMidiTransport* midi_transport;
|
||||
UsbMidiTransport* usb_midi;
|
||||
BleMidiTransport* ble_midi;
|
||||
|
||||
static const uint8_t NUM_PADS = 10;
|
||||
PadMapping pad_mapping[NUM_PADS];
|
||||
@@ -36,6 +38,11 @@ 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);
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -1,36 +0,0 @@
|
||||
#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);
|
||||
};
|
||||
@@ -8,6 +8,7 @@ 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
|
||||
@@ -16,6 +17,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
|
||||
|
||||
|
||||
+169
-3
@@ -1,5 +1,6 @@
|
||||
import os
|
||||
import fileinput
|
||||
import re
|
||||
|
||||
|
||||
def patch_usb_ids():
|
||||
# Find the core's pins_arduino.h for ESP32-S3 DevKitC-1
|
||||
@@ -15,7 +16,7 @@ def patch_usb_ids():
|
||||
pins_file = os.path.join(core_variants, "esp32s3", "pins_arduino.h")
|
||||
|
||||
if os.path.exists(pins_file):
|
||||
print(f"Patching {pins_file} with Launchpad X VID/PID")
|
||||
print(f"Patching {pins_file} with JOC Midi USB descriptors")
|
||||
|
||||
# Read and replace
|
||||
with open(pins_file, 'r') as f:
|
||||
@@ -46,4 +47,169 @@ def patch_usb_ids():
|
||||
else:
|
||||
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()
|
||||
@@ -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
|
||||
+29
-8
@@ -1,8 +1,8 @@
|
||||
#include "app_task.h"
|
||||
#include <Arduino.h>
|
||||
|
||||
AppTask::AppTask(LedStub* led, SwitchStub* sw, UsbMidiTransport* midi)
|
||||
: led_driver(led), switch_driver(sw), midi_transport(midi) {
|
||||
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) {
|
||||
|
||||
// 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};
|
||||
@@ -20,11 +20,15 @@ AppTask::AppTask(LedStub* led, SwitchStub* sw, UsbMidiTransport* midi)
|
||||
|
||||
void AppTask::begin() {
|
||||
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);
|
||||
});
|
||||
|
||||
};
|
||||
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);
|
||||
@@ -36,13 +40,22 @@ void AppTask::update() {
|
||||
bool is_pressed = switch_driver->is_pressed(i);
|
||||
|
||||
if (is_pressed && !last_switch_state[i]) {
|
||||
Serial.printf("[APP] Switch %d pressed\n", i);
|
||||
process_switch_event(i, true);
|
||||
last_switch_state[i] = true;
|
||||
} else if (!is_pressed && last_switch_state[i]) {
|
||||
Serial.printf("[APP] Switch %d released\n", i);
|
||||
process_switch_event(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) {
|
||||
@@ -128,12 +141,20 @@ void AppTask::process_switch_event(uint8_t switch_id, bool pressed) {
|
||||
if (pad_mapping[i].physical_switch == switch_id) {
|
||||
uint8_t channel = pad_mapping[i].midi_channel;
|
||||
uint8_t cc_num = cc_map[i];
|
||||
// Use palette index 127 (magenta) for visible feedback
|
||||
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");
|
||||
|
||||
if (pressed) {
|
||||
midi_transport->send_cc(channel, cc_num, value);
|
||||
// Send MIDI via a flag that loop() processes
|
||||
pending_cc = true;
|
||||
pending_channel = channel;
|
||||
pending_cc_num = cc_num;
|
||||
pending_value = value;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
#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);
|
||||
}
|
||||
+7
-32
@@ -142,35 +142,6 @@ static uint32_t velocity_to_color(uint8_t 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) {
|
||||
for (int i = 0; i < NUM_LEDS; i++) {
|
||||
led_states[i].active = false;
|
||||
@@ -232,14 +203,18 @@ void DefaultLedStub::begin() {
|
||||
void DefaultLedStub::set_led_state(uint8_t note, uint8_t channel, uint8_t velocity, int8_t led_index) {
|
||||
if (!initialized || !mux_ptr) return;
|
||||
|
||||
// Direct index mode (used by CC feedback / pad colors)
|
||||
// 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();
|
||||
apply_pad_color(led_index, velocity);
|
||||
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;
|
||||
@@ -290,7 +265,7 @@ void DefaultLedStub::clear_all() {
|
||||
led_states[i].note = 0;
|
||||
led_states[i].channel = 0;
|
||||
led_states[i].timestamp = 0;
|
||||
apply_pad_color(i, 0);
|
||||
mux_ptr->set_led_color(i, 0, 0, 0);
|
||||
}
|
||||
mux_ptr->show();
|
||||
Serial.println("[LED] All cleared");
|
||||
|
||||
+12
-15
@@ -5,21 +5,21 @@
|
||||
#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"
|
||||
#include "switch_stub.h"
|
||||
#include "app_task.h"
|
||||
#include "expression_pedal.h"
|
||||
|
||||
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);
|
||||
ExpressionPedal exp_pedal(4);
|
||||
AppTask controller(&led_driver, &switch_driver, &midi_transport, &ble_midi_transport);
|
||||
|
||||
TaskHandle_t midi_task_handle = NULL;
|
||||
|
||||
@@ -28,7 +28,7 @@ void midi_task(void* parameter) {
|
||||
|
||||
while (true) {
|
||||
midi_transport.update();
|
||||
exp_pedal.update(midi_transport);
|
||||
ble_midi_transport.update();
|
||||
vTaskDelay(1);
|
||||
}
|
||||
}
|
||||
@@ -86,9 +86,6 @@ void handle_serial_command(const String& cmd) {
|
||||
mux.set_led_color(1, 255, 255, 255);
|
||||
mux.show();
|
||||
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") {
|
||||
Serial.printf("[CMD] USB mounted: %s\n", TinyUSBDevice.mounted() ? "YES" : "NO");
|
||||
Serial.printf("[CMD] USB ready: %s\n", TinyUSBDevice.ready() ? "YES" : "NO");
|
||||
@@ -132,7 +129,6 @@ void handle_serial_command(const String& cmd) {
|
||||
Serial.println(" read - raw button read");
|
||||
Serial.println(" red/green/blue - solid colour");
|
||||
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(" gpiotest - raw GPIO pin diagnostic");
|
||||
Serial.println(" rawled - bit-bang WS2812 (no library)");
|
||||
@@ -335,7 +331,7 @@ void setup() {
|
||||
|
||||
Serial.println("=================================");
|
||||
Serial.println(" Loopy MIDI Controller v0.1");
|
||||
Serial.println(" Phase 1: USB MIDI + Expression Pedal");
|
||||
Serial.println(" Phase 1.5: USB + BLE MIDI");
|
||||
Serial.println(" Board: ESP32-S3-WROOM-1");
|
||||
Serial.println("=================================");
|
||||
|
||||
@@ -350,12 +346,13 @@ void setup() {
|
||||
switch_driver.set_mux(&mux);
|
||||
switch_driver.begin();
|
||||
|
||||
Serial.println("[INIT] Initializing USB MIDI...");
|
||||
Serial.println("[INIT] Initializing USB MIDI first (before BLE)...");
|
||||
midi_transport.begin();
|
||||
|
||||
Serial.println("[INIT] Initializing Expression Pedal...");
|
||||
exp_pedal.begin();
|
||||
|
||||
delay(1000);
|
||||
|
||||
Serial.println("[INIT] Initializing BLE MIDI...");
|
||||
ble_midi_transport.begin();
|
||||
|
||||
Serial.println("[INIT] Registering MIDI callbacks...");
|
||||
controller.begin();
|
||||
|
||||
@@ -387,5 +384,5 @@ void loop() {
|
||||
handle_serial_command(cmd);
|
||||
}
|
||||
|
||||
delay(1);
|
||||
delay(10);
|
||||
}
|
||||
|
||||
@@ -92,6 +92,7 @@ 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);
|
||||
}
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@ DefaultSwitchStub::DefaultSwitchStub() : initialized(false) {
|
||||
switch_states[i].current_state = false;
|
||||
switch_states[i].previous_state = false;
|
||||
switch_states[i].last_change_time = 0;
|
||||
switch_states[i].debounce_time = 5;
|
||||
switch_states[i].debounce_time = 50;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user