diff --git a/docs/_index.md b/docs/_index.md index c38da72..d1e5d4b 100644 --- a/docs/_index.md +++ b/docs/_index.md @@ -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 @@ -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` for incoming message handling ## Credits diff --git a/docs/examples/01-output/01-note-button.md b/docs/examples/01-output/01-note-button.md index ddb069d..629271f 100644 --- a/docs/examples/01-output/01-note-button.md +++ b/docs/examples/01-output/01-note-button.md @@ -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 diff --git a/docs/examples/01-output/02-cc-button.md b/docs/examples/01-output/02-cc-button.md index 4aedaca..427c6c0 100644 --- a/docs/examples/01-output/02-cc-button.md +++ b/docs/examples/01-output/02-cc-button.md @@ -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 diff --git a/docs/examples/01-output/03-pc-button.md b/docs/examples/01-output/03-pc-button.md index 4de08eb..c20d935 100644 --- a/docs/examples/01-output/03-pc-button.md +++ b/docs/examples/01-output/03-pc-button.md @@ -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 diff --git a/docs/examples/01-output/04-note-chord-button.md b/docs/examples/01-output/04-note-chord-button.md index a26bbd1..7b3007c 100644 --- a/docs/examples/01-output/04-note-chord-button.md +++ b/docs/examples/01-output/04-note-chord-button.md @@ -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 diff --git a/docs/examples/01-output/05-note-button-latched.md b/docs/examples/01-output/05-note-button-latched.md index bde21b6..5199e8b 100644 --- a/docs/examples/01-output/05-note-button-latched.md +++ b/docs/examples/01-output/05-note-button-latched.md @@ -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 diff --git a/docs/examples/01-output/06-note-button-latching.md b/docs/examples/01-output/06-note-button-latching.md index 6df821e..13057e6 100644 --- a/docs/examples/01-output/06-note-button-latching.md +++ b/docs/examples/01-output/06-note-button-latching.md @@ -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 diff --git a/docs/examples/01-output/07-note-buttons.md b/docs/examples/01-output/07-note-buttons.md index f510c2a..fb7d7c8 100644 --- a/docs/examples/01-output/07-note-buttons.md +++ b/docs/examples/01-output/07-note-buttons.md @@ -2,7 +2,7 @@ Multiple note buttons with sequential addresses. -```cpp +```cpp pico #include "pico/stdlib.h" #include "pico/cyw43_arch.h" #include diff --git a/docs/examples/01-output/08-note-button-matrix.md b/docs/examples/01-output/08-note-button-matrix.md index 97da84c..d4ddd57 100644 --- a/docs/examples/01-output/08-note-button-matrix.md +++ b/docs/examples/01-output/08-note-button-matrix.md @@ -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 diff --git a/docs/examples/01-output/09-cc-button-latched.md b/docs/examples/01-output/09-cc-button-latched.md index 75f2dd9..31be5d0 100644 --- a/docs/examples/01-output/09-cc-button-latched.md +++ b/docs/examples/01-output/09-cc-button-latched.md @@ -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 diff --git a/docs/examples/01-output/10-cc-button-latching.md b/docs/examples/01-output/10-cc-button-latching.md index 7c605f3..26dbc66 100644 --- a/docs/examples/01-output/10-cc-button-latching.md +++ b/docs/examples/01-output/10-cc-button-latching.md @@ -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 diff --git a/docs/examples/01-output/11-cc-buttons.md b/docs/examples/01-output/11-cc-buttons.md index f6598b9..b0d904d 100644 --- a/docs/examples/01-output/11-cc-buttons.md +++ b/docs/examples/01-output/11-cc-buttons.md @@ -2,7 +2,7 @@ Multiple CC buttons with sequential addresses. -```cpp +```cpp pico #include "pico/stdlib.h" #include "pico/cyw43_arch.h" #include diff --git a/docs/examples/01-output/12-cc-button-matrix.md b/docs/examples/01-output/12-cc-button-matrix.md index 5824146..f8e362c 100644 --- a/docs/examples/01-output/12-cc-button-matrix.md +++ b/docs/examples/01-output/12-cc-button-matrix.md @@ -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 diff --git a/docs/examples/01-output/13-cc-potentiometer.md b/docs/examples/01-output/13-cc-potentiometer.md index 7d04a6f..f9c2c40 100644 --- a/docs/examples/01-output/13-cc-potentiometer.md +++ b/docs/examples/01-output/13-cc-potentiometer.md @@ -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 diff --git a/docs/examples/01-output/14-pb-potentiometer.md b/docs/examples/01-output/14-pb-potentiometer.md index b6afacc..a0eecc2 100644 --- a/docs/examples/01-output/14-pb-potentiometer.md +++ b/docs/examples/01-output/14-pb-potentiometer.md @@ -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 diff --git a/docs/examples/01-output/15-cc-rotary-encoder.md b/docs/examples/01-output/15-cc-rotary-encoder.md index cc756d1..7e6d4bc 100644 --- a/docs/examples/01-output/15-cc-rotary-encoder.md +++ b/docs/examples/01-output/15-cc-rotary-encoder.md @@ -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 diff --git a/docs/examples/01-output/16-cc-absolute-encoder.md b/docs/examples/01-output/16-cc-absolute-encoder.md index 04ccfd9..07ce09e 100644 --- a/docs/examples/01-output/16-cc-absolute-encoder.md +++ b/docs/examples/01-output/16-cc-absolute-encoder.md @@ -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 diff --git a/docs/examples/01-output/17-pb-absolute-encoder.md b/docs/examples/01-output/17-pb-absolute-encoder.md index b6843cc..ec0e0ea 100644 --- a/docs/examples/01-output/17-pb-absolute-encoder.md +++ b/docs/examples/01-output/17-pb-absolute-encoder.md @@ -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 diff --git a/docs/examples/01-output/18-cc-increment-decrement-buttons.md b/docs/examples/01-output/18-cc-increment-decrement-buttons.md index 68abe3d..9c69d4e 100644 --- a/docs/examples/01-output/18-cc-increment-decrement-buttons.md +++ b/docs/examples/01-output/18-cc-increment-decrement-buttons.md @@ -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 diff --git a/docs/examples/01-output/19-program-changer.md b/docs/examples/01-output/19-program-changer.md index 4cfdca9..433cc84 100644 --- a/docs/examples/01-output/19-program-changer.md +++ b/docs/examples/01-output/19-program-changer.md @@ -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 diff --git a/docs/examples/01-output/20-cc-potentiometers.md b/docs/examples/01-output/20-cc-potentiometers.md index cf182a7..96af3d3 100644 --- a/docs/examples/01-output/20-cc-potentiometers.md +++ b/docs/examples/01-output/20-cc-potentiometers.md @@ -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 diff --git a/docs/examples/01-output/21-borrowed-cc-rotary-encoder.md b/docs/examples/01-output/21-borrowed-cc-rotary-encoder.md index af1db94..5eafe8d 100644 --- a/docs/examples/01-output/21-borrowed-cc-rotary-encoder.md +++ b/docs/examples/01-output/21-borrowed-cc-rotary-encoder.md @@ -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 diff --git a/docs/examples/01-output/22-cc-potentiometer-map.md b/docs/examples/01-output/22-cc-potentiometer-map.md new file mode 100644 index 0000000..034ea4c --- /dev/null +++ b/docs/examples/01-output/22-cc-potentiometer-map.md @@ -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 + +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); + } +} +``` diff --git a/docs/examples/01-output/23-finished-controller.md b/docs/examples/01-output/23-finished-controller.md new file mode 100644 index 0000000..cdb155c --- /dev/null +++ b/docs/examples/01-output/23-finished-controller.md @@ -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 + +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); + } +} +``` diff --git a/docs/examples/01-output/24-enable-disable.md b/docs/examples/01-output/24-enable-disable.md new file mode 100644 index 0000000..87ccc0c --- /dev/null +++ b/docs/examples/01-output/24-enable-disable.md @@ -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 + +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); + } +} +``` diff --git a/docs/examples/02-input/01-note-led.md b/docs/examples/02-input/01-note-led.md index 69f1c23..8091de5 100644 --- a/docs/examples/02-input/01-note-led.md +++ b/docs/examples/02-input/01-note-led.md @@ -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 diff --git a/docs/examples/02-input/02-note-range-leds.md b/docs/examples/02-input/02-note-range-leds.md index 57b89a6..50a047e 100644 --- a/docs/examples/02-input/02-note-range-leds.md +++ b/docs/examples/02-input/02-note-range-leds.md @@ -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 diff --git a/docs/examples/02-input/03-cc-value.md b/docs/examples/02-input/03-cc-value.md index 754a936..0f7fd64 100644 --- a/docs/examples/02-input/03-cc-value.md +++ b/docs/examples/02-input/03-cc-value.md @@ -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 diff --git a/docs/examples/02-input/04-pb-value.md b/docs/examples/02-input/04-pb-value.md index aa7920f..78219ba 100644 --- a/docs/examples/02-input/04-pb-value.md +++ b/docs/examples/02-input/04-pb-value.md @@ -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 diff --git a/docs/examples/02-input/05-note-value.md b/docs/examples/02-input/05-note-value.md index 7048c41..1b8aa11 100644 --- a/docs/examples/02-input/05-note-value.md +++ b/docs/examples/02-input/05-note-value.md @@ -2,7 +2,7 @@ Reads incoming Note On values (8-bit). -```cpp +```cpp pico #include "pico/stdlib.h" #include "pico/cyw43_arch.h" #include diff --git a/docs/examples/02-input/06-kp-value.md b/docs/examples/02-input/06-kp-value.md index e989d30..dc69144 100644 --- a/docs/examples/02-input/06-kp-value.md +++ b/docs/examples/02-input/06-kp-value.md @@ -2,7 +2,7 @@ Reads incoming Key Pressure (aftertouch) values. -```cpp +```cpp pico #include "pico/stdlib.h" #include "pico/cyw43_arch.h" #include diff --git a/docs/examples/02-input/07-cc-led.md b/docs/examples/02-input/07-cc-led.md index 1f7658d..0c6371c 100644 --- a/docs/examples/02-input/07-cc-led.md +++ b/docs/examples/02-input/07-cc-led.md @@ -2,7 +2,7 @@ LED responds to incoming CC messages. -```cpp +```cpp pico #include "pico/stdlib.h" #include "pico/cyw43_arch.h" #include diff --git a/docs/examples/02-input/08-note-range.md b/docs/examples/02-input/08-note-range.md index 2e231b0..0786ecb 100644 --- a/docs/examples/02-input/08-note-range.md +++ b/docs/examples/02-input/08-note-range.md @@ -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 diff --git a/docs/examples/02-input/09-cc-range.md b/docs/examples/02-input/09-cc-range.md index 1fe114f..0b24023 100644 --- a/docs/examples/02-input/09-cc-range.md +++ b/docs/examples/02-input/09-cc-range.md @@ -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 diff --git a/docs/examples/03-interfaces/01-ble-midi.md b/docs/examples/03-interfaces/01-ble-midi.md index dfca6f4..790c6af 100644 --- a/docs/examples/03-interfaces/01-ble-midi.md +++ b/docs/examples/03-interfaces/01-ble-midi.md @@ -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 diff --git a/docs/examples/03-interfaces/02-midi-pipes.md b/docs/examples/03-interfaces/02-midi-pipes.md index 02b9c07..d1bc725 100644 --- a/docs/examples/03-interfaces/02-midi-pipes.md +++ b/docs/examples/03-interfaces/02-midi-pipes.md @@ -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 diff --git a/docs/examples/03-interfaces/03-usb-midi.md b/docs/examples/03-interfaces/03-usb-midi.md index 7dd6d3c..f15d0f4 100644 --- a/docs/examples/03-interfaces/03-usb-midi.md +++ b/docs/examples/03-interfaces/03-usb-midi.md @@ -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 diff --git a/docs/examples/03-interfaces/04-serial-midi.md b/docs/examples/03-interfaces/04-serial-midi.md index aa28be0..8c0a6ec 100644 --- a/docs/examples/03-interfaces/04-serial-midi.md +++ b/docs/examples/03-interfaces/04-serial-midi.md @@ -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 diff --git a/docs/examples/03-interfaces/05-dual-midi.md b/docs/examples/03-interfaces/05-dual-midi.md index d44b81f..8db452d 100644 --- a/docs/examples/03-interfaces/05-dual-midi.md +++ b/docs/examples/03-interfaces/05-dual-midi.md @@ -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" diff --git a/docs/examples/03-interfaces/06-applemidi.md b/docs/examples/03-interfaces/06-applemidi.md index 545027c..4afec23 100644 --- a/docs/examples/03-interfaces/06-applemidi.md +++ b/docs/examples/03-interfaces/06-applemidi.md @@ -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 diff --git a/docs/examples/03-interfaces/07-applemidi-ble.md b/docs/examples/03-interfaces/07-applemidi-ble.md index defc469..e5fbb34 100644 --- a/docs/examples/03-interfaces/07-applemidi-ble.md +++ b/docs/examples/03-interfaces/07-applemidi-ble.md @@ -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 diff --git a/docs/examples/03-interfaces/08-midi-output.md b/docs/examples/03-interfaces/08-midi-output.md new file mode 100644 index 0000000..814d418 --- /dev/null +++ b/docs/examples/03-interfaces/08-midi-output.md @@ -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 + +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(); + } +} +``` diff --git a/docs/examples/03-interfaces/09-send-midi-notes.md b/docs/examples/03-interfaces/09-send-midi-notes.md new file mode 100644 index 0000000..eedb0ad --- /dev/null +++ b/docs/examples/03-interfaces/09-send-midi-notes.md @@ -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 + +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); + } +} +``` diff --git a/docs/examples/03-interfaces/10-send-all-midi-messages.md b/docs/examples/03-interfaces/10-send-all-midi-messages.md new file mode 100644 index 0000000..3e5ad8a --- /dev/null +++ b/docs/examples/03-interfaces/10-send-all-midi-messages.md @@ -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 + +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); + } +} +``` diff --git a/docs/examples/03-interfaces/11-midi-input-callback.md b/docs/examples/03-interfaces/11-midi-input-callback.md new file mode 100644 index 0000000..f5572b1 --- /dev/null +++ b/docs/examples/03-interfaces/11-midi-input-callback.md @@ -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 + +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); + } +} +``` diff --git a/docs/examples/03-interfaces/12-midi-pipes-filter.md b/docs/examples/03-interfaces/12-midi-pipes-filter.md new file mode 100644 index 0000000..3d20c29 --- /dev/null +++ b/docs/examples/03-interfaces/12-midi-pipes-filter.md @@ -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 + +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); + } +} +``` diff --git a/docs/examples/04-banks/01-bank-cc-values.md b/docs/examples/04-banks/01-bank-cc-values.md index bc521c1..d728322 100644 --- a/docs/examples/04-banks/01-bank-cc-values.md +++ b/docs/examples/04-banks/01-bank-cc-values.md @@ -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 diff --git a/docs/examples/04-banks/02-transposer.md b/docs/examples/04-banks/02-transposer.md index 51be60a..8a08f85 100644 --- a/docs/examples/04-banks/02-transposer.md +++ b/docs/examples/04-banks/02-transposer.md @@ -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 diff --git a/docs/examples/04-banks/03-encoder-selector.md b/docs/examples/04-banks/03-encoder-selector.md index ad81436..f3725a1 100644 --- a/docs/examples/04-banks/03-encoder-selector.md +++ b/docs/examples/04-banks/03-encoder-selector.md @@ -2,7 +2,7 @@ Bank selection via rotary encoder. -```cpp +```cpp pico #include "pico/stdlib.h" #include "pico/cyw43_arch.h" #include diff --git a/docs/examples/04-banks/04-many-buttons-selector.md b/docs/examples/04-banks/04-many-buttons-selector.md index b817e65..9f093f7 100644 --- a/docs/examples/04-banks/04-many-buttons-selector.md +++ b/docs/examples/04-banks/04-many-buttons-selector.md @@ -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 diff --git a/docs/examples/04-banks/05-switch-selector.md b/docs/examples/04-banks/05-switch-selector.md index 9e0aa0d..0fec223 100644 --- a/docs/examples/04-banks/05-switch-selector.md +++ b/docs/examples/04-banks/05-switch-selector.md @@ -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 diff --git a/docs/examples/04-banks/06-program-change-selector.md b/docs/examples/04-banks/06-program-change-selector.md index 97cf5b9..42de187 100644 --- a/docs/examples/04-banks/06-program-change-selector.md +++ b/docs/examples/04-banks/06-program-change-selector.md @@ -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 diff --git a/docs/examples/04-banks/07-bankable-note-led.md b/docs/examples/04-banks/07-bankable-note-led.md index c0de759..1c1a335 100644 --- a/docs/examples/04-banks/07-bankable-note-led.md +++ b/docs/examples/04-banks/07-bankable-note-led.md @@ -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 diff --git a/docs/examples/_index.md b/docs/examples/_index.md index 0ad7f23..f7bee92 100644 --- a/docs/examples/_index.md +++ b/docs/examples/_index.md @@ -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 diff --git a/docs/manual/01-getting-started/01-installation.md b/docs/manual/01-getting-started/01-installation.md new file mode 100644 index 0000000..93b71a3 --- /dev/null +++ b/docs/manual/01-getting-started/01-installation.md @@ -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. diff --git a/docs/manual/01-getting-started/02-first-output.md b/docs/manual/01-getting-started/02-first-output.md new file mode 100644 index 0000000..c3f44f8 --- /dev/null +++ b/docs/manual/01-getting-started/02-first-output.md @@ -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 + +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 + +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); + } +} +``` diff --git a/docs/manual/01-getting-started/03-first-input.md b/docs/manual/01-getting-started/03-first-input.md new file mode 100644 index 0000000..cfecf2a --- /dev/null +++ b/docs/manual/01-getting-started/03-first-input.md @@ -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 + +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); + } +} +``` diff --git a/docs/manual/01-getting-started/04-faq.md b/docs/manual/01-getting-started/04-faq.md new file mode 100644 index 0000000..b28c7f6 --- /dev/null +++ b/docs/manual/01-getting-started/04-faq.md @@ -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` 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 ` instead of `#include ` +- 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. diff --git a/docs/manual/01-getting-started/_index.md b/docs/manual/01-getting-started/_index.md new file mode 100644 index 0000000..68050fb --- /dev/null +++ b/docs/manual/01-getting-started/_index.md @@ -0,0 +1,3 @@ +# Getting Started + +Setup, installation, and first steps with cs-midi. diff --git a/docs/manual/02-midi/01-midi-tutorial.md b/docs/manual/02-midi/01-midi-tutorial.md new file mode 100644 index 0000000..6b8cb04 --- /dev/null +++ b/docs/manual/02-midi/01-midi-tutorial.md @@ -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(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 { + 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); + } +}; +``` diff --git a/docs/manual/02-midi/02-sending-messages.md b/docs/manual/02-midi/02-sending-messages.md new file mode 100644 index 0000000..8e9e6ca --- /dev/null +++ b/docs/manual/02-midi/02-sending-messages.md @@ -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 + +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. diff --git a/docs/manual/02-midi/03-receiving-messages.md b/docs/manual/02-midi/03-receiving-messages.md new file mode 100644 index 0000000..f7614d5 --- /dev/null +++ b/docs/manual/02-midi/03-receiving-messages.md @@ -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 + +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 { + 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. diff --git a/docs/manual/02-midi/04-routing-messages.md b/docs/manual/02-midi/04-routing-messages.md new file mode 100644 index 0000000..7d066f6 --- /dev/null +++ b/docs/manual/02-midi/04-routing-messages.md @@ -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 + +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); + } +} +``` diff --git a/docs/manual/02-midi/05-midi-over-usb.md b/docs/manual/02-midi/05-midi-over-usb.md new file mode 100644 index 0000000..bdd4ced --- /dev/null +++ b/docs/manual/02-midi/05-midi-over-usb.md @@ -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 + +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; +``` diff --git a/docs/manual/02-midi/06-midi-over-ble.md b/docs/manual/02-midi/06-midi-over-ble.md new file mode 100644 index 0000000..05ccc76 --- /dev/null +++ b/docs/manual/02-midi/06-midi-over-ble.md @@ -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; +``` + +## Basic usage + +```cpp pico +#include "pico/stdlib.h" +#include "pico/cyw43_arch.h" +#include + +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`. diff --git a/docs/manual/02-midi/_index.md b/docs/manual/02-midi/_index.md new file mode 100644 index 0000000..454df20 --- /dev/null +++ b/docs/manual/02-midi/_index.md @@ -0,0 +1,3 @@ +# MIDI Communication + +Sending, receiving, and routing MIDI messages. diff --git a/docs/manual/03-control-surface/_index.md b/docs/manual/03-control-surface/_index.md new file mode 100644 index 0000000..56a4e90 --- /dev/null +++ b/docs/manual/03-control-surface/_index.md @@ -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 + +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(); +``` diff --git a/docs/manual/04-output-elements/_index.md b/docs/manual/04-output-elements/_index.md new file mode 100644 index 0000000..902792e --- /dev/null +++ b/docs/manual/04-output-elements/_index.md @@ -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` | Array of N sequential note buttons | +| `CCButtons` | Array of N sequential CC buttons | +| `NoteButtonMatrix` | Row/column scanned note grid | +| `CCButtonMatrix` | 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` | 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}, +}}; +``` diff --git a/docs/manual/05-input-elements/_index.md b/docs/manual/05-input-elements/_index.md new file mode 100644 index 0000000..9da5d03 --- /dev/null +++ b/docs/manual/05-input-elements/_index.md @@ -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` | Array of N consecutive note values | +| `CCRange` | Array of N consecutive CC values | +| `KPRange` | 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. diff --git a/docs/manual/06-banks/_index.md b/docs/manual/06-banks/_index.md new file mode 100644 index 0000000..0159ce9 --- /dev/null +++ b/docs/manual/06-banks/_index.md @@ -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` 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` | Rotary encoder cycles banks | +| `IncrementDecrementSelector` | Two buttons: up/down | +| `IncrementSelector` | Single button, wraps around | +| `ManyButtonsSelector` | One button per bank | +| `SwitchSelector` | Two-position toggle | +| `ProgramChangeSelector` | Incoming MIDI PC selects bank | + +```cpp pico +IncrementDecrementSelector<4> selector {bank, {2, 3}, Wrap::Wrap}; +``` + +## Transposer + +`Transposer` 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. diff --git a/docs/manual/07-hardware/_index.md b/docs/manual/07-hardware/_index.md new file mode 100644 index 0000000..46c128f --- /dev/null +++ b/docs/manual/07-hardware/_index.md @@ -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. diff --git a/docs/manual/_index.md b/docs/manual/_index.md new file mode 100644 index 0000000..415f9ff --- /dev/null +++ b/docs/manual/_index.md @@ -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. diff --git a/docs/roadmap.md b/docs/roadmap.md index a20c7a5..185c16e 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -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 diff --git a/internal/render/markdown.go b/internal/render/markdown.go index 76052e6..f4026ae 100644 --- a/internal/render/markdown.go +++ b/internal/render/markdown.go @@ -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(`
`, 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("
")) + 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 diff --git a/internal/server/static/app.js b/internal/server/static/app.js new file mode 100644 index 0000000..93966db --- /dev/null +++ b/internal/server/static/app.js @@ -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(); + } +}); diff --git a/internal/server/static/search.js b/internal/server/static/search.js deleted file mode 100644 index cbb563f..0000000 --- a/internal/server/static/search.js +++ /dev/null @@ -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(); - } - }); -}); diff --git a/internal/server/static/style.css b/internal/server/static/style.css index a433b0e..0c82a96 100644 --- a/internal/server/static/style.css +++ b/internal/server/static/style.css @@ -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 } +} diff --git a/internal/server/templates/base.html b/internal/server/templates/base.html index 175e9bc..8b4d12f 100644 --- a/internal/server/templates/base.html +++ b/internal/server/templates/base.html @@ -15,6 +15,24 @@ + {{.Nav}} - + diff --git a/internal/server/templates/page.html b/internal/server/templates/page.html index 4924724..d3be230 100644 --- a/internal/server/templates/page.html +++ b/internal/server/templates/page.html @@ -1,6 +1,9 @@ {{define "content"}}
{{.Body}} +