Added more examples, etc

This commit is contained in:
pszsh 2026-03-09 01:55:11 -07:00
parent 3927d073e9
commit be1ee30967
79 changed files with 1906 additions and 66 deletions

View File

@ -1,12 +1,33 @@
# cs-midi
A pico-sdk port of [tttapa/Control-Surface](https://github.com/tttapa/Control-Surface) (GPL-3.0).
A standalone extraction of [tttapa/Control-Surface](https://github.com/tttapa/Control-Surface) (GPL-3.0) for embedded platforms.
cs-midi provides the full Control Surface MIDI element system — output elements, input elements, banks, selectors, MIDI routing, and the declarative `Control_Surface` singleton — for use with the Raspberry Pi Pico SDK.
cs-midi provides the full Control Surface MIDI element system — output elements, input elements, banks, selectors, MIDI routing, and the declarative `Control_Surface` singleton.
Supported platforms:
| Platform | Transport | Status |
|----------|-----------|--------|
| **RP2xxx** (pico-sdk) | BLE, USB, Serial, AppleMIDI | Stable |
| **ESP32-S3** | BLE, USB, Serial | Planned |
| **ESP32-C3** | BLE, Serial | Planned |
Use the **Platform** dropdown in the sidebar to view examples for your target device.
### RP2xxx Radio Module Note
The RP2xxx BLE transport uses BTstack with the CYW43 radio (Infineon RM2 module). While other CYW43-based radio modules may work, **only the RM2 has been tested** and has its driver fully implemented.
A pico-sdk board file for the Waveshare RP2350B Plus W (RM2-equipped) is provided at `boards/waveshare_rp2350b_plus_w.h`. To use it, set `PICO_BOARD_HEADER_DIRS` in your CMake config:
```cmake pico
set(PICO_BOARD waveshare_rp2350b_plus_w)
set(PICO_BOARD_HEADER_DIRS ${CMAKE_SOURCE_DIR}/lib/cs-midi/boards)
```
## Getting Started
```cpp
```cpp pico
#include "pico/stdlib.h"
#include <cs_midi.h>
@ -30,7 +51,7 @@ int main() {
cs-midi is a static library meant to be included in a pico-sdk project:
```cmake
```cmake pico
add_subdirectory(lib/cs-midi)
target_link_libraries(your_target cs_midi)
```
@ -43,6 +64,7 @@ CMake options (set before `add_subdirectory()`):
| `CS_MIDI_USB` | OFF | USB MIDI via TinyUSB |
| `CS_MIDI_SERIAL` | OFF | Serial MIDI over UART |
| `CS_MIDI_APPLEMIDI` | OFF | AppleMIDI (RTP-MIDI over WiFi) |
| `CS_MIDI_HID_KEYBOARD` | OFF | HID keyboard + Battery Service for BLE auto-reconnect |
USB MIDI requires `tusb_config.h` and `usb_descriptors.c` in your project — see `templates/` for reference files.
@ -58,6 +80,8 @@ cd lib/cs-midi && make tests
- Standard `main()` replaces Arduino `setup()`/`loop()`
- All types live in the `cs::` namespace
- No `MCU::` namespace yet — use raw CC numbers or `MIDI_CC::` constants
- Direct MIDI sending and callbacks available on all interface objects
- `MIDI_Callbacks` and `FineGrainedMIDI_Callbacks<T>` for incoming message handling
## Credits

View File

@ -4,7 +4,7 @@ Sends Note On/Off on button press/release.
> Original: `NoteButton.ino`
```cpp
```cpp pico
#include "pico/stdlib.h"
#include "pico/cyw43_arch.h"
#include <cs_midi.h>

View File

@ -4,7 +4,7 @@ Sends CC 127 on press, CC 0 on release.
> Original: `CCButton.ino`
```cpp
```cpp pico
#include "pico/stdlib.h"
#include "pico/cyw43_arch.h"
#include <cs_midi.h>

View File

@ -4,7 +4,7 @@ Sends Program Change on button press.
> Original: `PCButton.ino`
```cpp
```cpp pico
#include "pico/stdlib.h"
#include "pico/cyw43_arch.h"
#include <cs_midi.h>

View File

@ -4,7 +4,7 @@ Plays a chord on button press.
> Original: `NoteChordButton.ino`
```cpp
```cpp pico
#include "pico/stdlib.h"
#include "pico/cyw43_arch.h"
#include <cs_midi.h>

View File

@ -2,7 +2,7 @@
First press sends Note On, second press sends Note Off.
```cpp
```cpp pico
#include "pico/stdlib.h"
#include "pico/cyw43_arch.h"
#include <cs_midi.h>

View File

@ -2,7 +2,7 @@
Note On while held, Note Off on release; toggles state.
```cpp
```cpp pico
#include "pico/stdlib.h"
#include "pico/cyw43_arch.h"
#include <cs_midi.h>

View File

@ -2,7 +2,7 @@
Multiple note buttons with sequential addresses.
```cpp
```cpp pico
#include "pico/stdlib.h"
#include "pico/cyw43_arch.h"
#include <cs_midi.h>

View File

@ -4,7 +4,7 @@ Grid of note buttons using row/column scanning.
> Original: `NoteButtonMatrix.ino`
```cpp
```cpp pico
#include "pico/stdlib.h"
#include "pico/cyw43_arch.h"
#include <cs_midi.h>

View File

@ -2,7 +2,7 @@
First press sends CC 127, second press sends CC 0.
```cpp
```cpp pico
#include "pico/stdlib.h"
#include "pico/cyw43_arch.h"
#include <cs_midi.h>

View File

@ -2,7 +2,7 @@
CC 127 while held, CC 0 on release; toggles state.
```cpp
```cpp pico
#include "pico/stdlib.h"
#include "pico/cyw43_arch.h"
#include <cs_midi.h>

View File

@ -2,7 +2,7 @@
Multiple CC buttons with sequential addresses.
```cpp
```cpp pico
#include "pico/stdlib.h"
#include "pico/cyw43_arch.h"
#include <cs_midi.h>

View File

@ -2,7 +2,7 @@
Grid of CC buttons using row/column scanning.
```cpp
```cpp pico
#include "pico/stdlib.h"
#include "pico/cyw43_arch.h"
#include <cs_midi.h>

View File

@ -4,7 +4,7 @@ Analog input mapped to CC value 0-127.
> Original: `Control-Change-Potentiometer.ino`
```cpp
```cpp pico
#include "pico/stdlib.h"
#include "pico/cyw43_arch.h"
#include <cs_midi.h>

View File

@ -4,7 +4,7 @@ Analog input mapped to 14-bit Pitch Bend.
> Original: `Pitch-Bend-Potentiometer.ino`
```cpp
```cpp pico
#include "pico/stdlib.h"
#include "pico/cyw43_arch.h"
#include <cs_midi.h>

View File

@ -4,7 +4,7 @@ Relative CC encoder for DAW parameter control.
> Original: `RotaryEncoder.ino`
```cpp
```cpp pico
#include "pico/stdlib.h"
#include "pico/cyw43_arch.h"
#include <cs_midi.h>

View File

@ -4,7 +4,7 @@ Encoder position mapped to absolute CC value.
> Original: `AbsoluteRotaryEncoder.ino`
```cpp
```cpp pico
#include "pico/stdlib.h"
#include "pico/cyw43_arch.h"
#include <cs_midi.h>

View File

@ -2,7 +2,7 @@
Encoder position mapped to 14-bit Pitch Bend.
```cpp
```cpp pico
#include "pico/stdlib.h"
#include "pico/cyw43_arch.h"
#include <cs_midi.h>

View File

@ -4,7 +4,7 @@ Two buttons for CC increment/decrement with optional reset.
> Original: `CCIncrementDecrementButtons.ino`
```cpp
```cpp pico
#include "pico/stdlib.h"
#include "pico/cyw43_arch.h"
#include <cs_midi.h>

View File

@ -4,7 +4,7 @@ Sends different program changes based on selector state.
> Original: `Program-Changer.ino`
```cpp
```cpp pico
#include "pico/stdlib.h"
#include "pico/cyw43_arch.h"
#include <cs_midi.h>

View File

@ -4,7 +4,7 @@ Several analog inputs mapped to CC on different channels.
> Original: `Multiple-Control-Change-Potentiometers.ino`
```cpp
```cpp pico
#include "pico/stdlib.h"
#include "pico/cyw43_arch.h"
#include <cs_midi.h>

View File

@ -2,7 +2,7 @@
Shared encoder reference for CC output, allowing multiple elements to read the same physical encoder.
```cpp
```cpp pico
#include "pico/stdlib.h"
#include "pico/cyw43_arch.h"
#include <cs_midi.h>

View File

@ -0,0 +1,38 @@
# CCPotentiometer Map
Analog input with a custom mapping function to eliminate dead zones.
> Original: `CCPotentiometer-Map.ino`
```cpp pico
#include "pico/stdlib.h"
#include "pico/cyw43_arch.h"
#include <cs_midi.h>
using namespace cs;
BluetoothMIDI_Interface midi;
CCPotentiometer potentiometer {26, {MIDI_CC::Channel_Volume, Channel_1}};
constexpr analog_t maxRawValue = CCPotentiometer::getMaxRawValue();
constexpr analog_t minimumValue = maxRawValue / 64;
constexpr analog_t maximumValue = maxRawValue - maxRawValue / 64;
analog_t mappingFunction(analog_t raw) {
if (raw < minimumValue) raw = minimumValue;
if (raw > maximumValue) raw = maximumValue;
return (raw - minimumValue) * maxRawValue / (maximumValue - minimumValue);
}
int main() {
stdio_init_all();
if (cyw43_arch_init()) return 1;
potentiometer.map(mappingFunction);
Control_Surface.begin();
while (true) {
Control_Surface.loop();
sleep_ms(1);
}
}
```

View File

@ -0,0 +1,40 @@
# Finished Controller
Comprehensive example combining buttons, encoder, potentiometer, and bank switching with bankable input elements.
> Original: `MIDI-Controller-Finished-Example.ino` (adapted)
```cpp pico
#include "pico/stdlib.h"
#include "pico/cyw43_arch.h"
#include <cs_midi.h>
using namespace cs;
BluetoothMIDI_Interface midi;
// 4 banks, 2 tracks per bank
Bank<4> bank(2);
// Two buttons to cycle banks
IncrementDecrementSelector<4> bankSelector {bank, {2, 3}, Wrap::Wrap};
// Bankable CC value readers — bank switches the CC address
Bankable::CCValue<4> volume1 {{bank, BankType::ChangeAddress}, {16, Channel_1}};
Bankable::CCValue<4> volume2 {{bank, BankType::ChangeAddress}, {17, Channel_1}};
// Non-bankable output elements
NoteButton button {5, {MIDI_Notes::C[4], Channel_1}};
CCPotentiometer pot {26, {MIDI_CC::Channel_Volume, Channel_1}};
CCRotaryEncoder enc {{0, 1}, {MIDI_CC::Pan, Channel_1}, 1, 4};
int main() {
stdio_init_all();
if (cyw43_arch_init()) return 1;
Control_Surface.begin();
while (true) {
Control_Surface.loop();
sleep_ms(1);
}
}
```

View File

@ -0,0 +1,40 @@
# Enable/Disable Elements
Enable and disable MIDI elements at runtime using a toggle button.
> Original: `Enable-disable.ino`
```cpp pico
#include "pico/stdlib.h"
#include "pico/cyw43_arch.h"
#include <cs_midi.h>
using namespace cs;
BluetoothMIDI_Interface midi;
NoteButton button1 {5, {MIDI_Notes::C[4], Channel_1}};
NoteButton button2 {6, {MIDI_Notes::D[4], Channel_1}};
AH::Button toggleBtn {7};
bool button2Enabled = true;
int main() {
stdio_init_all();
if (cyw43_arch_init()) return 1;
toggleBtn.begin();
Control_Surface.begin();
while (true) {
Control_Surface.loop();
toggleBtn.update();
if (toggleBtn.getState() == AH::Button::Falling) {
button2Enabled = !button2Enabled;
if (button2Enabled)
button2.enable();
else
button2.disable();
}
sleep_ms(1);
}
}
```

View File

@ -4,7 +4,7 @@ LED responds to incoming Note On/Off messages.
> Original: `1.Note-LED.ino`
```cpp
```cpp pico
#include "pico/stdlib.h"
#include "pico/cyw43_arch.h"
#include <cs_midi.h>

View File

@ -4,7 +4,7 @@ Multiple LEDs respond to a range of incoming notes.
> Original: `2.Note-Range-LEDs.ino`
```cpp
```cpp pico
#include "pico/stdlib.h"
#include "pico/cyw43_arch.h"
#include <cs_midi.h>

View File

@ -2,7 +2,7 @@
Reads incoming CC messages and stores the value.
```cpp
```cpp pico
#include "pico/stdlib.h"
#include "pico/cyw43_arch.h"
#include <cs_midi.h>

View File

@ -4,7 +4,7 @@ Reads incoming Pitch Bend messages (14-bit).
> Original: `Pitch-Bend-Value.ino`
```cpp
```cpp pico
#include "pico/stdlib.h"
#include "pico/cyw43_arch.h"
#include <cs_midi.h>

View File

@ -2,7 +2,7 @@
Reads incoming Note On values (8-bit).
```cpp
```cpp pico
#include "pico/stdlib.h"
#include "pico/cyw43_arch.h"
#include <cs_midi.h>

View File

@ -2,7 +2,7 @@
Reads incoming Key Pressure (aftertouch) values.
```cpp
```cpp pico
#include "pico/stdlib.h"
#include "pico/cyw43_arch.h"
#include <cs_midi.h>

View File

@ -2,7 +2,7 @@
LED responds to incoming CC messages.
```cpp
```cpp pico
#include "pico/stdlib.h"
#include "pico/cyw43_arch.h"
#include <cs_midi.h>

View File

@ -2,7 +2,7 @@
Reads a contiguous range of incoming Note values.
```cpp
```cpp pico
#include "pico/stdlib.h"
#include "pico/cyw43_arch.h"
#include <cs_midi.h>

View File

@ -2,7 +2,7 @@
Reads a contiguous range of incoming CC values.
```cpp
```cpp pico
#include "pico/stdlib.h"
#include "pico/cyw43_arch.h"
#include <cs_midi.h>

View File

@ -2,7 +2,21 @@
Bluetooth Low Energy MIDI interface setup.
```cpp
Build with `CS_MIDI_HID_KEYBOARD=ON` to also advertise as a BLE HID
keyboard with a Battery Service. This serves two purposes:
1. **Auto-reconnect** — the host OS auto-reconnects to bonded HID
peripherals on power-up, which brings up the MIDI service without
manual intervention.
2. **Battery level** — the standard BLE Battery Service (UUID 0x180F)
reports charge level to the OS. Call `setBLEBatteryLevel(percent)` from
your application to update it (e.g. from an ADC reading on a voltage
divider).
No keyboard input is ever sent — the HID descriptor exists solely to
trigger the OS auto-connect behavior and surface the battery indicator.
```cpp pico
#include "pico/stdlib.h"
#include "pico/cyw43_arch.h"
#include <cs_midi.h>

View File

@ -4,7 +4,7 @@ Routing between MIDI interfaces using pipe operators.
> Original: `MIDI_Pipes-Routing.ino`
```cpp
```cpp pico
#include "pico/stdlib.h"
#include "pico/cyw43_arch.h"
#include <cs_midi.h>

View File

@ -4,7 +4,7 @@ USB MIDI device interface via TinyUSB.
Requires `tusb_config.h` and `usb_descriptors.c` in the project — see `lib/cs-midi/templates/` for reference files. Link `tinyusb_device` and `tinyusb_board` in CMake.
```cpp
```cpp pico
#include "pico/stdlib.h"
#include "tusb.h"
#include <cs_midi.h>

View File

@ -4,7 +4,7 @@ Classic 5-pin DIN MIDI over hardware UART at 31250 baud.
Constructor: `HardwareSerialMIDI_Interface(uart_inst_t *uart, uint tx_pin, uint rx_pin)`
```cpp
```cpp pico
#include "pico/stdlib.h"
#include <cs_midi.h>

View File

@ -2,7 +2,7 @@
Multiple MIDI interfaces with pipe routing. Elements send to both transports simultaneously.
```cpp
```cpp pico
#include "pico/stdlib.h"
#include "pico/cyw43_arch.h"
#include "tusb.h"

View File

@ -5,7 +5,16 @@ Windows rtpMIDI, and iOS. Requires WiFi connection before `Control_Surface.begin
Build with `CS_MIDI_APPLEMIDI=ON`.
```cpp
> **Reliability note:** AppleMIDI is implemented via the vendored
> [lathoub/Arduino-AppleMIDI-Library](https://github.com/lathoub/Arduino-AppleMIDI-Library)
> with a LwIPUDP adapter wrapping the lwIP raw UDP API and mDNS/Bonjour
> service advertisement. The protocol implementation is complete and the
> device does appear in Audio MIDI Setup, but in our testing the session
> would not reliably establish on at least one macOS host. We suspect this
> is device-specific — your mileage may vary. If you have success (or
> failure) on your setup, reports are welcome.
```cpp pico
#include "pico/stdlib.h"
#include "pico/cyw43_arch.h"
#include <cs_midi.h>

View File

@ -5,7 +5,11 @@ Elements send to both transports simultaneously via pipe routing.
Build with `CS_MIDI_BLE=ON CS_MIDI_APPLEMIDI=ON`.
```cpp
> **Note:** See the [AppleMIDI example](06-applemidi.md) for a reliability
> caveat on the WiFi transport. BLE remains the more reliable option on
> macOS/iOS.
```cpp pico
#include "pico/stdlib.h"
#include "pico/cyw43_arch.h"
#include <cs_midi.h>

View File

@ -0,0 +1,31 @@
# Direct MIDI Output
Send MIDI messages directly through the interface without using the element system.
> Original: `MIDI-Output.ino`
```cpp pico
#include "pico/stdlib.h"
#include "pico/cyw43_arch.h"
#include <cs_midi.h>
using namespace cs;
BluetoothMIDI_Interface midi;
const MIDIAddress address {MIDI_Notes::C[4], Channel_1};
const uint8_t velocity = 0x7F;
int main() {
stdio_init_all();
if (cyw43_arch_init()) return 1;
midi.begin();
while (true) {
midi.sendNoteOn(address, velocity);
sleep_ms(500);
midi.sendNoteOff(address, velocity);
sleep_ms(500);
midi.update();
}
}
```

View File

@ -0,0 +1,35 @@
# Send MIDI Notes
Button-driven Note On/Off using the MIDI interface directly, without the element system.
> Original: `Send-MIDI-Notes.ino`
```cpp pico
#include "pico/stdlib.h"
#include "pico/cyw43_arch.h"
#include <cs_midi.h>
using namespace cs;
BluetoothMIDI_Interface midi;
AH::Button pushbutton {5};
const MIDIAddress noteAddress {MIDI_Notes::C[4], Channel_1};
const uint8_t velocity = 0x7F;
int main() {
stdio_init_all();
if (cyw43_arch_init()) return 1;
pushbutton.begin();
midi.begin();
while (true) {
midi.update();
pushbutton.update();
if (pushbutton.getState() == AH::Button::Falling)
midi.sendNoteOn(noteAddress, velocity);
else if (pushbutton.getState() == AH::Button::Rising)
midi.sendNoteOff(noteAddress, velocity);
}
}
```

View File

@ -0,0 +1,59 @@
# Send All MIDI Messages
Demonstrates every MIDI message type: channel voice, system common, system exclusive, and real-time.
> Original: `Send-All-MIDI-Messages.ino`
```cpp pico
#include "pico/stdlib.h"
#include "pico/cyw43_arch.h"
#include <cs_midi.h>
using namespace cs;
BluetoothMIDI_Interface midi;
int main() {
stdio_init_all();
if (cyw43_arch_init()) return 1;
midi.begin();
// Channel voice messages
MIDIAddress note = {MIDI_Notes::C[4], Channel_6};
midi.sendNoteOn(note, 127);
midi.sendNoteOff(note, 127);
midi.sendKeyPressure(note, 64);
MIDIAddress controller = {MIDI_CC::Channel_Volume, Channel_2};
midi.sendControlChange(controller, 120);
MIDIAddress program = {MIDI_PC::Harpsichord, Channel_9};
midi.sendProgramChange(program);
midi.sendChannelPressure(Channel_3, 64);
midi.sendPitchBend(Channel_3, 16383);
// System common messages
midi.sendMTCQuarterFrame(2, 15);
midi.sendSongPositionPointer(10000);
midi.sendSongSelect(70);
midi.sendTuneRequest();
// System exclusive
uint8_t sysex[] = {0xF0, 0x00, 0x01, 0x02, 0x03, 0xF7};
midi.sendSysEx(sysex);
// Real-time messages
midi.sendTimingClock();
midi.sendStart();
midi.sendContinue();
midi.sendStop();
midi.sendActiveSensing();
midi.sendSystemReset();
while (true) {
midi.update();
sleep_ms(1);
}
}
```

View File

@ -0,0 +1,38 @@
# MIDI Input Callback
Receive and handle incoming MIDI messages using the `MIDI_Callbacks` class.
> Original: `MIDI-Input.ino`
```cpp pico
#include "pico/stdlib.h"
#include "pico/cyw43_arch.h"
#include <cs_midi.h>
using namespace cs;
BluetoothMIDI_Interface midi;
struct MyMIDI_Callbacks : MIDI_Callbacks {
void onChannelMessage(MIDI_Interface &, ChannelMessage msg) override {
(void)msg;
}
void onSysExMessage(MIDI_Interface &, SysExMessage msg) override {
(void)msg;
}
void onRealTimeMessage(MIDI_Interface &, RealTimeMessage msg) override {
(void)msg;
}
} callback;
int main() {
stdio_init_all();
if (cyw43_arch_init()) return 1;
midi.begin();
midi.setCallbacks(callback);
while (true) {
midi.update();
sleep_ms(1);
}
}
```

View File

@ -0,0 +1,49 @@
# MIDI Pipe Filter
Custom MIDI pipe that filters and modifies messages. Only Note messages pass through, transposed down one octave and redirected to Channel 5.
> Original: `MIDI_Pipes-Filter.ino`
```cpp pico
#include "pico/stdlib.h"
#include "pico/cyw43_arch.h"
#include <cs_midi.h>
using namespace cs;
BluetoothMIDI_Interface midi;
struct NoteFilter : MIDI_Pipe {
void mapForwardMIDI(ChannelMessage msg) override {
switch (msg.getMessageType()) {
case MIDIMessageType::NoteOff:
case MIDIMessageType::NoteOn:
msg.setChannel(Channel_5);
if (msg.data1 >= 12)
msg.data1 -= 12;
sourceMIDItoSink(msg);
break;
default:
break;
}
}
void mapForwardMIDI(SysExMessage) override {}
void mapForwardMIDI(SysCommonMessage) override {}
void mapForwardMIDI(RealTimeMessage msg) override {
sourceMIDItoSink(msg);
}
};
NoteFilter filter;
int main() {
stdio_init_all();
if (cyw43_arch_init()) return 1;
midi >> filter >> midi;
midi.begin();
while (true) {
midi.update();
sleep_ms(1);
}
}
```

View File

@ -4,7 +4,7 @@ Bank switching with bankable input elements. Bank switching changes the CC addre
> Original: `Bank.ino` (adapted)
```cpp
```cpp pico
#include "pico/stdlib.h"
#include "pico/cyw43_arch.h"
#include <cs_midi.h>

View File

@ -4,7 +4,7 @@ Note transposition via increment/decrement buttons.
> Original: `Transposer.ino`
```cpp
```cpp pico
#include "pico/stdlib.h"
#include "pico/cyw43_arch.h"
#include <cs_midi.h>

View File

@ -2,7 +2,7 @@
Bank selection via rotary encoder.
```cpp
```cpp pico
#include "pico/stdlib.h"
#include "pico/cyw43_arch.h"
#include <cs_midi.h>

View File

@ -2,7 +2,7 @@
One button per bank for direct bank selection.
```cpp
```cpp pico
#include "pico/stdlib.h"
#include "pico/cyw43_arch.h"
#include <cs_midi.h>

View File

@ -2,7 +2,7 @@
Two-state bank selection from a toggle switch.
```cpp
```cpp pico
#include "pico/stdlib.h"
#include "pico/cyw43_arch.h"
#include <cs_midi.h>

View File

@ -2,7 +2,7 @@
Bank selection triggered by incoming MIDI Program Change messages.
```cpp
```cpp pico
#include "pico/stdlib.h"
#include "pico/cyw43_arch.h"
#include <cs_midi.h>

View File

@ -2,7 +2,7 @@
Bank-switched LED responds to different notes per bank.
```cpp
```cpp pico
#include "pico/stdlib.h"
#include "pico/cyw43_arch.h"
#include <cs_midi.h>

View File

@ -1,3 +1,18 @@
# Examples
Each example is a 1:1 adaptation of the corresponding [Control Surface example](https://tttapa.github.io/Control-Surface/Doxygen/d4/de9/examples.html), rewritten for pico-sdk conventions. All examples are compile-verified.
Each example is a 1:1 adaptation of the corresponding [Control Surface example](https://tttapa.github.io/Control-Surface/Doxygen/d4/de9/examples.html), rewritten for pico-sdk conventions. All examples are compile-verified as tests.
Use the **Platform** dropdown in the sidebar to view examples for your target device. Examples not yet available for a platform will show a notice.
## Original examples not yet portable
The following Control Surface examples require features not yet implemented in cs-midi:
- **Bankable output elements** (Phase 11) — `Bank.ino` output side, `Bankable-Smart-Control-Change-Potentiometer.ino`, `Bank-Button-Matrix.ino`, `Note-ManyAddresses-Transposer.ino`, `ManyAddressesPCButton.ino`
- **MCP23017 / shift registers**`MCP23017-RotaryEncoder-*.ino`, `1.First-Output.ino` (mux), `2.First-Input.ino` (shift register)
- **Display / MCU** — all OLED and Mackie Control examples
- **FastLED / PWM LEDs**`Note-FastLED*.ino`, `Note-LED-PWM.ino`, `NoteLEDBar.ino`, `VULEDs.ino`
- **Debug interface**`Debug-MIDI-Interface.ino`, `MIDI-Monitor*.ino`
- **SysEx helpers**`SysEx-Send-Receive.ino`
- **USB/BLE adapters**`BLEMIDI-Adapter.ino`, `USBMIDI-Adapter.ino`, `USBHostMIDI_Interface-Passthrough.ino`
- **Board-specific** — Teensy audio, ESP32 VU bridge

View File

@ -0,0 +1,67 @@
# Installation
> Original: [Installation](https://tttapa.github.io/Control-Surface/Doxygen/d8/da8/md_pages_Installation.html)
cs-midi is a CMake static library included as a subdirectory of your pico-sdk project.
## Prerequisites
- [pico-sdk](https://github.com/raspberrypi/pico-sdk) (1.5.1+)
- CMake 3.13+
- ARM GCC toolchain (`arm-none-eabi-gcc`)
## Adding cs-midi to your project
Place cs-midi under `lib/` (or any subdirectory) and add it from your project's `CMakeLists.txt`:
```cmake pico
cmake_minimum_required(VERSION 3.13)
include(pico_sdk_import.cmake)
project(my_controller C CXX ASM)
set(CMAKE_C_STANDARD 11)
set(CMAKE_CXX_STANDARD 17)
pico_sdk_init()
# Enable desired transports before add_subdirectory
set(CS_MIDI_BLE ON)
set(CS_MIDI_USB OFF)
set(CS_MIDI_SERIAL OFF)
set(CS_MIDI_APPLEMIDI OFF)
add_subdirectory(lib/cs-midi)
add_executable(my_controller main.cpp)
target_link_libraries(my_controller cs_midi)
pico_add_extra_outputs(my_controller)
```
## Board configuration
For RP2350 boards with the RM2 radio module, cs-midi provides a board header:
```cmake pico
set(PICO_BOARD waveshare_rp2350b_plus_w)
set(PICO_BOARD_HEADER_DIRS ${CMAKE_SOURCE_DIR}/lib/cs-midi/boards)
```
For Pico W (RP2040), use the built-in `pico_w` board:
```cmake pico
set(PICO_BOARD pico_w)
```
## CMake options
| Option | Default | Description |
|--------|---------|-------------|
| `CS_MIDI_BLE` | ON | BLE MIDI via BTstack |
| `CS_MIDI_USB` | OFF | USB MIDI via TinyUSB |
| `CS_MIDI_SERIAL` | OFF | Serial MIDI over UART |
| `CS_MIDI_APPLEMIDI` | OFF | AppleMIDI (RTP-MIDI over WiFi) |
| `CS_MIDI_HID_KEYBOARD` | OFF | HID keyboard + Battery Service for BLE auto-reconnect |
USB MIDI requires `tusb_config.h` and `usb_descriptors.c` in your project — see `templates/` for reference files.

View File

@ -0,0 +1,95 @@
# First Output
> Original: [Getting Started — First Output](https://tttapa.github.io/Control-Surface/Doxygen/d5/d7d/md_pages_Getting-Started.html)
A minimal sketch that reads a physical control and sends MIDI.
## 1. Include the library
```cpp pico
#include "pico/stdlib.h"
#include <cs_midi.h>
using namespace cs;
```
`cs_midi.h` provides all Control Surface classes. The `cs::` namespace contains everything; `using namespace cs` keeps examples concise.
## 2. Instantiate a MIDI interface
At least one MIDI interface must exist before `Control_Surface.begin()`.
```cpp pico
BluetoothMIDI_Interface midi;
```
Available interfaces:
| Class | Transport | CMake option |
|-------|-----------|-------------|
| `BluetoothMIDI_Interface` | BLE MIDI (BTstack) | `CS_MIDI_BLE` |
| `USBMIDI_Interface` | USB MIDI (TinyUSB) | `CS_MIDI_USB` |
| `HardwareSerialMIDI_Interface` | 5-pin DIN MIDI | `CS_MIDI_SERIAL` |
| `AppleMIDI_Interface` | RTP-MIDI over WiFi | `CS_MIDI_APPLEMIDI` |
## 3. Add MIDI output elements
Output elements read physical inputs and send MIDI messages automatically.
A single potentiometer sending CC:
```cpp pico
CCPotentiometer pot {26, {MIDI_CC::Channel_Volume, Channel_1}};
```
Constructor arguments: GPIO pin, then MIDI address (controller number, channel).
Multiple buttons:
```cpp pico
NoteButton buttons[] {
{5, {MIDI_Notes::C[4], Channel_1}},
{6, {MIDI_Notes::D[4], Channel_1}},
{7, {MIDI_Notes::E[4], Channel_1}},
};
```
## 4. Initialize and run
```cpp pico
int main() {
stdio_init_all();
Control_Surface.begin();
while (true) {
Control_Surface.loop();
sleep_ms(1);
}
}
```
`Control_Surface.begin()` initializes all interfaces and elements. `Control_Surface.loop()` polls inputs and sends MIDI when values change.
## Complete example
```cpp pico
#include "pico/stdlib.h"
#include "pico/cyw43_arch.h"
#include <cs_midi.h>
using namespace cs;
BluetoothMIDI_Interface midi;
CCPotentiometer pot {26, {MIDI_CC::Channel_Volume, Channel_1}};
NoteButton button {5, {MIDI_Notes::C[4], Channel_1}};
int main() {
stdio_init_all();
if (cyw43_arch_init()) return 1;
Control_Surface.begin();
while (true) {
Control_Surface.loop();
sleep_ms(1);
}
}
```

View File

@ -0,0 +1,60 @@
# First Input
> Original: [Getting Started — First Input](https://tttapa.github.io/Control-Surface/Doxygen/d5/d7d/md_pages_Getting-Started.html)
A minimal sketch that receives MIDI and drives outputs (LEDs).
## MIDI input elements
Input elements listen for specific MIDI messages and update their state. `NoteLED` drives a GPIO pin high when its note is active:
```cpp pico
NoteLED led {25, {MIDI_Notes::C[4], Channel_1}};
```
An array of LEDs responding to consecutive notes:
```cpp pico
NoteLED leds[] {
{16, MIDI_Notes::C[4]},
{17, MIDI_Notes::D[4]},
{18, MIDI_Notes::E[4]},
{19, MIDI_Notes::F[4]},
};
```
Value readers store the incoming value without driving hardware:
```cpp pico
CCValue ccVal {{MIDI_CC::Channel_Volume, Channel_1}};
```
Access the current value with `ccVal.getValue()`.
## Complete example
```cpp pico
#include "pico/stdlib.h"
#include "pico/cyw43_arch.h"
#include <cs_midi.h>
using namespace cs;
BluetoothMIDI_Interface midi;
NoteLED led {25, {MIDI_Notes::C[4], Channel_1}};
CCValue volume {{MIDI_CC::Channel_Volume, Channel_1}};
int main() {
stdio_init_all();
if (cyw43_arch_init()) return 1;
Control_Surface.begin();
while (true) {
Control_Surface.loop();
uint8_t v = volume.getValue();
(void)v;
sleep_ms(1);
}
}
```

View File

@ -0,0 +1,53 @@
# FAQ
> Original: [Frequently Asked Questions](https://tttapa.github.io/Control-Surface/Doxygen/da/dc1/FAQ.html)
## Can cs-midi be used as a general MIDI library?
Yes. You can use the MIDI interface objects directly for sending and receiving, without using the element system or `Control_Surface` singleton.
Sending:
```cpp pico
BluetoothMIDI_Interface midi;
int main() {
stdio_init_all();
if (cyw43_arch_init()) return 1;
midi.begin();
midi.sendNoteOn({MIDI_Notes::C[4], Channel_1}, 127);
midi.sendNoteOff({MIDI_Notes::C[4], Channel_1}, 127);
while (true) { midi.update(); sleep_ms(1); }
}
```
Receiving with callbacks:
```cpp pico
struct MyCallbacks : MIDI_Callbacks {
void onChannelMessage(MIDI_Interface &, ChannelMessage msg) override {
// handle msg
(void)msg;
}
} cb;
// in main(): midi.setCallbacks(cb);
```
## Can addresses or channels be changed at runtime?
Yes, through banks. A `Bank<N>` groups bankable elements and a selector controls which bank is active. See the [Banks](../../examples/04-banks/) examples.
## What's the difference from Control Surface?
cs-midi is a source-level extraction of Control Surface for pico-sdk. Key differences:
- `#include <cs_midi.h>` instead of `#include <Control_Surface.h>`
- Standard `main()` replaces Arduino `setup()`/`loop()`
- `cs::` namespace instead of top-level
- BTstack BLE backend instead of ArduinoBLE/Bluedroid
- No MCU protocol, displays, FastLED, or hardware expanders (yet)
## What radio modules are supported?
The BLE transport uses BTstack with the CYW43 radio. Only the **Infineon RM2** module has been tested. Other CYW43 variants may work but are unverified.

View File

@ -0,0 +1,3 @@
# Getting Started
Setup, installation, and first steps with cs-midi.

View File

@ -0,0 +1,147 @@
# MIDI Tutorial
> Original: [MIDI Tutorial](https://tttapa.github.io/Control-Surface/Doxygen/d3/df7/midi-tutorial.html)
## MIDI Interfaces
A MIDI interface handles the transport layer — BLE, USB, serial, or WiFi. Instantiate one before sending or receiving:
```cpp pico
BluetoothMIDI_Interface midi; // BLE MIDI via BTstack
```
Initialize with `midi.begin()` (standalone) or let `Control_Surface.begin()` handle it.
## MIDI Addresses
`MIDIAddress` combines three components: address (0-127), channel (1-16), and cable (1-16, default 1).
```cpp pico
MIDIAddress note = {MIDI_Notes::C[4], Channel_1};
MIDIAddress cc = {MIDI_CC::Channel_Volume, Channel_2};
MIDIAddress pc = {MIDI_PC::Harpsichord, Channel_10};
```
Predefined constants:
- `MIDI_Notes::C[4]` — note number 60 (middle C)
- `MIDI_CC::Channel_Volume` — CC 7
- `MIDI_PC::Harpsichord` — program 7
Channel defaults to `Channel_1`, cable defaults to `Cable_1`.
## Sending Messages
All interface objects inherit from `MIDI_Sender` and provide:
```cpp pico
midi.sendNoteOn(address, velocity);
midi.sendNoteOff(address, velocity);
midi.sendControlChange(address, value);
midi.sendProgramChange(address);
midi.sendPitchBend(channel, value); // 14-bit (0-16383)
midi.sendChannelPressure(channel, pressure);
midi.sendKeyPressure(address, pressure);
```
System messages:
```cpp pico
midi.sendTimingClock();
midi.sendStart();
midi.sendContinue();
midi.sendStop();
midi.sendSysEx(data); // uint8_t array with F0...F7
```
Call `midi.sendNow()` to flush any buffered output.
## Receiving Messages
### MIDI_Callbacks
Subclass `MIDI_Callbacks` and override the handlers you need:
```cpp pico
struct MyCallbacks : MIDI_Callbacks {
void onChannelMessage(MIDI_Interface &, ChannelMessage msg) override {
uint8_t type = static_cast<uint8_t>(msg.getMessageType());
uint8_t d1 = msg.getData1();
uint8_t d2 = msg.getData2();
Channel ch = msg.getChannel();
(void)type; (void)d1; (void)d2; (void)ch;
}
void onSysExMessage(MIDI_Interface &, SysExMessage msg) override {
const uint8_t *data = msg.data;
uint16_t len = msg.length;
(void)data; (void)len;
}
void onRealTimeMessage(MIDI_Interface &, RealTimeMessage msg) override {
(void)msg;
}
} callbacks;
// Register: midi.setCallbacks(callbacks);
```
### FineGrainedMIDI_Callbacks
For per-message-type dispatch, use the CRTP variant:
```cpp pico
struct MyFineCallbacks : FineGrainedMIDI_Callbacks<MyFineCallbacks> {
void onNoteOn(Channel ch, uint8_t note, uint8_t vel, Cable cable) {
(void)ch; (void)note; (void)vel; (void)cable;
}
void onNoteOff(Channel ch, uint8_t note, uint8_t vel, Cable cable) {
(void)ch; (void)note; (void)vel; (void)cable;
}
void onControlChange(Channel ch, uint8_t cc, uint8_t val, Cable cable) {
(void)ch; (void)cc; (void)val; (void)cable;
}
} fineCallbacks;
```
Only override the methods you need — unoverridden methods are no-ops.
## Routing Messages
### Default routing
When using `Control_Surface.begin()`, the singleton automatically connects output elements to the default MIDI interface and dispatches incoming messages to input elements.
### Manual routing with pipes
`MIDI_Pipe` connects a source to a sink. Use operators for concise wiring:
```cpp pico
MIDI_PipeFactory<2> pipes;
midi1 >> pipes >> midi2; // midi1 output → midi2
midi2 >> pipes >> midi1; // midi2 output → midi1
```
Bidirectional:
```cpp pico
BidirectionalMIDI_PipeFactory<1> bpipes;
midi1 | bpipes | midi2; // full duplex
```
### Filtering
Subclass `MIDI_Pipe` and override `mapForwardMIDI` to filter or transform:
```cpp pico
struct NoteOnly : MIDI_Pipe {
void mapForwardMIDI(ChannelMessage msg) override {
if (msg.getMessageType() == MIDIMessageType::NoteOn ||
msg.getMessageType() == MIDIMessageType::NoteOff)
sourceMIDItoSink(msg);
}
void mapForwardMIDI(SysExMessage) override {}
void mapForwardMIDI(SysCommonMessage) override {}
void mapForwardMIDI(RealTimeMessage msg) override {
sourceMIDItoSink(msg);
}
};
```

View File

@ -0,0 +1,76 @@
# Sending MIDI Messages
> Original: [Sending MIDI messages](https://tttapa.github.io/Control-Surface/Doxygen/d3/df7/midi-tutorial.html)
## Direct sending
Use the interface object directly when you don't need the element system:
```cpp pico
#include "pico/stdlib.h"
#include "pico/cyw43_arch.h"
#include <cs_midi.h>
using namespace cs;
BluetoothMIDI_Interface midi;
int main() {
stdio_init_all();
if (cyw43_arch_init()) return 1;
midi.begin();
MIDIAddress note = {MIDI_Notes::C[4], Channel_1};
midi.sendNoteOn(note, 127);
sleep_ms(500);
midi.sendNoteOff(note, 0);
while (true) {
midi.update();
sleep_ms(1);
}
}
```
## Channel voice messages
| Method | Arguments | Description |
|--------|-----------|-------------|
| `sendNoteOn` | address, velocity | Note On |
| `sendNoteOff` | address, velocity | Note Off |
| `sendControlChange` | address, value | Control Change (0-127) |
| `sendProgramChange` | address | Program Change |
| `sendPitchBend` | channel, value | Pitch Bend (0-16383, center 8192) |
| `sendChannelPressure` | channel, pressure | Channel Pressure |
| `sendKeyPressure` | address, pressure | Polyphonic Key Pressure |
## System common messages
| Method | Arguments | Description |
|--------|-----------|-------------|
| `sendMTCQuarterFrame` | type, value | MIDI Time Code quarter frame |
| `sendSongPositionPointer` | beats | Song position (14-bit) |
| `sendSongSelect` | song | Song number (0-127) |
| `sendTuneRequest` | — | Tune request |
## System exclusive
```cpp pico
uint8_t sysex[] = {0xF0, 0x7E, 0x7F, 0x06, 0x01, 0xF7};
midi.sendSysEx(sysex);
```
## Real-time messages
| Method | Description |
|--------|-------------|
| `sendTimingClock` | MIDI clock tick |
| `sendStart` | Start sequence |
| `sendContinue` | Continue sequence |
| `sendStop` | Stop sequence |
| `sendActiveSensing` | Active sensing |
| `sendSystemReset` | System reset |
## Flushing
Call `midi.sendNow()` to flush buffered messages immediately.

View File

@ -0,0 +1,75 @@
# Receiving MIDI Messages
> Original: [Receiving MIDI messages](https://tttapa.github.io/Control-Surface/Doxygen/d3/df7/midi-tutorial.html)
## With MIDI_Callbacks
Register a callback object on the interface to handle incoming messages:
```cpp pico
#include "pico/stdlib.h"
#include "pico/cyw43_arch.h"
#include <cs_midi.h>
using namespace cs;
BluetoothMIDI_Interface midi;
struct MyCallbacks : MIDI_Callbacks {
void onChannelMessage(MIDI_Interface &, ChannelMessage msg) override {
auto type = msg.getMessageType();
Channel ch = msg.getChannel();
uint8_t d1 = msg.getData1();
uint8_t d2 = msg.getData2();
(void)type; (void)ch; (void)d1; (void)d2;
}
} cb;
int main() {
stdio_init_all();
if (cyw43_arch_init()) return 1;
midi.begin();
midi.setCallbacks(cb);
while (true) {
midi.update();
sleep_ms(1);
}
}
```
`midi.update()` reads incoming data and dispatches to callbacks.
## With FineGrainedMIDI_Callbacks
For per-message-type handlers, use the CRTP pattern:
```cpp pico
struct Handlers : FineGrainedMIDI_Callbacks<Handlers> {
void onNoteOn(Channel ch, uint8_t note, uint8_t velocity, Cable cable) {
(void)ch; (void)note; (void)velocity; (void)cable;
}
void onControlChange(Channel ch, uint8_t cc, uint8_t value, Cable cable) {
(void)ch; (void)cc; (void)value; (void)cable;
}
};
Handlers handlers;
// midi.setCallbacks(handlers);
```
Available overrides: `onNoteOn`, `onNoteOff`, `onKeyPressure`, `onControlChange`, `onProgramChange`, `onChannelPressure`, `onPitchBend`, `onSystemExclusive`, `onTimeCodeQuarterFrame`, `onSongPosition`, `onSongSelect`, `onTuneRequest`, `onClock`, `onStart`, `onContinue`, `onStop`, `onActiveSensing`, `onSystemReset`.
## ChannelMessage accessors
| Method | Returns |
|--------|---------|
| `getMessageType()` | `MIDIMessageType` enum |
| `getChannel()` | `Channel` |
| `getData1()` | First data byte (note/CC number) |
| `getData2()` | Second data byte (velocity/value) |
| `getData14bit()` | 14-bit value (Pitch Bend) |
| `getCable()` | `Cable` |
## With the element system
MIDI input elements (`CCValue`, `NoteLED`, etc.) handle message matching and state management automatically — no callbacks needed. They update on each `Control_Surface.loop()` call.

View File

@ -0,0 +1,78 @@
# Routing MIDI Messages
> Original: [Routing MIDI messages](https://tttapa.github.io/Control-Surface/Doxygen/d3/df7/midi-tutorial.html)
## Default routing
`Control_Surface.begin()` automatically connects the default MIDI interface to all output and input elements. For single-interface projects, no manual routing is needed.
## MIDI Pipes
Pipes connect sources to sinks. Use shift operators:
```cpp pico
MIDI_PipeFactory<2> pipes;
midi1 >> pipes >> midi2; // midi1 sends to midi2
midi2 >> pipes >> midi1; // midi2 sends to midi1
```
## Bidirectional pipes
```cpp pico
BidirectionalMIDI_PipeFactory<1> bpipes;
midi1 | bpipes | midi2;
```
## Custom filtering
Subclass `MIDI_Pipe` and override `mapForwardMIDI`:
```cpp pico
struct ChannelFilter : MIDI_Pipe {
Channel allowed;
ChannelFilter(Channel ch) : allowed(ch) {}
void mapForwardMIDI(ChannelMessage msg) override {
if (msg.getChannel() == allowed)
sourceMIDItoSink(msg);
}
void mapForwardMIDI(SysExMessage msg) override {
sourceMIDItoSink(msg);
}
void mapForwardMIDI(SysCommonMessage msg) override {
sourceMIDItoSink(msg);
}
void mapForwardMIDI(RealTimeMessage msg) override {
sourceMIDItoSink(msg);
}
};
ChannelFilter ch1Only {Channel_1};
// midi >> ch1Only >> otherMidi;
```
## Multi-interface example
```cpp pico
#include "pico/stdlib.h"
#include "pico/cyw43_arch.h"
#include <cs_midi.h>
using namespace cs;
BluetoothMIDI_Interface ble;
USBMIDI_Interface usb;
BidirectionalMIDI_PipeFactory<1> pipes;
int main() {
stdio_init_all();
if (cyw43_arch_init()) return 1;
ble | pipes | usb;
Control_Surface.begin();
while (true) {
Control_Surface.loop();
sleep_ms(1);
}
}
```

View File

@ -0,0 +1,55 @@
# MIDI over USB
> Original: [MIDI over USB](https://tttapa.github.io/Control-Surface/Doxygen/d8/d4a/md_pages_MIDI-over-USB.html)
USB MIDI uses TinyUSB to present the device as a standard USB MIDI class-compliant device. No drivers needed on macOS, Windows, or Linux.
## Setup
Enable in CMake:
```cmake pico
set(CS_MIDI_USB ON)
```
Your project needs two additional files:
- `tusb_config.h` — TinyUSB configuration
- `usb_descriptors.c` — USB device/configuration/string descriptors
Reference copies are in `lib/cs-midi/templates/`.
## Usage
```cpp pico
#include "pico/stdlib.h"
#include <cs_midi.h>
using namespace cs;
USBMIDI_Interface midi;
NoteButton button {5, {MIDI_Notes::C[4], Channel_1}};
int main() {
stdio_init_all();
Control_Surface.begin();
while (true) {
Control_Surface.loop();
sleep_ms(1);
}
}
```
No `cyw43_arch_init()` needed for USB-only configurations.
## Combining with BLE
Enable both `CS_MIDI_BLE` and `CS_MIDI_USB`, then route between them:
```cpp pico
BluetoothMIDI_Interface ble;
USBMIDI_Interface usb;
BidirectionalMIDI_PipeFactory<1> pipes;
// In main: ble | pipes | usb;
```

View File

@ -0,0 +1,58 @@
# MIDI over BLE
> Original: [MIDI over BLE](https://tttapa.github.io/Control-Surface/Doxygen/db/d99/md_pages_MIDI-over-BLE.html)
BLE MIDI uses BTstack to advertise a standard MIDI BLE service. Compatible with macOS, iOS, Windows 10+, and Android.
## Backend
cs-midi uses the `BTstackBackgroundBackend`, which runs BLE asynchronously but requires periodic `update()` calls for outgoing message buffering.
The convenience typedef:
```cpp pico
using BluetoothMIDI_Interface = GenericBLEMIDI_Interface<BTstackBackgroundBackend>;
```
## Basic usage
```cpp pico
#include "pico/stdlib.h"
#include "pico/cyw43_arch.h"
#include <cs_midi.h>
using namespace cs;
BluetoothMIDI_Interface midi;
NoteButton button {5, {MIDI_Notes::C[4], Channel_1}};
int main() {
stdio_init_all();
if (cyw43_arch_init()) return 1;
Control_Surface.begin();
while (true) {
Control_Surface.loop();
sleep_ms(1);
}
}
```
`cyw43_arch_init()` is required to bring up the radio before BLE operations.
## HID keyboard + Battery Service
Build with `CS_MIDI_HID_KEYBOARD=ON` to advertise as a BLE HID keyboard alongside MIDI:
1. **Auto-reconnect** — the host OS auto-reconnects to bonded HID peripherals on power-up, bringing up the MIDI service without manual intervention.
2. **Battery level** — the standard Battery Service (UUID 0x180F) reports charge level. Update with:
```cpp pico
setBLEBatteryLevel(percent);
```
No keyboard input is sent — the HID descriptor exists solely to trigger OS auto-connect behavior and surface the battery indicator.
## Radio module
The BLE transport requires a CYW43 radio. Only the **Infineon RM2 module** has been tested. A board file for the Waveshare RP2350B Plus W (RM2-equipped) is provided at `boards/waveshare_rp2350b_plus_w.h`.

View File

@ -0,0 +1,3 @@
# MIDI Communication
Sending, receiving, and routing MIDI messages.

View File

@ -0,0 +1,55 @@
# Control Surface Singleton
> Original: [Purpose of the Control_Surface singleton](https://tttapa.github.io/Control-Surface/Doxygen/d8/df5/control-surface-purpose.html)
The `Control_Surface` object is the central manager for all MIDI elements, interfaces, and lifecycle events.
## Program structure
```cpp pico
#include "pico/stdlib.h"
#include <cs_midi.h>
using namespace cs;
BluetoothMIDI_Interface midi;
NoteButton button {5, {MIDI_Notes::C[4], Channel_1}};
int main() {
stdio_init_all();
Control_Surface.begin();
while (true) {
Control_Surface.loop();
sleep_ms(1);
}
}
```
## Responsibilities
1. **Initialization**`begin()` activates all registered MIDI interfaces, output elements, input elements, and selectors.
2. **Periodic updates**`loop()` polls physical inputs (buttons, pots, encoders), sends MIDI when values change, and dispatches incoming MIDI to input elements.
3. **Message routing** — connects output elements to the default MIDI interface and delivers incoming messages to matching input elements.
## Element registration
Elements register themselves on construction and deregister on destruction. No explicit `add()` calls needed.
## Enable/disable
Elements can be removed from the update loop at runtime:
```cpp pico
button.disable(); // stops polling and sending
button.enable(); // re-adds to update loop
```
## Without the singleton
For direct MIDI sending/receiving without the element system, use the interface object directly:
```cpp pico
midi.begin();
midi.sendNoteOn({MIDI_Notes::C[4], Channel_1}, 127);
midi.update();
```

View File

@ -0,0 +1,71 @@
# MIDI Output Elements
> Original: [MIDI output elements](https://tttapa.github.io/Control-Surface/Doxygen/d7/dcd/group__MIDIOutputElements.html)
Output elements read physical controls and send MIDI messages. They register with `Control_Surface` on construction and update automatically in the `loop()` cycle.
## Buttons
| Class | Description | Constructor |
|-------|-------------|-------------|
| `NoteButton` | Note On/Off on press/release | `{pin, address}` |
| `CCButton` | CC 127/0 on press/release | `{pin, address}` |
| `PCButton` | Program Change on press | `{pin, address}` |
| `NoteButtonLatched` | Toggle Note each press | `{pin, address}` |
| `NoteButtonLatching` | Note while held, toggles | `{pin, address}` |
| `CCButtonLatched` | Toggle CC each press | `{pin, address}` |
| `CCButtonLatching` | CC while held, toggles | `{pin, address}` |
| `NoteChordButton` | Multiple notes on press | `{pin, address, chord}` |
## Button arrays and matrices
| Class | Description |
|-------|-------------|
| `NoteButtons<N>` | Array of N sequential note buttons |
| `CCButtons<N>` | Array of N sequential CC buttons |
| `NoteButtonMatrix<R,C>` | Row/column scanned note grid |
| `CCButtonMatrix<R,C>` | Row/column scanned CC grid |
## Potentiometers
| Class | Description |
|-------|-------------|
| `CCPotentiometer` | Analog input → CC 0-127 |
| `PBPotentiometer` | Analog input → 14-bit Pitch Bend |
| `CCPotentiometer` with `map()` | Custom mapping to calibrate or apply curves |
```cpp pico
CCPotentiometer pot {26, {MIDI_CC::Channel_Volume, Channel_1}};
pot.map(myMappingFunction);
```
## Encoders
| Class | Description |
|-------|-------------|
| `CCRotaryEncoder` | Relative CC from quadrature encoder |
| `CCAbsoluteEncoder` | Absolute CC position from encoder |
| `PBAbsoluteEncoder` | 14-bit Pitch Bend from encoder |
| `BorrowedCCRotaryEncoder` | Shared encoder reference |
| `CCIncrementDecrementButtons` | Two buttons for CC inc/dec |
```cpp pico
CCRotaryEncoder enc {{0, 1}, {MIDI_CC::Pan, Channel_1}, 1, 4};
```
Constructor: `{pin_pair, address, speed_multiplier, pulses_per_click}`.
## Program changers
| Class | Description |
|-------|-------------|
| `ProgramChanger<N>` | Selectable list of N program changes |
```cpp pico
ProgramChanger<4> pc {{
{MIDI_PC::Harpsichord, Channel_1},
{MIDI_PC::Organ, Channel_1},
{MIDI_PC::Guitar, Channel_1},
{MIDI_PC::Piano, Channel_1},
}};
```

View File

@ -0,0 +1,55 @@
# MIDI Input Elements
> Original: [MIDI input elements](https://tttapa.github.io/Control-Surface/Doxygen/df/d8b/group__MIDIInputElements.html)
Input elements receive MIDI messages and update stored values or drive hardware outputs.
## Value readers
| Class | Description |
|-------|-------------|
| `NoteValue` | Incoming Note velocity (8-bit) |
| `CCValue` | Incoming CC value (8-bit) |
| `KPValue` | Incoming Key Pressure (8-bit) |
| `PBValue` | Incoming Pitch Bend (14-bit) |
```cpp pico
CCValue vol {{MIDI_CC::Channel_Volume, Channel_1}};
// In loop: uint8_t v = vol.getValue();
```
## Range readers
| Class | Description |
|-------|-------------|
| `NoteRange<N>` | Array of N consecutive note values |
| `CCRange<N>` | Array of N consecutive CC values |
| `KPRange<N>` | Array of N consecutive key pressure values |
```cpp pico
CCRange<8> faders {{16, Channel_1}};
// faders.getValue(0) through faders.getValue(7)
```
## LED outputs
| Class | Description |
|-------|-------------|
| `NoteLED` | GPIO high on Note On, low on Note Off |
| `CCLED` | GPIO high when CC > 63 |
| `KPLED` | GPIO high when Key Pressure > 63 |
```cpp pico
NoteLED led {25, {MIDI_Notes::C[4], Channel_1}};
```
## Bankable variants
All input elements have bankable versions under `Bankable::`:
```cpp pico
Bank<4> bank(2);
Bankable::CCValue<4> bankCC {{bank, BankType::ChangeAddress}, {16, Channel_1}};
```
See [Banks and Selectors](../06-banks/) for details.

View File

@ -0,0 +1,62 @@
# Banks and Selectors
> Original: [Bank support](https://tttapa.github.io/Control-Surface/Doxygen/db/dbd/classBank.html)
Banks allow a single physical control to address multiple MIDI parameters by switching the active bank.
## How banks work
`Bank<N>` manages N settings. Bankable elements compute their effective address as:
effective_address = base_address + (bank_setting × offset)
The offset is set when constructing the bank, and the base address is set per-element.
```cpp pico
Bank<4> bank(2); // 4 settings, offset 2 between each
```
With base address 16, the four banks produce addresses 16, 18, 20, 22.
## Bank types
- `BankType::ChangeAddress` — shifts the CC/note number
- `BankType::ChangeChannel` — shifts the MIDI channel
## Selectors
Selectors control which bank setting is active:
| Class | Description |
|-------|-------------|
| `EncoderSelector<N>` | Rotary encoder cycles banks |
| `IncrementDecrementSelector<N>` | Two buttons: up/down |
| `IncrementSelector<N>` | Single button, wraps around |
| `ManyButtonsSelector<N>` | One button per bank |
| `SwitchSelector` | Two-position toggle |
| `ProgramChangeSelector<N>` | Incoming MIDI PC selects bank |
```cpp pico
IncrementDecrementSelector<4> selector {bank, {2, 3}, Wrap::Wrap};
```
## Transposer
`Transposer<Min, Max>` is a specialized bank that offsets note numbers:
```cpp pico
Transposer<-12, 12> transposer;
IncrementDecrementSelector<25> transposeSelect {transposer, {2, 3}, Wrap::Clamp};
```
## Bankable input elements
All input elements (`CCValue`, `NoteLED`, etc.) have bankable variants:
```cpp pico
Bankable::CCValue<4> cc {{bank, BankType::ChangeAddress}, {16, Channel_1}};
```
## Bankable output elements
Bankable output elements (`Bankable::NoteButton`, `Bankable::CCPotentiometer`, etc.) are not yet implemented (Phase 11). Bank switching currently works with input elements and selectors only.

View File

@ -0,0 +1,60 @@
# Hardware Support
> Original: [Hardware support](https://tttapa.github.io/Control-Surface/Doxygen/db/dd3/group__AH__ExtIO.html)
Hardware utilities in the `AH::` namespace for debouncing, filtering, and encoder reading.
## Button
Debounced button with state machine:
```cpp pico
AH::Button btn {5};
btn.begin();
// In loop:
btn.update();
if (btn.getState() == AH::Button::Falling)
// pressed
if (btn.getState() == AH::Button::Rising)
// released
```
States: `Pressed`, `Released`, `Falling` (just pressed), `Rising` (just released).
## FilteredAnalog
EMA-filtered analog input with hysteresis to reduce noise:
```cpp pico
AH::FilteredAnalog<10> pot {26}; // 10-bit, GPIO 26
// In loop:
if (pot.update())
uint16_t value = pot.getValue();
```
## AHEncoder
Interrupt-driven quadrature rotary encoder:
```cpp pico
AH::AHEncoder enc {0, 1}; // pin A, pin B
enc.begin();
// In loop:
int32_t delta = enc.read();
enc.write(0); // reset position
```
## GPIO on RP2xxx
cs-midi uses pico-sdk GPIO directly. Pin numbers are GPIO numbers (0-29 on RP2040, 0-47 on RP2350).
- Buttons use internal pull-ups (active low)
- ADC pins: GPIO 26-29 (ADC0-ADC3)
- Encoder pins: any GPIO with interrupt capability (all pins on RP2xxx)
## Extended I/O
Hardware expanders (MCP23017, MAX7219, shift registers) are not yet ported from Control Surface. Use direct GPIO pins for all inputs and outputs.

5
docs/manual/_index.md Normal file
View File

@ -0,0 +1,5 @@
# Manual
A translated reference of the [Control Surface manual](https://tttapa.github.io/Control-Surface/Doxygen/d3/da5/manual.html) for cs-midi on pico-sdk.
Each section mirrors the original structure. Code examples are adapted for the target platform selected in the sidebar.

View File

@ -55,6 +55,7 @@ Extraction status of [tttapa/Control-Surface](https://github.com/tttapa/Control-
- [x] `ExtendedInputOutput` — GPIO abstraction (direct pins)
- [x] MIDI constants (notes, CC numbers, PC programs, chords/intervals)
- [x] Platform shim (`pico_shim.h`) — replaces Arduino runtime
- [x] HID over GATT (HoG) keyboard + Battery Service — BLE auto-reconnect and battery level via `CS_MIDI_HID_KEYBOARD`
## Not Yet Implemented

View File

@ -2,7 +2,10 @@ package render
import (
"bytes"
"fmt"
"regexp"
chromahtml "github.com/alecthomas/chroma/v2/formatters/html"
"github.com/yuin/goldmark"
highlighting "github.com/yuin/goldmark-highlighting/v2"
"github.com/yuin/goldmark/extension"
@ -10,14 +13,20 @@ import (
"github.com/yuin/goldmark/renderer/html"
)
var md goldmark.Markdown
var (
md goldmark.Markdown
platformRe = regexp.MustCompile("^```(\\w+)\\s+(pico|esp32s3|esp32c3)\\s*$")
)
func init() {
md = goldmark.New(
goldmark.WithExtensions(
extension.GFM,
highlighting.NewHighlighting(
highlighting.WithStyle("monokai"),
highlighting.WithFormatOptions(
chromahtml.WithClasses(true),
chromahtml.ClassPrefix("hl-"),
),
),
),
goldmark.WithParserOptions(
@ -29,7 +38,49 @@ func init() {
)
}
// preprocessPlatformBlocks rewrites fenced code blocks tagged with a platform
// (e.g. ```cpp pico) into an HTML wrapper div + untagged fence so Goldmark
// renders them normally while preserving the data-platform attribute.
func preprocessPlatformBlocks(src []byte) []byte {
lines := bytes.Split(src, []byte("\n"))
var out [][]byte
inFence := false
platformFence := false
for _, line := range lines {
trimmed := bytes.TrimSpace(line)
if !inFence {
if m := platformRe.FindSubmatch(trimmed); m != nil {
out = append(out, []byte(fmt.Sprintf(`<div data-platform="%s">`, m[2])))
out = append(out, []byte{})
out = append(out, []byte("```"+string(m[1])))
inFence = true
platformFence = true
continue
}
if bytes.HasPrefix(trimmed, []byte("```")) {
inFence = true
}
} else if bytes.Equal(trimmed, []byte("```")) {
inFence = false
if platformFence {
out = append(out, line)
out = append(out, []byte{})
out = append(out, []byte("</div>"))
platformFence = false
continue
}
}
out = append(out, line)
}
return bytes.Join(out, []byte("\n"))
}
func Markdown(src []byte) ([]byte, error) {
src = preprocessPlatformBlocks(src)
var buf bytes.Buffer
if err := md.Convert(src, &buf); err != nil {
return nil, err

View File

@ -0,0 +1,64 @@
document.addEventListener('DOMContentLoaded', function() {
var input = document.querySelector('.search-form input');
if (input) {
input.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
input.value = '';
input.blur();
}
});
}
// --- Platform selector ---
var platformSel = document.getElementById('platform-select');
if (platformSel) {
var storedPlatform = localStorage.getItem('cs-midi-platform');
if (storedPlatform) platformSel.value = storedPlatform;
function applyPlatform() {
var platform = platformSel.value;
localStorage.setItem('cs-midi-platform', platform);
var blocks = document.querySelectorAll('[data-platform]');
var anyVisible = false;
blocks.forEach(function(el) {
if (el.getAttribute('data-platform') === platform) {
el.style.display = '';
anyVisible = true;
} else {
el.style.display = 'none';
}
});
var notice = document.getElementById('platform-notice');
if (notice) {
notice.style.display = (blocks.length > 0 && !anyVisible) ? '' : 'none';
}
}
platformSel.addEventListener('change', applyPlatform);
applyPlatform();
}
// --- Theme selector ---
var themeSel = document.getElementById('theme-select');
if (themeSel) {
var storedTheme = localStorage.getItem('cs-midi-theme') || 'auto';
themeSel.value = storedTheme;
function applyTheme() {
var theme = themeSel.value;
localStorage.setItem('cs-midi-theme', theme);
if (theme === 'auto') {
document.documentElement.removeAttribute('data-theme');
} else {
document.documentElement.setAttribute('data-theme', theme);
}
}
themeSel.addEventListener('change', applyTheme);
applyTheme();
}
});

View File

@ -1,10 +0,0 @@
document.addEventListener('DOMContentLoaded', function() {
var input = document.querySelector('.search-form input');
if (!input) return;
input.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
input.value = '';
input.blur();
}
});
});

View File

@ -7,17 +7,29 @@
--sidebar-w: 280px;
--accent: #2563eb;
--border: #ddd;
--code-bg: #f5f5f5;
--code-bg: #f6f8fa;
--code-fg: #24292e;
}
[data-theme="dark"] {
--bg: #1a1a2e;
--fg: #e0e0e0;
--sidebar-bg: #16213e;
--accent: #60a5fa;
--border: #333;
--code-bg: #0d1117;
--code-fg: #e0e0e0;
}
@media (prefers-color-scheme: dark) {
:root {
:root:not([data-theme="light"]) {
--bg: #1a1a2e;
--fg: #e0e0e0;
--sidebar-bg: #16213e;
--accent: #60a5fa;
--border: #333;
--code-bg: #0f3460;
--code-bg: #0d1117;
--code-fg: #e0e0e0;
}
}
@ -113,6 +125,46 @@ body {
font-weight: 600;
}
.sidebar-controls {
padding: 0.8em 1em;
border-bottom: 1px solid var(--border);
display: flex;
gap: 0.6em;
}
.sidebar-controls > div {
flex: 1;
}
.sidebar-controls label {
display: block;
font-size: 0.75em;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
opacity: 0.6;
margin-bottom: 0.4em;
}
.sidebar-controls select {
width: 100%;
padding: 0.4em 0.6em;
border: 1px solid var(--border);
border-radius: 4px;
background: var(--bg);
color: var(--fg);
font-size: 0.85em;
}
.platform-notice {
padding: 1em;
border: 1px dashed var(--border);
border-radius: 6px;
opacity: 0.6;
font-style: italic;
margin: 1em 0;
}
.sidebar-footer {
padding: 1em;
border-top: 1px solid var(--border);
@ -139,11 +191,14 @@ body {
.content p { margin: 0.8em 0; }
.content ul, .content ol { margin: 0.8em 0; padding-left: 2em; }
.content li { margin: 0.3em 0; }
.content blockquote { border-left: 3px solid var(--border); padding-left: 1em; margin: 0.8em 0; opacity: 0.8; }
.content pre {
background: var(--code-bg);
color: var(--code-fg);
padding: 1em;
border-radius: 6px;
border: 1px solid var(--border);
overflow-x: auto;
margin: 1em 0;
font-size: 0.9em;
@ -154,7 +209,7 @@ body {
font-size: 0.9em;
}
.content p code, .content li code {
.content p code, .content li code, .content td code {
background: var(--code-bg);
padding: 0.15em 0.4em;
border-radius: 3px;
@ -205,3 +260,187 @@ body {
.content { margin-left: 0; padding: 1.5em; }
.layout { flex-direction: column; }
}
/* ------------------------------------------------------------------ */
/* Syntax highlighting — light theme (github) */
/* ------------------------------------------------------------------ */
.hl-chroma { background-color: transparent; }
.hl-chroma .hl-err { color: #a61717; background-color: #e3d2d2 }
.hl-chroma .hl-k { color: #0550ae; font-weight: bold }
.hl-chroma .hl-kc { color: #0550ae; font-weight: bold }
.hl-chroma .hl-kd { color: #0550ae; font-weight: bold }
.hl-chroma .hl-kn { color: #0550ae; font-weight: bold }
.hl-chroma .hl-kp { color: #0550ae; font-weight: bold }
.hl-chroma .hl-kr { color: #0550ae; font-weight: bold }
.hl-chroma .hl-kt { color: #445588; font-weight: bold }
.hl-chroma .hl-na { color: #008080 }
.hl-chroma .hl-nb { color: #0086b3 }
.hl-chroma .hl-bp { color: #999999 }
.hl-chroma .hl-nc { color: #445588; font-weight: bold }
.hl-chroma .hl-no { color: #008080 }
.hl-chroma .hl-nd { color: #3c5d5d; font-weight: bold }
.hl-chroma .hl-ni { color: #800080 }
.hl-chroma .hl-ne { color: #990000; font-weight: bold }
.hl-chroma .hl-nf { color: #6f42c1 }
.hl-chroma .hl-nl { color: #990000; font-weight: bold }
.hl-chroma .hl-nn { color: #555555 }
.hl-chroma .hl-nt { color: #22863a }
.hl-chroma .hl-nv { color: #008080 }
.hl-chroma .hl-vc { color: #008080 }
.hl-chroma .hl-vg { color: #008080 }
.hl-chroma .hl-vi { color: #008080 }
.hl-chroma .hl-s { color: #032f62 }
.hl-chroma .hl-sa { color: #032f62 }
.hl-chroma .hl-sb { color: #032f62 }
.hl-chroma .hl-sc { color: #032f62 }
.hl-chroma .hl-dl { color: #032f62 }
.hl-chroma .hl-sd { color: #032f62 }
.hl-chroma .hl-s2 { color: #032f62 }
.hl-chroma .hl-se { color: #032f62 }
.hl-chroma .hl-sh { color: #032f62 }
.hl-chroma .hl-si { color: #032f62 }
.hl-chroma .hl-sx { color: #032f62 }
.hl-chroma .hl-sr { color: #009926 }
.hl-chroma .hl-s1 { color: #032f62 }
.hl-chroma .hl-ss { color: #990073 }
.hl-chroma .hl-m { color: #005cc5 }
.hl-chroma .hl-mb { color: #005cc5 }
.hl-chroma .hl-mf { color: #005cc5 }
.hl-chroma .hl-mh { color: #005cc5 }
.hl-chroma .hl-mi { color: #005cc5 }
.hl-chroma .hl-il { color: #005cc5 }
.hl-chroma .hl-mo { color: #005cc5 }
.hl-chroma .hl-o { color: #d73a49 }
.hl-chroma .hl-ow { color: #d73a49 }
.hl-chroma .hl-c { color: #6a737d; font-style: italic }
.hl-chroma .hl-ch { color: #6a737d; font-style: italic }
.hl-chroma .hl-cm { color: #6a737d; font-style: italic }
.hl-chroma .hl-c1 { color: #6a737d; font-style: italic }
.hl-chroma .hl-cs { color: #6a737d; font-weight: bold; font-style: italic }
.hl-chroma .hl-cp { color: #6a737d; font-weight: bold; font-style: italic }
.hl-chroma .hl-cpf { color: #6a737d; font-weight: bold; font-style: italic }
.hl-chroma .hl-gd { color: #b31d28; background-color: #ffeef0 }
.hl-chroma .hl-gi { color: #22863a; background-color: #f0fff4 }
.hl-chroma .hl-ge { font-style: italic }
.hl-chroma .hl-gs { font-weight: bold }
.hl-chroma .hl-gh { color: #999999 }
.hl-chroma .hl-gu { color: #aaaaaa }
/* ------------------------------------------------------------------ */
/* Syntax highlighting — dark theme (monokai) */
/* ------------------------------------------------------------------ */
[data-theme="dark"] .hl-chroma .hl-err { color: #960050; background-color: #1e0010 }
[data-theme="dark"] .hl-chroma .hl-k { color: #66d9ef; font-weight: normal }
[data-theme="dark"] .hl-chroma .hl-kc { color: #66d9ef; font-weight: normal }
[data-theme="dark"] .hl-chroma .hl-kd { color: #66d9ef; font-weight: normal }
[data-theme="dark"] .hl-chroma .hl-kn { color: #f92672; font-weight: normal }
[data-theme="dark"] .hl-chroma .hl-kp { color: #66d9ef; font-weight: normal }
[data-theme="dark"] .hl-chroma .hl-kr { color: #66d9ef; font-weight: normal }
[data-theme="dark"] .hl-chroma .hl-kt { color: #66d9ef; font-weight: normal }
[data-theme="dark"] .hl-chroma .hl-na { color: #a6e22e }
[data-theme="dark"] .hl-chroma .hl-nb { color: #f8f8f2 }
[data-theme="dark"] .hl-chroma .hl-bp { color: #f8f8f2 }
[data-theme="dark"] .hl-chroma .hl-nc { color: #a6e22e; font-weight: normal }
[data-theme="dark"] .hl-chroma .hl-no { color: #66d9ef }
[data-theme="dark"] .hl-chroma .hl-nd { color: #a6e22e; font-weight: normal }
[data-theme="dark"] .hl-chroma .hl-ni { color: #f8f8f2 }
[data-theme="dark"] .hl-chroma .hl-ne { color: #a6e22e; font-weight: normal }
[data-theme="dark"] .hl-chroma .hl-nf { color: #a6e22e }
[data-theme="dark"] .hl-chroma .hl-nl { color: #f8f8f2 }
[data-theme="dark"] .hl-chroma .hl-nn { color: #f8f8f2 }
[data-theme="dark"] .hl-chroma .hl-nt { color: #f92672 }
[data-theme="dark"] .hl-chroma .hl-nv { color: #f8f8f2 }
[data-theme="dark"] .hl-chroma .hl-vc { color: #f8f8f2 }
[data-theme="dark"] .hl-chroma .hl-vg { color: #f8f8f2 }
[data-theme="dark"] .hl-chroma .hl-vi { color: #f8f8f2 }
[data-theme="dark"] .hl-chroma .hl-s { color: #e6db74 }
[data-theme="dark"] .hl-chroma .hl-sa { color: #e6db74 }
[data-theme="dark"] .hl-chroma .hl-sb { color: #e6db74 }
[data-theme="dark"] .hl-chroma .hl-sc { color: #e6db74 }
[data-theme="dark"] .hl-chroma .hl-dl { color: #e6db74 }
[data-theme="dark"] .hl-chroma .hl-sd { color: #e6db74 }
[data-theme="dark"] .hl-chroma .hl-s2 { color: #e6db74 }
[data-theme="dark"] .hl-chroma .hl-se { color: #ae81ff }
[data-theme="dark"] .hl-chroma .hl-sh { color: #e6db74 }
[data-theme="dark"] .hl-chroma .hl-si { color: #e6db74 }
[data-theme="dark"] .hl-chroma .hl-sx { color: #e6db74 }
[data-theme="dark"] .hl-chroma .hl-sr { color: #e6db74 }
[data-theme="dark"] .hl-chroma .hl-s1 { color: #e6db74 }
[data-theme="dark"] .hl-chroma .hl-ss { color: #e6db74 }
[data-theme="dark"] .hl-chroma .hl-m { color: #ae81ff }
[data-theme="dark"] .hl-chroma .hl-mb { color: #ae81ff }
[data-theme="dark"] .hl-chroma .hl-mf { color: #ae81ff }
[data-theme="dark"] .hl-chroma .hl-mh { color: #ae81ff }
[data-theme="dark"] .hl-chroma .hl-mi { color: #ae81ff }
[data-theme="dark"] .hl-chroma .hl-il { color: #ae81ff }
[data-theme="dark"] .hl-chroma .hl-mo { color: #ae81ff }
[data-theme="dark"] .hl-chroma .hl-o { color: #f92672; font-weight: normal }
[data-theme="dark"] .hl-chroma .hl-ow { color: #f92672; font-weight: normal }
[data-theme="dark"] .hl-chroma .hl-c { color: #75715e; font-style: italic }
[data-theme="dark"] .hl-chroma .hl-ch { color: #75715e; font-style: italic }
[data-theme="dark"] .hl-chroma .hl-cm { color: #75715e; font-style: italic }
[data-theme="dark"] .hl-chroma .hl-c1 { color: #75715e; font-style: italic }
[data-theme="dark"] .hl-chroma .hl-cs { color: #75715e; font-style: italic }
[data-theme="dark"] .hl-chroma .hl-cp { color: #75715e; font-style: italic }
[data-theme="dark"] .hl-chroma .hl-cpf { color: #75715e; font-style: italic }
[data-theme="dark"] .hl-chroma .hl-gd { color: #f92672 }
[data-theme="dark"] .hl-chroma .hl-gi { color: #a6e22e }
[data-theme="dark"] .hl-chroma .hl-ge { font-style: italic }
[data-theme="dark"] .hl-chroma .hl-gs { font-weight: bold }
[data-theme="dark"] .hl-chroma .hl-gh { color: #75715e }
[data-theme="dark"] .hl-chroma .hl-gu { color: #75715e }
/* Dark syntax when auto-detected via media query */
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) .hl-chroma .hl-err { color: #960050; background-color: #1e0010 }
:root:not([data-theme="light"]) .hl-chroma .hl-k { color: #66d9ef; font-weight: normal }
:root:not([data-theme="light"]) .hl-chroma .hl-kc { color: #66d9ef; font-weight: normal }
:root:not([data-theme="light"]) .hl-chroma .hl-kd { color: #66d9ef; font-weight: normal }
:root:not([data-theme="light"]) .hl-chroma .hl-kn { color: #f92672; font-weight: normal }
:root:not([data-theme="light"]) .hl-chroma .hl-kp { color: #66d9ef; font-weight: normal }
:root:not([data-theme="light"]) .hl-chroma .hl-kr { color: #66d9ef; font-weight: normal }
:root:not([data-theme="light"]) .hl-chroma .hl-kt { color: #66d9ef; font-weight: normal }
:root:not([data-theme="light"]) .hl-chroma .hl-na { color: #a6e22e }
:root:not([data-theme="light"]) .hl-chroma .hl-nb { color: #f8f8f2 }
:root:not([data-theme="light"]) .hl-chroma .hl-bp { color: #f8f8f2 }
:root:not([data-theme="light"]) .hl-chroma .hl-nc { color: #a6e22e; font-weight: normal }
:root:not([data-theme="light"]) .hl-chroma .hl-no { color: #66d9ef }
:root:not([data-theme="light"]) .hl-chroma .hl-nd { color: #a6e22e; font-weight: normal }
:root:not([data-theme="light"]) .hl-chroma .hl-ne { color: #a6e22e; font-weight: normal }
:root:not([data-theme="light"]) .hl-chroma .hl-nf { color: #a6e22e }
:root:not([data-theme="light"]) .hl-chroma .hl-nn { color: #f8f8f2 }
:root:not([data-theme="light"]) .hl-chroma .hl-nt { color: #f92672 }
:root:not([data-theme="light"]) .hl-chroma .hl-nv { color: #f8f8f2 }
:root:not([data-theme="light"]) .hl-chroma .hl-s { color: #e6db74 }
:root:not([data-theme="light"]) .hl-chroma .hl-sa { color: #e6db74 }
:root:not([data-theme="light"]) .hl-chroma .hl-sb { color: #e6db74 }
:root:not([data-theme="light"]) .hl-chroma .hl-sc { color: #e6db74 }
:root:not([data-theme="light"]) .hl-chroma .hl-dl { color: #e6db74 }
:root:not([data-theme="light"]) .hl-chroma .hl-sd { color: #e6db74 }
:root:not([data-theme="light"]) .hl-chroma .hl-s2 { color: #e6db74 }
:root:not([data-theme="light"]) .hl-chroma .hl-se { color: #ae81ff }
:root:not([data-theme="light"]) .hl-chroma .hl-sh { color: #e6db74 }
:root:not([data-theme="light"]) .hl-chroma .hl-si { color: #e6db74 }
:root:not([data-theme="light"]) .hl-chroma .hl-sx { color: #e6db74 }
:root:not([data-theme="light"]) .hl-chroma .hl-sr { color: #e6db74 }
:root:not([data-theme="light"]) .hl-chroma .hl-s1 { color: #e6db74 }
:root:not([data-theme="light"]) .hl-chroma .hl-ss { color: #e6db74 }
:root:not([data-theme="light"]) .hl-chroma .hl-m { color: #ae81ff }
:root:not([data-theme="light"]) .hl-chroma .hl-mb { color: #ae81ff }
:root:not([data-theme="light"]) .hl-chroma .hl-mf { color: #ae81ff }
:root:not([data-theme="light"]) .hl-chroma .hl-mh { color: #ae81ff }
:root:not([data-theme="light"]) .hl-chroma .hl-mi { color: #ae81ff }
:root:not([data-theme="light"]) .hl-chroma .hl-il { color: #ae81ff }
:root:not([data-theme="light"]) .hl-chroma .hl-mo { color: #ae81ff }
:root:not([data-theme="light"]) .hl-chroma .hl-o { color: #f92672; font-weight: normal }
:root:not([data-theme="light"]) .hl-chroma .hl-ow { color: #f92672; font-weight: normal }
:root:not([data-theme="light"]) .hl-chroma .hl-c { color: #75715e; font-style: italic }
:root:not([data-theme="light"]) .hl-chroma .hl-ch { color: #75715e; font-style: italic }
:root:not([data-theme="light"]) .hl-chroma .hl-cm { color: #75715e; font-style: italic }
:root:not([data-theme="light"]) .hl-chroma .hl-c1 { color: #75715e; font-style: italic }
:root:not([data-theme="light"]) .hl-chroma .hl-cs { color: #75715e; font-style: italic }
:root:not([data-theme="light"]) .hl-chroma .hl-cp { color: #75715e; font-style: italic }
:root:not([data-theme="light"]) .hl-chroma .hl-cpf { color: #75715e; font-style: italic }
:root:not([data-theme="light"]) .hl-chroma .hl-gd { color: #f92672 }
:root:not([data-theme="light"]) .hl-chroma .hl-gi { color: #a6e22e }
}

View File

@ -15,6 +15,24 @@
<input type="search" name="q" placeholder="Search...">
</form>
</div>
<div class="sidebar-controls">
<div>
<label for="platform-select">Platform</label>
<select id="platform-select">
<option value="pico">RP2xxx (pico-sdk)</option>
<option value="esp32s3">ESP32-S3</option>
<option value="esp32c3">ESP32-C3</option>
</select>
</div>
<div>
<label for="theme-select">Theme</label>
<select id="theme-select">
<option value="auto">Auto</option>
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
</div>
</div>
{{.Nav}}
<div class="sidebar-footer">
<a href="/dl/book.md">Download .md</a>
@ -25,6 +43,6 @@
{{template "content" .}}
</main>
</div>
<script src="/static/search.js"></script>
<script src="/static/app.js"></script>
</body>
</html>

View File

@ -1,6 +1,9 @@
{{define "content"}}
<article>
{{.Body}}
<p id="platform-notice" class="platform-notice" style="display:none">
No examples available for this platform yet.
</p>
</article>
<nav class="page-nav">
{{if .PrevPath}}<a href="{{.PrevPath}}" class="prev">&larr; {{.PrevTitle}}</a>{{end}}