diff --git a/.github/workflows/examples.yaml b/.github/workflows/examples.yaml index 1706727561..1d4c907a48 100644 --- a/.github/workflows/examples.yaml +++ b/.github/workflows/examples.yaml @@ -8,7 +8,7 @@ jobs: runs-on: ubuntu-20.04 strategy: matrix: - board: [AVR, Nano 33 BLE, Pi Pico, Nano 33 IoT, UNO R4, ESP32, ESP8266, AVR USB, Mega, Leonardo, Due, Nano Every, Teensy 3.x, Teensy 4.1] + board: [AVR, Nano 33 BLE, Pi Pico, Pi Pico W, Nano 33 IoT, UNO R4, ESP32, ESP32-S3, ESP8266, AVR USB, Mega, Leonardo, Due, Nano Every, Teensy 3.x, Teensy 4.1] steps: - name: Checkout @@ -56,7 +56,7 @@ jobs: - name: Install Arduino Boards run: | - arduino-cli core install arduino:avr arduino:mbed_nano arduino:mbed_rp2040 arduino:samd esp32:esp32 esp8266:esp8266 arduino:sam arduino:megaavr arduino:renesas_uno teensy:avr + arduino-cli core install arduino:avr arduino:mbed_nano arduino:mbed_rp2040 arduino:samd esp32:esp32 esp8266:esp8266 arduino:sam arduino:megaavr arduino:renesas_uno teensy:avr rp2040:rp2040 arduino-cli core upgrade arduino-cli cache clean version=$(arduino-cli core list | grep 'teensy:avr' | awk '{print $2}') && { sed -i 's/^recipe.hooks.postbuild/# recipe.hooks.postbuild/g' $HOME/.arduino15/packages/teensy/hardware/avr/$version/platform.txt ||:; } @@ -70,6 +70,7 @@ jobs: [ -d FastLED ] || git clone https://github.com/FastLED/FastLED.git --depth=1 & [ -d MIDIUSB ] || git clone https://github.com/arduino-libraries/MIDIUSB.git --depth=1 & [ -d Audio ] || git clone https://github.com/PaulStoffregen/Audio.git --depth=1 & + [ -d ArduinoBLE ] || git clone https://github.com/arduino-libraries/ArduinoBLE.git --depth=1 & [ -d Arduino-AppleMIDI-Library ] || git clone https://github.com/lathoub/Arduino-AppleMIDI-Library.git --depth=1 --branch v3.2.0 & [ -d arduino_midi_library ] || git clone https://github.com/FortySevenEffects/arduino_midi_library.git --depth=1 & ln -snf "$GITHUB_WORKSPACE" "$HOME/Arduino/libraries/" diff --git a/README.md b/README.md index 9247ce9a59..0b7ef56d55 100644 --- a/README.md +++ b/README.md @@ -8,124 +8,40 @@ Control Surface is an Arduino library for building MIDI controllers and control surfaces. -At its core is a -[general-purpose MIDI abstraction layer](https://tttapa.github.io/Control-Surface-doc/Doxygen/d3/df7/midi-tutorial.html) -with support for serial MIDI, MIDI over USB, MIDI over BLE, etc., which can be -useful for any MIDI-related project. -Besides MIDI input/output, Control Surface also includes easy-to-use utilities -specifically for building MIDI controllers, supporting controls that send MIDI + +At its core, the library features a flexible [**MIDI abstraction layer**](https://tttapa.github.io/Control-Surface-doc/Doxygen/d3/df7/midi-tutorial.html) +with support for serial **5-pin DIN** MIDI, MIDI over **USB**, MIDI over **BLE**, etc. +These MIDI interfaces are compatible with a wide range of Arduino boards +(a full table can be found [here](https://tttapa.github.io/Control-Surface-doc/Doxygen/d8/d4a/md_pages_MIDI-over-USB.html)) +and are useful in any Arduino MIDI project. + +In addition to MIDI input/output, Control Surface also provides easy-to-use utilities +intended for building MIDI controllers, supporting controls that send MIDI messages ─ like potentiometers, push buttons, rotary encoders, etc. ─ -and controls that react to incoming MIDI ─ LEDs, displays, and so on. -They can also be combined into controls that use both MIDI input and output, -such as motorized faders. +and controls that react to incoming MIDI messages ─ LEDs, displays, and so on. +More advanced controls that combine MIDI input and output ─ +such as [motorized faders](https://github.com/tttapa/Control-Surface-Motor-Fader) +─ are supported as well. + +In projects with large numbers of inputs and outputs, Control Surface allows you +to seamlessly add multiplexers, shift registers and other port expanders, and +treat them as if they were ordinary GPIO pins. > Table of contents ->  [Overview](#overview) >  [Example usage](#example-usage) >  [Getting started](#getting-started) >  [Documentation](#documentation) +>  [Feature overview](#feature-overview) >  [Supported boards](#supported-boards) ->  [Information for developers](#information-for-developers) >  [Change log and updating](#change-log-and-updating) - - -## Overview - -This library turns your Arduino-compatible board into a MIDI control surface. -Just connect some push buttons, potentiometers, LEDs ... and declare them in -your code. - -The following sections give a brief overview of the features of the library. - -### MIDI Interfaces - - - **MIDI over USB** - - **Serial MIDI** (e.g. 5-pin DIN MIDI) - - **Debug MIDI** (prints out the messages in a readable format, and allows you - to input text based messages, like a MIDI monitor) - - **MIDI over Bluetooth LE** - - **AppleMIDI** over WiFi or Ethernet - -→ [_MIDI Interfaces documentation_](https://tttapa.github.io/Control-Surface-doc/Doxygen/dc/df0/group__MIDIInterfaces.html) - -### MIDI Control Output - - - **Push buttons** and **toggle switches** - - **Potentiometers**, **faders** and other analog sensors - - **Rotary encoders** - - **Scanning keyboard matrices** - -Digital inputs are **debounced**, and analog inputs are filtered using -**digital filters and hysteresis**. This results in high accuracy without noise, -without introducing latency. - -These MIDI control outputs can be used to send MIDI notes, Control Change, -Pitch Bend, Program/Patch change, etc. - -→ [_MIDI Output Elements documentation_](https://tttapa.github.io/Control-Surface-doc/Doxygen/d7/dcd/group__MIDIOutputElements.html) - -### MIDI Control Input - - - **LEDs** (e.g. to indicate whether a track is muted/armed/soloed) - - **LED rings** (e.g. to indicate the position of a pan knob) - - **LED strips** (using the [FastLED](https://github.com/FastLED/FastLED) - library) - - **VU meters** - - **OLED displays** - - **7-segment displays** - -A large portion of the **Mackie Control Universal** (MCU) protocol is -implemented. - -→ [_MIDI Input Elements documentation_](https://tttapa.github.io/Control-Surface-doc/Doxygen/df/d8b/group__MIDIInputElements.html) - -### Motorized faders - -- **Motorized faders** are supported through the [tttapa/Control-Surface-Motor-Fader](https://github.com/tttapa/Control-Surface-Motor-Fader) repository. - -→ [_Control Surface Motor Fader documentation_](https://tttapa.github.io/Pages/Arduino/Control-Theory/Motor-Fader/) - -### Bank support - -All controls can be arranged in **banks**: for example, if you have only 4 -physical faders, you can make them bankable, so they can be used to control -the volume of many more different tracks. Changing banks can be done using push -buttons, rotary encoders, etc. -Apart from banks and bank selectors, you can also add **transposers** to change -the key of your notes, for example. - -### Extended input/output - -In order to save some IO pins, the library natively supports **multiplexers** -(e.g. 74HC4051 or 74HC4067) to read many switches or potentiometers, -**Shift Registers** (e.g. 74HC595) to drive many LEDs, **MAX7219 LED drivers**, -etc. - -→ [_Extended IO documentation_](https://tttapa.github.io/Control-Surface-doc/Doxygen/db/dd3/group__AH__ExtIO.html) - -### Audio - -If you are using a Teensy 3.x or 4.x, you can use it as a -**USB audio interface**. Just add an I²S DAC (e.g. PCM5102) and 5 lines of code, -and you can start playing audio through your Teensy, by combining Control -Surface with the Teensy Audio library. -You can also add volume controls and VU meters for these audio connections. - -→ [_Teensy Audio documentation_](https://tttapa.github.io/Control-Surface-doc/Doxygen/d3/d5c/group__Audio.html) - -### Modular and extensible - -Thanks to the structure of the library, you can easily add your own MIDI or -display elements, using some minimal, high level code. All low level stuff is -completely **reusable** (e.g. all MIDI operations, debouncing switches, -filtering analog inputs, and so on). - - - ## Example usage -A complete sketch for a MIDI controller with a potentiometer that sends out MIDI +An extensive list of examples can be found [in the documentation](https://tttapa.github.io/Control-Surface-doc/Doxygen/examples.html). +Below are a handful of simple examples that give an idea of how the Control +Surface library can be used. + +**Example 1**: A complete sketch for a MIDI controller with a potentiometer that sends out MIDI Control Change message can be written in just five lines of code: ```cpp @@ -138,10 +54,11 @@ void setup() { Control_Surface.begin(); } void loop() { Control_Surface.loop(); } ``` -Larger MIDI controllers can implemented very easily as well, with clean and easy -to modify code. +**Example 2**: Larger MIDI controllers can be implemented very easily as well, with clean and +easy to modify code. The following sketch is for 8 potentiometers (connected using an analog -multiplexer) that send out MIDI Control Change messages over USB. +multiplexer) that send out MIDI Control Change messages over USB. A detailed +walkthrough of this example can be found on the [Getting Started](https://tttapa.github.io/Control-Surface-doc/Doxygen/d5/d7d/md_pages_Getting-Started.html) page. ```cpp #include // Include the library @@ -176,7 +93,7 @@ void loop() { } ``` -Control Surface supports many types of MIDI inputs. +**Example 3**: Control Surface also supports many types of MIDI inputs. For example, an LED that turns on when a MIDI Note On message for middle C is received: ```cpp @@ -189,7 +106,30 @@ void setup() { Control_Surface.begin(); } void loop() { Control_Surface.loop(); } ``` - +**Example 4**: Control Surface's MIDI interfaces can also be used directly, for example, to +implement a MIDI-over-USB to MIDI-over-BLE adapter: +```cpp +#include + +// Instantiate MIDI over BLE and MIDI over USB interfaces +BluetoothMIDI_Interface midi_ble; +USBMIDI_Interface midi_usb; +// Pipes allow routing between MIDI interfaces +BidirectionalMIDI_Pipe pipes; + +void setup() { + // Route the MIDI input from the USB interface to the BLE interface, + // and the MIDI input from the BLE interface to the USB interface + midi_usb | pipes | midi_ble; + // Initialize the MIDI interfaces + MIDI_Interface::beginAll(); +} + +void loop() { + // Continuously poll all interfaces and route the traffic between them + MIDI_Interface::updateAll(); +} +``` ## Getting started @@ -201,8 +141,6 @@ The [**MIDI tutorial**](https://tttapa.github.io/Control-Surface-doc/Doxygen/d3/ might be useful if you want to use Control Surface as a regular MIDI library, for sending and receiving MIDI messages. - - ## Documentation The automatically generated Doxygen documentation for this library can be found @@ -221,7 +159,96 @@ You can find an answer to some frequently asked questions on the [**FAQ**](https://tttapa.github.io/Control-Surface-doc/Doxygen/da/dc1/FAQ.html) page. - +## Feature overview + +This library turns your Arduino-compatible board into a MIDI control surface. +Just connect some push buttons, potentiometers, LEDs ... and declare them in +your code. + +The following sections give a brief overview of the features of the library. + +### MIDI Interfaces + + - **MIDI over USB** + - **Serial MIDI** (e.g. 5-pin DIN MIDI) + - **Debug MIDI** (prints out the messages in a readable format, and allows you + to input text based messages, like a MIDI monitor) + - **MIDI over Bluetooth LE** + - **AppleMIDI** over WiFi or Ethernet + +→ [_MIDI Interfaces documentation_](https://tttapa.github.io/Control-Surface-doc/Doxygen/dc/df0/group__MIDIInterfaces.html) + +### MIDI Control Output + + - **Push buttons** and **toggle switches** + - **Potentiometers**, **faders** and other analog sensors + - **Rotary encoders** + - **Scanning keyboard matrices** + +Digital inputs are **debounced**, and analog inputs are filtered using +**digital filters and hysteresis**. This results in high accuracy without noise, +without introducing latency. + +These MIDI control outputs can be used to send MIDI notes, Control Change, +Pitch Bend, Program/Patch change, etc. + +→ [_MIDI Output Elements documentation_](https://tttapa.github.io/Control-Surface-doc/Doxygen/d7/dcd/group__MIDIOutputElements.html) + +### MIDI Control Input + + - **LEDs** (e.g. to indicate whether a track is muted/armed/soloed) + - **LED rings** (e.g. to indicate the position of a pan knob) + - **LED strips** (using the [FastLED](https://github.com/FastLED/FastLED) + library) + - **VU meters** + - **OLED displays** + - **7-segment displays** + +A large portion of the **Mackie Control Universal** (MCU) protocol is +implemented. + +→ [_MIDI Input Elements documentation_](https://tttapa.github.io/Control-Surface-doc/Doxygen/df/d8b/group__MIDIInputElements.html) + +### Motorized faders + +- **Motorized faders** are supported through the [tttapa/Control-Surface-Motor-Fader](https://github.com/tttapa/Control-Surface-Motor-Fader) repository. + +→ [_Control Surface Motor Fader documentation_](https://tttapa.github.io/Pages/Arduino/Control-Theory/Motor-Fader/) + +### Bank support + +All controls can be arranged in **banks**: for example, if you have only 4 +physical faders, you can make them bankable, so they can be used to control +the volume of many more different tracks. Changing banks can be done using push +buttons, rotary encoders, etc. +Apart from banks and bank selectors, you can also add **transposers** to change +the key of your notes, for example. + +### Extended input/output + +In order to save some IO pins, the library natively supports **multiplexers** +(e.g. 74HC4051 or 74HC4067) to read many switches or potentiometers, +**Shift Registers** (e.g. 74HC595) to drive many LEDs, **MAX7219 LED drivers**, +etc. + +→ [_Extended IO documentation_](https://tttapa.github.io/Control-Surface-doc/Doxygen/db/dd3/group__AH__ExtIO.html) + +### Audio + +If you are using a Teensy 3.x or 4.x, you can use it as a +**USB audio interface**. Just add an I²S DAC (e.g. PCM5102) and 5 lines of code, +and you can start playing audio through your Teensy, by combining Control +Surface with the Teensy Audio library. +You can also add volume controls and VU meters for these audio connections. + +→ [_Teensy Audio documentation_](https://tttapa.github.io/Control-Surface-doc/Doxygen/d3/d5c/group__Audio.html) + +### Modular and extensible + +Thanks to the structure of the library, you can easily add your own MIDI or +display elements, using some minimal, high level code. All low level stuff is +completely **reusable** (e.g. all MIDI operations, debouncing switches, +filtering analog inputs, and so on). ## Supported boards @@ -239,8 +266,10 @@ following boards: - Arduino Nano 33 BLE - Arduino Nano Every - Arduino UNO R4 Minima +- Arduino UNO R4 WiFi - ESP8266 - ESP32 +- ESP32-S3 - Raspberry Pi Pico This covers a very large part of the Arduino platform, and similar boards will @@ -251,29 +280,16 @@ If you have a board that's not supported, please [open an issue](https://github.com/tttapa/Control-Surface/issues/new) and let me know! -Note that MIDI over USB and MIDI over Bluetooth are not supported on all boards. -For MIDI over USB support, check out the [**MIDI over USB**](https://tttapa.github.io/Control-Surface-doc/Doxygen/d8/d4a/md_pages_MIDI-over-USB.html) -documentation page. As a general rule of thumb, if your board is supported by -the [MIDIUSB library](https://github.com/arduino-libraries/MIDIUSB) or if it's -a Teensy, MIDI over USB should be supported. -MIDI over BLE is currently only supported on ESP32. - - - -## Information for developers - -Information for people that would like to help improve the Control Surface -library can be found here: - -It covers installation instructions for developers, instructions for running the -tests and generating documentation, a style guide, etc. - - +Note that MIDI over USB and MIDI over Bluetooth are not supported on all boards. +See the [**MIDI over USB**](https://tttapa.github.io/Control-Surface-doc/Doxygen/d8/d4a/md_pages_MIDI-over-USB.html) +documentation page for a table with supported features per board. ## Change log and updating ### 2.x +- ([fae1933](https://github.com/tttapa/Control-Surface/commit/3ae51946d57e27f0e37d001bb8e0ce418fae1933)) + Completely refactored `BluetoothMIDI_Interface`, with support for the NimBLE and ArduinoBLE backends. - ([ba7f42e](https://github.com/tttapa/Control-Surface/commit/9c4cdd452990c470ee429b3121bdeb178ba7f42e)) More upper case constants and enumerators have been deprecated. For example, `ControlChange` should be used instead of `CONTROL_CHANGE`. If you continue diff --git a/doxygen/Doxyfile b/doxygen/Doxyfile index 41287b5c58..221506176c 100644 --- a/doxygen/Doxyfile +++ b/doxygen/Doxyfile @@ -280,7 +280,7 @@ TAB_SIZE = 4 # @} or use a double escape (\\{ and \\}) ALIASES = "boardsinfo=🛈" \ - "boards=@par Boards: @boardsinfo ^^  " + "boards=@par Boards: @boardsinfo ^^" # Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C sources # only. Doxygen will then generate output that is more tailored for C. For @@ -369,7 +369,7 @@ TOC_INCLUDE_HEADINGS = 3 # The default value is: DOXYGEN. # This tag requires that the tag MARKDOWN_SUPPORT is set to YES. -MARKDOWN_ID_STYLE = DOXYGEN +MARKDOWN_ID_STYLE = GITHUB # When enabled doxygen tries to link words that correspond to documented # classes, or namespaces to their corresponding documentation. Such a link can @@ -456,7 +456,7 @@ INLINE_GROUPED_CLASSES = NO # Man pages) or section (for LaTeX and RTF). # The default value is: NO. -INLINE_SIMPLE_STRUCTS = YES +INLINE_SIMPLE_STRUCTS = NO # When TYPEDEF_HIDES_STRUCT tag is enabled, a typedef of a struct, union, or # enum is documented as struct, union, or enum with the name of the typedef. So @@ -945,6 +945,7 @@ INPUT = ../examples/examples.dox \ pages/Getting-Started.md \ pages/Installation.md \ pages/MIDI-over-USB.md \ + pages/MIDI-over-BLE.md \ pages/MIDI.md \ pages/Debug.md \ pages/Basics.md \ @@ -1011,6 +1012,7 @@ RECURSIVE = YES EXCLUDE = ../src/AH/STL \ ../src/MIDI_Interfaces/BLEMIDI/ESP32/README.md \ + ../src/MIDI_Interfaces/BLEMIDI/ESP32-NimBLE/README.md \ ../src/Submodules/Encoder/README.md # The EXCLUDE_SYMLINKS tag can be used to select whether or not files or diff --git a/doxygen/custom_stylesheet.css b/doxygen/custom_stylesheet.css index 1aa156b5ba..9ebf8ff417 100644 --- a/doxygen/custom_stylesheet.css +++ b/doxygen/custom_stylesheet.css @@ -35,6 +35,14 @@ html.dark-mode { --header-background-color: var(--nav-background-color); --search-box-shadow: inset 0.5px 0.5px 3px 0px #444a6d; --footer-logo-width: 80px; + + --warning-color-text: white; + --note-color-text: white; + --todo-color-text: white; + --test-color-text: white; + --deprecated-color-text: white; + --bug-color-text: white; + --invariant-color-text: white; } html.dark-mode img.footer { filter: brightness(0.666); @@ -196,6 +204,10 @@ b, dt, caption, div.groupHeader, td.indexkey, th.dirtab, a.el { font-weight: 500; } +/* Paragraph indentation */ +dl.section dd, dl.bug dd, dl.deprecated dd, dl.todo dd, dl.test dd { + margin-inline-start: 1.6em; +} /* Size of titles and headings */ div.contents > div.textblock > h1 { border-bottom: 1px solid #c8d0e2; diff --git a/doxygen/pages/MIDI-over-BLE.md b/doxygen/pages/MIDI-over-BLE.md new file mode 100644 index 0000000000..ae3facd34e --- /dev/null +++ b/doxygen/pages/MIDI-over-BLE.md @@ -0,0 +1,82 @@ +# MIDI over BLE {#md_pages_MIDI-over-BLE} +Control Surface has different MIDI over BLE backends for different +Arduino-compatible boards. + +To use MIDI over Bluetooth Low Energy in your code, you usually don't have to +worry about these backends, and you can simply instantiate a +@ref BluetoothMIDI_Interface. For an example, see @ref BLEMIDI-Adapter.ino. + +## BLE Backends + +A MIDI over BLE backend handles the low-level BLE communication, from setting +up the BLE stack and configuring the GATT characteristics to sending and +receiving MIDI messages over BLE. The backend is used by the +@ref GenericBLEMIDI_Interface class which provides the actual high-level +@ref MIDI_Interface API. The @ref BluetoothMIDI_Interface class uses the default +backend for the particular board. + +| Board | Default backend | Other supported backends | +|:------------------------------------|:---------------------------|:-------------------------| +| Arduino Nano 33 IoT | @ref ArduinoBLEBackend | | +| Arduino Nano RP2040 | @ref ArduinoBLEBackend | | +| Arduino Nano ESP32 | @ref ESP32BluedroidBackend | @ref ESP32NimBLEBackend | +| Arduino Nano 33 BLE | @ref ArduinoBLEBackend | | +| Arduino MKR 1010 WiFi | @ref ArduinoBLEBackend | | +| Arduino UNO R4 WiFi | @ref ArduinoBLEBackend | | +| Arduino GIGA R1 WiFi | @ref ArduinoBLEBackend | | +| Raspberry Pi Pico W (RP2040) | @ref BTstackBackgroundBackend | | +| ESP32 | @ref ESP32BluedroidBackend | @ref ESP32NimBLEBackend | +| ESP32-S3 | @ref ESP32BluedroidBackend | @ref ESP32NimBLEBackend | +| ESP32-C3, ESP32-C6, ESP32-H2 | @ref ESP32BluedroidBackend | @ref ESP32NimBLEBackend | + +### ArduinoBLEBackend + +Uses the [ArduinoBLE](https://github.com/arduino-libraries/ArduinoBLE) library, +and should support any boards supported by that library. + +This is a polling backend: you should call @ref GenericBLEMIDI_Interface::update +regularly to ensure proper operation. + +Because of limitations in the ArduinoBLE library, sending large packets is not +possible. The default MTU is set to 23, with no way to negotiate a larger MTU, +or even query the current MTU. This leaves only 20 bytes of MIDI data per packet, +so the throughput is quite poor. + +### ESP32BluedroidBackend + +This is the default backend for ESP32 boards. It uses the ESP-IDF [Bluedroid](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/bluetooth/bt_le.html) +stack. + +The Bluetooth stack runs in its own task, and events are delivered +asynchronously. The sending of messages also happens asynchronously. Calling +@ref GenericBLEMIDI_Interface::update is therefore less critical compared to the +ArduinoBLEBackend, but should still be done regularly to avoid the receiving +buffer from filling up, since there is no BLE flow control. + +### ESP32NimBLEBackend + +This backend uses the newer [Apache MyNewt NimBLE](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/bluetooth/nimble/index.html) +stack. + +Like the @ref ESP32BluedroidBackend, the NimBLE backend uses its own threads and +does not require polling. + +The NimBLE ESP-IDF component is disabled by default in the [arduino-esp32](https://github.com/espressif/arduino-esp32) +core, so you'll need to install the [h2zero/NimBLE-Arduino](https://github.com/h2zero/NimBLE-Arduino) +library in order to use this backend. + +Defining @ref CS_USE_NIMBLE before including Control Surface changes the default +backend for ESP32 to the @ref ESP32NimBLEBackend. + +### BTstackBackgroundBackend + +This backend uses the [BTstack](https://github.com/bluekitchen/btstack) stack +that ships with the [pico-sdk](https://github.com/raspberrypi/pico-sdk). + +All Bluetooth events are handled asynchronously (using the background +[`async_context`](https://www.raspberrypi.com/documentation/pico-sdk/networking.html#ga092b97e879be5b9aa9121abda23e1337)). +Currently, buffering outgoing MIDI messages still requires polling, although +this could be replaced by a BTstack timer in the future. + +To use the @ref BluetoothMIDI_Interface on the Pico W, you have to enable the +Bluetooth stack in the _Tools > IP/Bluetooth Stack_ menu in the IDE: diff --git a/doxygen/pages/MIDI-over-USB.md b/doxygen/pages/MIDI-over-USB.md index 73e526e4ca..11cfd7df5a 100644 --- a/doxygen/pages/MIDI-over-USB.md +++ b/doxygen/pages/MIDI-over-USB.md @@ -10,25 +10,26 @@ There are some differences in MIDI over USB implementation between different typ |:------------------------------------|:---:|:---:|:---:|:---:| | Arduino UNO R3 | ✅ |  ❌⁽¹⁾ | ❌ | ❌ | | Arduino Nano | ✅ | ❌ | ❌ | ❌ | -| Arduino Nano 33 IoT | ✅ | 🔼 | ❓ | ❌ | -| Arduino Nano RP2040 | ✅ | 🔼 | ❓ | ❓ | +| Arduino Nano 33 IoT | ✅ | 🔼 | ❓ | 🔼 | +| Arduino Nano RP2040 | ✅ | 🔼 | ❓ | 🔼 | | Arduino Nano ESP32 | ✅ | 🔼 | ❓ | 🔼 | -| Arduino Nano 33 BLE | ✅ | ✅ |  ❌⁽⁴⁾ | ❓ | +| Arduino Nano 33 BLE | ✅ | ✅ |  ❌⁽⁴⁾ | ✅ | | Arduino Nano Every | ✅ |  ❌⁽²⁾ | ❌ | ❌ | -| Arduino MKR 1000 | ✅ | 🔼 | ❓ | ❓ | +| Arduino MKR 1000 | ✅ | 🔼 | ❓ | ❌ | +| Arduino MKR 1010 WiFi | ✅ | 🔼 | ❓ | 🔼 | | Arduino UNO R4 Minima | ✅ |  ❌⁽³⁾ |  ❌⁽⁴⁾ | ❌ | -| Arduino UNO R4 WiFi | ✅ |  ❌⁽³⁾ |  ❌⁽⁴⁾ | ❓ | +| Arduino UNO R4 WiFi | ✅ |  ❌⁽³⁾ |  ❌⁽⁴⁾ | 🔼 | | Arduino Leonardo | ✅ | ✅ | ❌ | ❌ | | Arduino Micro | ✅ | ✅ | ❌ | ❌ | | Arduino Zero | ✅ | 🔼 | ❓ | ❌ | | Arduino Mega 2560 | ✅ |  ❌⁽¹⁾ | ❌ | ❌ | | Arduino Due | ✅ | ✅ | ❓ | ❌ | -| Arduino GIGA R1 WiFi | ✅ | ❓ |  ❌⁽⁴⁾ | ❌ | +| Arduino GIGA R1 WiFi | ✅ | ❓ |  ❌⁽⁴⁾ | 🔼 | | Teensy 2.0, Teensy++ 2.0 | ✅ | 🔼 | ❌ | ❌ | | Teensy LC, 3.0, 3.1, 3.2, 3.5, 4.0 | ✅ | ✅ | ❓ | ❌ | | Teensy 3.6, 4.1 | ✅ | ✅ | ✅ | ❌ | | Raspberry Pi Pico (RP2040) | ✅ | ✅ | ❓ | ❌ | -| Raspberry Pi Pico W (RP2040) | ✅ | ✅ | ❓ | ❓ | +| Raspberry Pi Pico W (RP2040) | ✅ | ✅ | ❓ | ✅ | | ESP8266 | ✅ | ❌ | ❌ | ❌ | | ESP32 | ✅ | ❌ | ❌ | ✅ | | ESP32-S2 | ✅ | 🔼 | ❓ | ❌ | @@ -39,6 +40,8 @@ There are some differences in MIDI over USB implementation between different typ (2) Secondary microcontroller could _in theory_ be flashed with custom MIDI firmware. (3) Hardware supports it, but the Arduino core explicitly disables MIDI over USB support by setting `CFG_TUD_MIDI=0` in [their TinyUSB config](https://github.com/arduino/ArduinoCore-renesas/blob/6ee152ff2a9c00c8ab2ccff4f1eaee7e1f3388c1/variants/MINIMA/tusb_config.h#L81). (4) Hardware supports it, but the Arduino core does not support it. + +See the @ref md_pages_MIDI-over-BLE page for more information about Bluetooth Low Energy support. ## Arduino boards with native USB support _Arduino Due, Arduino Leonardo, Arduino Micro, Arduino Nano 33 IOT, Arduino Zero, Arduino MKR Zero, Arduino MKR1000 ..._ diff --git a/doxygen/pages/MIDI.md b/doxygen/pages/MIDI.md index ed2bb0fa77..1b2163887a 100644 --- a/doxygen/pages/MIDI.md +++ b/doxygen/pages/MIDI.md @@ -28,19 +28,22 @@ The interfaces you're most likely to use are: from the Serial Monitor. - @ref HardwareSerialMIDI_Interface : sends and receives MIDI over the TX and RX pins of the Arduino, can be used with standard 5-pin DIN MIDI. +- @ref BluetoothMIDI_Interface : makes the Arduino advertise itself as a + Bluetooth Low Energy (BLE) MIDI peripheral, allowing you to send and receive + MIDI messages wirelessly. -Other available interfaces are @ref BluetoothMIDI_Interface, -@ref HairlessMIDI_Interface, @ref SoftwareSerialMIDI_Interface, -@ref USBHostMIDI_Interface ... +Other available interfaces are @ref HairlessMIDI_Interface, +@ref SoftwareSerialMIDI_Interface, @ref USBHostMIDI_Interface ... ### Supported Arduino-compatible boards {#midi_md-interfaces-supported-boards} Not all MIDI interfaces are supported on all Arduino boards. For example, not all Arduino boards support MIDI over USB natively. You can find an overview of boards that do support it on the @ref md_pages_MIDI-over-USB page. +USB Host MIDI is currently only supported on the Teensy 3.6 and 4.1 boards. -MIDI over BLE is currently only supported on the ESP32. -USB Host MIDI is only supported on the Teensy 3.6 and 4.1 boards. +MIDI over BLE is supported on the ESP32 and on boards supported by the +ArduinoBLE library. More information can be found on the @ref md_pages_MIDI-over-BLE page. ### Functionality {#midi_md-interfaces-functionality} @@ -50,9 +53,9 @@ whatever device is connected on the other side of the interface. - **Receiving MIDI**: MIDI messages sent by the device on the other side can be read and inspected in your code, or you can register a callback that gets called whenever a message arrives through the interface. -- **Routing MIDI**: %Interfaces can be set up such that MIDI messages that arrive -on one interface are automatically routed to other interfaces, and you can -filter or modify the messages in between. +- **Routing MIDI**: %Interfaces can be configured to automatically route MIDI +messages from one interface to others, allowing you to filter or modify the +messages as they travel between interfaces. In the remainder of this tutorial, one section will be devoted to each of these functions. @@ -804,14 +807,15 @@ USBDebugMIDI_Interface midi_dbg; BidirectionalMIDI_Pipe pipes; void setup() { - // Manually route MIDI output from Control_Surface to the MIDI interface, - // and the MIDI output from the MIDI interface to Control_Surface + // Manually route MIDI input from the debug interface to the USB interface, + // and the MIDI input from the USB interface to the debug interface midi_dbg | pipes | midi_usb; // Initialize the MIDI interfaces MIDI_Interface::beginAll(); } void loop() { + // Continuously poll all interfaces and route the traffic between them MIDI_Interface::updateAll(); } ``` @@ -882,3 +886,5 @@ functions. See @ref MIDI_Pipes-Filter.ino for more details. - @ref Dual-MIDI-Interface.ino - @ref MIDI-Monitor.ino - @ref MIDI_Pipes-Filter.ino +- @ref USBMIDI-Adapter.ino +- @ref BLEMIDI-Adapter.ino diff --git a/examples/3. MIDI Interfaces/BLEMIDI-Adapter/BLEMIDI-Adapter.ino b/examples/3. MIDI Interfaces/BLEMIDI-Adapter/BLEMIDI-Adapter.ino new file mode 100644 index 0000000000..e40ec80e73 --- /dev/null +++ b/examples/3. MIDI Interfaces/BLEMIDI-Adapter/BLEMIDI-Adapter.ino @@ -0,0 +1,74 @@ +/** + * Turns an Arduino into a Bluetooth Low Energy (BLE) to 5-pin DIN MIDI adapter. + * + * @boards Nano 33 IoT, Nano 33 BLE, ESP32, ESP32-S3, Pi Pico W + * + * Configuration + * ------------- + * + * - If you're using a Pi Pico W, you'll have to enable the Bluetooth stack + * from the _Tools > IP/Bluetooth Stack_ menu in the IDE. + * - If you're using an ESP32, you can optionally switch to the NimBLE backend + * by installing the [h2zero/NimBLE-Arduino](https://github.com/h2zero/NimBLE-Arduino) + * library, and adding `#define CS_USE_NIMBLE` at the top of this sketch. + * - If you're not using a Pico or an ESP32, you'll have to install the + * [ArduinoBLE](https://github.com/arduino-libraries/ArduinoBLE) library. + * + * Connections + * ----------- + * + * - TXD: connected to a MIDI 5-pin DIN output connector + * (with series resistor, possibly through a buffer) + * - RXD: connected to a MIDI 5-pin DIN input connector + * (with an optocoupler) + * + * See https://midi.org/specifications/midi-transports-specifications/5-pin-din-electrical-specs + * for the schematic, optocoupler models and resistor values. + * + * Behavior + * -------- + * + * - The Arduino will advertise itself as a Bluetooth Low Energy MIDI device + * with the name `MIDI Adapter`. + * - When you connect to the Arduino using your phone or computer, the built-in + * LED turns on to indicate that the connection was successful. + * - Any MIDI messages sent to the Arduino over BLE are sent out to the 5-pin + * DIN output connector. + * - Any MIDI messages sent to the Arduino through the 5-pin DIN input connector + * are sent over BLE. + * + * @see @ref md_pages_MIDI-over-BLE + * @see @ref midi-tutorial + * + * Written by PieterP, 2024-01-21 + * https://github.com/tttapa/Control-Surface + */ + +#include +#include + +// Instantiate a MIDI over BLE interface +BluetoothMIDI_Interface midi_ble; +// Instantiate a 5-pin DIN MIDI interface (on the TX and RX pins of Serial1) +HardwareSerialMIDI_Interface midi_ser {Serial1}; +// Instantiate the pipe to connect the two interfaces +BidirectionalMIDI_Pipe pipes; + +void setup() { + // Change the name of the BLE device (must be done before initializing it) + midi_ble.setName("MIDI Adapter"); + // Manually route MIDI input from the serial interface to the BLE interface, + // and the MIDI input from the BLE interface to the serial interface + midi_ser | pipes | midi_ble; + // Initialize the MIDI interfaces + MIDI_Interface::beginAll(); + // Initialize the built-in LED + pinMode(LED_BUILTIN, OUTPUT); +} + +void loop() { + // Continuously poll all interfaces and route the traffic between them + MIDI_Interface::updateAll(); + // Display the connection status using the built-in LED + digitalWrite(LED_BUILTIN, midi_ble.isConnected() ? HIGH : LOW); +} diff --git a/examples/3. MIDI Interfaces/USBMIDI-Adapter/USBMIDI-Adapter.ino b/examples/3. MIDI Interfaces/USBMIDI-Adapter/USBMIDI-Adapter.ino new file mode 100644 index 0000000000..3b14033342 --- /dev/null +++ b/examples/3. MIDI Interfaces/USBMIDI-Adapter/USBMIDI-Adapter.ino @@ -0,0 +1,53 @@ +/** + * Turns an Arduino into a USB to 5-pin DIN MIDI adapter. + * + * @boards AVR USB, Due, Nano 33 IoT, Nano 33 BLE, Pi Pico, ESP32-S3, Teensy 3.x + * + * Connections + * ----------- + * + * - TXD: connected to a MIDI 5-pin DIN output connector + * (with series resistor, possibly through a buffer) + * - RXD: connected to a MIDI 5-pin DIN input connector + * (with an optocoupler) + * + * See https://midi.org/specifications/midi-transports-specifications/5-pin-din-electrical-specs + * for the schematic, optocoupler models and resistor values. + * + * Behavior + * -------- + * + * - The Arduino will show up as a USB MIDI device. + * - Any MIDI messages sent to the Arduino over USB are sent out to the 5-pin + * DIN output connector. + * - Any MIDI messages sent to the Arduino through the 5-pin DIN input connector + * are sent over USB. + * + * @see @ref md_pages_MIDI-over-USB + * @see @ref midi-tutorial + * + * Written by PieterP, 2024-01-21 + * https://github.com/tttapa/Control-Surface + */ + +#include + +// Instantiate a MIDI over USB interface +USBMIDI_Interface midi_usb; +// Instantiate a 5-pin DIN MIDI interface (on the TX and RX pins of Serial1) +HardwareSerialMIDI_Interface midi_ser {Serial1}; +// Instantiate the pipe to connect the two interfaces +BidirectionalMIDI_Pipe pipes; + +void setup() { + // Manually route MIDI input from the serial interface to the USB interface, + // and the MIDI input from the USB interface to the serial interface + midi_ser | pipes | midi_usb; + // Initialize the MIDI interfaces + MIDI_Interface::beginAll(); +} + +void loop() { + // Continuously poll all interfaces and route the traffic between them + MIDI_Interface::updateAll(); +} diff --git a/examples/examples.dox b/examples/examples.dox index f04d50035a..fb0d29c822 100644 --- a/examples/examples.dox +++ b/examples/examples.dox @@ -1647,6 +1647,94 @@ * https://github.com/tttapa/Control-Surface */ +/** + * @example "USBMIDI-Adapter.ino" + * + * USBMIDI-Adapter + * =============== + * + * Turns an Arduino into a USB to 5-pin DIN MIDI adapter. + * + * @boards AVR USB, Due, Nano 33 IoT, Nano 33 BLE, Pi Pico, ESP32-S3, Teensy 3.x + * + * Connections + * ----------- + * + * - TXD: connected to a MIDI 5-pin DIN output connector + * (with series resistor, possibly through a buffer) + * - RXD: connected to a MIDI 5-pin DIN input connector + * (with an optocoupler) + * + * See https://midi.org/specifications/midi-transports-specifications/5-pin-din-electrical-specs + * for the schematic, optocoupler models and resistor values. + * + * Behavior + * -------- + * + * - The Arduino will show up as a USB MIDI device. + * - Any MIDI messages sent to the Arduino over USB are sent out to the 5-pin + * DIN output connector. + * - Any MIDI messages sent to the Arduino through the 5-pin DIN input connector + * are sent over USB. + * + * @see @ref md_pages_MIDI-over-USB + * @see @ref midi-tutorial + * + * Written by PieterP, 2024-01-21 + * https://github.com/tttapa/Control-Surface + */ + +/** + * @example "BLEMIDI-Adapter.ino" + * + * BLEMIDI-Adapter + * =============== + * + * Turns an Arduino into a Bluetooth Low Energy (BLE) to 5-pin DIN MIDI adapter. + * + * @boards Nano 33 IoT, Nano 33 BLE, ESP32, ESP32-S3, Pi Pico W + * + * Configuration + * ------------- + * + * - If you're using a Pi Pico W, you'll have to enable the Bluetooth stack + * from the _Tools > IP/Bluetooth Stack_ menu in the IDE. + * - If you're using an ESP32, you can optionally switch to the NimBLE backend + * by installing the [h2zero/NimBLE-Arduino](https://github.com/h2zero/NimBLE-Arduino) + * library, and adding `#define CS_USE_NIMBLE` at the top of this sketch. + * - If you're not using a Pico or an ESP32, you'll have to install the + * [ArduinoBLE](https://github.com/arduino-libraries/ArduinoBLE) library. + * + * Connections + * ----------- + * + * - TXD: connected to a MIDI 5-pin DIN output connector + * (with series resistor, possibly through a buffer) + * - RXD: connected to a MIDI 5-pin DIN input connector + * (with an optocoupler) + * + * See https://midi.org/specifications/midi-transports-specifications/5-pin-din-electrical-specs + * for the schematic, optocoupler models and resistor values. + * + * Behavior + * -------- + * + * - The Arduino will advertise itself as a Bluetooth Low Energy MIDI device + * with the name `MIDI Adapter`. + * - When you connect to the Arduino using your phone or computer, the built-in + * LED turns on to indicate that the connection was successful. + * - Any MIDI messages sent to the Arduino over BLE are sent out to the 5-pin + * DIN output connector. + * - Any MIDI messages sent to the Arduino through the 5-pin DIN input connector + * are sent over BLE. + * + * @see @ref md_pages_MIDI-over-BLE + * @see @ref midi-tutorial + * + * Written by PieterP, 2024-01-21 + * https://github.com/tttapa/Control-Surface + */ + /** * @example "Bank.ino" * diff --git a/src/MIDI_Interfaces/BLEMIDI/ArduinoBLE/midi.hpp b/src/MIDI_Interfaces/BLEMIDI/ArduinoBLE/midi.hpp new file mode 100644 index 0000000000..be1fe1e063 --- /dev/null +++ b/src/MIDI_Interfaces/BLEMIDI/ArduinoBLE/midi.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include + +BEGIN_CS_NAMESPACE + +namespace arduino_ble_midi { + +bool init(MIDIBLEInstance &instance, BLESettings ble_settings); +void poll(); +void notify(BLEDataView data); + +} // namespace arduino_ble_midi + +END_CS_NAMESPACE + +// We cannot do this in a separate .cpp file, because the user might not have +// the ArduinoBLE library installed, and the Arduino library dependency scanner +// does not support __has_include. +#include "midi.ipp" diff --git a/src/MIDI_Interfaces/BLEMIDI/ArduinoBLE/midi.ipp b/src/MIDI_Interfaces/BLEMIDI/ArduinoBLE/midi.ipp new file mode 100644 index 0000000000..236e95d2d5 --- /dev/null +++ b/src/MIDI_Interfaces/BLEMIDI/ArduinoBLE/midi.ipp @@ -0,0 +1,121 @@ +#include + +#include +#include + +#include + +BEGIN_CS_NAMESPACE + +namespace arduino_ble_midi { + +namespace { + +BLEService midi_service { + "03B80E5A-EDE8-4B33-A751-6CE34EC4C700", +}; +BLECharacteristic midi_char { + "7772E5DB-3868-4112-A1A9-F2669D106BF3", + BLEWriteWithoutResponse | BLERead | BLENotify, + 512, + false, +}; +MIDIBLEInstance *midi_instance = nullptr; + +bool is_midi_char(const BLECharacteristic &characteristic) { + return strcasecmp(midi_char.uuid(), characteristic.uuid()) == 0; +} + +// Here I assume that all callbacks and handlers execute in the same task/thread +// as the main program. + +void on_connect([[maybe_unused]] BLEDevice central) { + DEBUGREF("CS-BLEMIDI connected, central: " << central.address()); + if (midi_instance) { + midi_instance->handleConnect(BLEConnectionHandle {0}); + midi_instance->handleSubscribe(BLEConnectionHandle {0}, + BLECharacteristicHandle {0}, true); + } +} + +void on_disconnect([[maybe_unused]] BLEDevice central) { + DEBUGREF("CS-BLEMIDI disconnected, central: " << central.address()); + if (midi_instance) + midi_instance->handleDisconnect(BLEConnectionHandle {}); +} + +void on_write([[maybe_unused]] BLEDevice central, + BLECharacteristic characteristic) { + DEBUGREF( + "CS-BLEMIDI write, central: " + << central.address() << ", char: " << characteristic.uuid() + << ", data: [" << characteristic.valueLength() << "] " + << AH::HexDump(characteristic.value(), characteristic.valueLength())); + if (!is_midi_char(characteristic)) + return; + if (!midi_instance) + return; + BLEDataView data {characteristic.value(), + static_cast(characteristic.valueLength())}; + auto data_gen = [data {data}]() mutable { return std::exchange(data, {}); }; + midi_instance->handleData( + BLEConnectionHandle {0}, + BLEDataGenerator {compat::in_place, std::move(data_gen)}, + BLEDataLifetime::ConsumeImmediately); +} + +void on_read([[maybe_unused]] BLEDevice central, + BLECharacteristic characteristic) { + DEBUGREF("CS-BLEMIDI read, central: " << central.address() << ", char: " + << characteristic.uuid()); + if (!is_midi_char(characteristic)) + return; + characteristic.setValue(nullptr, 0); +} + +} // namespace + +inline bool init(MIDIBLEInstance &instance, BLESettings ble_settings) { + midi_instance = &instance; + // Initialize the BLE hardware + if (!BLE.begin()) { + ERROR(F("Starting Bluetooth® Low Energy module failed!"), 0x7532); + return false; + } + + // Set the local name and advertise the MIDI service + BLE.setLocalName(ble_settings.device_name); + BLE.setAdvertisedService(midi_service); + // Note: advertising connection interval range not supported by ArduinoBLE + + // Configure the MIDI service and characteristic + midi_service.addCharacteristic(midi_char); + BLE.addService(midi_service); + + // Assign event handlers + BLE.setEventHandler(BLEConnected, on_connect); + BLE.setEventHandler(BLEDisconnected, on_disconnect); + midi_char.setEventHandler(BLEWritten, on_write); + midi_char.setEventHandler(BLERead, on_read); + + // Start advertising + BLE.advertise(); + + return true; +} + +inline void poll() { + // poll for Bluetooth® Low Energy events + BLE.poll(); +} + +// TODO: there is currently no way in ArduinoBLE to request the MTU. So we +// assume the tiny default of 23 bytes. + +inline void notify(BLEDataView data) { + midi_char.writeValue(data.data, data.length); +} + +} // namespace arduino_ble_midi + +END_CS_NAMESPACE diff --git a/src/MIDI_Interfaces/BLEMIDI/ArduinoBLEBackend.hpp b/src/MIDI_Interfaces/BLEMIDI/ArduinoBLEBackend.hpp new file mode 100644 index 0000000000..a69553073f --- /dev/null +++ b/src/MIDI_Interfaces/BLEMIDI/ArduinoBLEBackend.hpp @@ -0,0 +1,97 @@ +#pragma once + +#include + +#include "ArduinoBLE/midi.hpp" +#include "BLEAPI.hpp" +#include "BufferedBLEMIDIParser.hpp" +#include "PollingBLEMIDISender.hpp" + +BEGIN_CS_NAMESPACE + +/// ArduinoBLE backend intended to be plugged into +/// @ref GenericBLEMIDI_Interface. +class ArduinoBLEBackend : private PollingBLEMIDISender, + private MIDIBLEInstance { + private: + // Callbacks from the ArduinoBLE stack. + void handleConnect(BLEConnectionHandle) override { connected = true; } + void handleDisconnect(BLEConnectionHandle) override { + connected = subscribed = false; + } + void handleMTU(BLEConnectionHandle, uint16_t mtu) override { + Sender::updateMTU(mtu); + } + void handleSubscribe(BLEConnectionHandle, BLECharacteristicHandle, + bool notify) override { + subscribed = notify; + } + void handleData(BLEConnectionHandle, BLEDataGenerator &&data, + BLEDataLifetime) override { + while (true) { + BLEDataView packet = data(); + if (packet.length == 0) { + break; + } else if (!parser.pushPacket(packet)) { + DEBUGREF(F("BLE packet dropped, size: ") << packet.length); + break; + } + } + } + + private: + /// Are we connected to a BLE Central? + bool connected = false; + /// Did the BLE Central subscribe to be notified for the MIDI characteristic? + bool subscribed = false; + /// Contains incoming BLE MIDI data to be parsed. + BufferedBLEMIDIParser<1024> parser; + + public: + using IncomingMIDIMessage = AnyMIDIMessage; + + /// Retrieve and remove a single incoming MIDI message from the buffer. + bool popMessage(IncomingMIDIMessage &incomingMessage) { + // This function is assumed to be polled regularly by the higher-level + // MIDI_Interface, so we check the sender's timer here, and we poll + // the ArduinoBLE library. + auto lck = Sender::acquirePacket(); + Sender::releasePacketAndNotify(lck); + arduino_ble_midi::poll(); + // Actually get a MIDI message from the buffer + return parser.popMessage(incomingMessage); + } + + public: + /// Initialize the BLE stack etc. + void begin(BLESettings ble_settings) { + arduino_ble_midi::init(*this, ble_settings); + Sender::begin(); + } + /// Deinitialize the BLE stack. + /// @todo Not yet implemented. + void end() {} + /// Returns true if we are connected to a BLE Central device. + bool isConnected() const { return connected; } + + private: + // Implement the interface for the BLE sender. + using Sender = PollingBLEMIDISender; + friend Sender; + /// Send the given MIDI BLE packet. + void sendData(BLEDataView data) { + if (connected && subscribed) + arduino_ble_midi::notify(data); + } + + public: + // Expose the necessary BLE sender functions. + using Sender::acquirePacket; + using Sender::forceMinMTU; + using Sender::getMinMTU; + using Sender::releasePacketAndNotify; + using Sender::sendNow; + using Sender::setTimeout; +}; + +END_CS_NAMESPACE \ No newline at end of file diff --git a/src/MIDI_Interfaces/BLEMIDI/BLEAPI.hpp b/src/MIDI_Interfaces/BLEMIDI/BLEAPI.hpp new file mode 100644 index 0000000000..501f1fcdfc --- /dev/null +++ b/src/MIDI_Interfaces/BLEMIDI/BLEAPI.hpp @@ -0,0 +1,163 @@ +#pragma once + +/** + * @file + * Type definitions and callback interfaces for communication between the + * low-level BLE stacks and higher-level MIDI BLE backends. + */ + +#include + +#include "Util/compat.hpp" + +#include +#include + +BEGIN_CS_NAMESPACE + +/// Represents a handle to the connection to another device. +struct BLEConnectionHandle { + uint16_t conn = 0xFFFF; + explicit operator bool() const { return conn != 0xFFFF; } + +#if __cplusplus < 201402L + BLEConnectionHandle() = default; + BLEConnectionHandle(uint16_t conn) : conn {conn} {} +#endif +}; + +/// Represents a handle to a local GATT characteristic. +struct BLECharacteristicHandle { + uint16_t characteristic = 0xFFFF; + explicit operator bool() const { return characteristic != 0xFFFF; } + +#if __cplusplus < 201402L + BLECharacteristicHandle() = default; + BLECharacteristicHandle(uint16_t characteristic) + : characteristic {characteristic} {} +#endif +}; + +/// Non-owning, std::span-style read-only view of BLE data. +struct BLEDataView { + const uint8_t *data = nullptr; + uint16_t length = 0; + explicit operator bool() const { return length > 0; } + +#if __cplusplus < 201402L + BLEDataView() = default; + BLEDataView(const uint8_t *data, uint16_t length) + : data {data}, length {length} {} +#endif +}; + +/// Describes a byte buffer containing (part of) a BLE packet. +/// Packets can be stored across multiple buffers, in which case the first +/// first buffer has type `Packet` and subsequent buffers of the same packet +/// have the type `Continuation`. +enum class BLEDataType : uint8_t { + None = 0, ///< No buffers available. + Packet, ///< Buffer contains the start of a BLE packet. + Continuation, ///< Buffer contains a chunk of a BLE packet. +}; + +/// Callable that returns the next chunk of data from a BLE packet when called. +/// Uses type erasure with a static buffer (no dynamic memory allocations). +class BLEDataGenerator { + public: + /// Get the next chunk of data from the BLE packet. + /// Returns a chunk of size zero to indicate completion. + /// @pre This wrapper is not empty. + /// @pre There is still data available. Calling this function again after + /// the previous call returned an empty chunk is not allowed. + BLEDataView operator()(); + /// Release the resources of the underlying data generator. + void clear(); + /// Check if this wrapper contains an underlying data generator. + explicit operator bool() const { return instance; } + + /// Create an empty BLEDataGenerator. + BLEDataGenerator() = default; + /// Store a callable of type @p T and initialize it by @p args. + template + BLEDataGenerator(compat::in_place_type_t, Args &&...args); + /// Store a callable of type @p T (with cv qualifiers and references + /// removed) and initialize it by forwarding @p t. + template + BLEDataGenerator(compat::in_place_t, T &&t); + BLEDataGenerator(const BLEDataGenerator &) = delete; + BLEDataGenerator &operator=(const BLEDataGenerator &) = delete; + BLEDataGenerator(BLEDataGenerator &&other) noexcept; + BLEDataGenerator &operator=(BLEDataGenerator &&other) noexcept; + ~BLEDataGenerator() { clear(); } + + private: + /// Type-erased interface. + struct Iface; + /// Specific class that implements the type-erased interface, wrapping the + /// type @p T. + template + struct Impl; + /// Alignment of the buffer to allocate the underlying data generator. + using buffer_align_t = max_align_t; + /// Size of the buffer to allocate the underlying data generator. + static constexpr size_t capacity = 4 * sizeof(void *) - sizeof(Iface *); + /// Buffer used for allocation of the underlying data generator. + alignas(buffer_align_t) compat::byte storage[capacity]; + //// Type-erased pointer to the underlying data generator in @ref storage. + Iface *instance = nullptr; +}; + +/// Should a buffer of BLEData be consumed immediately inside of the callback, +/// or can we hold on to it and process it later? +enum class BLEDataLifetime { + /// Buffer is valid only during the callback. Do not keep any pointers to it. + ConsumeImmediately, + /// Buffer is valid for as long as the owning @ref BLEDataGenerator is not + /// resumed or destroyed. + Managed, +}; + +/// Defines the interface for callback functions registered by the low-level +/// BLE code. +/// @warning These functions may be called from different tasks/threads or +/// low-priority interrupt handlers. You cannot take locks, and you +/// need to synchronize appropriately (e.g. using `std::atomic` or +/// by using critical sections). +class MIDIBLEInstance { + public: + virtual ~MIDIBLEInstance() = default; + /// Called by the BLE stack when a connection is established. + virtual void handleConnect(BLEConnectionHandle conn_handle) = 0; + /// Called by the BLE stack when a connection is terminated. + virtual void handleDisconnect(BLEConnectionHandle conn_handle) = 0; + /// Called by the BLE stack when the maximum transmission unit for the + /// connection changes. + virtual void handleMTU(BLEConnectionHandle conn_handle, uint16_t mtu) = 0; + /// Called by the BLE stack when the central subscribes to receive + /// notifications for the MIDI GATT characteristic. + virtual void handleSubscribe(BLEConnectionHandle conn_handle, + BLECharacteristicHandle char_handle, + bool notify) = 0; + /// Called by the BLE stack when the central writes data to the MIDI GATT + /// characteristic. + virtual void handleData(BLEConnectionHandle conn_handle, + BLEDataGenerator &&data, + BLEDataLifetime lifetime) = 0; +}; + +/// Configuration options for the low-level BLE code. +struct BLESettings { + /// Device name (used for advertising) + const char *device_name = "Control Surface MIDI"; + /// Connection intervals as multiples of 1.25 milliseconds + /// (e.g.0x000C = 15 ms). + struct { + uint16_t minimum = 0x000C; + uint16_t maximum = 0x000C; + } connection_interval {}; +}; + +END_CS_NAMESPACE + +#include "BLEAPI.ipp" diff --git a/src/MIDI_Interfaces/BLEMIDI/BLEAPI.ipp b/src/MIDI_Interfaces/BLEMIDI/BLEAPI.ipp new file mode 100644 index 0000000000..a459d2af1b --- /dev/null +++ b/src/MIDI_Interfaces/BLEMIDI/BLEAPI.ipp @@ -0,0 +1,87 @@ +#pragma once + +#include "BLEAPI.hpp" + +#include +#include +#include + +#include + +BEGIN_CS_NAMESPACE + +struct BLEDataGenerator::Iface { + virtual ~Iface() = default; + virtual BLEDataView next() = 0; + virtual Iface *move_into(void *storage) noexcept = 0; + + Iface(const Iface &) = delete; + Iface &operator=(const Iface &) = delete; + + protected: + Iface() = default; + Iface(Iface &&) = default; +}; + +template +struct BLEDataGenerator::Impl : Iface { + T inst; + template + Impl(Args &&...args) : inst {std::forward(args)...} {} +#if __cplusplus >= 201703L + BLEDataView next() override { return std::invoke(inst); } +#else + BLEDataView next() override { return inst(); } +#endif + Iface *move_into(void *storage) noexcept override { + return new (storage) Impl {std::move(*this)}; + } +}; + +inline BLEDataView BLEDataGenerator::operator()() { + assert(instance); + return instance->next(); +} + +inline void BLEDataGenerator::clear() { +#if __cplusplus >= 201402 + if (auto *inst = std::exchange(instance, nullptr)) + inst->~Iface(); +#else + if (instance) + instance->~Iface(); + instance = nullptr; +#endif +} + +template +BLEDataGenerator::BLEDataGenerator(compat::in_place_type_t, Args &&...args) { + static_assert(sizeof(Impl) <= sizeof(storage), ""); + static_assert(alignof(Impl) <= alignof(buffer_align_t), ""); + instance = new (storage) Impl {std::forward(args)...}; +} + +template +BLEDataGenerator::BLEDataGenerator(compat::in_place_t, T &&t) + : BLEDataGenerator( + compat::in_place_type::type>, + std::forward(t)) {} + +inline BLEDataGenerator::BLEDataGenerator(BLEDataGenerator &&other) noexcept { + if (other.instance) { + this->instance = other.instance->move_into(this->storage); + other.clear(); + } +} + +inline BLEDataGenerator & +BLEDataGenerator::operator=(BLEDataGenerator &&other) noexcept { + clear(); + if (other.instance) { + this->instance = other.instance->move_into(this->storage); + other.clear(); + } + return *this; +} + +END_CS_NAMESPACE \ No newline at end of file diff --git a/src/MIDI_Interfaces/BLEMIDI/BLERingBuf.hpp b/src/MIDI_Interfaces/BLEMIDI/BLERingBuf.hpp new file mode 100644 index 0000000000..c12cce66c1 --- /dev/null +++ b/src/MIDI_Interfaces/BLEMIDI/BLERingBuf.hpp @@ -0,0 +1,163 @@ +#pragma once + +#include +#include + +#include + +#include + +BEGIN_CS_NAMESPACE + +template +struct NonatomicBLERingBufSize { + T value; + /// Alignment for size, and read/write pointers to avoid false sharing. + constexpr static size_t alignment = alignof(T); + T load_acquire() const { return value; } + void add_release(T t) { value += t; } + void sub_release(T t) { value -= t; } +}; + +/// Circular FIFO buffer for buffering BLE packet data. It supports both +/// complete BLE packets and packets split over multiple chunks. Full packets +/// that are added to the FIFO might be split up over multiple chunks. +/// @tparam Capacity +/// Buffer size (bytes). Note that the actual maximum data size may be +/// up to 6 bytes less because of data structure overhead. +/// @tparam SizeT +/// The type to use for the size of tbe buffer. Should be atomic if this +/// buffer is to be used as a SPSC queue between two threads. +/// See @ref NonatomicBLERingBufSize for an example. +template > +class BLERingBuf { + private: + struct Header { + uint16_t size : 14; + uint8_t type : 2; + Header() = default; + Header(uint16_t size, BLEDataType type) + : size {size}, type {static_cast(type)} {} + BLEDataType getType() const { return static_cast(type); } + }; + constexpr static uint_fast16_t header_size = sizeof(Header); + static_assert(header_size == 2, ""); + + constexpr static uint_fast16_t capacity = Capacity; + unsigned char buffer[capacity]; + alignas(SizeT::alignment) uint_fast16_t read_p = 0; + alignas(SizeT::alignment) uint_fast16_t write_p = header_size; + alignas(SizeT::alignment) SizeT size {header_size}; + + constexpr static uint_fast16_t ceil_h(uint_fast16_t i) { + return ((i + header_size - 1) / header_size) * header_size; + } + + public: + BLERingBuf() { + Header header {0, BLEDataType::None}; + static_assert(capacity % header_size == 0, ""); + std::memcpy(buffer, &header, header_size); + } + + /// Copy the given data into the buffer. May be split up into two chunks, + /// in which case the type will be set to @ref BLEDataType::Continuation + /// for the second chunk. + /// @retval false The buffer is full, nothing added to the buffer. + bool push(BLEDataView data, BLEDataType type = BLEDataType::Packet) { + if (type == BLEDataType::None) + return false; + uint_fast16_t loc_size = size.load_acquire(); + uint_fast16_t add_size = 0; + assert(loc_size <= capacity); + assert(write_p < capacity); + assert(write_p % header_size == 0); + // We need to write at least one header + uint_fast16_t expected_req_size = loc_size + data.length + header_size; + if (expected_req_size > capacity) + return false; // not enough space + // Contiguous size remaining (excluding header) + uint_fast16_t contig_size = capacity - write_p - header_size; + assert(contig_size < capacity); + // We may need to write a second header + uint_fast16_t worst_case_req_size = expected_req_size + header_size; + if (data.length > contig_size && worst_case_req_size > capacity) + return false; // not enough space + // Write the first header for the packet + uint16_t size_1 = std::min(contig_size, data.length); + Header header {size_1, type}; + std::memcpy(buffer + write_p, &header, sizeof(header)); + write_p += header_size; + add_size += header_size; + // Write first data + if (size_1 > 0) // avoid memcpy with nullptr + std::memcpy(buffer + write_p, data.data, size_1); + write_p += ceil_h(size_1); + add_size += ceil_h(size_1); + if (write_p == capacity) + write_p = 0; + // Now write the remainder at the beginning of the circular buffer + uint16_t size_2 = data.length - size_1; + if (size_2 > 0) { + // Write the continuation header + Header header {size_2, BLEDataType::Continuation}; + std::memcpy(buffer + write_p, &header, sizeof(header)); + write_p += header_size; + add_size += header_size; + // Write the remainder of the data + std::memcpy(buffer + write_p, data.data + size_1, size_2); + write_p += ceil_h(size_2); + add_size += ceil_h(size_2); + } + size.add_release(add_size); + return true; + } + + /// Get a view to the next chunk of data. The view remains valid until the + /// next call to @ref pop. + /// @retval BLEDataType::None + /// No data available. + /// @retval BLEDataType::Packet + /// The @p data output parameter points to the first chunk of a + /// packet. + /// @retval BLEDataType::Continuation + /// The @p data output parameter points to a chunk of continuation + /// data of the same packet. + BLEDataType pop(BLEDataView &data) { + uint_fast16_t loc_size = size.load_acquire(); + assert(loc_size >= header_size); + assert(read_p < capacity); + assert(read_p % header_size == 0); + // Read the old header + Header old_header; + std::memcpy(&old_header, buffer + read_p, sizeof(old_header)); + // If the previous chunk is the only thing left in the buffer + if (loc_size - header_size == ceil_h(old_header.size)) { + // If there is still actual data left in the buffer + if (old_header.getType() != BLEDataType::None) { + // Free the old data, preserving space for a header + read_p += ceil_h(old_header.size); + size.sub_release(ceil_h(old_header.size)); + // Write an empty header + Header header {0, BLEDataType::None}; + std::memcpy(buffer + read_p, &header, sizeof(header)); + } + data = {nullptr, 0}; + return BLEDataType::None; + } else { + // Otherwise, discard the old data and header + read_p += header_size + ceil_h(old_header.size); + size.sub_release(header_size + ceil_h(old_header.size)); + if (read_p == capacity) + read_p = 0; + // Read the next header + Header header; + std::memcpy(&header, buffer + read_p, sizeof(header)); + data = {buffer + read_p + header_size, header.size}; + return header.getType(); + } + } +}; + +END_CS_NAMESPACE diff --git a/src/MIDI_Interfaces/BLEMIDI/BTstack/advertising.cpp b/src/MIDI_Interfaces/BLEMIDI/BTstack/advertising.cpp new file mode 100644 index 0000000000..5f7ea6c403 --- /dev/null +++ b/src/MIDI_Interfaces/BLEMIDI/BTstack/advertising.cpp @@ -0,0 +1,69 @@ +#if defined(ARDUINO_RASPBERRY_PI_PICO_W) && ENABLE_BLE + +#include + +#include "advertising.hpp" + +#include + +namespace cs::midi_ble_btstack { + +namespace { + +uint8_t adv_data[] { + // Flags general discoverable + 0x02, BLUETOOTH_DATA_TYPE_FLAGS, 0x06, + // Connection interval range + 0x05, BLUETOOTH_DATA_TYPE_SLAVE_CONNECTION_INTERVAL_RANGE, 0x0c, 0x00, 0x0c, + 0x00, + // Service UUID + 0x11, BLUETOOTH_DATA_TYPE_COMPLETE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS, + 0x00, 0xc7, 0xc4, 0x4e, 0xe3, 0x6c, 0x51, 0xa7, 0x33, 0x4b, 0xe8, 0xed, + 0x5a, 0x0e, 0xb8, 0x03}; +static_assert(sizeof(adv_data) <= LE_ADVERTISING_DATA_SIZE); +uint8_t adv_rsp_data[LE_ADVERTISING_DATA_SIZE] { + // Name header + 0x15, BLUETOOTH_DATA_TYPE_COMPLETE_LOCAL_NAME, + // Name value + 'C', 'o', 'n', 't', 'r', 'o', 'l', ' ', 'S', 'u', 'r', 'f', 'a', 'c', 'e', + ' ', 'M', 'I', 'D', 'I'}; +uint8_t adv_rsp_data_len() { return adv_rsp_data[0] + 1; } + +void set_adv_connection_interval(uint16_t min_itvl, uint16_t max_itvl) { + uint8_t *slave_itvl_range = adv_data + 5; + slave_itvl_range[0] = (min_itvl >> 0) & 0xFF; + slave_itvl_range[1] = (min_itvl >> 8) & 0xFF; + slave_itvl_range[2] = (max_itvl >> 0) & 0xFF; + slave_itvl_range[3] = (max_itvl >> 8) & 0xFF; +} + +void set_adv_name(const char *name) { + auto len = std::min(std::strlen(name), sizeof(adv_rsp_data) - 2); + uint8_t *adv_name_len = adv_rsp_data; + uint8_t *adv_name = adv_rsp_data + 2; + std::memcpy(adv_name, name, len); + *adv_name_len = static_cast(len + 1); +} + +} // namespace + +void le_midi_setup_adv(const BLESettings &ble_settings) { + set_adv_name(ble_settings.device_name); + set_adv_connection_interval(ble_settings.connection_interval.minimum, + ble_settings.connection_interval.maximum); + uint16_t adv_int_min = 0x0020; // 20 ms (multiple of 0.625ms) + uint16_t adv_int_max = 0x0040; // 40 ms (multiple of 0.625ms) + uint8_t adv_type = 0; + bd_addr_t null_addr {}; + uint8_t channel_map = 0x07; // All channels + uint8_t filter_policy = 0x00; // Allow any + gap_advertisements_set_params(adv_int_min, adv_int_max, adv_type, 0, + null_addr, channel_map, filter_policy); + gap_advertisements_set_data(sizeof(adv_data), adv_data); + gap_scan_response_set_data(adv_rsp_data_len(), adv_rsp_data); + gap_advertisements_enable(1); +} + +} // namespace cs::midi_ble_btstack + +#endif \ No newline at end of file diff --git a/src/MIDI_Interfaces/BLEMIDI/BTstack/advertising.hpp b/src/MIDI_Interfaces/BLEMIDI/BTstack/advertising.hpp new file mode 100644 index 0000000000..381327facc --- /dev/null +++ b/src/MIDI_Interfaces/BLEMIDI/BTstack/advertising.hpp @@ -0,0 +1,9 @@ +#pragma once + +#include "../BLEAPI.hpp" + +namespace cs::midi_ble_btstack { + +void le_midi_setup_adv(const BLESettings &ble_settings); + +} // namespace cs::midi_ble_btstack diff --git a/src/MIDI_Interfaces/BLEMIDI/BTstack/gatt_midi.cpp b/src/MIDI_Interfaces/BLEMIDI/BTstack/gatt_midi.cpp new file mode 100644 index 0000000000..6bdf0360ad --- /dev/null +++ b/src/MIDI_Interfaces/BLEMIDI/BTstack/gatt_midi.cpp @@ -0,0 +1,290 @@ +#if defined(ARDUINO_RASPBERRY_PI_PICO_W) && ENABLE_BLE + +#define BTSTACK_FILE__ "gatt_midi.cpp" + +#include +#include +#include +#include +#include +#include + +#include + +#include "../BLEAPI.hpp" +#include "advertising.hpp" +#include "gatt_midi.h" +#include "hci_event_names.hpp" + +#include +#include + +namespace cs::midi_ble_btstack { + +namespace { + +constexpr uint16_t midi_char_value_handle = + ATT_CHARACTERISTIC_7772E5DB_3868_4112_A1A9_F2669D106BF3_01_VALUE_HANDLE; +constexpr uint16_t midi_cccd_handle = + ATT_CHARACTERISTIC_7772E5DB_3868_4112_A1A9_F2669D106BF3_01_CLIENT_CONFIGURATION_HANDLE; + +MIDIBLEInstance *instance = nullptr; +btstack_packet_callback_registration_t hci_event_callback_registration; + +// callback/event functions + +// HCI_SUBEVENT_LE_CONNECTION_COMPLETE +void connection_handler(uint8_t *packet, [[maybe_unused]] uint16_t size) { + if (!instance) + return; + if (hci_subevent_le_connection_complete_get_status(packet) != 0) + return; + uint16_t conn_handle = + hci_subevent_le_connection_complete_get_connection_handle(packet); + instance->handleConnect(BLEConnectionHandle {conn_handle}); +} +// HCI_SUBEVENT_LE_CONNECTION_UPDATE_COMPLETE +void connection_update_handler([[maybe_unused]] uint8_t *packet, + [[maybe_unused]] uint16_t size) { + DEBUGREF( // clang-format off + "Connection update: status=" + << hci_subevent_le_connection_update_complete_get_status(packet) + << ", connection interval=" + << hci_subevent_le_connection_update_complete_get_conn_interval(packet) + << ", connection latency=" + << hci_subevent_le_connection_update_complete_get_conn_latency(packet) + << ", supervision timeout=" + << hci_subevent_le_connection_update_complete_get_supervision_timeout(packet)); + // clang-format on +} +// HCI_SUBEVENT_LE_REMOTE_CONNECTION_PARAMETER_REQUEST +void connection_param_req_handler([[maybe_unused]] uint8_t *packet, + [[maybe_unused]] uint16_t size) { + DEBUGREF( // clang-format off + "Connection parameter request: interval min=" + << hci_subevent_le_remote_connection_parameter_request_get_interval_min(packet) + << ", interval max=" + << hci_subevent_le_remote_connection_parameter_request_get_interval_max(packet) + << ", latency=" + << hci_subevent_le_remote_connection_parameter_request_get_latency(packet) + << ", timeout=" + << hci_subevent_le_remote_connection_parameter_request_get_timeout(packet)); + // clang-format on +} +// HCI_EVENT_LE_META +void le_packet_handler(uint8_t *packet, uint16_t size) { + uint8_t type = hci_event_le_meta_get_subevent_code(packet); + DEBUGREF("LE event: " << le_event_names[type] << " (0x" << hex << type + << dec << ")"); + switch (type) { + case HCI_SUBEVENT_LE_CONNECTION_COMPLETE: + connection_handler(packet, size); + break; + case HCI_SUBEVENT_LE_CONNECTION_UPDATE_COMPLETE: + connection_update_handler(packet, size); + break; + case HCI_SUBEVENT_LE_REMOTE_CONNECTION_PARAMETER_REQUEST: + connection_param_req_handler(packet, size); + break; + default: break; + } +} +// HCI_EVENT_LE_META +void gattservice_handler(uint8_t *packet, [[maybe_unused]] uint16_t size) { + [[maybe_unused]] uint8_t type = + hci_event_gattservice_meta_get_subevent_code(packet); + DEBUGREF("GATT service event: " << gattservice_event_names[type] << " (0x" + << hex << type << dec << ")"); +} +// HCI_EVENT_DISCONNECTION_COMPLETE +void disconnect_handler(uint8_t *packet, [[maybe_unused]] uint16_t size) { + assert(instance); + uint16_t conn_handle = + hci_event_disconnection_complete_get_connection_handle(packet); + instance->handleDisconnect(BLEConnectionHandle {conn_handle}); +} +// ATT_EVENT_MTU_EXCHANGE_COMPLETE +void mtu_exchange_complete_handler(uint8_t *packet, + [[maybe_unused]] uint16_t size) { + assert(instance); + uint16_t conn_handle = att_event_mtu_exchange_complete_get_handle(packet); + uint16_t mtu = att_event_mtu_exchange_complete_get_MTU(packet); + DEBUGREF("mtu=" << mtu); + instance->handleMTU(BLEConnectionHandle {conn_handle}, mtu); +} +// GATT_EVENT_MTU +void gatt_event_mtu_handler(uint8_t *packet, [[maybe_unused]] uint16_t size) { + assert(instance); + uint16_t conn_handle = gatt_event_mtu_get_handle(packet); + uint16_t mtu = gatt_event_mtu_get_MTU(packet); + instance->handleMTU(BLEConnectionHandle {conn_handle}, mtu); +} +// BTSTACK_EVENT_STATE +void btstack_event_state_handler(uint8_t *packet, + [[maybe_unused]] uint16_t size) { + if (btstack_event_state_get_state(packet) != HCI_STATE_WORKING) + return; + bd_addr_t local_addr; + gap_local_bd_addr(local_addr); + DEBUGREF("BTstack up and running on " << bd_addr_to_str(local_addr)); +} + +// Handles all HCI event packets. +void packet_handler(uint8_t packet_type, [[maybe_unused]] uint16_t channel, + uint8_t *packet, uint16_t size) { + if (packet_type != HCI_EVENT_PACKET) + return; + auto type = hci_event_packet_get_type(packet); + DEBUGREF("HCI event: " << hci_event_names[type] << " (0x" << hex << type + << dec << ")"); + switch (type) { + case HCI_EVENT_LE_META: le_packet_handler(packet, size); break; + case HCI_EVENT_GATTSERVICE_META: + gattservice_handler(packet, size); + break; + case HCI_EVENT_DISCONNECTION_COMPLETE: + disconnect_handler(packet, size); + break; + default: break; + case ATT_EVENT_MTU_EXCHANGE_COMPLETE: + mtu_exchange_complete_handler(packet, size); + break; + // TODO: what's the difference with the previous one? + case GATT_EVENT_MTU: gatt_event_mtu_handler(packet, size); break; + case BTSTACK_EVENT_STATE: + btstack_event_state_handler(packet, size); + break; + } +} + +// ATT Client Read Callback for Dynamic Data +// - if buffer == NULL, don't copy data, just return size of value +// - if buffer != NULL, copy data and return number bytes copied +uint16_t att_read_callback([[maybe_unused]] hci_con_handle_t connection_handle, + uint16_t att_handle, + [[maybe_unused]] uint16_t offset, + [[maybe_unused]] uint8_t *buffer, + [[maybe_unused]] uint16_t buffer_size) { + if (att_handle == midi_char_value_handle) + return 0; // MIDI always responds with no data + return 0; +} + +int midi_cccd_write(hci_con_handle_t conn_handle, uint8_t *buffer, + [[maybe_unused]] uint16_t buffer_size) { + assert(instance); + bool notify = (little_endian_read_16(buffer, 0) & + GATT_CLIENT_CHARACTERISTICS_CONFIGURATION_NOTIFICATION) != 0; + instance->handleSubscribe(BLEConnectionHandle {conn_handle}, + BLECharacteristicHandle {midi_char_value_handle}, + notify); + return 0; +} + +int midi_value_write(hci_con_handle_t conn_handle, uint8_t *buffer, + uint16_t buffer_size) { + assert(instance); + BLEDataView data {buffer, buffer_size}; + auto data_gen = [data {data}]() mutable { return std::exchange(data, {}); }; + instance->handleData( + BLEConnectionHandle {conn_handle}, + BLEDataGenerator {compat::in_place, std::move(data_gen)}, + BLEDataLifetime::ConsumeImmediately); + return 0; +} + +int att_write_callback(hci_con_handle_t conn_handle, uint16_t att_handle, + [[maybe_unused]] uint16_t transaction_mode, + uint16_t offset, uint8_t *buffer, uint16_t buffer_size) { + DEBUGREF("ATT write: handle=" << att_handle << ", offset=" << offset + << ", size=" << buffer_size); + // Only support regular writes (no prepared/long writes) + if (transaction_mode != ATT_TRANSACTION_MODE_NONE) + return ATT_ERROR_REQUEST_NOT_SUPPORTED; + // Only support writes without offset + if (offset != 0) + return ATT_ERROR_INVALID_OFFSET; + // Client configuration update + if (att_handle == midi_cccd_handle) + return midi_cccd_write(conn_handle, buffer, buffer_size); + // MIDI data received + else if (att_handle == midi_char_value_handle) + return midi_value_write(conn_handle, buffer, buffer_size); + return 0; +} + +void le_midi_setup(const BLESettings &ble_settings) { + l2cap_init(); + // setup SM: no input, no output + sm_init(); + // setup ATT server + att_server_init(profile_data, att_read_callback, att_write_callback); + // setup advertisements + le_midi_setup_adv(ble_settings); + // register for HCI events + hci_event_callback_registration.callback = &packet_handler; + hci_add_event_handler(&hci_event_callback_registration); + // register for ATT event + att_server_register_packet_handler(packet_handler); +} + +template +btstack_context_callback_registration_t create_context_callback(F &f) { + btstack_context_callback_registration_t ret {}; + ret.callback = +[](void *context) { (*static_cast(context))(); }; + ret.context = &f; + return ret; +} + +} // namespace + +bool init(MIDIBLEInstance &instance, BLESettings settings) { + cs::midi_ble_btstack::instance = &instance; + le_midi_setup(settings); + hci_power_control(HCI_POWER_ON); + // btstack_run_loop_execute(); // not necessary in background mode + return true; +} + +void notify(BLEConnectionHandle conn_handle, + BLECharacteristicHandle char_handle, BLEDataView data) { + // DEBUGREF("[" << data.length << "] " + // << (AH::HexDump {data.data, data.length})); + [[maybe_unused]] const auto t0 = micros(); + // Don't bother sending empty packets + if (!data) + return; + // Flag to know when the can-send-now callback is done + volatile std::atomic_bool notify_done {false}; + // The following is executed in the BTstack can-send-now callback, so it is + // synchronized with the BTstack code + auto send = [&] { + DEBUGREF("notify " << micros() - t0); + att_server_notify(conn_handle.conn, char_handle.characteristic, + data.data, data.length); + DEBUGREF("notify done " << micros() - t0); + notify_done.store(true, std::memory_order_release); + }; + auto send_ctx = create_context_callback(send); + // The following is executed in the async_context, so it is synchronized + // with the BTstack code + auto run = [&] { + // Request can-send-now callback to be fired later + DEBUGREF("req send " << micros() - t0); + auto ret = att_server_request_to_send_notification(&send_ctx, + conn_handle.conn); + assert(ret == ERROR_CODE_SUCCESS); + }; + auto run_ctx = create_context_callback(run); + // Schedule the run callback on the BTstack thread, which will then schedule + // the send callback as soon as sending data is possible. + DEBUGREF("req main thread " << micros() - t0); + btstack_run_loop_execute_on_main_thread(&run_ctx); + // Wait for the can-send-now callback to clear the flag + while (!notify_done.load(std::memory_order_acquire)) tight_loop_contents(); + DEBUGREF("all done " << micros() - t0); +} + +} // namespace cs::midi_ble_btstack + +#endif \ No newline at end of file diff --git a/src/MIDI_Interfaces/BLEMIDI/BTstack/gatt_midi.gatt b/src/MIDI_Interfaces/BLEMIDI/BTstack/gatt_midi.gatt new file mode 100644 index 0000000000..85f8f6a8ac --- /dev/null +++ b/src/MIDI_Interfaces/BLEMIDI/BTstack/gatt_midi.gatt @@ -0,0 +1,7 @@ +PRIMARY_SERVICE, GATT_SERVICE +CHARACTERISTIC, GATT_DATABASE_HASH, READ, + +// MIDI Service +PRIMARY_SERVICE, 03B80E5A-EDE8-4B33-A751-6CE34EC4C700 +// MIDI Characteristic +CHARACTERISTIC, 7772E5DB-3868-4112-A1A9-F2669D106BF3, READ | WRITE_WITHOUT_RESPONSE | NOTIFY | DYNAMIC, diff --git a/src/MIDI_Interfaces/BLEMIDI/BTstack/gatt_midi.h b/src/MIDI_Interfaces/BLEMIDI/BTstack/gatt_midi.h new file mode 100644 index 0000000000..7e9cc4ec28 --- /dev/null +++ b/src/MIDI_Interfaces/BLEMIDI/BTstack/gatt_midi.h @@ -0,0 +1,65 @@ + +// ~/pico-examples/build/pico_w/bt/gatt_midi/generated/gatt_midi.h generated from ~/pico-sdk/lib/btstack/example/gatt_midi.gatt for BTstack +// it needs to be regenerated when the .gatt file is updated. + +// To generate ~/pico-examples/build/pico_w/bt/gatt_midi/generated/gatt_midi.h: +// ~/pico-sdk/lib/btstack/tool/compile_gatt.py ~/pico-sdk/lib/btstack/example/gatt_midi.gatt ~/pico-examples/build/pico_w/bt/gatt_midi/generated/gatt_midi.h + +// att db format version 1 + +// binary attribute representation: +// - size in bytes (16), flags(16), handle (16), uuid (16/128), value(...) + +#include + +// Reference: https://en.cppreference.com/w/cpp/feature_test +#if __cplusplus >= 200704L +constexpr +#endif +const uint8_t profile_data[] = +{ + // ATT DB Version + 1, + + // 0x0001 PRIMARY_SERVICE-GATT_SERVICE + 0x0a, 0x00, 0x02, 0x00, 0x01, 0x00, 0x00, 0x28, 0x01, 0x18, + // 0x0002 CHARACTERISTIC-GATT_DATABASE_HASH - READ + 0x0d, 0x00, 0x02, 0x00, 0x02, 0x00, 0x03, 0x28, 0x02, 0x03, 0x00, 0x2a, 0x2b, + // 0x0003 VALUE CHARACTERISTIC-GATT_DATABASE_HASH - READ -'' + // READ_ANYBODY + 0x18, 0x00, 0x02, 0x00, 0x03, 0x00, 0x2a, 0x2b, 0x81, 0x7e, 0xe3, 0x3a, 0xc7, 0x7b, 0x2e, 0xcd, 0x01, 0x0a, 0x6c, 0xe7, 0xfb, 0xb9, 0xb9, 0x68, + // MIDI Service + // 0x0004 PRIMARY_SERVICE-03B80E5A-EDE8-4B33-A751-6CE34EC4C700 + 0x18, 0x00, 0x02, 0x00, 0x04, 0x00, 0x00, 0x28, 0x00, 0xc7, 0xc4, 0x4e, 0xe3, 0x6c, 0x51, 0xa7, 0x33, 0x4b, 0xe8, 0xed, 0x5a, 0x0e, 0xb8, 0x03, + // MIDI Characteristic + // 0x0005 CHARACTERISTIC-7772E5DB-3868-4112-A1A9-F2669D106BF3 - READ | WRITE_WITHOUT_RESPONSE | NOTIFY | DYNAMIC + 0x1b, 0x00, 0x02, 0x00, 0x05, 0x00, 0x03, 0x28, 0x16, 0x06, 0x00, 0xf3, 0x6b, 0x10, 0x9d, 0x66, 0xf2, 0xa9, 0xa1, 0x12, 0x41, 0x68, 0x38, 0xdb, 0xe5, 0x72, 0x77, + // 0x0006 VALUE CHARACTERISTIC-7772E5DB-3868-4112-A1A9-F2669D106BF3 - READ | WRITE_WITHOUT_RESPONSE | NOTIFY | DYNAMIC + // READ_ANYBODY, WRITE_ANYBODY + 0x16, 0x00, 0x06, 0x03, 0x06, 0x00, 0xf3, 0x6b, 0x10, 0x9d, 0x66, 0xf2, 0xa9, 0xa1, 0x12, 0x41, 0x68, 0x38, 0xdb, 0xe5, 0x72, 0x77, + // 0x0007 CLIENT_CHARACTERISTIC_CONFIGURATION + // READ_ANYBODY, WRITE_ANYBODY + 0x0a, 0x00, 0x0e, 0x01, 0x07, 0x00, 0x02, 0x29, 0x00, 0x00, + // END + 0x00, 0x00, +}; // total size 76 bytes + + +// +// list service handle ranges +// +#define ATT_SERVICE_GATT_SERVICE_START_HANDLE 0x0001 +#define ATT_SERVICE_GATT_SERVICE_END_HANDLE 0x0003 +#define ATT_SERVICE_GATT_SERVICE_01_START_HANDLE 0x0001 +#define ATT_SERVICE_GATT_SERVICE_01_END_HANDLE 0x0003 +#define ATT_SERVICE_03B80E5A_EDE8_4B33_A751_6CE34EC4C700_START_HANDLE 0x0004 +#define ATT_SERVICE_03B80E5A_EDE8_4B33_A751_6CE34EC4C700_END_HANDLE 0x0007 +#define ATT_SERVICE_03B80E5A_EDE8_4B33_A751_6CE34EC4C700_01_START_HANDLE 0x0004 +#define ATT_SERVICE_03B80E5A_EDE8_4B33_A751_6CE34EC4C700_01_END_HANDLE 0x0007 + +// +// list mapping between characteristics and handles +// +#define ATT_CHARACTERISTIC_GATT_DATABASE_HASH_01_VALUE_HANDLE 0x0003 +#define ATT_CHARACTERISTIC_7772E5DB_3868_4112_A1A9_F2669D106BF3_01_VALUE_HANDLE 0x0006 +#define ATT_CHARACTERISTIC_7772E5DB_3868_4112_A1A9_F2669D106BF3_01_CLIENT_CONFIGURATION_HANDLE 0x0007 diff --git a/src/MIDI_Interfaces/BLEMIDI/BTstack/gatt_midi.hpp b/src/MIDI_Interfaces/BLEMIDI/BTstack/gatt_midi.hpp new file mode 100644 index 0000000000..b927a2e33c --- /dev/null +++ b/src/MIDI_Interfaces/BLEMIDI/BTstack/gatt_midi.hpp @@ -0,0 +1,11 @@ +#pragma once + +#include "../BLEAPI.hpp" + +namespace cs::midi_ble_btstack { + +bool init(MIDIBLEInstance &instance, BLESettings settings); +void notify(BLEConnectionHandle conn_handle, + BLECharacteristicHandle char_handle, BLEDataView data); + +} // namespace cs::midi_ble_btstack diff --git a/src/MIDI_Interfaces/BLEMIDI/BTstack/hci_event_names.hpp b/src/MIDI_Interfaces/BLEMIDI/BTstack/hci_event_names.hpp new file mode 100644 index 0000000000..9a282afa9c --- /dev/null +++ b/src/MIDI_Interfaces/BLEMIDI/BTstack/hci_event_names.hpp @@ -0,0 +1,426 @@ +#pragma once + +namespace cs::midi_ble_btstack { + +constexpr const char *hci_event_names[256] { + "NOP", + "INQUIRY_COMPLETE", + "INQUIRY_RESULT", + "CONNECTION_COMPLETE", + "CONNECTION_REQUEST", + "DISCONNECTION_COMPLETE", + "AUTHENTICATION_COMPLETE", + "REMOTE_NAME_REQUEST_COMPLETE", + "ENCRYPTION_CHANGE", + "CHANGE_CONNECTION_LINK_KEY_COMPLETE", + "MASTER_LINK_KEY_COMPLETE", + "READ_REMOTE_SUPPORTED_FEATURES_COMPLETE", + "READ_REMOTE_VERSION_INFORMATION_COMPLETE", + "QOS_SETUP_COMPLETE", + "COMMAND_COMPLETE", + "COMMAND_STATUS", + "HARDWARE_ERROR", + "FLUSH_OCCURRED", + "ROLE_CHANGE", + "NUMBER_OF_COMPLETED_PACKETS", + "MODE_CHANGE", + "RETURN_LINK_KEYS", + "PIN_CODE_REQUEST", + "LINK_KEY_REQUEST", + "LINK_KEY_NOTIFICATION", + "LOOPBACK_COMMAND", + "DATA_BUFFER_OVERFLOW", + "MAX_SLOTS_CHANGED", + "READ_CLOCK_OFFSET_COMPLETE", + "CONNECTION_PACKET_TYPE_CHANGED", + "QOS_VIOLATION", + "(unknown)", + "PAGE_SCAN_REPETITION_MODE_CHANGE", + "FLOW_SPECIFICATION_COMPLETE", + "INQUIRY_RESULT_WITH_RSSI", + "READ_REMOTE_EXTENDED_FEATURES_COMPLETE", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "SYNCHRONOUS_CONNECTION_COMPLETE", + "SYNCHRONOUS_CONNECTION_CHANGED", + "SNIFF_SUBRATING", + "EXTENDED_INQUIRY_RESPONSE", + "ENCRYPTION_KEY_REFRESH_COMPLETE", + "IO_CAPABILITY_REQUEST", + "IO_CAPABILITY_RESPONSE", + "USER_CONFIRMATION_REQUEST", + "USER_PASSKEY_REQUEST", + "REMOTE_OOB_DATA_REQUEST", + "SIMPLE_PAIRING_COMPLETE", + "(unknown)", + "LINK_SUPERVISION_TIMEOUT_CHANGED", + "ENHANCED_FLUSH_COMPLETE", + "(unknown)", + "USER_PASSKEY_NOTIFICATION", + "KEYPRESS_NOTIFICATION", + "REMOTE_HOST_SUPPORTED_FEATURES", + "LE_META", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "NUMBER_OF_COMPLETED_DATA_BLOCKS", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "ENCRYPTION_CHANGE_V2", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "TRANSPORT_SLEEP_MODE", + "TRANSPORT_USB_INFO", + "BIS_CAN_SEND_NOW", + "CIS_CAN_SEND_NOW", + "TRANSPORT_READY", + "TRANSPORT_PACKET_SENT", + "SCO_CAN_SEND_NOW", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "GATT_EVENT_QUERY_COMPLETE", + "GATT_EVENT_SERVICE_QUERY_RESULT", + "GATT_EVENT_CHARACTERISTIC_QUERY_RESULT", + "GATT_EVENT_INCLUDED_SERVICE_QUERY_RESULT", + "GATT_EVENT_ALL_CHARACTERISTIC_DESCRIPTORS_QUERY_RESULT", + "GATT_EVENT_CHARACTERISTIC_VALUE_QUERY_RESULT", + "GATT_EVENT_LONG_CHARACTERISTIC_VALUE_QUERY_RESULT", + "GATT_EVENT_NOTIFICATION", + "GATT_EVENT_INDICATION", + "GATT_EVENT_CHARACTERISTIC_DESCRIPTOR_QUERY_RESULT", + "GATT_EVENT_LONG_CHARACTERISTIC_DESCRIPTOR_QUERY_RESULT", + "GATT_EVENT_MTU", + "GATT_EVENT_CAN_WRITE_WITHOUT_RESPONSE", + "GATT_EVENT_CONNECTED", + "GATT_EVENT_DISCONNECTED", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "ATT_EVENT_CONNECTED", + "ATT_EVENT_DISCONNECTED", + "ATT_EVENT_MTU_EXCHANGE_COMPLETE", + "ATT_EVENT_HANDLE_VALUE_INDICATION_COMPLETE", + "ATT_EVENT_CAN_SEND_NOW", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "BNEP_EVENT_SERVICE_REGISTERED", + "BNEP_EVENT_CHANNEL_OPENED", + "BNEP_EVENT_CHANNEL_CLOSED", + "BNEP_EVENT_CHANNEL_TIMEOUT", + "BNEP_EVENT_CAN_SEND_NOW", + "(unknown)", + "(unknown)", + "(unknown)", + "SM_EVENT_JUST_WORKS_REQUEST", + "SM_EVENT_PASSKEY_DISPLAY_NUMBER", + "SM_EVENT_PASSKEY_DISPLAY_CANCEL", + "SM_EVENT_PASSKEY_INPUT_NUMBER", + "SM_EVENT_NUMERIC_COMPARISON_REQUEST", + "SM_EVENT_IDENTITY_RESOLVING_STARTED", + "SM_EVENT_IDENTITY_RESOLVING_FAILED", + "SM_EVENT_IDENTITY_RESOLVING_SUCCEEDED", + "SM_EVENT_AUTHORIZATION_REQUEST", + "SM_EVENT_AUTHORIZATION_RESULT", + "SM_EVENT_KEYPRESS_NOTIFICATION", + "SM_EVENT_IDENTITY_CREATED", + "SM_EVENT_PAIRING_STARTED", + "SM_EVENT_PAIRING_COMPLETE", + "SM_EVENT_REENCRYPTION_STARTED", + "SM_EVENT_REENCRYPTION_COMPLETE", + "GAP_EVENT_SECURITY_LEVEL", + "GAP_EVENT_DEDICATED_BONDING_COMPLETED", + "GAP_EVENT_ADVERTISING_REPORT", + "GAP_EVENT_EXTENDED_ADVERTISING_REPORT", + "GAP_EVENT_INQUIRY_RESULT", + "GAP_EVENT_INQUIRY_COMPLETE", + "GAP_EVENT_RSSI_MEASUREMENT", + "GAP_EVENT_LOCAL_OOB_DATA", + "GAP_EVENT_PAIRING_STARTED", + "GAP_EVENT_PAIRING_COMPLETE", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "META_GAP", + "HSP_META", + "HFP_META", + "ANCS_META", + "AVDTP_META", + "AVRCP_META", + "GOEP_META", + "PBAP_META", + "HID_META", + "A2DP_META", + "HIDS_META", + "GATTSERVICE_META", + "BIP_META", + "MAP_META", + "MESH_META", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "VENDOR_SPECIFIC", +}; + +constexpr const char *gattservice_event_names[114] { + "(unknown)", + "CYCLING_POWER_START_CALIBRATION", + "CYCLING_POWER_BROADCAST_START", + "CYCLING_POWER_BROADCAST_STOP", + "BATTERY_SERVICE_CONNECTED", + "BATTERY_SERVICE_LEVEL", + "DEVICE_INFORMATION_DONE", + "DEVICE_INFORMATION_MANUFACTURER_NAME", + "DEVICE_INFORMATION_MODEL_NUMBER", + "DEVICE_INFORMATION_SERIAL_NUMBER", + "DEVICE_INFORMATION_HARDWARE_REVISION", + "DEVICE_INFORMATION_FIRMWARE_REVISION", + "DEVICE_INFORMATION_SOFTWARE_REVISION", + "DEVICE_INFORMATION_SYSTEM_ID", + "DEVICE_INFORMATION_IEEE_REGULATORY_CERTIFICATION", + "DEVICE_INFORMATION_PNP_ID", + "SCAN_PARAMETERS_SERVICE_CONNECTED", + "SPP_SERVICE_CONNECTED", + "SPP_SERVICE_DISCONNECTED", + "HID_SERVICE_CONNECTED", + "HID_REPORT", + "HID_INFORMATION", + "HID_PROTOCOL_MODE", + "HID_SERVICE_REPORTS_NOTIFICATION", + "SCAN_PARAMETERS_SERVICE_SCAN_INTERVAL_UPDATE", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "BASS_SERVER_SCAN_STOPPED", + "BASS_SERVER_SCAN_STARTED", + "BASS_SERVER_BROADCAST_CODE", + "BASS_SERVER_SOURCE_ADDED", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "BASS_SERVER_SOURCE_MODIFIED", + "BASS_SERVER_SOURCE_DELETED", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "BASS_CLIENT_CONNECTED", + "BASS_CLIENT_DISCONNECTED", + "BASS_CLIENT_SCAN_OPERATION_COMPLETE", + "BASS_NOTIFY_RECEIVE_STATE_BASE", + "BASS_CLIENT_NOTIFY_RECEIVE_STATE_SUBGROUP", + "BASS_CLIENT_NOTIFICATION_COMPLETE", + "BASS_CLIENT_SOURCE_OPERATION_COMPLETE", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "(unknown)", + "BASS_SERVER_CONNECTED", + "BASS_SERVER_DISCONNECTED", +}; + +constexpr const char *le_event_names[42] { + "(unknown)", + "HCI_SUBEVENT_LE_CONNECTION_COMPLETE", + "HCI_SUBEVENT_LE_ADVERTISING_REPORT", + "HCI_SUBEVENT_LE_CONNECTION_UPDATE_COMPLETE", + "HCI_SUBEVENT_LE_READ_REMOTE_FEATURES_COMPLETE", + "HCI_SUBEVENT_LE_LONG_TERM_KEY_REQUEST", + "HCI_SUBEVENT_LE_REMOTE_CONNECTION_PARAMETER_REQUEST", + "HCI_SUBEVENT_LE_DATA_LENGTH_CHANGE", + "HCI_SUBEVENT_LE_READ_LOCAL_P256_PUBLIC_KEY_COMPLETE", + "HCI_SUBEVENT_LE_GENERATE_DHKEY_COMPLETE", + "HCI_SUBEVENT_LE_ENHANCED_CONNECTION_COMPLETE_V1", + "HCI_SUBEVENT_LE_DIRECT_ADVERTISING_REPORT", + "HCI_SUBEVENT_LE_PHY_UPDATE_COMPLETE", + "HCI_SUBEVENT_LE_EXTENDED_ADVERTISING_REPORT", + "HCI_SUBEVENT_LE_PERIODIC_ADVERTISING_SYNC_ESTABLISHMENT", + "HCI_SUBEVENT_LE_PERIODIC_ADVERTISING_REPORT", + "HCI_SUBEVENT_LE_PERIODIC_ADVERTISING_SYNC_LOST", + "HCI_SUBEVENT_LE_SCAN_TIMEOUT", + "HCI_SUBEVENT_LE_ADVERTISING_SET_TERMINATED", + "HCI_SUBEVENT_LE_SCAN_REQUEST_RECEIVED", + "HCI_SUBEVENT_LE_CHANNEL_SELECTION_ALGORITHM", + "HCI_SUBEVENT_LE_CONNECTIONLESS_IQ_REPORT", + "HCI_SUBEVENT_LE_CONNECTION_IQ_REPORT", + "HCI_SUBEVENT_LE_LE_CTE_REQUEST_FAILED", + "HCI_SUBEVENT_LE_PERIODIC_ADVERTISING_SYNC_TRANSFER_RECEIVED", + "HCI_SUBEVENT_LE_CIS_ESTABLISHED", + "HCI_SUBEVENT_LE_CIS_REQUEST", + "HCI_SUBEVENT_LE_CREATE_BIG_COMPLETE", + "HCI_SUBEVENT_LE_TERMINATE_BIG_COMPLETE", + "HCI_SUBEVENT_LE_BIG_SYNC_ESTABLISHED", + "HCI_SUBEVENT_LE_BIG_SYNC_LOST", + "HCI_SUBEVENT_LE_REQUEST_PEER_SCA_COMPLETE", + "(unknown)", + "HCI_SUBEVENT_LE_TRANSMIT_POWER_REPORTING", + "HCI_SUBEVENT_LE_BIGINFO_ADVERTISING_REPORT", + "HCI_SUBEVENT_LE_SUBRATE_CHANGE", + "(unknown)", + "(unknown)", + "(unknown)", + "HCI_SUBEVENT_LE_PERIODIC_ADVERTISING_DATA_REQUEST", + "(unknown)", + "HCI_SUBEVENT_LE_ENHANCED_CONNECTION_COMPLETE_V2", +}; + +} // namespace cs::midi_ble_btstack diff --git a/src/MIDI_Interfaces/BLEMIDI/BTstackBackgroundBackend.hpp b/src/MIDI_Interfaces/BLEMIDI/BTstackBackgroundBackend.hpp new file mode 100644 index 0000000000..0ed927c4fe --- /dev/null +++ b/src/MIDI_Interfaces/BLEMIDI/BTstackBackgroundBackend.hpp @@ -0,0 +1,135 @@ +#pragma once + +#include + +#include "BLEAPI.hpp" +#include "BTstack/gatt_midi.hpp" +#include "BufferedBLEMIDIParser.hpp" +#include "PollingBLEMIDISender.hpp" + +#include +#include +#include + +BEGIN_CS_NAMESPACE + +/// Raspberry Pi Pico BTstack background backend intended to be plugged into +/// @ref GenericBLEMIDI_Interface. +/// +/// @todo Implement BTstack timer-based sender timeouts. +class BTstackBackgroundBackend + : private PollingBLEMIDISender, + private MIDIBLEInstance { + private: + // Callbacks from the ArduinoBLE stack. + void handleConnect(BLEConnectionHandle conn_handle) override { + connected = conn_handle.conn; + } + void handleDisconnect(BLEConnectionHandle) override { + connected = subscribed = 0xFFFF; + } + void handleMTU(BLEConnectionHandle, uint16_t mtu) override { + Sender::updateMTU(mtu); + } + void handleSubscribe(BLEConnectionHandle, + BLECharacteristicHandle char_handle, + bool notify) override { + subscribed = notify ? char_handle.characteristic : 0xFFFF; + } + void handleData(BLEConnectionHandle, BLEDataGenerator &&data, + BLEDataLifetime) override { + while (true) { + BLEDataView packet = data(); + if (packet.length == 0) { + break; + } else if (!parser.pushPacket(packet)) { + DEBUGREF(F("BLE packet dropped, size: ") << packet.length); + break; + } + } + } + + private: + // We cannot use atomics here, because they might not be lock-free on the + // Pico's Cortex-M0+ cores. + static_assert(sizeof(sig_atomic_t) > sizeof(uint16_t)); + /// Are we connected to a BLE Central? + volatile sig_atomic_t connected = 0xFFFF; + /// Did the BLE Central subscribe to be notified for the MIDI characteristic? + volatile sig_atomic_t subscribed = 0xFFFF; + struct VolatileSize { + constexpr static size_t alignment = alignof(sig_atomic_t); + VolatileSize(sig_atomic_t value) : value {value} {} + volatile sig_atomic_t value; + sig_atomic_t load_acquire() const { + auto state = save_and_disable_interrupts(); // Probably overkill ... + auto t = value; + restore_interrupts(state); + std::atomic_signal_fence(std::memory_order_acquire); + return t; + } + void add_release(sig_atomic_t t) { + std::atomic_signal_fence(std::memory_order_release); + auto state = save_and_disable_interrupts(); + value += t; + restore_interrupts(state); + } + void sub_release(sig_atomic_t t) { + std::atomic_signal_fence(std::memory_order_release); + auto state = save_and_disable_interrupts(); + value -= t; + restore_interrupts(state); + } + }; + /// Contains incoming BLE MIDI data to be parsed. + BufferedBLEMIDIParser<1024, VolatileSize> parser; + + public: + using IncomingMIDIMessage = AnyMIDIMessage; + + /// Retrieve and remove a single incoming MIDI message from the buffer. + bool popMessage(IncomingMIDIMessage &incomingMessage) { + // This function is assumed to be polled regularly by the higher-level + // MIDI_Interface, so we check the sender's timer here. + auto lck = Sender::acquirePacket(); + Sender::releasePacketAndNotify(lck); + // Actually get a MIDI message from the buffer + return parser.popMessage(incomingMessage); + } + + public: + /// Initialize the BLE stack etc. + void begin(BLESettings ble_settings) { + midi_ble_btstack::init(*this, ble_settings); + Sender::begin(); + } + /// Deinitialize the BLE stack. + /// @todo Not yet implemented. + void end() {} + /// Returns true if we are connected to a BLE Central device. + bool isConnected() const { return connected != 0xFFFF; } + + private: + // Implement the interface for the BLE sender. + using Sender = PollingBLEMIDISender; + friend Sender; + /// Send the given MIDI BLE packet. + void sendData(BLEDataView data) { + if (connected != 0xFFFF && subscribed != 0xFFFF) + midi_ble_btstack::notify( + BLEConnectionHandle {static_cast(connected)}, + BLECharacteristicHandle {static_cast(subscribed)}, + data); + } + + public: + // Expose the necessary BLE sender functions. + using Sender::acquirePacket; + using Sender::forceMinMTU; + using Sender::getMinMTU; + using Sender::releasePacketAndNotify; + using Sender::sendNow; + using Sender::setTimeout; +}; + +END_CS_NAMESPACE \ No newline at end of file diff --git a/src/MIDI_Interfaces/BLEMIDI/BufferedBLEMIDIParser.hpp b/src/MIDI_Interfaces/BLEMIDI/BufferedBLEMIDIParser.hpp new file mode 100644 index 0000000000..bd9e3cd467 --- /dev/null +++ b/src/MIDI_Interfaces/BLEMIDI/BufferedBLEMIDIParser.hpp @@ -0,0 +1,79 @@ +#pragma once + +#include + +#include "BLERingBuf.hpp" +#include +#include +#include + +BEGIN_CS_NAMESPACE + +/// FIFO buffer that you can push BLE packets into, and pop MIDI messages out of. +/// If @p SizeT is chosen to be atomic, one thread can push packets, and another +/// thread can pop MIDI messages, without additional synchronization. +template > +class BufferedBLEMIDIParser { + private: + /// Contains incoming data to be parsed. + BLERingBuf ble_buffer {}; + /// Parses the (chunked) BLE packet obtained from @ref ble_buffer. + BLEMIDIParser ble_parser {nullptr, 0}; + /// Parser for MIDI data extracted from the BLE packet by @ref ble_parser. + SerialMIDI_Parser parser {false}; + + public: + using IncomingMIDIMessage = AnyMIDIMessage; + + /// Add a new BLE packet or chunk to the buffer. + bool pushPacket(BLEDataView packet, + BLEDataType type = BLEDataType::Packet) { + return ble_buffer.push(packet, type); + } + + /// Retrieve and remove a single incoming MIDI message from the buffer. + bool popMessage(IncomingMIDIMessage &incomingMessage) { + // Try reading a MIDI message from the parser + auto try_read = [&] { + MIDIReadEvent event = parser.pull(ble_parser); + switch (event) { + case MIDIReadEvent::CHANNEL_MESSAGE: + incomingMessage = {parser.getChannelMessage(), + ble_parser.getTimestamp()}; + return true; + case MIDIReadEvent::SYSEX_CHUNK: // fallthrough + case MIDIReadEvent::SYSEX_MESSAGE: + incomingMessage = {parser.getSysExMessage(), + ble_parser.getTimestamp()}; + return true; + case MIDIReadEvent::REALTIME_MESSAGE: + incomingMessage = {parser.getRealTimeMessage(), + ble_parser.getTimestamp()}; + return true; + case MIDIReadEvent::SYSCOMMON_MESSAGE: + incomingMessage = {parser.getSysCommonMessage(), + ble_parser.getTimestamp()}; + return true; + case MIDIReadEvent::NO_MESSAGE: return false; + default: break; // LCOV_EXCL_LINE + } + return false; + }; + while (true) { + // Try reading a MIDI message from the current buffer + if (try_read()) + return true; // success, incomingMessage updated + // Get the next chunk of the BLE packet (if available) + BLEDataView chunk; + auto popped = ble_buffer.pop(chunk); + if (popped == BLEDataType::None) + return false; // no more BLE data available + else if (popped == BLEDataType::Continuation) + ble_parser.extend(chunk.data, chunk.length); // same BLE packet + else if (popped == BLEDataType::Packet) + ble_parser = {chunk.data, chunk.length}; // new BLE packet + } + } +}; + +END_CS_NAMESPACE \ No newline at end of file diff --git a/src/MIDI_Interfaces/BLEMIDI/ESP32-NimBLE/README.md b/src/MIDI_Interfaces/BLEMIDI/ESP32-NimBLE/README.md new file mode 100644 index 0000000000..7028cf6fba --- /dev/null +++ b/src/MIDI_Interfaces/BLEMIDI/ESP32-NimBLE/README.md @@ -0,0 +1,13 @@ +# ESP32-NimBLE + +MIDI over BLE backend for the [Apache Mynewt NimBLE](https://github.com/apache/mynewt-nimble) library +on ESP32. Tested using the [h2zero/NimBLE-Arduino](https://github.com/h2zero/NimBLE-Arduino) library. + +Ideally, all .ipp files would be .cpp files, and all .c.ipp files would be .c +files. However, since we want this component to be optional, it should not +compile anything unless the user explicitly includes any of the BLE-specific +headers. Otherwise, we would require every user to install the NimBLE-Arduino +library. Maybe we can clean this up later once the arduino-esp32 core ships +with NimBLE out of the box, or once Arduino comes up with a decent dependency +or library management system. In such a scenario, replace the inline functions +and variables with non-inline or even static functions/variables. diff --git a/src/MIDI_Interfaces/BLEMIDI/ESP32-NimBLE/advertise.hpp b/src/MIDI_Interfaces/BLEMIDI/ESP32-NimBLE/advertise.hpp new file mode 100644 index 0000000000..b05cee9c10 --- /dev/null +++ b/src/MIDI_Interfaces/BLEMIDI/ESP32-NimBLE/advertise.hpp @@ -0,0 +1,12 @@ +#pragma once + +#include + +namespace cs::midi_ble_nimble { + +void advertise(uint8_t addr_type); +void set_advertise_connection_interval(uint16_t min_itvl, uint16_t max_itvl); + +} // namespace cs::midi_ble_nimble + +#include "advertise.ipp" \ No newline at end of file diff --git a/src/MIDI_Interfaces/BLEMIDI/ESP32-NimBLE/advertise.ipp b/src/MIDI_Interfaces/BLEMIDI/ESP32-NimBLE/advertise.ipp new file mode 100644 index 0000000000..6b73ca23b9 --- /dev/null +++ b/src/MIDI_Interfaces/BLEMIDI/ESP32-NimBLE/advertise.ipp @@ -0,0 +1,84 @@ +#ifdef ESP32 +#include +#if CONFIG_BT_BLE_ENABLED + +#include "ble.h" + +#if CS_MIDI_BLE_ESP_IDF_NIMBLE +#include +#include +#include +#else +#include +#include +#include +#endif + +#include "ble-macro-fix.h" + +#include "callbacks.h" +#include "gatt.h" +#include "util.hpp" + +namespace cs::midi_ble_nimble { + +inline uint8_t slave_itvl_range[4] {0xFF, 0xFF, 0xFF, 0xFF}; + +/// Begin advertising, including the MIDI service UUID and the device name. +/// Attaches the @ref cs_midi_ble_gap_callback. +inline void advertise(uint8_t addr_type) { + // Set the advertisement data included in our advertisements: + // - Flags (indicates advertisement type and other general info). + // - Advertising tx power. + // - 128-bit MIDI service UUID + struct ble_hs_adv_fields fields {}; + // The scan response includes the following data: + // - Device name + struct ble_hs_adv_fields rsp_fields {}; + + // - Discoverability in forthcoming advertisement (general) + // - BLE-only (BR/EDR unsupported). + fields.flags = BLE_HS_ADV_F_DISC_GEN | BLE_HS_ADV_F_BREDR_UNSUP; + // Indicate that the TX power level field should be included; have the + // stack fill this value automatically. + fields.tx_pwr_lvl = BLE_HS_ADV_TX_PWR_LVL_AUTO; + fields.tx_pwr_lvl_is_present = 1; + // Advertise the MIDI service ID + fields.uuids128 = &midi_ble_service_uuid; + fields.num_uuids128 = 1; + fields.uuids128_is_complete = 1; + // Specify the desired connection interval + fields.slave_itvl_range = slave_itvl_range; + + // Respond with the device name (it may be too long to fit in the + // first advertisement packet, since it already contains a 16-byte UUID). + const char *name = ble_svc_gap_device_name(); + rsp_fields.name = reinterpret_cast(name); + rsp_fields.name_len = strlen(name); + rsp_fields.name_is_complete = 1; + + // Pass the data to the BLE stack + CS_CHECK_ZERO_V(ble_gap_adv_set_fields(&fields)); + CS_CHECK_ZERO_V(ble_gap_adv_rsp_set_fields(&rsp_fields)); + // Begin advertising + struct ble_gap_adv_params adv_params {}; + adv_params.conn_mode = BLE_GAP_CONN_MODE_UND; + adv_params.disc_mode = BLE_GAP_DISC_MODE_GEN; + ESP_LOGI("CS-BLEMIDI", "Start advertising"); + CS_CHECK_ZERO_V(ble_gap_adv_start(addr_type, NULL, BLE_HS_FOREVER, + &adv_params, cs_midi_ble_gap_callback, + NULL)); +} + +inline void set_advertise_connection_interval(uint16_t min_itvl, + uint16_t max_itvl) { + slave_itvl_range[0] = (min_itvl >> 0) & 0xFF; + slave_itvl_range[1] = (min_itvl >> 8) & 0xFF; + slave_itvl_range[2] = (max_itvl >> 0) & 0xFF; + slave_itvl_range[3] = (max_itvl >> 8) & 0xFF; +} + +} // namespace cs::midi_ble_nimble + +#endif // CONFIG_BT_BLE_ENABLED +#endif // ESP32 \ No newline at end of file diff --git a/src/MIDI_Interfaces/BLEMIDI/ESP32-NimBLE/app.hpp b/src/MIDI_Interfaces/BLEMIDI/ESP32-NimBLE/app.hpp new file mode 100644 index 0000000000..ba47fac009 --- /dev/null +++ b/src/MIDI_Interfaces/BLEMIDI/ESP32-NimBLE/app.hpp @@ -0,0 +1,13 @@ +#pragma once + +#include "../BLEAPI.hpp" + +namespace cs::midi_ble_nimble { + +bool init(MIDIBLEInstance &instance, BLESettings ble_settings); +bool notify(BLEConnectionHandle conn_handle, + BLECharacteristicHandle char_handle, BLEDataView data); + +} // namespace cs::midi_ble_nimble + +#include "app.ipp" diff --git a/src/MIDI_Interfaces/BLEMIDI/ESP32-NimBLE/app.ipp b/src/MIDI_Interfaces/BLEMIDI/ESP32-NimBLE/app.ipp new file mode 100644 index 0000000000..2223dd21e1 --- /dev/null +++ b/src/MIDI_Interfaces/BLEMIDI/ESP32-NimBLE/app.ipp @@ -0,0 +1,102 @@ +#ifdef ESP32 +#include +#if CONFIG_BT_BLE_ENABLED + +#include "ble.h" + +#if CS_MIDI_BLE_ESP_IDF_NIMBLE +#include +#include +#include +#include +#include +#include +#else +#include +#include +#include +#include +#include +#include +#endif + +#include "ble-macro-fix.h" + +#include "app.hpp" +#include "callbacks.h" +#include "events.hpp" +#include "gatt.h" +#include "init.hpp" +#include "util.hpp" + +inline cs::midi_ble_nimble::MIDIBLEState cs_midi_ble_state; + +extern "C" void ble_store_config_init(void); + +namespace cs::midi_ble_nimble { + +inline bool init(MIDIBLEInstance &instance, BLESettings ble_settings) { + // Configure the hardware to support BLE using NimBLE. + if (!init_hardware()) + return false; + + // Initialize the NimBLE host configuration. + ble_hs_cfg.reset_cb = cs_midi_ble_on_reset; + ble_hs_cfg.sync_cb = cs_midi_ble_on_sync; + ble_hs_cfg.gatts_register_cb = cs_midi_ble_service_register_callback; + ble_hs_cfg.store_status_cb = ble_store_util_status_rr; + // XXX Need to have template for store + ble_store_config_init(); + + // No input or output capabilities. + ble_hs_cfg.sm_io_cap = BLE_SM_IO_CAP_NO_IO; + ble_hs_cfg.sm_bonding = 1; + // Enable the appropriate bit masks to make sure the keys that are needed + // are exchanged + ble_hs_cfg.sm_our_key_dist |= BLE_SM_PAIR_KEY_DIST_ENC; + ble_hs_cfg.sm_their_key_dist |= BLE_SM_PAIR_KEY_DIST_ENC; + ble_hs_cfg.sm_mitm = 1; + ble_hs_cfg.sm_sc = 1; + // Stores the IRK + ble_hs_cfg.sm_our_key_dist |= BLE_SM_PAIR_KEY_DIST_ID; + ble_hs_cfg.sm_their_key_dist |= BLE_SM_PAIR_KEY_DIST_ID; + + CS_CHECK_ZERO(ble_gatts_reset()); + ble_svc_gap_init(); + ble_svc_gatt_init(); + + // Initialize the MIDI service and characteristic + cs_midi_ble_state.instance = &instance; + cs::midi_ble_nimble::state = &cs_midi_ble_state; + const auto *gatt_server_services = midi_ble_get_service(); + CS_CHECK_ZERO(ble_gatts_count_cfg(gatt_server_services)); + CS_CHECK_ZERO(ble_gatts_add_svcs(gatt_server_services)); + + // Set the default device name + CS_CHECK_ZERO(ble_svc_gap_device_name_set(ble_settings.device_name)); + set_advertise_connection_interval(ble_settings.connection_interval.minimum, + ble_settings.connection_interval.maximum); + + // Start the FreeRTOS task that runs the NimBLE stack + nimble_port_freertos_init([](void *) { + ESP_LOGI("CS-BLEMIDI", "BLE Host Task Started"); + // This function will return only when nimble_port_stop() is executed + nimble_port_run(); + ESP_LOGI("CS-BLEMIDI", "BLE Host Task Ended"); + nimble_port_freertos_deinit(); + }); + return true; +} + +inline bool notify(BLEConnectionHandle conn_handle, + BLECharacteristicHandle char_handle, BLEDataView data) { + auto om = ble_hs_mbuf_from_flat(data.data, data.length); + CS_CHECK_ZERO(ble_gattc_notify_custom(conn_handle.conn, + char_handle.characteristic, om)); + return true; +} + +} // namespace cs::midi_ble_nimble + +#endif // CONFIG_BT_BLE_ENABLED +#endif // ESP32 \ No newline at end of file diff --git a/src/MIDI_Interfaces/BLEMIDI/ESP32-NimBLE/ble-macro-fix.h b/src/MIDI_Interfaces/BLEMIDI/ESP32-NimBLE/ble-macro-fix.h new file mode 100644 index 0000000000..50fc08345f --- /dev/null +++ b/src/MIDI_Interfaces/BLEMIDI/ESP32-NimBLE/ble-macro-fix.h @@ -0,0 +1,7 @@ +#ifdef min +#undef min +#endif + +#ifdef max +#undef max +#endif \ No newline at end of file diff --git a/src/MIDI_Interfaces/BLEMIDI/ESP32-NimBLE/ble.h b/src/MIDI_Interfaces/BLEMIDI/ESP32-NimBLE/ble.h new file mode 100644 index 0000000000..3963cb96d9 --- /dev/null +++ b/src/MIDI_Interfaces/BLEMIDI/ESP32-NimBLE/ble.h @@ -0,0 +1,12 @@ +#pragma once + +#ifdef __cplusplus +#include +#endif + +#if __has_include() +#define CS_MIDI_BLE_ESP_IDF_NIMBLE 1 +#else +#include +#define CS_MIDI_BLE_ESP_IDF_NIMBLE 0 +#endif diff --git a/src/MIDI_Interfaces/BLEMIDI/ESP32-NimBLE/callbacks.h b/src/MIDI_Interfaces/BLEMIDI/ESP32-NimBLE/callbacks.h new file mode 100644 index 0000000000..e4ef03ea9d --- /dev/null +++ b/src/MIDI_Interfaces/BLEMIDI/ESP32-NimBLE/callbacks.h @@ -0,0 +1,31 @@ +#pragma once + +#include "ble.h" + +#if CS_MIDI_BLE_ESP_IDF_NIMBLE +#include +#include +#else +#include +#include +#endif + +#include "ble-macro-fix.h" + +#ifdef __cplusplus +extern "C" { +#endif + +int cs_midi_ble_characteristic_callback(uint16_t conn_handle, + uint16_t attr_handle, + struct ble_gatt_access_ctxt *ctxt, + void *arg); +void cs_midi_ble_service_register_callback(struct ble_gatt_register_ctxt *ctxt, + void *arg); +int cs_midi_ble_gap_callback(struct ble_gap_event *event, void *arg); +void cs_midi_ble_on_sync(void); +void cs_midi_ble_on_reset(int reason); + +#ifdef __cplusplus +} // extern "C" +#endif diff --git a/src/MIDI_Interfaces/BLEMIDI/ESP32-NimBLE/events.hpp b/src/MIDI_Interfaces/BLEMIDI/ESP32-NimBLE/events.hpp new file mode 100644 index 0000000000..f7c3cfea29 --- /dev/null +++ b/src/MIDI_Interfaces/BLEMIDI/ESP32-NimBLE/events.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include "ble.h" + +#if CS_MIDI_BLE_ESP_IDF_NIMBLE +#include +#include +#else +#include +#include +#endif + +#include "ble-macro-fix.h" + +#include "../BLEAPI.hpp" + +namespace cs::midi_ble_nimble { + +struct MIDIBLEState { + constexpr static uint16_t invalid_handle = 0xFFFF; + MIDIBLEInstance *instance = nullptr; + uint16_t midi_characteristic_handle = invalid_handle; + uint8_t address_type = 0; +}; + +extern MIDIBLEState *state; + +} // namespace cs::midi_ble_nimble + +#include "events.ipp" diff --git a/src/MIDI_Interfaces/BLEMIDI/ESP32-NimBLE/events.ipp b/src/MIDI_Interfaces/BLEMIDI/ESP32-NimBLE/events.ipp new file mode 100644 index 0000000000..3c41dc0b9b --- /dev/null +++ b/src/MIDI_Interfaces/BLEMIDI/ESP32-NimBLE/events.ipp @@ -0,0 +1,262 @@ +#ifdef ESP32 +#include +#if CONFIG_BT_BLE_ENABLED + +#include "advertise.hpp" +#include "callbacks.h" +#include "events.hpp" +#include "gatt.h" +#include "util.hpp" + +namespace cs::midi_ble_nimble { +inline MIDIBLEState *state; + +namespace { + +[[maybe_unused]] std::string fmt_address(const void *addr) { + std::string str {"XX:XX:XX:XX:XX:XX"}; + auto *u8p = reinterpret_cast(addr); + snprintf(str.data(), str.size() + 1, "%02x:%02x:%02x:%02x:%02x:%02x", + u8p[5], u8p[4], u8p[3], u8p[2], u8p[1], u8p[0]); + return str; +} + +void print_conn_desc(struct ble_gap_conn_desc *desc) { + ESP_LOGI( + "CS-BLEMIDI", + "connection info: handle=%d\n" + " our_ota_addr_type=%d our_ota_addr=%s\n" + " our_id_addr_type=%d our_id_addr=%s\n" + " peer_ota_addr_type=%d peer_ota_addr=%s\n" + " peer_id_addr_type=%d peer_id_addr=%s\n" + " conn_itvl=%d conn_latency=%d supervision_timeout=%d\n" + " encrypted=%d authenticated=%d bonded=%d", + desc->conn_handle, desc->our_ota_addr.type, + fmt_address(desc->our_ota_addr.val).c_str(), desc->our_id_addr.type, + fmt_address(desc->our_id_addr.val).c_str(), desc->peer_ota_addr.type, + fmt_address(desc->peer_ota_addr.val).c_str(), desc->peer_id_addr.type, + fmt_address(desc->peer_id_addr.val).c_str(), desc->conn_itvl, + desc->conn_latency, desc->supervision_timeout, + desc->sec_state.encrypted, desc->sec_state.authenticated, + desc->sec_state.bonded); +} + +} // namespace + +} // namespace cs::midi_ble_nimble + +/// Called when the host and controller become synced (i.e. after successful +/// startup). +inline void cs_midi_ble_on_sync() { + ESP_LOGD("CS-BLEMIDI", "sync"); + CS_CHECK_ZERO_V( + ble_hs_id_infer_auto(0, &cs::midi_ble_nimble::state->address_type)); + uint8_t addr_val[6] = {0}; + CS_CHECK_ZERO_V(ble_hs_id_copy_addr( + cs::midi_ble_nimble::state->address_type, addr_val, NULL)); + ESP_LOGD("CS-BLEMIDI", "address=%s", + cs::midi_ble_nimble::fmt_address(addr_val).c_str()); + cs::midi_ble_nimble::advertise(cs::midi_ble_nimble::state->address_type); +} + +/// Called when the stack is reset. +inline void cs_midi_ble_on_reset(int reason) { + ESP_LOGE("CS-BLEMIDI", "Resetting state; reason=%d", reason); +} + +/// Called when any GATT characteristic (or descriptor) is accessed. +inline int +cs_midi_ble_characteristic_callback(uint16_t conn_handle, uint16_t attr_handle, + struct ble_gatt_access_ctxt *ctxt, void *) { + ESP_LOGD("CS-BLEMIDI", "gatt callback %d", ctxt->op); + switch (ctxt->op) { + // READ: BLE MIDI should respond with no payload + case BLE_GATT_ACCESS_OP_READ_CHR: { + ESP_LOGD("CS-BLEMIDI", "gatt read"); + static uint8_t dummy = 0; + auto rc = os_mbuf_append(ctxt->om, &dummy, 0); + return rc == 0 ? 0 : BLE_ATT_ERR_INSUFFICIENT_RES; + } break; + + // Write: pass the received MIDI data on to the driver + case BLE_GATT_ACCESS_OP_WRITE_CHR: { + ESP_LOGD("CS-BLEMIDI", "gatt write"); + auto data_gen = [om {ctxt->om}]() mutable { + if (!om) + return cs::BLEDataView {}; + cs::BLEDataView data {om->om_data, om->om_len}; + om = om->om_next.sle_next; + return data; + }; + if (auto *inst = cs::midi_ble_nimble::state->instance) + inst->handleData(cs::BLEConnectionHandle {conn_handle}, + cs::BLEDataGenerator {compat::in_place, + std::move(data_gen)}, + cs::BLEDataLifetime::ConsumeImmediately); + return 0; + } break; + + default: return 0; + } +} + +/// Called during GATT initialization, for each service, characteristic and +/// descriptor that is registered. +inline void +cs_midi_ble_service_register_callback(struct ble_gatt_register_ctxt *ctxt, + void *) { + ESP_LOGI("CS-BLEMIDI", "service event %d", ctxt->op); + [[maybe_unused]] char buf[BLE_UUID_STR_LEN] {}; + + switch (ctxt->op) { + case BLE_GATT_REGISTER_OP_SVC: + ESP_LOGI("CS-BLEMIDI", "registering service %s with handle=%d", + ble_uuid_to_str(ctxt->svc.svc_def->uuid, buf), + ctxt->svc.handle); + break; + + case BLE_GATT_REGISTER_OP_CHR: + ESP_LOGI("CS-BLEMIDI", + "registering characteristic %s with " + "def_handle=%d val_handle=%d", + ble_uuid_to_str(ctxt->chr.chr_def->uuid, buf), + ctxt->chr.def_handle, ctxt->chr.val_handle); + // Save the handle to the MIDI characteristic + if (ble_uuid_cmp(ctxt->chr.chr_def->uuid, + &midi_ble_characteristic_uuid.u) == 0) { + ESP_LOGI("CS-BLEMIDI", "MIDI char: %d", ctxt->chr.val_handle); + cs::midi_ble_nimble::state->midi_characteristic_handle = + ctxt->chr.val_handle; + } + break; + + case BLE_GATT_REGISTER_OP_DSC: + ESP_LOGI("CS-BLEMIDI", "registering descriptor %s with handle=%d", + ble_uuid_to_str(ctxt->dsc.dsc_def->uuid, buf), + ctxt->dsc.handle); + break; + + default: ESP_LOGI("CS-BLEMIDI", "Unknown op %d", ctxt->op); break; + } +} + +/// Called for GAP events like (dis)connection, MTU updates, etc. +inline int cs_midi_ble_gap_callback(struct ble_gap_event *event, void *) { + ESP_LOGI("CS-BLEMIDI", "gap event %d", +event->type); + switch (event->type) { + // A new connection was established or a connection attempt failed + case BLE_GAP_EVENT_CONNECT: { + ESP_LOGI("CS-BLEMIDI", "connection %s; status=%d", + event->connect.status == 0 ? "established" : "failed", + event->connect.status); + if (auto rc = ble_att_set_preferred_mtu(512); rc != 0) { + ESP_LOGE("CS-BLEMIDI", "failed to set preferred MTU; rc=%d", + rc); + return rc; + } + + if (event->connect.status == 0) { + struct ble_gap_conn_desc desc; + auto rc = ble_gap_conn_find(event->connect.conn_handle, &desc); + assert(rc == 0); + cs::midi_ble_nimble::print_conn_desc(&desc); + if (auto *inst = cs::midi_ble_nimble::state->instance) + inst->handleConnect( + cs::BLEConnectionHandle {event->connect.conn_handle}); + } else { + // Connection failed; resume advertising + cs::midi_ble_nimble::advertise( + cs::midi_ble_nimble::state->address_type); + } + } break; + + // Disconnected + case BLE_GAP_EVENT_DISCONNECT: { + ESP_LOGI("CS-BLEMIDI", "disconnect; reason=%d", + event->disconnect.reason); + cs::midi_ble_nimble::print_conn_desc(&event->disconnect.conn); + + if (auto *inst = cs::midi_ble_nimble::state->instance) + inst->handleDisconnect(cs::BLEConnectionHandle { + event->disconnect.conn.conn_handle}); + // Connection terminated; resume advertising + cs::midi_ble_nimble::advertise( + cs::midi_ble_nimble::state->address_type); + } break; + + // Central has updated the connection parameters + case BLE_GAP_EVENT_CONN_UPDATE: { + ESP_LOGI("CS-BLEMIDI", "connection updated; status=%d ", + event->conn_update.status); + struct ble_gap_conn_desc desc; + auto rc = ble_gap_conn_find(event->conn_update.conn_handle, &desc); + assert(rc == 0); + cs::midi_ble_nimble::print_conn_desc(&desc); + } break; + + // Advertising done (e.g. after reaching the specified timeout) + case BLE_GAP_EVENT_ADV_COMPLETE: { + ESP_LOGI("CS-BLEMIDI", "adv complete"); + cs::midi_ble_nimble::advertise( + cs::midi_ble_nimble::state->address_type); + } break; + + // Encryption has been enabled or disabled for this connection + case BLE_GAP_EVENT_ENC_CHANGE: { + ESP_LOGI("CS-BLEMIDI", "encryption change event; status=%d ", + event->enc_change.status); + struct ble_gap_conn_desc desc; + auto rc = ble_gap_conn_find(event->enc_change.conn_handle, &desc); + assert(rc == 0); + cs::midi_ble_nimble::print_conn_desc(&desc); + } break; + + // Subscription (e.g. when a CCCD is updated) + case BLE_GAP_EVENT_SUBSCRIBE: { + ESP_LOGI("CS-BLEMIDI", + "subscribe event; cur_notify=%d val_handle=%d", + event->subscribe.cur_notify, event->subscribe.attr_handle); + if (event->subscribe.attr_handle == + cs::midi_ble_nimble::state->midi_characteristic_handle) + if (auto *inst = cs::midi_ble_nimble::state->instance) + inst->handleSubscribe( + cs::BLEConnectionHandle {event->subscribe.conn_handle}, + cs::BLECharacteristicHandle { + event->subscribe.attr_handle}, + event->subscribe.cur_notify); + } break; + + // MTU updated (used to update the packet/buffer size) + case BLE_GAP_EVENT_MTU: { + ESP_LOGI("CS-BLEMIDI", "mtu update event; conn_handle=%d mtu=%d", + event->mtu.conn_handle, event->mtu.value); + if (auto *inst = cs::midi_ble_nimble::state->instance) + inst->handleMTU( + cs::BLEConnectionHandle {event->mtu.conn_handle}, + event->mtu.value); + } break; + + // Repeat pairing + case BLE_GAP_EVENT_REPEAT_PAIRING: { + // We already have a bond with the peer, but it is attempting to + // establish a new secure link. This app sacrifices security for + // convenience: just throw away the old bond and accept the new link. + + // Delete the old bond. + struct ble_gap_conn_desc desc; + auto rc = + ble_gap_conn_find(event->repeat_pairing.conn_handle, &desc); + assert(rc == 0); + ble_store_util_delete_peer(&desc.peer_id_addr); + + // Return BLE_GAP_REPEAT_PAIRING_RETRY to indicate that the host should + // continue with the pairing operation. + return BLE_GAP_REPEAT_PAIRING_RETRY; + } + } + + return 0; +} + +#endif // CONFIG_BT_BLE_ENABLED +#endif // ESP32 \ No newline at end of file diff --git a/src/MIDI_Interfaces/BLEMIDI/ESP32-NimBLE/gatt.c.ipp b/src/MIDI_Interfaces/BLEMIDI/ESP32-NimBLE/gatt.c.ipp new file mode 100644 index 0000000000..8087e45942 --- /dev/null +++ b/src/MIDI_Interfaces/BLEMIDI/ESP32-NimBLE/gatt.c.ipp @@ -0,0 +1,96 @@ +#ifdef ESP32 +#include +#if CONFIG_BT_BLE_ENABLED + +#include "ble.h" + +#if CS_MIDI_BLE_ESP_IDF_NIMBLE +#include +#else +#include +#endif + +#include "callbacks.h" + +#ifdef __cplusplus + +// This should really be done in C, but we have to compile only if the user +// includes these files explicitly, so we rely on some GCC C++ extensions here. + +inline const ble_uuid128_t midi_ble_service_uuid { + .u {.type = BLE_UUID_TYPE_128}, + .value {0x00, 0xc7, 0xc4, 0x4e, 0xe3, 0x6c, // + 0x51, 0xa7, // + 0x33, 0x4b, // + 0xe8, 0xed, // + 0x5a, 0x0e, 0xb8, 0x03}, +}; +inline const ble_uuid128_t midi_ble_characteristic_uuid { + .u {.type = BLE_UUID_TYPE_128}, + .value {0xf3, 0x6b, 0x10, 0x9d, 0x66, 0xf2, // + 0xa9, 0xa1, // + 0x12, 0x41, // + 0x68, 0x38, // + 0xdb, 0xe5, 0x72, 0x77}, +}; + +inline const struct ble_gatt_chr_def midi_ble_characteristic[] = { + {.uuid = &midi_ble_characteristic_uuid.u, + .access_cb = cs_midi_ble_characteristic_callback, + .arg = nullptr, + .descriptors = nullptr, + .flags = BLE_GATT_CHR_F_READ | // BLE_GATT_CHR_F_READ_ENC | + BLE_GATT_CHR_F_WRITE_NO_RSP | // BLE_GATT_CHR_F_WRITE_ENC | + BLE_GATT_CHR_F_NOTIFY, + .min_key_size = 0, + .val_handle = nullptr}, + {}, // sentinel +}; + +inline const struct ble_gatt_svc_def midi_ble_service[] = { + {.type = BLE_GATT_SVC_TYPE_PRIMARY, + .uuid = &midi_ble_service_uuid.u, + .includes = nullptr, + .characteristics = midi_ble_characteristic}, + {}, // sentinel +}; + +#else + +const ble_uuid128_t midi_ble_service_uuid = + BLE_UUID128_INIT(0x00, 0xc7, 0xc4, 0x4e, 0xe3, 0x6c, // + 0x51, 0xa7, // + 0x33, 0x4b, // + 0xe8, 0xed, // + 0x5a, 0x0e, 0xb8, 0x03); +const ble_uuid128_t midi_ble_characteristic_uuid = + BLE_UUID128_INIT(0xf3, 0x6b, 0x10, 0x9d, 0x66, 0xf2, // + 0xa9, 0xa1, // + 0x12, 0x41, // + 0x68, 0x38, // + 0xdb, 0xe5, 0x72, 0x77); + +static const struct ble_gatt_chr_def midi_ble_characteristic[] = { + {.uuid = &midi_ble_characteristic_uuid.u, + .access_cb = cs_midi_ble_characteristic_callback, + .flags = BLE_GATT_CHR_F_READ | // BLE_GATT_CHR_F_READ_ENC | + BLE_GATT_CHR_F_WRITE_NO_RSP | // BLE_GATT_CHR_F_WRITE_ENC | + BLE_GATT_CHR_F_NOTIFY}, + {0}, // sentinel +}; + +static const struct ble_gatt_svc_def midi_ble_service[] = { + {.type = BLE_GATT_SVC_TYPE_PRIMARY, + .uuid = &midi_ble_service_uuid.u, + .characteristics = midi_ble_characteristic}, + {0}, // sentinel +}; + +#endif + +inline const struct ble_gatt_svc_def *midi_ble_get_service(void) { + return midi_ble_service; +} + +#endif // CONFIG_BT_BLE_ENABLED +#endif // ESP32 diff --git a/src/MIDI_Interfaces/BLEMIDI/ESP32-NimBLE/gatt.h b/src/MIDI_Interfaces/BLEMIDI/ESP32-NimBLE/gatt.h new file mode 100644 index 0000000000..3c27a60c69 --- /dev/null +++ b/src/MIDI_Interfaces/BLEMIDI/ESP32-NimBLE/gatt.h @@ -0,0 +1,26 @@ +#pragma once + +#include "ble.h" + +#if CS_MIDI_BLE_ESP_IDF_NIMBLE +#include +#else +#include +#endif + +#include "ble-macro-fix.h" + +extern const ble_uuid128_t midi_ble_service_uuid; +extern const ble_uuid128_t midi_ble_characteristic_uuid; + +#ifdef __cplusplus +extern "C" { +#endif + +const struct ble_gatt_svc_def *midi_ble_get_service(void); + +#ifdef __cplusplus +} +#endif + +#include "gatt.c.ipp" diff --git a/src/MIDI_Interfaces/BLEMIDI/ESP32-NimBLE/init.hpp b/src/MIDI_Interfaces/BLEMIDI/ESP32-NimBLE/init.hpp new file mode 100644 index 0000000000..0af3622618 --- /dev/null +++ b/src/MIDI_Interfaces/BLEMIDI/ESP32-NimBLE/init.hpp @@ -0,0 +1,9 @@ +#pragma once + +namespace cs::midi_ble_nimble { + +bool init_hardware(); + +} + +#include "init.ipp" diff --git a/src/MIDI_Interfaces/BLEMIDI/ESP32-NimBLE/init.ipp b/src/MIDI_Interfaces/BLEMIDI/ESP32-NimBLE/init.ipp new file mode 100644 index 0000000000..f1472ebbb2 --- /dev/null +++ b/src/MIDI_Interfaces/BLEMIDI/ESP32-NimBLE/init.ipp @@ -0,0 +1,60 @@ +#ifdef ESP32 +#include +#if CONFIG_BT_BLE_ENABLED + +#include "ble.h" + +#if CS_MIDI_BLE_ESP_IDF_NIMBLE +#include +#else +#include +#include +#endif +#ifdef ESP_PLATFORM +#include +#include +#endif + +#include "ble-macro-fix.h" +#include "util.hpp" + +namespace cs::midi_ble_nimble { + +inline bool init_hardware() { +#ifdef ESP_PLATFORM +#ifdef CONFIG_ENABLE_ARDUINO_DEPENDS + // make sure the linker includes esp32-hal-bt.c so Arduino init doesn't release BLE memory. + btStarted(); +#endif + auto nvs_flash_init_rc = nvs_flash_init(); + if (nvs_flash_init_rc == ESP_ERR_NVS_NO_FREE_PAGES || + nvs_flash_init_rc == ESP_ERR_NVS_NEW_VERSION_FOUND) { + ESP_ERROR_CHECK(nvs_flash_erase()); + nvs_flash_init_rc = nvs_flash_init(); + } + ESP_ERROR_CHECK(nvs_flash_init_rc); + + esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT(); +#if defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S3) + bt_cfg.bluetooth_mode = ESP_BT_MODE_BLE; +#else + bt_cfg.mode = ESP_BT_MODE_BLE; +#endif + ESP_ERROR_CHECK(esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT)); + ESP_ERROR_CHECK(esp_bt_controller_init(&bt_cfg)); + ESP_ERROR_CHECK(esp_bt_controller_enable(ESP_BT_MODE_BLE)); + ESP_ERROR_CHECK(esp_nimble_hci_init()); +#endif + +#if CS_MIDI_BLE_ESP_IDF_NIMBLE + CS_CHECK_ESP(nimble_port_init()); +#else + nimble_port_init(); +#endif + return true; +} + +} // namespace cs::midi_ble_nimble + +#endif // CONFIG_BT_BLE_ENABLED +#endif // ESP32 \ No newline at end of file diff --git a/src/MIDI_Interfaces/BLEMIDI/ESP32-NimBLE/util.hpp b/src/MIDI_Interfaces/BLEMIDI/ESP32-NimBLE/util.hpp new file mode 100644 index 0000000000..c30e3e69b5 --- /dev/null +++ b/src/MIDI_Interfaces/BLEMIDI/ESP32-NimBLE/util.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include "ble.h" + +#define CS_CHECK_ESP(x) \ + do { \ + if (auto ret = x; ret != ESP_OK) { \ + ESP_LOGE("CS-BLEMIDI", "Failed to call '%s': %s (%d)", #x, \ + esp_err_to_name(ret), static_cast(ret)); \ + return false; \ + } \ + } while (0) +#define CS_CHECK_ZERO(x) \ + do { \ + if (auto ret = x; ret != 0) { \ + ESP_LOGE("CS-BLEMIDI", "Failed to call '%s': (%d)", #x, \ + static_cast(ret)); \ + return false; \ + } \ + } while (0) +#define CS_CHECK_ZERO_V(x) \ + do { \ + if (auto ret = x; ret != 0) { \ + ESP_LOGE("CS-BLEMIDI", "Failed to call '%s': (%d)", #x, \ + static_cast(ret)); \ + return; \ + } \ + } while (0) diff --git a/src/MIDI_Interfaces/BLEMIDI/ESP32/advertising.c b/src/MIDI_Interfaces/BLEMIDI/ESP32/advertising.c index 4166132ede..9f358b26ac 100644 --- a/src/MIDI_Interfaces/BLEMIDI/ESP32/advertising.c +++ b/src/MIDI_Interfaces/BLEMIDI/ESP32/advertising.c @@ -10,7 +10,7 @@ static esp_ble_adv_data_t adv_data = { .set_scan_rsp = false, .include_name = false, - .include_txpower = false, + .include_txpower = true, // Intervals as multiples of 1.25 milliseconds (e.g.0x000C = 15 ms) .min_interval = 0x000C, .max_interval = 0x000C, @@ -28,16 +28,15 @@ static esp_ble_adv_data_t adv_data = { static esp_ble_adv_data_t adv_data_rsp = { .set_scan_rsp = true, .include_name = true, - .include_txpower = true, - // Intervals as multiples of 1.25 milliseconds (e.g.0x000C = 15 ms) - .min_interval = 0x000C, - .max_interval = 0x000C, + .include_txpower = false, + // Zero means not included + .min_interval = 0x0000, + .max_interval = 0x0000, .appearance = 0x00, .manufacturer_len = 0, .p_manufacturer_data = NULL, .service_data_len = 0, .p_service_data = NULL, - // Service advertisement will be set later: .service_uuid_len = 0, .p_service_uuid = NULL, .flag = ESP_BLE_ADV_FLAG_GEN_DISC | ESP_BLE_ADV_FLAG_BREDR_NOT_SPT, @@ -65,6 +64,17 @@ void advertising_set_service_uuid(const uint8_t uuid[], uint16_t length) { adv_data.service_uuid_len = length; } +void advertising_set_connection_interval(uint16_t itvl_min, uint16_t itvl_max) { + adv_data.min_interval = itvl_min; + adv_data.max_interval = itvl_max; +} + +void advertising_get_connection_interval(uint16_t *itvl_min, + uint16_t *itvl_max) { + *itvl_min = adv_data.min_interval; + *itvl_max = adv_data.max_interval; +} + bool advertising_config(void) { ESP_LOGI("MIDIBLE", "advertising_config"); esp_err_t ret = esp_ble_gap_config_adv_data(&adv_data); diff --git a/src/MIDI_Interfaces/BLEMIDI/ESP32/advertising.h b/src/MIDI_Interfaces/BLEMIDI/ESP32/advertising.h index 565ae28ece..b968461637 100644 --- a/src/MIDI_Interfaces/BLEMIDI/ESP32/advertising.h +++ b/src/MIDI_Interfaces/BLEMIDI/ESP32/advertising.h @@ -15,6 +15,11 @@ extern "C" { /// Set the UUID of the service to be advertised. /// @note UUID should persist, captures pointer, doesn't copy data. void advertising_set_service_uuid(const uint8_t uuid[], uint16_t length); +/// Set the connection interval range in the advertising data. +void advertising_set_connection_interval(uint16_t itvl_min, uint16_t itvl_max); +/// Get the connection interval range from the advertising data. +void advertising_get_connection_interval(uint16_t *itvl_min, + uint16_t *itvl_max); /// Configure the advertising data, register with the Bluetooth driver. Will /// eventually trigger the callbacks to start advertising. bool advertising_config(void); diff --git a/src/MIDI_Interfaces/BLEMIDI/ESP32/app.cpp b/src/MIDI_Interfaces/BLEMIDI/ESP32/app.cpp new file mode 100644 index 0000000000..c01f1e027b --- /dev/null +++ b/src/MIDI_Interfaces/BLEMIDI/ESP32/app.cpp @@ -0,0 +1,79 @@ +#ifdef ESP32 +#include +#if CONFIG_BT_BLE_ENABLED + +#include "advertising.h" +#include "app.h" +#include "midi.h" + +namespace cs::midi_ble_bluedroid { +namespace { +MIDIBLEInstance *midi_ble_bluedroid_instance = nullptr; +} +} // namespace cs::midi_ble_bluedroid + +using namespace cs; +using namespace cs::midi_ble_bluedroid; + +void midi_ble_instance_handle_connect(uint16_t conn_handle) { + if (midi_ble_bluedroid_instance) + midi_ble_bluedroid_instance->handleConnect( + BLEConnectionHandle {conn_handle}); +} +void midi_ble_instance_handle_disconnect(uint16_t conn_handle) { + if (midi_ble_bluedroid_instance) + midi_ble_bluedroid_instance->handleDisconnect( + BLEConnectionHandle {conn_handle}); +} +void midi_ble_instance_handle_mtu(uint16_t conn_handle, uint16_t mtu) { + if (midi_ble_bluedroid_instance) + midi_ble_bluedroid_instance->handleMTU( + BLEConnectionHandle {conn_handle}, mtu); +} +void midi_ble_instance_handle_subscribe(uint16_t conn_handle, + uint16_t char_handle, bool notify) { + if (midi_ble_bluedroid_instance) + midi_ble_bluedroid_instance->handleSubscribe( + BLEConnectionHandle {conn_handle}, + BLECharacteristicHandle {char_handle}, notify); +} +void midi_ble_instance_handle_data(uint16_t conn_handle, const uint8_t *data, + uint16_t length) { + if (midi_ble_bluedroid_instance) { + BLEDataView view {data, length}; + auto data_gen = [view {view}]() mutable { +#if __cplusplus >= 201402 + return std::exchange(view, {}); +#else + auto ret = view; + view = {}; + return ret; +#endif + }; + midi_ble_bluedroid_instance->handleData( + BLEConnectionHandle {conn_handle}, + BLEDataGenerator {compat::in_place, std::move(data_gen)}, + BLEDataLifetime::ConsumeImmediately); + } +} + +namespace cs::midi_ble_bluedroid { + +bool init(MIDIBLEInstance &instance, BLESettings settings) { + midi_ble_bluedroid_instance = &instance; + set_midi_ble_name(settings.device_name); + advertising_set_connection_interval(settings.connection_interval.minimum, + settings.connection_interval.maximum); + return midi_init(); +} + +bool notify(BLEConnectionHandle conn_handle, + BLECharacteristicHandle char_handle, BLEDataView data) { + return midi_notify(conn_handle.conn, char_handle.characteristic, data.data, + data.length); +} + +} // namespace cs::midi_ble_bluedroid + +#endif +#endif \ No newline at end of file diff --git a/src/MIDI_Interfaces/BLEMIDI/ESP32/app.h b/src/MIDI_Interfaces/BLEMIDI/ESP32/app.h new file mode 100644 index 0000000000..f0a0fdb7ac --- /dev/null +++ b/src/MIDI_Interfaces/BLEMIDI/ESP32/app.h @@ -0,0 +1,33 @@ +#pragma once + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +void midi_ble_instance_handle_connect(uint16_t conn_handle); +void midi_ble_instance_handle_disconnect(uint16_t conn_handle); +void midi_ble_instance_handle_mtu(uint16_t conn_handle, uint16_t mtu); +void midi_ble_instance_handle_subscribe(uint16_t conn_handle, + uint16_t char_handle, bool notify); +void midi_ble_instance_handle_data(uint16_t conn_handle, const uint8_t *data, + uint16_t length); + +#ifdef __cplusplus +} +#endif + +#ifdef __cplusplus +#include "../BLEAPI.hpp" + +namespace cs::midi_ble_bluedroid { + +bool init(MIDIBLEInstance &instance, BLESettings settings); +bool notify(BLEConnectionHandle conn_handle, + BLECharacteristicHandle char_handle, BLEDataView data); + +} // namespace cs::midi_ble_bluedroid + +#endif \ No newline at end of file diff --git a/src/MIDI_Interfaces/BLEMIDI/ESP32/ble2902.c b/src/MIDI_Interfaces/BLEMIDI/ESP32/ble2902.c index 28bafca0f3..fd3df40b84 100644 --- a/src/MIDI_Interfaces/BLEMIDI/ESP32/ble2902.c +++ b/src/MIDI_Interfaces/BLEMIDI/ESP32/ble2902.c @@ -2,8 +2,10 @@ #include #if CONFIG_BT_BLE_ENABLED +#include "app.h" #include "ble2902.h" #include "logging.h" +#include "midi-private.h" #include // memcpy @@ -12,18 +14,20 @@ void ble2902_handle_write(esp_gatt_if_t gatts_if, // The actual writing of data and sending a response is already handled // by Bluedroid. uint16_t handle = param->write.handle; - if (ble2902_get_value(handle) == 0x0001) { + uint16_t value = ble2902_get_value(handle); + if (value == 0x0001) { ESP_LOGI("MIDIBLE", "notify enable"); - } else if (ble2902_get_value(handle) == 0x0002) { + } else if (value == 0x0002) { ESP_LOGI("MIDIBLE", "indicate enable"); - } else if (ble2902_get_value(handle) == 0x0003) { + } else if (value == 0x0003) { ESP_LOGI("MIDIBLE", "notify & indicate enable"); - } else if (ble2902_get_value(handle) == 0x0000) { + } else if (value == 0x0000) { ESP_LOGI("MIDIBLE", "notify/indicate disable "); } else { - ESP_LOGE("MIDIBLE", "Unknown descriptor value %04x", - ble2902_get_value(handle)); + ESP_LOGE("MIDIBLE", "Unknown descriptor value %04x", value); } + midi_ble_instance_handle_subscribe( + param->write.conn_id, midi_get_characteristic_handle(), value & 0x0001); } uint16_t ble2902_get_value(uint16_t handle) { @@ -39,13 +43,5 @@ uint16_t ble2902_get_value(uint16_t handle) { return ret; } -bool ble2902_notifications_enabled(uint16_t handle) { - return ble2902_get_value(handle) & 0x0001; -} - -bool ble2902_indications_enabled(uint16_t handle) { - return ble2902_get_value(handle) & 0x0002; -} - #endif #endif \ No newline at end of file diff --git a/src/MIDI_Interfaces/BLEMIDI/ESP32/ble2902.h b/src/MIDI_Interfaces/BLEMIDI/ESP32/ble2902.h index 66c0d685ec..497267a6fc 100644 --- a/src/MIDI_Interfaces/BLEMIDI/ESP32/ble2902.h +++ b/src/MIDI_Interfaces/BLEMIDI/ESP32/ble2902.h @@ -24,14 +24,6 @@ extern "C" { /// debug information or error messages. void ble2902_handle_write(esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param); -/// Check if notifications are enabled by the client. -/// @param handle -/// The handle of the CCC descriptor. -bool ble2902_notifications_enabled(uint16_t handle); -/// Check if indications are enabled by the client. -/// @param handle -/// The handle of the CCC descriptor. -bool ble2902_indications_enabled(uint16_t handle); /// Get the value of the descriptor. /// @param handle /// The handle of the CCC descriptor. diff --git a/src/MIDI_Interfaces/BLEMIDI/ESP32/midi-app.c b/src/MIDI_Interfaces/BLEMIDI/ESP32/midi-app.c index 05b4772507..4d09b6b1f5 100644 --- a/src/MIDI_Interfaces/BLEMIDI/ESP32/midi-app.c +++ b/src/MIDI_Interfaces/BLEMIDI/ESP32/midi-app.c @@ -125,15 +125,11 @@ static const esp_gatts_attr_db_t static uint16_t MIDI_handle_table[MIDI_ATTRIBUTE_TABLE_SIZE] = {}; static uint16_t midi_gatts_if = ESP_GATT_IF_NONE; -static uint16_t midi_conn_id = 0; void midi_register_interface(esp_gatt_if_t gatts_if) { midi_gatts_if = gatts_if; } -void midi_set_connection_id(uint16_t conn_id) { midi_conn_id = conn_id; } -uint16_t midi_get_connection_id(void) { return midi_conn_id; } - void midi_handle_register_app_event(esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param) { // Set the Service UUID for advertisement diff --git a/src/MIDI_Interfaces/BLEMIDI/ESP32/midi-connection.c b/src/MIDI_Interfaces/BLEMIDI/ESP32/midi-connection.c index 99c1ccbe3c..4152b9b8f8 100644 --- a/src/MIDI_Interfaces/BLEMIDI/ESP32/midi-connection.c +++ b/src/MIDI_Interfaces/BLEMIDI/ESP32/midi-connection.c @@ -9,6 +9,7 @@ */ #include "advertising.h" +#include "app.h" #include "logging.h" #include "midi-private.h" @@ -26,11 +27,11 @@ void midi_handle_connect_event(esp_gatt_if_t gatts_if, param->connect.remote_bda[3], param->connect.remote_bda[4], param->connect.remote_bda[5]); - midi_set_connection_id(param->connect.conn_id); + midi_ble_instance_handle_connect(param->connect.conn_id); - // Why do we need to update the connection parameters? - // How are these parameters different from the advertising - // parameters? + // Do we need to update the connection parameters? + // Can we just rely on the central picking sensible defaults or + // honoring the advertising connection interval range? // For the IOS system, please reference the apple official documents about // the ble connection parameters restrictions: @@ -39,8 +40,8 @@ void midi_handle_connect_event(esp_gatt_if_t gatts_if, esp_ble_conn_update_params_t conn_params; memcpy(conn_params.bda, param->connect.remote_bda, sizeof(esp_bd_addr_t)); - conn_params.max_int = 0x000C; - conn_params.min_int = 0x000C; + advertising_get_connection_interval(&conn_params.min_int, + &conn_params.max_int); conn_params.latency = 0; conn_params.timeout = 400; // timeout = 400*10ms = 4s esp_ble_gap_update_conn_params(&conn_params); @@ -49,9 +50,8 @@ void midi_handle_connect_event(esp_gatt_if_t gatts_if, void midi_handle_disconnect_event(esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param) { ESP_LOGI("MIDIBLE", "Disconnect reason: %d", param->disconnect.reason); - - midi_set_connection_id(0); - + midi_ble_instance_handle_disconnect(param->disconnect.conn_id); + // Start advertising again advertising_config(); } diff --git a/src/MIDI_Interfaces/BLEMIDI/ESP32/midi-mtu.c b/src/MIDI_Interfaces/BLEMIDI/ESP32/midi-mtu.c index 0027268079..88e94d8530 100644 --- a/src/MIDI_Interfaces/BLEMIDI/ESP32/midi-mtu.c +++ b/src/MIDI_Interfaces/BLEMIDI/ESP32/midi-mtu.c @@ -9,24 +9,16 @@ * BLE. */ +#include "app.h" #include "logging.h" #include "midi-private.h" -static midi_mtu_callback_t midi_mtu_callback = NULL; -static uint16_t midi_mtu = 0; - -void midi_set_mtu_callback(midi_mtu_callback_t cb) { midi_mtu_callback = cb; } - void midi_handle_mtu_event(esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param) { - midi_mtu = param->mtu.mtu; + uint16_t midi_mtu = param->mtu.mtu; ESP_LOGI("MIDIBLE", "MTU: %d", midi_mtu); - if (midi_mtu_callback) { - midi_mtu_callback(midi_mtu); - } + midi_ble_instance_handle_mtu(param->mtu.conn_id, midi_mtu); } -uint16_t midi_get_mtu(void) { return midi_mtu; } - #endif #endif \ No newline at end of file diff --git a/src/MIDI_Interfaces/BLEMIDI/ESP32/midi-notify.c b/src/MIDI_Interfaces/BLEMIDI/ESP32/midi-notify.c index 9bf409142c..d84a1ae339 100644 --- a/src/MIDI_Interfaces/BLEMIDI/ESP32/midi-notify.c +++ b/src/MIDI_Interfaces/BLEMIDI/ESP32/midi-notify.c @@ -11,16 +11,13 @@ #include "ble2902.h" #include "midi-private.h" -bool midi_notify(const uint8_t *data, size_t length) { +bool midi_notify(uint16_t conn_handle, uint16_t char_handle, + const uint8_t *data, size_t length) { if (midi_get_gatts_if() == ESP_GATT_IF_NONE) return false; - if (!ble2902_notifications_enabled(midi_get_descriptor_handle())) - return false; - if (length > midi_get_mtu() - 3) - return false; - esp_err_t ret = esp_ble_gatts_send_indicate( - midi_get_gatts_if(), midi_get_connection_id(), - midi_get_characteristic_handle(), length, (uint8_t *)data, false); + esp_err_t ret = esp_ble_gatts_send_indicate(midi_get_gatts_if(), + conn_handle, char_handle, + length, (uint8_t *)data, false); return ret == ESP_OK; } diff --git a/src/MIDI_Interfaces/BLEMIDI/ESP32/midi-private.h b/src/MIDI_Interfaces/BLEMIDI/ESP32/midi-private.h index 7a4ce9d024..e263827cee 100644 --- a/src/MIDI_Interfaces/BLEMIDI/ESP32/midi-private.h +++ b/src/MIDI_Interfaces/BLEMIDI/ESP32/midi-private.h @@ -37,6 +37,4 @@ uint16_t midi_get_descriptor_handle(void); void midi_register_interface(esp_gatt_if_t gatts_if); uint16_t midi_get_app_id(void); -void midi_set_connection_id(uint16_t conn_id); -uint16_t midi_get_connection_id(void); uint16_t midi_get_gatts_if(void); diff --git a/src/MIDI_Interfaces/BLEMIDI/ESP32/midi-write.c b/src/MIDI_Interfaces/BLEMIDI/ESP32/midi-write.c index a1c9a8d6a3..e8f6a2de9b 100644 --- a/src/MIDI_Interfaces/BLEMIDI/ESP32/midi-write.c +++ b/src/MIDI_Interfaces/BLEMIDI/ESP32/midi-write.c @@ -7,99 +7,33 @@ * * Handler for BLE MIDI write events. These events occur when the client sends * us MIDI data. - * - * @todo This file also implements long writes (prepare write/execute write) - * events. This hasn't been tested, since it seems like most MIDI BLE - * clients use normal write packets only. */ +#include "app.h" #include "logging.h" #include "midi-private.h" #include -#include -#include // malloc -#include // memcpy - -typedef struct { - uint8_t *prepare_buf; - int prepare_len; -} prepare_type_env_t; -static prepare_type_env_t MIDI_prepare_write_env; -const static size_t PREPARE_BUF_MAX_SIZE = ESP_GATT_MAX_MTU_SIZE; - -static midi_write_callback_t midi_callback = NULL; - -void midi_set_write_callback(midi_write_callback_t cb) { midi_callback = cb; } - void midi_handle_write_event(esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param) { - if (param->write.need_rsp) - return; // MIDI never needs response, return without acknowledgement - - // If this is a long write across multiple packets - if (param->write.is_prep) { - esp_gatt_status_t status = ESP_GATT_OK; - // Allocate a buffer to receive the write data - if (MIDI_prepare_write_env.prepare_buf == NULL) { - MIDI_prepare_write_env.prepare_buf = - (uint8_t *)malloc(PREPARE_BUF_MAX_SIZE * sizeof(uint8_t)); - MIDI_prepare_write_env.prepare_len = 0; - if (MIDI_prepare_write_env.prepare_buf == NULL) { - ESP_LOGE("MIDIBLE", "Gatt_server prep no mem\n"); - status = ESP_GATT_NO_RESOURCES; - } - } - // Make sure that the data fits in the buffer - if (param->write.offset > PREPARE_BUF_MAX_SIZE) { - status = ESP_GATT_INVALID_OFFSET; - } - if ((param->write.offset + param->write.len) > PREPARE_BUF_MAX_SIZE) { - status = ESP_GATT_INVALID_ATTR_LEN; - } - - // If we're either out of buffer space or out of memory, don't store the - // data - if (status != ESP_GATT_OK) { - // We don't need to send a response for MIDI - return; - } - - // Copy the write data to the buffer - memcpy(MIDI_prepare_write_env.prepare_buf + param->write.offset, - param->write.value, param->write.len); - MIDI_prepare_write_env.prepare_len += param->write.len; - } else { - // Not a prepare event, handle the data immediately - if (midi_callback) - midi_callback(param->write.value, param->write.len); + if (param->write.need_rsp) { + // MIDI never needs a response, send an error response + esp_ble_gatts_send_response(gatts_if, param->write.conn_id, + param->write.trans_id, + ESP_GATT_REQ_NOT_SUPPORTED, NULL); + return; } + // Not a prepare event, handle the data immediately + midi_ble_instance_handle_data(param->write.conn_id, param->write.value, + param->write.len); } -// Logs prepared written data and frees the buffer void midi_handle_write_exec_event(esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param) { - if (param->exec_write.exec_write_flag == ESP_GATT_PREP_WRITE_CANCEL) { - ESP_LOGI("MIDIBLE", "ESP_GATT_PREP_WRITE_CANCEL"); - } - - if (midi_callback && MIDI_prepare_write_env.prepare_buf && - MIDI_prepare_write_env.prepare_len) - midi_callback(MIDI_prepare_write_env.prepare_buf, - MIDI_prepare_write_env.prepare_len); - - // if (*MIDI_char_handle == param->exec_write.handle) { - // TODO: how do we know the write was for this characteristic? - // Check transfer ID? Connection ID? - if (MIDI_prepare_write_env.prepare_buf) { - free(MIDI_prepare_write_env.prepare_buf); - MIDI_prepare_write_env.prepare_buf = NULL; - } - MIDI_prepare_write_env.prepare_len = 0; - esp_ble_gatts_send_response(gatts_if, param->exec_write.conn_id, - param->exec_write.trans_id, ESP_GATT_OK, NULL); + param->exec_write.trans_id, + ESP_GATT_REQ_NOT_SUPPORTED, NULL); } #endif diff --git a/src/MIDI_Interfaces/BLEMIDI/ESP32/midi.h b/src/MIDI_Interfaces/BLEMIDI/ESP32/midi.h index f32f0a8a0e..bd46a8a790 100644 --- a/src/MIDI_Interfaces/BLEMIDI/ESP32/midi.h +++ b/src/MIDI_Interfaces/BLEMIDI/ESP32/midi.h @@ -14,27 +14,9 @@ extern "C" { #endif -/// Type for the MTU negotiation callback. -/// @warning Used in C code, use C calling convention (use `extern "C"` to -/// declare this function in C++ code). -typedef void (*midi_mtu_callback_t)(uint16_t); -/// Set the callback that is to be called when the MTU negotiation with the BLE -/// client is finished. -void midi_set_mtu_callback(midi_mtu_callback_t cb); -/// Get the current MTU (maximum transmission unit) of the link with the BLE -/// client. -uint16_t midi_get_mtu(void); - -/// Type for the BLE MIDI write callback. -/// @warning Used in C code, use C calling convention (use `extern "C"` to -/// declare this function in C++ code). -typedef void (*midi_write_callback_t)(const uint8_t *, size_t); -/// Set the callback that is to be called when the client writes (sends) a MIDI -/// packet. -void midi_set_write_callback(midi_write_callback_t cb); - /// Send a MIDI BLE notification to the client. -bool midi_notify(const uint8_t *data, size_t len); +bool midi_notify(uint16_t conn_handle, uint16_t char_handle, + const uint8_t *data, size_t len); /// Set the name of the BLE device. Must be set before calling @ref midi_init(). void set_midi_ble_name(const char *name); diff --git a/src/MIDI_Interfaces/BLEMIDI/ESP32Backend.hpp b/src/MIDI_Interfaces/BLEMIDI/ESP32Backend.hpp new file mode 100644 index 0000000000..9443dd9884 --- /dev/null +++ b/src/MIDI_Interfaces/BLEMIDI/ESP32Backend.hpp @@ -0,0 +1,140 @@ +#pragma once + +#include + +#include "BLEAPI.hpp" +#include "BufferedBLEMIDIParser.hpp" +#include "ThreadedBLEMIDISender.hpp" +#include "Util/ESP32Threads.hpp" + +#include + +#ifndef ARDUINO +#define ESP_LOGD(...) ((void)0) +#define ESP_LOGE(...) ((void)0) +#define ESP_LOGI(...) ((void)0) +#endif + +BEGIN_CS_NAMESPACE + +/// ESP32 backend intended to be plugged into @ref GenericBLEMIDI_Interface. +/// @p Impl can be used to select different low-level BLE stacks. +template +class ESP32BLEBackend : private ThreadedBLEMIDISender>, + private MIDIBLEInstance { + protected: + [[no_unique_address]] Impl impl; + using Sender = ThreadedBLEMIDISender; + friend Sender; + void sendData(BLEDataView data) { + auto chr = characteristic.load(); + auto con = connection.load(); + ESP_LOGD("CS-BLEMIDI", "conn=%d, char=%d", con.conn, + chr.characteristic); + if (chr.characteristic == 0xFFFF) + return; + impl.notify(con, chr, data); + } + std::atomic connection; + std::atomic characteristic; + + protected: + void handleConnect(BLEConnectionHandle conn_handle) override { + ESP_LOGD("CS-BLEMIDI", "conn=%d", conn_handle.conn); + this->connection.store(conn_handle); + } + void handleDisconnect( + [[maybe_unused]] BLEConnectionHandle conn_handle) override { + ESP_LOGD("CS-BLEMIDI", "conn=%d", conn_handle.conn); + this->connection.store({}); + this->characteristic.store({}); + } + void handleMTU([[maybe_unused]] BLEConnectionHandle conn_handle, + uint16_t mtu) override { + ESP_LOGD("CS-BLEMIDI", "conn=%d, mtu=%d", conn_handle.conn, mtu); + Sender::updateMTU(mtu); + } + void handleSubscribe(BLEConnectionHandle conn_handle, + BLECharacteristicHandle char_handle, + bool notify) override { + ESP_LOGD("CS-BLEMIDI", "conn=%d, char=%d, notify=%d", conn_handle.conn, + char_handle.characteristic, +notify); + if (notify) { + this->connection.store(conn_handle); + this->characteristic.store(char_handle); + } else { + this->characteristic.store({}); + } + } + void handleData([[maybe_unused]] BLEConnectionHandle conn_handle, + BLEDataGenerator &&data, BLEDataLifetime) override { + ESP_LOGD("CS-BLEMIDI", "conn=%d", conn_handle.conn); + BLEDataView packet = data(); + if (!packet) + return; + if (!parser.pushPacket(packet)) { + ESP_LOGE("CS-BLEMIDI", "BLE packet dropped, size=%d", + packet.length); + return; + } + while (BLEDataView cont = data()) { + if (!parser.pushPacket(cont, BLEDataType::Continuation)) { + ESP_LOGE("CS-BLEMIDI", "BLE chunk dropped, size=%d", + cont.length); + return; + } else { + ESP_LOGI("CS-BLEMIDI", "added chunk, size=%d", cont.length); + } + } + } + + private: + struct AtomicSize { +#ifdef ESP32 + constexpr static size_t alignment = 32; // default cache size +#else + constexpr static size_t alignment = 64; +#endif + AtomicSize(uint_fast16_t value) : value {value} {} + std::atomic_uint_fast16_t value; + uint_fast16_t load_acquire() const { + return value.load(std::memory_order_acquire); + } + void add_release(uint_fast16_t t) { + value.fetch_add(t, std::memory_order_release); + } + void sub_release(uint_fast16_t t) { + value.fetch_sub(t, std::memory_order_release); + } + }; + /// Contains incoming BLE MIDI data to be parsed. + BufferedBLEMIDIParser<4096, AtomicSize> parser; + + public: + using IncomingMIDIMessage = AnyMIDIMessage; + bool popMessage(IncomingMIDIMessage &incomingMessage) { + return parser.popMessage(incomingMessage); + } + + public: + void begin(BLESettings ble_settings) { + impl.init(*this, ble_settings); + // Need larger stack than default, pin to non-Arduino core + ScopedThreadConfig sc {4096, 3, true, "CS-BLEMIDI", 0}; + Sender::begin(); + } + void end() { + FATAL_ERROR(F("ESP32BLEBackend::end not implemented"), 0x3278); + } + bool isConnected() const { + return connection.load(std::memory_order_relaxed).conn != 0xFFFF; + } + using Sender::acquirePacket; + using Sender::forceMinMTU; + using Sender::getMinMTU; + using Sender::releasePacketAndNotify; + using Sender::sendNow; + using Sender::setTimeout; +}; + +END_CS_NAMESPACE \ No newline at end of file diff --git a/src/MIDI_Interfaces/BLEMIDI/ESP32BluedroidBackend.hpp b/src/MIDI_Interfaces/BLEMIDI/ESP32BluedroidBackend.hpp new file mode 100644 index 0000000000..785de1560e --- /dev/null +++ b/src/MIDI_Interfaces/BLEMIDI/ESP32BluedroidBackend.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include "ESP32/app.h" +#include "ESP32Backend.hpp" + +BEGIN_CS_NAMESPACE + +namespace ble_backend { +struct ESP32BluedroidBLE { + static constexpr auto notify = midi_ble_bluedroid::notify; + static constexpr auto init = midi_ble_bluedroid::init; +}; +} // namespace ble_backend + +/// ESP32 Bluedroid backend intended to be plugged into +/// @ref GenericBLEMIDI_Interface. +using ESP32BluedroidBackend = ESP32BLEBackend; + +END_CS_NAMESPACE \ No newline at end of file diff --git a/src/MIDI_Interfaces/BLEMIDI/ESP32NimBLEBackend.hpp b/src/MIDI_Interfaces/BLEMIDI/ESP32NimBLEBackend.hpp new file mode 100644 index 0000000000..84b06b4514 --- /dev/null +++ b/src/MIDI_Interfaces/BLEMIDI/ESP32NimBLEBackend.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include "ESP32/app.h" +#include "ESP32Backend.hpp" + +BEGIN_CS_NAMESPACE + +namespace ble_backend { +struct ESP32NimBLE { + static constexpr auto notify = midi_ble_bluedroid::notify; + static constexpr auto init = midi_ble_bluedroid::init; +}; +} // namespace ble_backend + +/// ESP32 NimBLE backend intended to be plugged into +/// @ref GenericBLEMIDI_Interface. +using ESP32NimBLEBackend = ESP32BLEBackend; + +END_CS_NAMESPACE \ No newline at end of file diff --git a/src/MIDI_Interfaces/BLEMIDI/MIDIMessageQueue.cpp b/src/MIDI_Interfaces/BLEMIDI/MIDIMessageQueue.cpp deleted file mode 100644 index 8f373daa38..0000000000 --- a/src/MIDI_Interfaces/BLEMIDI/MIDIMessageQueue.cpp +++ /dev/null @@ -1,103 +0,0 @@ -#if defined(ESP32) || !defined(ARDUINO) - -#include "MIDIMessageQueue.hpp" - -BEGIN_CS_NAMESPACE - -MIDIMessageQueue::MIDIMessageQueueElement::MIDIMessageQueueElement( - SysExMessage message, uint16_t timestamp) - : eventType(message.isLastChunk() ? MIDIReadEvent::SYSEX_MESSAGE - : MIDIReadEvent::SYSEX_CHUNK), - message(message), timestamp(timestamp) { - // SysEx data is copied, not stored by pointer, so allocate new - // storage for the SysEx data, and copy it if allocation was - // successful. - uint8_t *newBuffer = new uint8_t[message.length]; - // Make sure that allocation was successful. - if (newBuffer == nullptr) { - DEBUGREF(F("SysEx buffer allocation failed")); - this->eventType = MIDIReadEvent::NO_MESSAGE; - this->message.realtimemessage = 0x00; - } else { - // Copy the SysEx data to the newly allocated buffer - memcpy(newBuffer, message.data, message.length); - this->message.sysexmessage.data = newBuffer; - } -} - -MIDIMessageQueue::MIDIMessageQueueElement::MIDIMessageQueueElement( - MIDIMessageQueue::MIDIMessageQueueElement &&that) { - *this = std::move(that); -} - -MIDIMessageQueue::MIDIMessageQueueElement & -MIDIMessageQueue::MIDIMessageQueueElement::operator=( - MIDIMessageQueue::MIDIMessageQueueElement &&that) { - std::swap(this->eventType, that.eventType); - std::swap(this->message, that.message); - std::swap(this->timestamp, that.timestamp); - return *this; -} - -void MIDIMessageQueue::MIDIMessageQueueElement::release() { - if (eventType == MIDIReadEvent::SYSEX_CHUNK || - eventType == MIDIReadEvent::SYSEX_MESSAGE) { - delete[] message.sysexmessage.data; - message.sysexmessage.data = nullptr; - message.sysexmessage.length = 0; - eventType = MIDIReadEvent::NO_MESSAGE; - message.realtimemessage = 0x00; - } -} - -bool MIDIMessageQueue::push(ChannelMessage message, uint16_t timestamp) { - return push(MIDIMessageQueueElement(message, timestamp)); -} - -bool MIDIMessageQueue::push(SysCommonMessage message, uint16_t timestamp) { - return push(MIDIMessageQueueElement(message, timestamp)); -} - -bool MIDIMessageQueue::push(RealTimeMessage message, uint16_t timestamp) { - return push(MIDIMessageQueueElement(message, timestamp)); -} - -bool MIDIMessageQueue::push(SysExMessage message, uint16_t timestamp) { - if (storage.size() == size.load(std::memory_order_acquire)) - return false; - - // Allocate storage for the actual SysEx data and copy the data - MIDIMessageQueueElement el(message, timestamp); - // Check if allocation failed - if (el.eventType == MIDIReadEvent::NO_MESSAGE) - return true; // TODO: should we try again later? - - *write_p = std::move(el); - inc(write_p); - size.fetch_add(1, std::memory_order_release); - - return true; -} - -bool MIDIMessageQueue::pop(MIDIMessageQueueElement &message) { - if (size.load(std::memory_order_acquire) == 0) - return false; - message = std::move(*read_p); - read_p->release(); // release memory of old message - inc(read_p); - size.fetch_sub(1, std::memory_order_release); - return true; -} - -bool MIDIMessageQueue::push(MIDIMessageQueueElement &&message) { - if (storage.size() == size.load(std::memory_order_acquire)) - return false; - *write_p = std::move(message); - inc(write_p); - size.fetch_add(1, std::memory_order_release); - return true; -} - -END_CS_NAMESPACE - -#endif diff --git a/src/MIDI_Interfaces/BLEMIDI/MIDIMessageQueue.hpp b/src/MIDI_Interfaces/BLEMIDI/MIDIMessageQueue.hpp deleted file mode 100644 index 9fe3949ed0..0000000000 --- a/src/MIDI_Interfaces/BLEMIDI/MIDIMessageQueue.hpp +++ /dev/null @@ -1,83 +0,0 @@ -#pragma once - -#include -#include - -#include - -BEGIN_CS_NAMESPACE - -class MIDIMessageQueue { - public: - MIDIMessageQueue(size_t capacity) : storage(storage_t(capacity)) {} - - struct MIDIMessageQueueElement { - MIDIReadEvent eventType = MIDIReadEvent::NO_MESSAGE; - union Message { - ChannelMessage channelmessage; - SysCommonMessage syscommonmessage; - RealTimeMessage realtimemessage; - SysExMessage sysexmessage; - - Message() : realtimemessage(0x00) {} - Message(ChannelMessage msg) : channelmessage(msg) {} - Message(SysCommonMessage msg) : syscommonmessage(msg) {} - Message(RealTimeMessage msg) : realtimemessage(msg) {} - Message(SysExMessage msg) : sysexmessage(msg) {} - } message; - uint16_t timestamp = 0xFFFF; - - MIDIMessageQueueElement() = default; - MIDIMessageQueueElement(ChannelMessage message, uint16_t timestamp) - : eventType(MIDIReadEvent::CHANNEL_MESSAGE), message(message), - timestamp(timestamp) {} - MIDIMessageQueueElement(SysCommonMessage message, uint16_t timestamp) - : eventType(MIDIReadEvent::SYSCOMMON_MESSAGE), message(message), - timestamp(timestamp) {} - MIDIMessageQueueElement(RealTimeMessage message, uint16_t timestamp) - : eventType(MIDIReadEvent::REALTIME_MESSAGE), message(message), - timestamp(timestamp) {} - MIDIMessageQueueElement(SysExMessage message, uint16_t timestamp); - - /// No copy constructor. - MIDIMessageQueueElement(const MIDIMessageQueueElement &) = delete; - /// No copy assignment. - MIDIMessageQueueElement & - operator=(const MIDIMessageQueueElement &) = delete; - /// Move constructor. - MIDIMessageQueueElement(MIDIMessageQueueElement &&that); - /// Move assignemnt. - MIDIMessageQueueElement &operator=(MIDIMessageQueueElement &&that); - - /// Deallocate the storage for the SysEx data (if present). - void release(); - - ~MIDIMessageQueueElement() { release(); } - }; - - using storage_t = std::vector; - using iter_t = storage_t::iterator; - - public: - bool push(ChannelMessage message, uint16_t timestamp); - bool push(SysCommonMessage message, uint16_t timestamp); - bool push(RealTimeMessage message, uint16_t timestamp); - bool push(SysExMessage message, uint16_t timestamp); - - bool pop(MIDIMessageQueueElement &message); - - private: - storage_t storage = storage_t(64); - iter_t write_p = storage.begin(); - iter_t read_p = storage.begin(); - std::atomic_size_t size {0}; - - bool push(MIDIMessageQueueElement &&message); - - void inc(iter_t &it) { - if (++it == storage.end()) - it = storage.begin(); - } -}; - -END_CS_NAMESPACE diff --git a/src/MIDI_Interfaces/BLEMIDI/PollingBLEMIDISender.hpp b/src/MIDI_Interfaces/BLEMIDI/PollingBLEMIDISender.hpp new file mode 100644 index 0000000000..1efe87f0f6 --- /dev/null +++ b/src/MIDI_Interfaces/BLEMIDI/PollingBLEMIDISender.hpp @@ -0,0 +1,79 @@ +#pragma once + +#include + +#include + +#include "BLEAPI.hpp" +#include + +BEGIN_CS_NAMESPACE + +/// Class that buffers MIDI BLE packets. +template +class PollingBLEMIDISender { + public: + PollingBLEMIDISender() = default; + PollingBLEMIDISender(const PollingBLEMIDISender &) = delete; + PollingBLEMIDISender &operator=(const PollingBLEMIDISender &) = delete; + ~PollingBLEMIDISender(); + + /// Initialize. + void begin(); + + /// RAII lock for access to the packet builder. + struct ProtectedBuilder; + /// Acquire exclusive access to the buffer. + /// @return A RAII wrapper that automatically releases the buffer upon + /// destruction. Just make sure you don't keep any pointers to the + /// `packet` member. + ProtectedBuilder acquirePacket(); + /// Release exclusive access to the buffer and notify the sender thread that + /// data is available. + void releasePacketAndNotify(ProtectedBuilder &lck); + + /// Sends the data immediately without waiting for the timeout. + void sendNow(ProtectedBuilder &lck); + + /// Set the maximum transmission unit of the Bluetooth link. Used to compute + /// the MIDI BLE packet size. + void updateMTU(uint16_t mtu); + /// Get the minimum MTU of all connected clients. + uint16_t getMinMTU() const { return min_mtu; } + /// Force the MTU to an artificially small value (used for testing). + void forceMinMTU(uint16_t mtu); + + /// Set the timeout, the number of milliseconds to buffer the outgoing MIDI + /// messages. + void setTimeout(std::chrono::milliseconds timeout); + + private: + /// Actually perform the BLE notification with the given data. + void sendData(BLEDataView) = delete; // should be implemented by subclass + + private: + /// View of the data to send + BLEMIDIPacketBuilder packet; + /// Timeout before the sender thread sends a packet. + /// @see @ref setTimeout() + unsigned long timeout {10}; + /// Time point when the packet was started. + unsigned long packet_start_time {0}; + + private: + /// The minimum MTU of all connected clients. + uint16_t min_mtu {23}; + /// Override the minimum MTU (0 means don't override, nonzero overrides if + /// it's smaller than the minimum MTU of the clients). + /// @see @ref forceMinMTU() + uint16_t force_min_mtu {515}; + + public: + struct ProtectedBuilder { + BLEMIDIPacketBuilder *packet; + }; +}; + +END_CS_NAMESPACE + +#include "PollingBLEMIDISender.ipp" diff --git a/src/MIDI_Interfaces/BLEMIDI/PollingBLEMIDISender.ipp b/src/MIDI_Interfaces/BLEMIDI/PollingBLEMIDISender.ipp new file mode 100644 index 0000000000..b211576b33 --- /dev/null +++ b/src/MIDI_Interfaces/BLEMIDI/PollingBLEMIDISender.ipp @@ -0,0 +1,59 @@ +#include "PollingBLEMIDISender.hpp" + +#include + +BEGIN_CS_NAMESPACE + +template +PollingBLEMIDISender::~PollingBLEMIDISender() = default; + +template +void PollingBLEMIDISender::begin() {} + +template +auto PollingBLEMIDISender::acquirePacket() -> ProtectedBuilder { + if (packet.getSize() == 0) + packet_start_time = millis(); + return {&packet}; +} + +template +void PollingBLEMIDISender::releasePacketAndNotify(ProtectedBuilder &lck) { + if (lck.packet->getSize() > 0 && millis() - packet_start_time > timeout) + sendNow(lck); +} + +template +void PollingBLEMIDISender::sendNow(ProtectedBuilder &lck) { + BLEDataView data {lck.packet->getBuffer(), lck.packet->getSize()}; + if (data.length > 0) { + CRTP(Derived).sendData(data); + lck.packet->reset(); + lck.packet->setCapacity(min_mtu - 3); + } +} + +template +void PollingBLEMIDISender::updateMTU(uint16_t mtu) { + if (force_min_mtu == 0) + min_mtu = mtu; + else + min_mtu = std::min(force_min_mtu, mtu); + DEBUGFN(NAMEDVALUE(min_mtu)); + auto lck = acquirePacket(); + if (lck.packet->getSize() == 0) + lck.packet->setCapacity(min_mtu - 3); +} + +template +void PollingBLEMIDISender::forceMinMTU(uint16_t mtu) { + force_min_mtu = mtu; + updateMTU(min_mtu); +} + +template +void PollingBLEMIDISender::setTimeout(std::chrono::milliseconds timeout) { + this->timeout = timeout.count(); +} + +END_CS_NAMESPACE diff --git a/src/MIDI_Interfaces/BLEMIDI/ThreadedBLEMIDISender.hpp b/src/MIDI_Interfaces/BLEMIDI/ThreadedBLEMIDISender.hpp new file mode 100644 index 0000000000..0dc6814436 --- /dev/null +++ b/src/MIDI_Interfaces/BLEMIDI/ThreadedBLEMIDISender.hpp @@ -0,0 +1,103 @@ +#pragma once + +#include + +#include +#include +#include +#include + +#include "BLEAPI.hpp" +#include + +BEGIN_CS_NAMESPACE + +/// Class that manages a background thread that sends BLE packets asynchronously. +template +class ThreadedBLEMIDISender { + public: + ThreadedBLEMIDISender() = default; + ThreadedBLEMIDISender(const ThreadedBLEMIDISender &) = delete; + ThreadedBLEMIDISender &operator=(const ThreadedBLEMIDISender &) = delete; + ~ThreadedBLEMIDISender(); + + /// Start the background thread. + void begin(); + + struct ProtectedBuilder; + + /// Acquire exclusive access to the buffer to be sent by the timer. + /// @return A RAII wrapper that automatically releases the buffer upon + /// destruction. Just make sure you don't keep any pointers to the + /// `packet` member. + ProtectedBuilder acquirePacket(); + /// Release exclusive access to the buffer and notify the sender thread that + /// data is available. + void releasePacketAndNotify(ProtectedBuilder &lck); + + /// Sends the data immediately without waiting for the timeout. + void sendNow(ProtectedBuilder &lck); + + /// Set the maximum transmission unit of the Bluetooth link. Used to compute + /// the MIDI BLE packet size. + void updateMTU(uint16_t mtu); + /// Get the minimum MTU of all connected clients. + uint16_t getMinMTU() const { return min_mtu; } + /// Force the MTU to an artificially small value (used for testing). + void forceMinMTU(uint16_t mtu); + + /// Set the timeout, the number of milliseconds to buffer the outgoing MIDI + /// messages. + void setTimeout(std::chrono::milliseconds timeout); + + private: + /// Actually perform the BLE notification with the given data. + void sendData(BLEDataView) = delete; // should be implemented by subclass + + /// Function that waits for BLE packets and sends them in the background. + /// It either sends them after a timeout (a given number of milliseconds + /// after the first data was added to the packet), or immediately when it + /// receives a flush signal from the main thread. + bool handleSendEvents(); + + private: + struct { + /// View of the data to send + BLEMIDIPacketBuilder packet; + /// Flag to stop the background thread. + bool stop = false; + /// Flag to tell the sender thread to send the packet immediately. + bool flush = false; + /// Timeout before the sender thread sends a packet. + /// @see @ref setTimeout() + std::chrono::milliseconds timeout {10}; + /// Lock to protect all shared data in this struct. + std::mutex mtx; + } shared {}; + /// Condition variable used by the background sender thread to wait for + /// data to send, and for the main thread to wait for the data to be flushed + /// by the sender thread. + std::condition_variable cv; + /// Lock type used to lock the mutex + using lock_t = std::unique_lock; + /// The background thread responsible for sending the data. + std::thread send_thread; + + private: + /// The minimum MTU of all connected clients. + std::atomic_uint_fast16_t min_mtu {23}; + /// Override the minimum MTU (0 means don't override, nonzero overrides if + /// it's smaller than the minimum MTU of the clients). + /// @see @ref forceMinMTU() + std::atomic_uint_fast16_t force_min_mtu {515}; + + public: + struct ProtectedBuilder { + BLEMIDIPacketBuilder *packet; + lock_t lck; + }; +}; + +END_CS_NAMESPACE + +#include "ThreadedBLEMIDISender.ipp" diff --git a/src/MIDI_Interfaces/BLEMIDI/ThreadedBLEMIDISender.ipp b/src/MIDI_Interfaces/BLEMIDI/ThreadedBLEMIDISender.ipp new file mode 100644 index 0000000000..d2bd45b60d --- /dev/null +++ b/src/MIDI_Interfaces/BLEMIDI/ThreadedBLEMIDISender.ipp @@ -0,0 +1,124 @@ +#include "ThreadedBLEMIDISender.hpp" + +#include + +BEGIN_CS_NAMESPACE + +template +ThreadedBLEMIDISender::~ThreadedBLEMIDISender() { + lock_t lck(shared.mtx); + // Tell the sender that this is the last packet + shared.stop = true; + // Tell the sender to not to wait for the timeout + shared.flush = true; + lck.unlock(); + cv.notify_one(); + // Wait for it to be sent, and join the thread when done + if (send_thread.joinable()) + send_thread.join(); +} + +template +void ThreadedBLEMIDISender::begin() { + send_thread = std::thread([this] { + // As long as you didn't get the stop signal, wait for data to send + while (handleSendEvents()) + ; // loop + }); +} + +template +auto ThreadedBLEMIDISender::acquirePacket() -> ProtectedBuilder { + return {&shared.packet, lock_t {shared.mtx}}; +} + +template +void ThreadedBLEMIDISender::releasePacketAndNotify( + ProtectedBuilder &lck) { + lck.lck.unlock(); + cv.notify_one(); +} + +template +void ThreadedBLEMIDISender::sendNow(ProtectedBuilder &lck) { + assert(lck.lck.owns_lock()); + // No need to send empty packets + if (shared.packet.empty()) + return; + + // Tell the background sender thread to send the packet now + shared.flush = true; + lck.lck.unlock(); + cv.notify_one(); + + // Wait for flush to complete (when the sender clears the flush flag) + lck.lck.lock(); + cv.wait(lck.lck, [this] { return !shared.flush; }); +} + +template +void ThreadedBLEMIDISender::updateMTU(uint16_t mtu) { + uint16_t force_min_mtu_c = force_min_mtu; + if (force_min_mtu_c == 0) + min_mtu = mtu; + else + min_mtu = std::min(force_min_mtu_c, mtu); + DEBUGFN(NAMEDVALUE(min_mtu)); + auto lck = acquirePacket(); + if (lck.packet->getSize() == 0) + lck.packet->setCapacity(min_mtu - 3); +} + +template +void ThreadedBLEMIDISender::forceMinMTU(uint16_t mtu) { + force_min_mtu = mtu; + updateMTU(min_mtu); +} + +template +void ThreadedBLEMIDISender::setTimeout( + std::chrono::milliseconds timeout) { + lock_t lck(shared.mtx); + shared.timeout = timeout; +} + +template +bool ThreadedBLEMIDISender::handleSendEvents() { + lock_t lck(shared.mtx); + + // Wait for a packet to be started (or for a stop signal) + cv.wait(lck, [this] { return !shared.packet.empty() || shared.stop; }); + // Wait for flush signal or timeout. + auto timeout = shared.timeout; + cv.wait_for(lck, timeout, [this] { return shared.flush; }); + + // Stop this thread + if (shared.stop) + return false; + // Note: do not send anything in this case, because we might be in the base + // class destructor, and the subclass implementing the sendData function + // might already be destroyed. + + // Send the packet over BLE, empty the buffer, and update the buffer + // size based on the MTU of the connected clients. + BLEDataView data {shared.packet.getBuffer(), shared.packet.getSize()}; + if (data.length > 0) + CRTP(Derived).sendData(data); + shared.packet.reset(); + shared.packet.setCapacity(min_mtu - 3); + // Note: the MTU may have been reduced asynchronously, in which case the + // sending of the data may fail, or it may be truncated. However, since + // updating the MTU while a transmission is already going on is rare, we + // don't handle this case, as it would require parsing and re-encoding the + // buffer into two or more packets. + + // Notify the main thread that the flush was done. + if (shared.flush) { + shared.flush = false; + lck.unlock(); + cv.notify_one(); + } + return true; +} + +END_CS_NAMESPACE diff --git a/src/MIDI_Interfaces/Util/ESP32Threads.hpp b/src/MIDI_Interfaces/BLEMIDI/Util/ESP32Threads.hpp similarity index 100% rename from src/MIDI_Interfaces/Util/ESP32Threads.hpp rename to src/MIDI_Interfaces/BLEMIDI/Util/ESP32Threads.hpp diff --git a/src/MIDI_Interfaces/BLEMIDI/Util/compat.hpp b/src/MIDI_Interfaces/BLEMIDI/Util/compat.hpp new file mode 100644 index 0000000000..538275a389 --- /dev/null +++ b/src/MIDI_Interfaces/BLEMIDI/Util/compat.hpp @@ -0,0 +1,57 @@ +#pragma once + +#include + +#include +#include +#include + +BEGIN_CS_NAMESPACE + +#if __cplusplus >= 201703L + +namespace compat { +using std::byte; +using std::in_place; +using std::in_place_t; +using std::in_place_type; +using std::in_place_type_t; +} // namespace compat + +#else + +namespace compat { +struct in_place_t { + explicit in_place_t() = default; +}; +static in_place_t in_place {}; +template +struct in_place_type_t { + explicit in_place_type_t() = default; +}; +template +static in_place_type_t in_place_type {}; +enum class byte : unsigned char {}; +} // namespace compat + +#endif + +#if __cplusplus >= 202002L + +namespace compat { +using std::remove_cvref; +} // namespace compat + +#else + +namespace compat { +template +struct remove_cvref { + using type = + typename std::remove_cv::type>::type; +}; +} // namespace compat + +#endif + +END_CS_NAMESPACE diff --git a/src/MIDI_Interfaces/BluetoothMIDI_Interface.cpp b/src/MIDI_Interfaces/BluetoothMIDI_Interface.cpp deleted file mode 100644 index e5060cc88f..0000000000 --- a/src/MIDI_Interfaces/BluetoothMIDI_Interface.cpp +++ /dev/null @@ -1,334 +0,0 @@ -#if !defined(ARDUINO) || defined(ESP32) || defined(DOXYGEN) - -#include "BluetoothMIDI_Interface.hpp" -#include "BLEMIDI/ESP32/midi.h" - -BEGIN_CS_NAMESPACE - -// -------------------------------------------------------------------------- // - -// The following section defines functions that send the MIDI BLE packets in -// the background. - -void BluetoothMIDI_Interface::startSendingThread() { - // As long as you didn't get the stop signal, wait for data to send - auto send_loop = [this] { - while (handleSendEvents()) - ; // loop - }; - // Need larger stack than default, pin to non-Arduino core - ScopedThreadConfig sc {4096, 3, true, "SendBLEMIDI", 0}; - // Launch the thread - send_thread = std::thread(send_loop); -} - -bool BluetoothMIDI_Interface::handleSendEvents() { - lock_t lock(mtx); - - // Wait for a packet to be started (or for a stop signal) - cv.wait(lock, [this] { return !packetbuilder.empty() || stop_sending; }); - bool keep_going = !stop_sending; - // Wait for flush signal or timeout - bool flushing = cv.wait_for(lock, timeout, [this] { return flushnow; }); - - // Send the packet over BLE, empty the buffer, and update the buffer size - // based on the MTU of the connected clients. - if (!packetbuilder.empty()) - notifyMIDIBLE(packetbuilder.getPacket()); - packetbuilder.reset(); - packetbuilder.setCapacity(min_mtu - 3); - - // Notify the main thread that the flush was done - if (flushing) { - flushnow = false; - lock.unlock(); - cv.notify_one(); - } - return keep_going; -} - -void BluetoothMIDI_Interface::flushImpl(lock_t &lock) { - assert(lock.owns_lock()); - // No need to send empty packets - if (packetbuilder.empty()) - return; - - // Tell the background sender thread to send the packet now - flushnow = true; - lock.unlock(); - cv.notify_one(); - - // Wait for flush to complete (when the sender clears the flushnow flag) - lock.lock(); - cv.wait(lock, [this] { return !flushnow; }); - assert(lock.owns_lock()); -} - -void BluetoothMIDI_Interface::stopSendingThread() { - // Tell the sender that this is the last packet - stop_sending = true; - - // Tell it to send the packet right now (flush) - lock_t lock(mtx); - flushnow = true; - lock.unlock(); - cv.notify_one(); - - // Wait for it to be sent, and join the thread when done - if (send_thread.joinable()) - send_thread.join(); -} - -// -------------------------------------------------------------------------- // - -#ifdef ARDUINO -void BluetoothMIDI_Interface::notifyMIDIBLE( - const std::vector &packet) { - midi_notify(packet.data(), packet.size()); -} -#endif - -// -------------------------------------------------------------------------- // - -// The following section implements the MIDI sending functions. - -void BluetoothMIDI_Interface::sendChannelMessageImpl(ChannelMessage msg) { - msg.hasTwoDataBytes() ? sendChannelMessageImpl3Bytes(msg) - : sendChannelMessageImpl2Bytes(msg); -} - -void BluetoothMIDI_Interface::sendChannelMessageImpl3Bytes(ChannelMessage msg) { - // BLE packets are sent asynchronously, so we need a lock to access the - // packet buffer - lock_t lock(mtx); - uint16_t timestamp = millis(); - // Try adding the message to the current packet - if (!packetbuilder.add3B(msg.header, msg.data1, msg.data2, timestamp)) { - // If that doesn't work, flush the packet (send it now and wait until - // it is sent) - flushImpl(lock); - // And then add it to the (now emtpy) buffer - packetbuilder.add3B(msg.header, msg.data1, msg.data2, timestamp); - } - // Notify the packet sender that data has been added to the buffer - lock.unlock(); - cv.notify_one(); -} - -void BluetoothMIDI_Interface::sendChannelMessageImpl2Bytes(ChannelMessage msg) { - // For comments, see - // sendChannelMessageImpl3Bytes() above - lock_t lock(mtx); - uint16_t timestamp = millis(); - if (!packetbuilder.add2B(msg.header, msg.data1, timestamp)) { - flushImpl(lock); - packetbuilder.add2B(msg.header, msg.data1, timestamp); - } - lock.unlock(); - cv.notify_one(); -} - -void BluetoothMIDI_Interface::sendRealTimeImpl(RealTimeMessage msg) { - // For comments, see - // sendChannelMessageImpl3Bytes() above - lock_t lock(mtx); - uint16_t timestamp = millis(); - if (!packetbuilder.addRealTime(msg.message, timestamp)) { - flushImpl(lock); - packetbuilder.addRealTime(msg.message, timestamp); - } - lock.unlock(); - cv.notify_one(); -} - -void BluetoothMIDI_Interface::sendSysCommonImpl(SysCommonMessage msg) { - // For comments, see - // sendChannelMessageImpl3Bytes() above - lock_t lock(mtx); - uint16_t timestamp = millis(); - uint8_t num_data = msg.getNumberOfDataBytes(); - if (!packetbuilder.addSysCommon(num_data, msg.header, msg.data1, msg.data2, - timestamp)) { - flushImpl(lock); - packetbuilder.addSysCommon(num_data, msg.header, msg.data1, msg.data2, - timestamp); - } - lock.unlock(); - cv.notify_one(); -} - -void BluetoothMIDI_Interface::sendSysExImpl(SysExMessage msg) { - lock_t lock(mtx); - - size_t length = msg.length; - const uint8_t *data = msg.data; - uint16_t timestamp = millis(); // BLE MIDI timestamp - // TODO: I have no idea why, but the last byte gets cut off when the LSB - // of the timestamp is 0x77 ... (Problem is probably in the BlueZ parser) - if ((timestamp & 0x77) == 0x77) - timestamp &= 0xFFFE; - - // Try adding at least the SysExStart header to the current packet - if (!packetbuilder.addSysEx(data, length, timestamp)) { - // If that didn't fit, flush the packet - flushImpl(lock); - // Add the first part of the SysEx message to this packet - packetbuilder.addSysEx(data, length, timestamp); - } - // As long as there's data to be sent in the next packet - while (data) { - // Send the previous (full) packet - flushImpl(lock); - // And add the next part of the SysEx message to a continuation packet - packetbuilder.continueSysEx(data, length, timestamp); - } - // Notify the packet sender that data has been added to the buffer - lock.unlock(); - cv.notify_one(); -} - -// -------------------------------------------------------------------------- // - -void BluetoothMIDI_Interface::parse(const uint8_t *const data, - const size_t len) { - auto mididata = BLEMIDIParser(data, len); - MIDIReadEvent event = parser.pull(mididata); - // TODO: add a timeout instead of busy waiting? - while (event != MIDIReadEvent::NO_MESSAGE) { - switch (event) { - case MIDIReadEvent::CHANNEL_MESSAGE: - while (!queue.push(parser.getChannelMessage(), - mididata.getTimestamp())) - std::this_thread::yield(); - break; - case MIDIReadEvent::SYSEX_CHUNK: // fallthrough - case MIDIReadEvent::SYSEX_MESSAGE: - while (!queue.push(parser.getSysExMessage(), - mididata.getTimestamp())) - std::this_thread::yield(); - break; - case MIDIReadEvent::REALTIME_MESSAGE: - while (!queue.push(parser.getRealTimeMessage(), - mididata.getTimestamp())) - std::this_thread::yield(); - break; - case MIDIReadEvent::SYSCOMMON_MESSAGE: - while (!queue.push(parser.getSysCommonMessage(), - mididata.getTimestamp())) - std::this_thread::yield(); - break; - case MIDIReadEvent::NO_MESSAGE: break; // LCOV_EXCL_LINE - default: break; // LCOV_EXCL_LINE - } - event = parser.pull(mididata); - } - parser.cancelRunningStatus(); -} - -MIDIReadEvent BluetoothMIDI_Interface::read() { - // Pop a new message from the queue - if (!queue.pop(incomingMessage)) - return MIDIReadEvent::NO_MESSAGE; - return incomingMessage.eventType; -} - -ChannelMessage BluetoothMIDI_Interface::getChannelMessage() const { - return incomingMessage.eventType == MIDIReadEvent::CHANNEL_MESSAGE - ? incomingMessage.message.channelmessage - : ChannelMessage(0, 0, 0); -} - -SysCommonMessage BluetoothMIDI_Interface::getSysCommonMessage() const { - return incomingMessage.eventType == MIDIReadEvent::SYSCOMMON_MESSAGE - ? incomingMessage.message.syscommonmessage - : SysCommonMessage(0, 0, 0); -} - -RealTimeMessage BluetoothMIDI_Interface::getRealTimeMessage() const { - return incomingMessage.eventType == MIDIReadEvent::REALTIME_MESSAGE - ? incomingMessage.message.realtimemessage - : RealTimeMessage(0); -} - -SysExMessage BluetoothMIDI_Interface::getSysExMessage() const { - auto evt = incomingMessage.eventType; - bool hasSysEx = evt == MIDIReadEvent::SYSEX_MESSAGE || - evt == MIDIReadEvent::SYSEX_CHUNK; - return hasSysEx ? incomingMessage.message.sysexmessage - : SysExMessage(nullptr, 0); -} - -uint16_t BluetoothMIDI_Interface::getTimestamp() const { - return incomingMessage.timestamp; -} - -// -------------------------------------------------------------------------- // - -void BluetoothMIDI_Interface::updateMTU(uint16_t mtu) { - uint16_t force_min_mtu_c = force_min_mtu; - if (force_min_mtu_c == 0) - min_mtu = mtu; - else - min_mtu = std::min(force_min_mtu_c, mtu); - DEBUGFN(NAMEDVALUE(min_mtu)); - lock_t lock(mtx); - if (packetbuilder.getSize() == 0) - packetbuilder.setCapacity(min_mtu - 3); -} - -void BluetoothMIDI_Interface::forceMinMTU(uint16_t mtu) { - force_min_mtu = mtu; - updateMTU(min_mtu); -} - -// -------------------------------------------------------------------------- // - -extern "C" void BluetoothMIDI_Interface_midi_mtu_callback(uint16_t mtu) { - BluetoothMIDI_Interface::midi_mtu_callback(mtu); -} - -extern "C" void BluetoothMIDI_Interface_midi_write_callback(const uint8_t *data, - size_t length) { - BluetoothMIDI_Interface::midi_write_callback(data, length); -} - -// -------------------------------------------------------------------------- // - -void BluetoothMIDI_Interface::setName(const char *name) { -#ifdef ARDUINO - set_midi_ble_name(name); -#else - (void)name; -#endif -} - -void BluetoothMIDI_Interface::begin() { -#ifdef ARDUINO - midi_set_mtu_callback(BluetoothMIDI_Interface_midi_mtu_callback); - midi_set_write_callback(BluetoothMIDI_Interface_midi_write_callback); - DEBUGFN(F("Initializing BLE MIDI Interface")); - if (!midi_init()) { - ERROR(F("Error initializing BLE MIDI interface"), 0x2022); - return; - } -#endif - startSendingThread(); -} - -void BluetoothMIDI_Interface::end() { -#ifdef ARDUINO - DEBUGFN(F("Deinitializing BLE MIDI Interface")); - if (!midi_deinit()) { - ERROR(F("Error deinitializing BLE MIDI interface"), 0x2023); - return; - } -#endif -} - -// -------------------------------------------------------------------------- // - -BluetoothMIDI_Interface *BluetoothMIDI_Interface::instance = nullptr; - -END_CS_NAMESPACE - -#endif \ No newline at end of file diff --git a/src/MIDI_Interfaces/BluetoothMIDI_Interface.hpp b/src/MIDI_Interfaces/BluetoothMIDI_Interface.hpp index 5831533b4c..01323a18c4 100644 --- a/src/MIDI_Interfaces/BluetoothMIDI_Interface.hpp +++ b/src/MIDI_Interfaces/BluetoothMIDI_Interface.hpp @@ -1,198 +1,81 @@ #pragma once -#include +#include -#include "BLEMIDI/BLEMIDIPacketBuilder.hpp" -#include "BLEMIDI/MIDIMessageQueue.hpp" -#include "MIDI_Interface.hpp" -#include "Util/ESP32Threads.hpp" -#include -#include - -#include -#include -#include -#include -#include - -#ifndef ARDUINO -#include +#ifdef DOXYGEN +BEGIN_CS_NAMESPACE +/// Default backend for the @ref BluetoothMIDI_Interface class. +/// @see @ref md_pages_MIDI-over-BLE +struct BLEMIDIBackend {}; +END_BEGIN_CS_NAMESPACE +/// Indicates whether @ref BLEMIDIBackend and @ref BluetoothMIDI_Interface are +/// defined for this board. +#define CS_BLE_MIDI_SUPPORTED 1 +/// On ESP32, changes the default MIDI over BLE backend from Bluedroid to NimBLE. +/// This macro should be defined before including any Control Surface headers. +/// Requires the [NimBLE-Arduino](https://github.com/h2zero/NimBLE-Arduino) library. +#define CS_USE_NIMBLE + +#elif defined(ESP32) +#include +#if CONFIG_BT_BLE_ENABLED +// ESP32 with BLE support +#ifdef CS_USE_NIMBLE +// NimBLE backend +#include "BLEMIDI/ESP32NimBLEBackend.hpp" +BEGIN_CS_NAMESPACE +using BLEMIDIBackend = ESP32NimBLEBackend; +END_CS_NAMESPACE +#define CS_BLE_MIDI_SUPPORTED 1 +#else +// Bluedroid backend (default) +#include "BLEMIDI/ESP32BluedroidBackend.hpp" +BEGIN_CS_NAMESPACE +using BLEMIDIBackend = ESP32BluedroidBackend; +END_CS_NAMESPACE +#define CS_BLE_MIDI_SUPPORTED 1 +#endif #endif +#elif defined(ARDUINO_RASPBERRY_PI_PICO_W) +// Pico W +#if ENABLE_BLE +#include "BLEMIDI/BTstackBackgroundBackend.hpp" BEGIN_CS_NAMESPACE - -/** - * @brief Bluetooth Low Energy MIDI Interface for the ESP32. - * - * @ingroup MIDIInterfaces - */ -class BluetoothMIDI_Interface : public MIDI_Interface { - - public: - BluetoothMIDI_Interface() { - if (instance) - FATAL_ERROR(F("Only one instance is supported"), 0x1345); - instance = this; - }; - ~BluetoothMIDI_Interface() { - instance = nullptr; - stopSendingThread(); - end(); - } - - public: - /// Send the buffered MIDI BLE packet immediately. - void flush() { - lock_t lock(mtx); - flushImpl(lock); - } - - /// Set the timeout, the number of milliseconds to buffer the outgoing MIDI - /// messages. A shorter timeout usually results in lower latency, but also - /// causes more overhead, because more packets might be required. - void setTimeout(std::chrono::milliseconds timeout) { - lock_t lock(mtx); - this->timeout = timeout; - } - - public: - /// Set the BLE device name. Must be called before @ref begin(). - void setName(const char *name); - - void begin() override; - void end(); - - MIDIReadEvent read(); - - void update() override { MIDI_Interface::updateIncoming(this); } - - private: -#if !DISABLE_PIPES - void handleStall() override { MIDI_Interface::handleStall(this); } +using BLEMIDIBackend = BTstackBackgroundBackend; +END_CS_NAMESPACE +#define CS_BLE_MIDI_SUPPORTED 1 #endif - public: - /// Return the received channel voice message. - ChannelMessage getChannelMessage() const; - /// Return the received system common message. - SysCommonMessage getSysCommonMessage() const; - /// Return the received real-time message. - RealTimeMessage getRealTimeMessage() const; - /// Return the received system exclusive message. - SysExMessage getSysExMessage() const; - /// Get the BLE-MIDI timestamp of the latest MIDI message. - /// @note Invalid for SysEx chunks (except the last chunk of a message). - uint16_t getTimestamp() const; - - protected: - // MIDI send implementations - void sendChannelMessageImpl(ChannelMessage) override; - void sendSysCommonImpl(SysCommonMessage) override; - void sendSysExImpl(SysExMessage) override; - void sendRealTimeImpl(RealTimeMessage) override; - void sendNowImpl() override { flush(); } - - void sendChannelMessageImpl3Bytes(ChannelMessage); - void sendChannelMessageImpl2Bytes(ChannelMessage); - - public: - void parse(const uint8_t *const data, const size_t len); - - private: - /// The minimum MTU of all connected clients. - std::atomic_uint_fast16_t min_mtu {23}; - /// Override the minimum MTU (0 means don't override, nonzero overrides if - /// it's smaller than the minimum MTU of the clients). - /// @see @ref forceMinMTU() - std::atomic_uint_fast16_t force_min_mtu {0}; - - /// Set the maximum transmission unit of the Bluetooth link. Used to compute - /// the MIDI BLE packet size. - void updateMTU(uint16_t mtu); - - public: - /// Get the minimum MTU of all connected clients. - uint16_t getMinMTU() const { return min_mtu; } - - /// Force the MTU to an artificially small value (used for testing). - void forceMinMTU(uint16_t mtu); - - private: - /// Only one active instance. - static BluetoothMIDI_Interface *instance; - /// MIDI Parser for incoming data. - SerialMIDI_Parser parser {false}; - /// Builds outgoing MIDI BLE packets. - BLEMIDIPacketBuilder packetbuilder; - /// Queue for incoming MIDI messages. - MIDIMessageQueue queue {64}; - /// Incoming message that can be from retrieved using the - /// `getChannelMessage()`, `getSysCommonMessage()`, `getRealTimeMessage()` - /// and `getSysExMessage()` methods. - MIDIMessageQueue::MIDIMessageQueueElement incomingMessage; - - private: - // Synchronization for asynchronous BLE sending - - /// Lock type used to lock the mutex - using lock_t = std::unique_lock; - /// Mutex to lock the MIDI BLE packet builder and the flush flag. - std::mutex mtx; - /// Condition variable used by the background sender thread to wait for - /// data to send, and for the main thread to wait for the data to be flushed - /// by the sender thread. - std::condition_variable cv; - /// Background thread that sends the actual MIDI BLE packets. - std::thread send_thread; - /// Flag to stop the background thread. - std::atomic_bool stop_sending {false}; - /// Flag to tell the sender thread to send the packet immediately. - bool flushnow = false; - /// Timeout before the sender thread sends a packet. - /// @see @ref setTimeout() - std::chrono::milliseconds timeout {10}; - - private: - /// Launch a thread that sends the BLE packets in the background. - void startSendingThread(); - - /// Function that waits for BLE packets and sends them in the background. - /// It either sends them after a timeout (a given number of milliseconds - /// after the first data was added to the packet), or immediately when it - /// receives a flush signal from the main thread. - bool handleSendEvents(); - - /// Tell the background BLE sender thread to send the current packet. - /// Blocks until the packet is sent. - /// - /// @param lock - /// Lock should be locked at entry, will still be locked on exit. - void flushImpl(lock_t &lock); - -#if !defined(ARDUINO) && !defined(DOXYGEN) - public: +#elif (defined(ARDUINO_ARCH_MBED) && defined(ARDUINO_ARDUINO_NANO33BLE)) || \ + (defined(ARDUINO_ARCH_MBED) && defined(ARDUINO_ARCH_RP2040)) || \ + (defined(ARDUINO_ARCH_MBED) && defined(ARDUINO_GIGA)) || \ + defined(ARDUINO_UNOR4_WIFI) || defined(ARDUINO_SAMD_NANO_33_IOT) || \ + defined(ARDUINO_SAMD_MKRWIFI1010) +// Arduino Nano 33 BLE, RP2040, Arduino GIGA, Arduino UNO R4 WiFi, +// Arduino Nano 33 IoT, Arduino MKR 1010 WiFi +#include "BLEMIDI/ArduinoBLEBackend.hpp" +BEGIN_CS_NAMESPACE +using BLEMIDIBackend = ArduinoBLEBackend; +END_CS_NAMESPACE +#define CS_BLE_MIDI_SUPPORTED 1 #endif - /// Tell the background BLE sender thread to stop gracefully, and join it. - void stopSendingThread(); - - public: - static void midi_write_callback(const uint8_t *data, size_t length) { - if (instance) - instance->parse(data, length); - } - static void midi_mtu_callback(uint16_t mtu) { - if (instance) - instance->updateMTU(mtu); - } - -#ifdef ARDUINO - private: - void notifyMIDIBLE(const std::vector &packet); -#else - public: - MOCK_METHOD(void, notifyMIDIBLE, (const std::vector &), ()); +#ifdef CS_BLE_MIDI_SUPPORTED +#include "GenericBLEMIDI_Interface.hpp" +BEGIN_CS_NAMESPACE +/// @brief A class for MIDI interfaces sending MIDI messages over a Bluetooth +/// Low Energy (BLE) connection. +/// +/// Configures the Arduino as a BLE peripheral. +/// +/// @see @ref md_pages_MIDI-over-USB for a list of supported boards +/// @see @ref md_pages_MIDI-over-BLE for more information and a list of backends +/// @ingroup MIDIInterfaces +struct BluetoothMIDI_Interface : GenericBLEMIDI_Interface {}; +END_CS_NAMESPACE #endif -}; -END_CS_NAMESPACE \ No newline at end of file +#ifndef CS_BLE_MIDI_SUPPORTED +#define CS_BLE_MIDI_NOT_SUPPORTED +#endif diff --git a/src/MIDI_Interfaces/GenericBLEMIDI_Interface.hpp b/src/MIDI_Interfaces/GenericBLEMIDI_Interface.hpp new file mode 100644 index 0000000000..e981e8af73 --- /dev/null +++ b/src/MIDI_Interfaces/GenericBLEMIDI_Interface.hpp @@ -0,0 +1,112 @@ +#pragma once + +#include + +#include "BLEMIDI/BLEAPI.hpp" +#include "MIDI_Interface.hpp" + +#include + +BEGIN_CS_NAMESPACE + +/** + * @brief Bluetooth Low Energy MIDI Interface. + */ +template +class GenericBLEMIDI_Interface : public MIDI_Interface { + public: + template + GenericBLEMIDI_Interface(Args &&...args) + : backend {std::forward(args)...} {} + + private: + // MIDI send implementations + template + void sendImpl(L &lck, F add_to_buffer); + void sendChannelMessageImpl(ChannelMessage) override; + void sendSysCommonImpl(SysCommonMessage) override; + void sendSysExImpl(SysExMessage) override; + void sendRealTimeImpl(RealTimeMessage) override; + void sendNowImpl() override; + + private: +#if !DISABLE_PIPES + void handleStall() override { MIDI_Interface::handleStall(this); } +#endif + + public: + /// @name Initialization and polling + /// @{ + + /// Initialize the BLE hardware and start advertising as a MIDI over BLE + /// peripheral. + void begin() override; + /// @todo Currently not implemented. + void end(); + /// Poll the backend (if necessary) and invoke the callbacks for any + /// received MIDI messages, as well as sending them over the pipes connected + /// to this interface. + void update() override { MIDI_Interface::updateIncoming(this); } + /// Returns true if a BLE central is currently connected. + /// @note This is useful for e.g. turning on/off an LED or showing a + /// message to the user, but it's not all that useful for anything + /// else, because the connection could be terminated at any moment. + bool isConnected() const { return backend.isConnected(); } + + /// @} + + public: + /// @name Reading incoming MIDI messages + /// @{ + + /// Try reading and parsing a single incoming MIDI message. + /// @return Returns the type of the message read, or + /// `MIDIReadEvent::NO_MESSAGE` if no MIDI message was available. + MIDIReadEvent read(); + + /// Return the received channel voice message. + ChannelMessage getChannelMessage() const; + /// Return the received system common message. + SysCommonMessage getSysCommonMessage() const; + /// Return the received real-time message. + RealTimeMessage getRealTimeMessage() const; + /// Return the received system exclusive message. + SysExMessage getSysExMessage() const; + /// Get the BLE-MIDI timestamp of the latest MIDI message. + /// @note Invalid for SysEx chunks (except the last chunk of a message). + uint16_t getTimestamp() const; + + /// @} + + private: + /// Incoming message that can be from retrieved using the + /// `getChannelMessage()`, `getSysCommonMessage()`, `getRealTimeMessage()` + /// and `getSysExMessage()` methods. + typename Backend::IncomingMIDIMessage incomingMessage; + + public: + /// @name BLE configuration options + /// @{ + + /// Set the BLE device name. Must be called before @ref begin(). + /// Length is limited by the size of the BLE advertising packets. + void setName(const char *name); + /// Set the timeout, the number of milliseconds to buffer the outgoing MIDI + /// messages. A shorter timeout usually results in lower latency, but also + /// causes more overhead, because more packets might be required. + void setTimeout(std::chrono::milliseconds timeout) { + backend.setTimeout(timeout); + } + /// BLE backend configuration option. + BLESettings ble_settings; + + /// @} + + public: + /// Low-level BLE backend for sending and receiving MIDI over BLE packets. + Backend backend; +}; + +END_CS_NAMESPACE + +#include "GenericBLEMIDI_Interface.ipp" diff --git a/src/MIDI_Interfaces/GenericBLEMIDI_Interface.ipp b/src/MIDI_Interfaces/GenericBLEMIDI_Interface.ipp new file mode 100644 index 0000000000..e5d8efe46b --- /dev/null +++ b/src/MIDI_Interfaces/GenericBLEMIDI_Interface.ipp @@ -0,0 +1,168 @@ +#include "GenericBLEMIDI_Interface.hpp" + +BEGIN_CS_NAMESPACE + +// -------------------------------------------------------------------------- // + +// The following section implements the MIDI sending functions. + +template +template +void GenericBLEMIDI_Interface::sendImpl(L &lck, F add_to_buffer) { + // BLE packets are sent asynchronously, so we need a lock to access the + // packet buffer + // assert(lck.lck.owns_lock()); + + // Try adding the message to the current packet + if (!add_to_buffer()) { + // If that doesn't work, flush the packet (send it now and wait until + // it is sent) + backend.sendNow(lck); + // And then add it to the (now emtpy) buffer + add_to_buffer(); + } + // Notify the sending thread that data has been added to the buffer + backend.releasePacketAndNotify(lck); +} + +template +void GenericBLEMIDI_Interface::sendChannelMessageImpl( + ChannelMessage msg) { + uint16_t timestamp = millis(); // BLE MIDI timestamp + auto lck = backend.acquirePacket(); + if (msg.hasTwoDataBytes()) { + sendImpl(lck, [&] { + return lck.packet->add3B(msg.header, msg.data1, msg.data2, + timestamp); + }); + } else { + sendImpl(lck, [&] { + return lck.packet->add2B(msg.header, msg.data1, timestamp); + }); + } +} + +template +void GenericBLEMIDI_Interface::sendRealTimeImpl(RealTimeMessage msg) { + uint16_t timestamp = millis(); // BLE MIDI timestamp + auto lck = backend.acquirePacket(); + sendImpl(lck, + [&] { return lck.packet->addRealTime(msg.message, timestamp); }); +} + +template +void GenericBLEMIDI_Interface::sendSysCommonImpl( + SysCommonMessage msg) { + uint16_t timestamp = millis(); // BLE MIDI timestamp + auto lck = backend.acquirePacket(); + sendImpl(lck, [&] { + return lck.packet->addSysCommon(msg.getNumberOfDataBytes(), msg.header, + msg.data1, msg.data2, timestamp); + }); +} + +template +void GenericBLEMIDI_Interface::sendSysExImpl(SysExMessage msg) { + uint16_t timestamp = millis(); // BLE MIDI timestamp + size_t length = msg.length; + const uint8_t *data = msg.data; + + // BLE packets are sent asynchronously, so we need a lock to access the + // packet buffer + auto lck = backend.acquirePacket(); + + // TODO: I have no idea why, but the last byte gets cut off when the LSB + // of the timestamp is 0x77 ... (Problem is probably in the BlueZ parser) + if ((timestamp & 0x77) == 0x77) + timestamp &= 0xFFFE; + + // Try adding at least the SysExStart header to the current packet + if (!lck.packet->addSysEx(data, length, timestamp)) { + // If that didn't fit, flush the packet + backend.sendNow(lck); + // Add the first part of the SysEx message to this packet + lck.packet->addSysEx(data, length, timestamp); + } + // As long as there's data to be sent in the next packet + while (data) { + // Send the previous (full) packet + backend.sendNow(lck); + // And add the next part of the SysEx message to a continuation packet + lck.packet->continueSysEx(data, length, timestamp); + } + // Notify the sending thread that data has been added to the buffer + backend.releasePacketAndNotify(lck); +} + +template +void GenericBLEMIDI_Interface::sendNowImpl() { + auto lck = backend.acquirePacket(); + backend.sendNow(lck); +} + +// -------------------------------------------------------------------------- // + +template +MIDIReadEvent GenericBLEMIDI_Interface::read() { + // Pop a new message from the queue + if (!backend.popMessage(incomingMessage)) + return MIDIReadEvent::NO_MESSAGE; + return incomingMessage.eventType; +} + +template +ChannelMessage GenericBLEMIDI_Interface::getChannelMessage() const { + return incomingMessage.eventType == MIDIReadEvent::CHANNEL_MESSAGE + ? incomingMessage.message.channelmessage + : ChannelMessage(0, 0, 0); +} + +template +SysCommonMessage +GenericBLEMIDI_Interface::getSysCommonMessage() const { + return incomingMessage.eventType == MIDIReadEvent::SYSCOMMON_MESSAGE + ? incomingMessage.message.syscommonmessage + : SysCommonMessage(0, 0, 0); +} + +template +RealTimeMessage GenericBLEMIDI_Interface::getRealTimeMessage() const { + return incomingMessage.eventType == MIDIReadEvent::REALTIME_MESSAGE + ? incomingMessage.message.realtimemessage + : RealTimeMessage(0); +} + +template +SysExMessage GenericBLEMIDI_Interface::getSysExMessage() const { + auto evt = incomingMessage.eventType; + bool hasSysEx = evt == MIDIReadEvent::SYSEX_MESSAGE || + evt == MIDIReadEvent::SYSEX_CHUNK; + return hasSysEx ? incomingMessage.message.sysexmessage + : SysExMessage(nullptr, 0); +} + +template +uint16_t GenericBLEMIDI_Interface::getTimestamp() const { + return incomingMessage.timestamp; +} + +// -------------------------------------------------------------------------- // + +template +void GenericBLEMIDI_Interface::setName(const char *name) { + ble_settings.device_name = name; +} + +template +void GenericBLEMIDI_Interface::begin() { + backend.begin(ble_settings); +} + +template +void GenericBLEMIDI_Interface::end() { + backend.end(); +} + +// -------------------------------------------------------------------------- // + +END_CS_NAMESPACE diff --git a/src/MIDI_Interfaces/SerialMIDI_Interface.hpp b/src/MIDI_Interfaces/SerialMIDI_Interface.hpp index 39cb98e2b2..dbb620808d 100644 --- a/src/MIDI_Interfaces/SerialMIDI_Interface.hpp +++ b/src/MIDI_Interfaces/SerialMIDI_Interface.hpp @@ -144,8 +144,7 @@ class USBSerialMIDI_Interface : public SerialMIDI_Interface { /** * @brief A class for MIDI Interfaces sending and receiving * data over the USB Serial CDC connection for the use - * with the [Hairless MIDI<->Serial Bridge] - * (http://projectgus.github.io/hairless-midiserial/). + * with the [Hairless MIDI<->Serial Bridge](http://projectgus.github.io/hairless-midiserial/). * * @ingroup MIDIInterfaces */ diff --git a/src/MIDI_Interfaces/USBMIDI_Interface.hpp b/src/MIDI_Interfaces/USBMIDI_Interface.hpp index 06bfc31268..5908e6500c 100644 --- a/src/MIDI_Interfaces/USBMIDI_Interface.hpp +++ b/src/MIDI_Interfaces/USBMIDI_Interface.hpp @@ -40,15 +40,24 @@ class GenericUSBMIDI_Interface : public MIDI_Interface { #endif public: + /// @name Initialization and polling + /// @{ + + /// Initialize. void begin() override; + /// Poll the backend (if necessary) and invoke the callbacks for any + /// received MIDI messages, as well as sending them over the pipes connected + /// to this interface. void update() override; + /// @} + public: /// @name Reading incoming MIDI messages /// @{ /// Try reading and parsing a single incoming MIDI message. - /// @return Returns the type of the read message, or + /// @return Returns the type of the message read, or /// `MIDIReadEvent::NO_MESSAGE` if no MIDI message was available. MIDIReadEvent read(); diff --git a/src/MIDI_Parsers/AnyMIDI_Message.hpp b/src/MIDI_Parsers/AnyMIDI_Message.hpp new file mode 100644 index 0000000000..3433fdfe7a --- /dev/null +++ b/src/MIDI_Parsers/AnyMIDI_Message.hpp @@ -0,0 +1,42 @@ +#pragma once + +#include "MIDIReadEvent.hpp" +#include "MIDI_MessageTypes.hpp" +#include + +BEGIN_CS_NAMESPACE + +/// MIDI message variant type (with timestamp). +struct AnyMIDIMessage { + MIDIReadEvent eventType = MIDIReadEvent::NO_MESSAGE; + union Message { + ChannelMessage channelmessage; + SysCommonMessage syscommonmessage; + RealTimeMessage realtimemessage; + SysExMessage sysexmessage; + + Message() : realtimemessage(0x00) {} + Message(ChannelMessage msg) : channelmessage(msg) {} + Message(SysCommonMessage msg) : syscommonmessage(msg) {} + Message(RealTimeMessage msg) : realtimemessage(msg) {} + Message(SysExMessage msg) : sysexmessage(msg) {} + } message; + uint16_t timestamp = 0xFFFF; + + AnyMIDIMessage() = default; + AnyMIDIMessage(ChannelMessage message, uint16_t timestamp) + : eventType(MIDIReadEvent::CHANNEL_MESSAGE), message(message), + timestamp(timestamp) {} + AnyMIDIMessage(SysCommonMessage message, uint16_t timestamp) + : eventType(MIDIReadEvent::SYSCOMMON_MESSAGE), message(message), + timestamp(timestamp) {} + AnyMIDIMessage(RealTimeMessage message, uint16_t timestamp) + : eventType(MIDIReadEvent::REALTIME_MESSAGE), message(message), + timestamp(timestamp) {} + AnyMIDIMessage(SysExMessage message, uint16_t timestamp) + : eventType(message.isLastChunk() ? MIDIReadEvent::SYSEX_MESSAGE + : MIDIReadEvent::SYSEX_CHUNK), + message(message), timestamp(timestamp) {} +}; + +END_CS_NAMESPACE \ No newline at end of file diff --git a/src/Submodules/Encoder/AHEncoder.cpp b/src/Submodules/Encoder/AHEncoder.cpp index 4b4b288a6a..93c788393a 100644 --- a/src/Submodules/Encoder/AHEncoder.cpp +++ b/src/Submodules/Encoder/AHEncoder.cpp @@ -20,7 +20,11 @@ AHEncoder::AHEncoder(uint8_t pinA, uint8_t pinB) // but here, we look them up once in the constructor. } -AHEncoder::AHEncoder(AHEncoder &&other) { swap(*this, other); } +AHEncoder::AHEncoder(AHEncoder &&other) + : pins (other.pins), direct_pins (std::move(other.direct_pins)) { + if (other.interrupts_in_use) + FATAL_ERROR(F("Cannot move from initialized AHEncoder."), 0x9311); +} AHEncoder &AHEncoder::operator=(AHEncoder &&other) { swap(*this, other); diff --git a/src/Submodules/Encoder/AHEncoder.hpp b/src/Submodules/Encoder/AHEncoder.hpp index 284caf8c81..3a745e3c68 100644 --- a/src/Submodules/Encoder/AHEncoder.hpp +++ b/src/Submodules/Encoder/AHEncoder.hpp @@ -3,6 +3,7 @@ #include "AtomicPosition.hpp" #include "DirectPinRead.hpp" #include "NumInterrupts.hpp" +#include BEGIN_CS_NAMESPACE @@ -77,10 +78,10 @@ class AHEncoder { void detachInterruptCtx(int interrupt); private: - uint8_t pins[2]; + AH::Array pins; uint8_t interrupts_in_use = 0; uint8_t state = 0; - DirectPinRead direct_pins[2]; + AH::Array direct_pins; AtomicPosition position {0}; private: diff --git a/src/Submodules/Encoder/DirectPinRead.hpp b/src/Submodules/Encoder/DirectPinRead.hpp index f169614567..198ccc93c7 100644 --- a/src/Submodules/Encoder/DirectPinRead.hpp +++ b/src/Submodules/Encoder/DirectPinRead.hpp @@ -75,7 +75,9 @@ inline DirectPinRead direct_pin_read(pin_t pin) { // Raspberry Pi RP2040 #elif defined(ARDUINO_ARCH_RP2040) +END_CS_NAMESPACE #include +BEGIN_CS_NAMESPACE using DirectPinRead = DirectPinReadReg; inline DirectPinRead direct_pin_read(pin_t pin) { return {&sio_hw->gpio_in, uint32_t(1) << pin}; @@ -84,10 +86,14 @@ inline DirectPinRead direct_pin_read(pin_t pin) { // ARM mbed OS #elif defined(ARDUINO_ARCH_MBED) +END_CS_NAMESPACE +#include +// ↑ Must be first #include +BEGIN_CS_NAMESPACE struct DirectPinRead { mbed::DigitalIn pin; - bool read() const { return pin.read(); } + bool read() { return pin.read(); } }; inline DirectPinRead direct_pin_read(pin_t pin) { return {mbed::DigitalIn {digitalPinToPinName(pin), PullUp}}; diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index ab0447a873..6144b1051e 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -50,6 +50,7 @@ add_executable(tests "MIDI_Interfaces/test-BluetoothMIDI_Interface.cpp" "MIDI_Interfaces/test-MIDI_Pipes.cpp" "MIDI_Interfaces/test-BLEMIDIPacketBuilder.cpp" + "MIDI_Interfaces/test-BLEAPI.cpp" "Banks/test-Banks.cpp" "Selectors/test-ManyButtonsSelector.cpp" "Selectors/test-IncrementDecrementSelector.cpp" diff --git a/test/MIDI_Interfaces/test-BLEAPI.cpp b/test/MIDI_Interfaces/test-BLEAPI.cpp new file mode 100644 index 0000000000..79fcaab550 --- /dev/null +++ b/test/MIDI_Interfaces/test-BLEAPI.cpp @@ -0,0 +1,119 @@ +#include +#include + +#include +#include + +USING_CS_NAMESPACE; + +TEST(BLERingBuf, popempty) { + BLERingBuf<1024> buf; + BLEDataView data; + EXPECT_EQ(buf.pop(data), BLEDataType::None); + EXPECT_EQ(buf.pop(data), BLEDataType::None); + EXPECT_EQ(buf.pop(data), BLEDataType::None); +} + +template +BLEDataView view(const char (&str)[N]) { + return BLEDataView {reinterpret_cast(str), N}; +} + +TEST(BLERingBuf, pushpop) { + BLERingBuf<1024> buf; + EXPECT_TRUE(buf.push(view("abc"))); + EXPECT_TRUE(buf.push(view("defgh"))); + BLEDataView data; + EXPECT_EQ(buf.pop(data), BLEDataType::Packet); + ASSERT_EQ(data.length, 4); + EXPECT_STREQ(reinterpret_cast(data.data), "abc"); + EXPECT_EQ(buf.pop(data), BLEDataType::Packet); + ASSERT_EQ(data.length, 6); + EXPECT_STREQ(reinterpret_cast(data.data), "defgh"); + EXPECT_EQ(buf.pop(data), BLEDataType::None); +} + +TEST(BLERingBuf, pushpopOdd) { + BLERingBuf<1024> buf; + EXPECT_TRUE(buf.push(view("ab"))); + EXPECT_TRUE(buf.push(view("cdef"))); + BLEDataView data; + EXPECT_EQ(buf.pop(data), BLEDataType::Packet); + ASSERT_EQ(data.length, 3); + EXPECT_STREQ(reinterpret_cast(data.data), "ab"); + EXPECT_EQ(buf.pop(data), BLEDataType::Packet); + ASSERT_EQ(data.length, 5); + EXPECT_STREQ(reinterpret_cast(data.data), "cdef"); + EXPECT_EQ(buf.pop(data), BLEDataType::None); +} + +TEST(BLERingBuf, pushfull) { + BLERingBuf<12> buf; + // |HH|--|--|--|--|--| + EXPECT_TRUE(buf.push(view("abc"))); + // |HH|HH|ab|c-|--|--| + EXPECT_FALSE(buf.push(view("def"))); + BLEDataView data; + EXPECT_EQ(buf.pop(data), BLEDataType::Packet); + // |--|HH|ab|c-|--|--| + ASSERT_EQ(data.length, 4); + EXPECT_STREQ(reinterpret_cast(data.data), "abc"); + EXPECT_EQ(buf.pop(data), BLEDataType::None); + // |--|--|--|HH|--|--| +} + +TEST(BLERingBuf, pushpopNonContiguous) { + BLERingBuf<12> buf; + // |HH|--|--|--|--|--| + EXPECT_TRUE(buf.push(view("abc"))); + // |HH|HH|ab|c-|--|--| + BLEDataView data; + EXPECT_EQ(buf.pop(data), BLEDataType::Packet); + // |--|HH|ab|c-|--|--| + ASSERT_EQ(data.length, 4); + EXPECT_STREQ(reinterpret_cast(data.data), "abc"); + EXPECT_EQ(buf.pop(data), BLEDataType::None); + // |--|--|--|HH|--|--| + EXPECT_TRUE(buf.push(view("d\0efg"))); + // |HH|ef|g-|HH|HH|d-| + EXPECT_EQ(buf.pop(data), BLEDataType::Packet); + // |HH|ef|g-|--|HH|d-| + ASSERT_EQ(data.length, 2); + EXPECT_STREQ(reinterpret_cast(data.data), "d"); + EXPECT_EQ(buf.pop(data), BLEDataType::Continuation); + // |HH|ef|g-|--|--|--| + ASSERT_EQ(data.length, 4); + EXPECT_STREQ(reinterpret_cast(data.data), "efg"); + EXPECT_EQ(buf.pop(data), BLEDataType::None); + // |--|--|HH|--|--|--| + EXPECT_TRUE(buf.push(view("hij\0k"))); + // |HH|k-|HH|HH|hi|j-| + EXPECT_EQ(buf.pop(data), BLEDataType::Packet); + // |HH|k-|--|HH|hi|j-| + ASSERT_EQ(data.length, 4); + EXPECT_STREQ(reinterpret_cast(data.data), "hij"); + EXPECT_EQ(buf.pop(data), BLEDataType::Continuation); + // |HH|g-|--|--|--|--| + ASSERT_EQ(data.length, 2); + EXPECT_STREQ(reinterpret_cast(data.data), "k"); + EXPECT_EQ(buf.pop(data), BLEDataType::None); + // |--|HH|--|--|--|--| + EXPECT_EQ(buf.pop(data), BLEDataType::None); + EXPECT_TRUE(buf.push(view("lmnop"))); + // |--|HH|HH|lm|no|p-| + EXPECT_TRUE(buf.push({})); + // |HH|HH|HH|lm|no|p-| + EXPECT_FALSE(buf.push({})); + // |HH|HH|HH|lm|no|p-| + EXPECT_EQ(buf.pop(data), BLEDataType::Packet); + // |HH|--|HH|lm|no|p-| + ASSERT_EQ(data.length, 6); + EXPECT_STREQ(reinterpret_cast(data.data), "lmnop"); + EXPECT_EQ(buf.pop(data), BLEDataType::Packet); + // |HH|--|--|--|--|HH| + ASSERT_EQ(data.length, 0); + EXPECT_EQ(buf.pop(data), BLEDataType::None); + // |HH|--|--|--|--|--| + EXPECT_EQ(buf.pop(data), BLEDataType::None); + EXPECT_EQ(buf.pop(data), BLEDataType::None); +} diff --git a/test/MIDI_Interfaces/test-BluetoothMIDI_Interface.cpp b/test/MIDI_Interfaces/test-BluetoothMIDI_Interface.cpp index c9a2d0e489..a9aecb4863 100644 --- a/test/MIDI_Interfaces/test-BluetoothMIDI_Interface.cpp +++ b/test/MIDI_Interfaces/test-BluetoothMIDI_Interface.cpp @@ -1,9 +1,52 @@ +#include #include +#include #include using namespace cs; using testing::Mock; +struct MockBLEBackend; + +struct MockBLEImpl { + MockBLEBackend *instance; + void init(ESP32BLEBackend &instance, BLESettings); + void notify(BLEConnectionHandle, BLECharacteristicHandle, BLEDataView data); +}; + +struct MockBLEBackend : public ESP32BLEBackend { + MOCK_METHOD(void, notifyMIDIBLE, (const std::vector &)); + using Parent = ESP32BLEBackend; + using IncomingMIDIMessage = typename Parent::IncomingMIDIMessage; + using Parent::handleData; + friend MockBLEImpl; +}; + +void MockBLEImpl::init(ESP32BLEBackend &instance, BLESettings) { + this->instance = &dynamic_cast(instance); + this->instance->handleConnect(BLEConnectionHandle {0}); + this->instance->handleSubscribe(BLEConnectionHandle {0}, + BLECharacteristicHandle {0}, true); +} + +void MockBLEImpl::notify(BLEConnectionHandle, BLECharacteristicHandle, + BLEDataView data) { + std::vector d {data.data, data.data + data.length}; + this->instance->notifyMIDIBLE(d); +} + +struct BluetoothMIDI_Interface : GenericBLEMIDI_Interface { + void parse(const uint8_t *data, size_t length) { + BLEDataView view {data, static_cast(length)}; + auto data_gen = [view {view}]() mutable { + return std::exchange(view, {}); + }; + backend.handleData(BLEConnectionHandle {0}, + BLEDataGenerator {compat::in_place, data_gen}, + BLEDataLifetime::ConsumeImmediately); + } +}; + class MockMIDI_Callbacks : public MIDI_Callbacks { public: void onChannelMessage(MIDI_Interface &, ChannelMessage msg) override { @@ -414,10 +457,10 @@ TEST(BluetoothMIDIInterface, sendOneNoteMessage) { EXPECT_CALL(ArduinoMock::getInstance(), millis()) .Times(1) // For time stamp .WillRepeatedly(Return(timestamp(0x01, 0x02))); - EXPECT_CALL(midi, notifyMIDIBLE(expected)); + EXPECT_CALL(midi.backend, notifyMIDIBLE(expected)); midi.sendNoteOn({0x12, Channel_3}, 0x34); - midi.flush(); + midi.sendNow(); Mock::VerifyAndClear(&ArduinoMock::getInstance()); } @@ -432,11 +475,11 @@ TEST(BluetoothMIDIInterface, sendTwoNoteMessages) { EXPECT_CALL(ArduinoMock::getInstance(), millis()) .Times(2) // For time stamp .WillRepeatedly(Return(timestamp(0x01, 0x02))); - EXPECT_CALL(midi, notifyMIDIBLE(expected)); + EXPECT_CALL(midi.backend, notifyMIDIBLE(expected)); midi.sendNoteOn({0x12, Channel_3}, 0x34); midi.sendNoteOn({0x56, Channel_10}, 0x78); - midi.flush(); + midi.sendNow(); Mock::VerifyAndClear(&ArduinoMock::getInstance()); } @@ -451,11 +494,11 @@ TEST(BluetoothMIDIInterface, sendTwoNoteMessagesRunningStatus) { EXPECT_CALL(ArduinoMock::getInstance(), millis()) .Times(2) // For time stamp .WillRepeatedly(Return(timestamp(0x01, 0x02))); - EXPECT_CALL(midi, notifyMIDIBLE(expected)); + EXPECT_CALL(midi.backend, notifyMIDIBLE(expected)); midi.sendNoteOn({0x12, Channel_3}, 0x34); midi.sendNoteOn({0x56, Channel_3}, 0x78); - midi.flush(); + midi.sendNow(); Mock::VerifyAndClear(&ArduinoMock::getInstance()); } @@ -463,7 +506,7 @@ TEST(BluetoothMIDIInterface, sendTwoNoteMessagesRunningStatus) { TEST(BluetoothMIDIInterface, sendNoteMessageBufferFull) { BluetoothMIDI_Interface midi; midi.begin(); - midi.forceMinMTU(7 + 3); + midi.backend.forceMinMTU(7 + 3); std::vector expected1 = {0x81, 0x82, 0x85, 0x56, 0x78}; std::vector expected2 = {0x81, 0x83, 0x86, 0x66, 0x79}; @@ -472,12 +515,12 @@ TEST(BluetoothMIDIInterface, sendNoteMessageBufferFull) { .WillOnce(Return(timestamp(0x01, 0x02))) .WillOnce(Return(timestamp(0x01, 0x03))); testing::Sequence s; - EXPECT_CALL(midi, notifyMIDIBLE(expected1)).InSequence(s); - EXPECT_CALL(midi, notifyMIDIBLE(expected2)).InSequence(s); + EXPECT_CALL(midi.backend, notifyMIDIBLE(expected1)).InSequence(s); + EXPECT_CALL(midi.backend, notifyMIDIBLE(expected2)).InSequence(s); midi.sendNoteOff({0x56, Channel_6}, 0x78); midi.sendNoteOff({0x66, Channel_7}, 0x79); - midi.flush(); + midi.sendNow(); Mock::VerifyAndClear(&ArduinoMock::getInstance()); } @@ -490,10 +533,10 @@ TEST(BluetoothMIDIInterface, sendOneProgramChangeMessage) { EXPECT_CALL(ArduinoMock::getInstance(), millis()) .Times(1) // For time stamp .WillRepeatedly(Return(timestamp(0x01, 0x02))); - EXPECT_CALL(midi, notifyMIDIBLE(expected)); + EXPECT_CALL(midi.backend, notifyMIDIBLE(expected)); midi.sendProgramChange(Channel_6, 0x78); - midi.flush(); + midi.sendNow(); Mock::VerifyAndClear(&ArduinoMock::getInstance()); } @@ -501,7 +544,7 @@ TEST(BluetoothMIDIInterface, sendOneProgramChangeMessage) { TEST(BluetoothMIDIInterface, sendProgramChangeMessageBufferFull) { BluetoothMIDI_Interface midi; midi.begin(); - midi.forceMinMTU(6 + 3); + midi.backend.forceMinMTU(6 + 3); std::vector expected1 = {0x81, 0x82, 0xC5, 0x78}; std::vector expected2 = {0x81, 0x83, 0xC6, 0x79}; @@ -510,12 +553,12 @@ TEST(BluetoothMIDIInterface, sendProgramChangeMessageBufferFull) { .WillOnce(Return(timestamp(0x01, 0x02))) .WillOnce(Return(timestamp(0x01, 0x03))); testing::Sequence s; - EXPECT_CALL(midi, notifyMIDIBLE(expected1)).InSequence(s); - EXPECT_CALL(midi, notifyMIDIBLE(expected2)).InSequence(s); + EXPECT_CALL(midi.backend, notifyMIDIBLE(expected1)).InSequence(s); + EXPECT_CALL(midi.backend, notifyMIDIBLE(expected2)).InSequence(s); midi.sendProgramChange(Channel_6, 0x78); midi.sendProgramChange(Channel_7, 0x79); - midi.flush(); + midi.sendNow(); Mock::VerifyAndClear(&ArduinoMock::getInstance()); } @@ -528,10 +571,10 @@ TEST(BluetoothMIDIInterface, sendRealTimeMessage) { EXPECT_CALL(ArduinoMock::getInstance(), millis()) .Times(1) // For time stamp .WillOnce(Return(timestamp(0x01, 0x02))); - EXPECT_CALL(midi, notifyMIDIBLE(expected)); + EXPECT_CALL(midi.backend, notifyMIDIBLE(expected)); midi.sendRealTime(0xF8); - midi.flush(); + midi.sendNow(); Mock::VerifyAndClear(&ArduinoMock::getInstance()); } @@ -539,7 +582,7 @@ TEST(BluetoothMIDIInterface, sendRealTimeMessage) { TEST(BluetoothMIDIInterface, sendRealTimeMessageBufferFull) { BluetoothMIDI_Interface midi; midi.begin(); - midi.forceMinMTU(5 + 3); + midi.backend.forceMinMTU(5 + 3); std::vector expected1 = {0x81, 0x82, 0xF8, 0x83, 0xF9}; std::vector expected2 = {0x81, 0x84, 0xFA}; @@ -549,13 +592,13 @@ TEST(BluetoothMIDIInterface, sendRealTimeMessageBufferFull) { .WillOnce(Return(timestamp(0x01, 0x03))) .WillOnce(Return(timestamp(0x01, 0x04))); testing::Sequence s; - EXPECT_CALL(midi, notifyMIDIBLE(expected1)).InSequence(s); - EXPECT_CALL(midi, notifyMIDIBLE(expected2)).InSequence(s); + EXPECT_CALL(midi.backend, notifyMIDIBLE(expected1)).InSequence(s); + EXPECT_CALL(midi.backend, notifyMIDIBLE(expected2)).InSequence(s); midi.sendRealTime(0xF8); midi.sendRealTime(0xF9); midi.sendRealTime(0xFA); - midi.flush(); + midi.sendNow(); Mock::VerifyAndClear(&ArduinoMock::getInstance()); } @@ -568,10 +611,10 @@ TEST(BluetoothMIDIInterface, sendSysCommon) { EXPECT_CALL(ArduinoMock::getInstance(), millis()) .Times(1) // For time stamp .WillOnce(Return(timestamp(0x01, 0x02))); - EXPECT_CALL(midi, notifyMIDIBLE(expected)); + EXPECT_CALL(midi.backend, notifyMIDIBLE(expected)); midi.sendSysCommon(MIDIMessageType::SongPositionPointer, 0x12, 0x34); - midi.flush(); + midi.sendNow(); Mock::VerifyAndClear(&ArduinoMock::getInstance()); } @@ -579,7 +622,7 @@ TEST(BluetoothMIDIInterface, sendSysCommon) { TEST(BluetoothMIDIInterface, sendSysCommonMessageBufferFull) { BluetoothMIDI_Interface midi; midi.begin(); - midi.forceMinMTU(5 + 3); + midi.backend.forceMinMTU(5 + 3); std::vector expected1 = {0x81, 0x82, 0xF2, 0x12, 0x34}; std::vector expected2 = {0x81, 0x83, 0xF1, 0x56}; @@ -588,12 +631,12 @@ TEST(BluetoothMIDIInterface, sendSysCommonMessageBufferFull) { .WillOnce(Return(timestamp(0x01, 0x02))) .WillOnce(Return(timestamp(0x01, 0x03))); testing::Sequence s; - EXPECT_CALL(midi, notifyMIDIBLE(expected1)).InSequence(s); - EXPECT_CALL(midi, notifyMIDIBLE(expected2)).InSequence(s); + EXPECT_CALL(midi.backend, notifyMIDIBLE(expected1)).InSequence(s); + EXPECT_CALL(midi.backend, notifyMIDIBLE(expected2)).InSequence(s); midi.sendSysCommon(MIDIMessageType::SongPositionPointer, 0x12, 0x34); midi.sendSysCommon(MIDIMessageType::MTCQuarterFrame, 0x56); - midi.flush(); + midi.sendNow(); Mock::VerifyAndClear(&ArduinoMock::getInstance()); } @@ -602,7 +645,7 @@ TEST(BluetoothMIDIInterface, sendLongSysEx) { std::chrono::milliseconds timeout {100}; BluetoothMIDI_Interface midi; midi.begin(); - midi.forceMinMTU(5 + 3); + midi.backend.forceMinMTU(5 + 3); midi.setTimeout(timeout); std::vector sysex = { @@ -637,8 +680,8 @@ TEST(BluetoothMIDIInterface, sendLongSysEx) { .WillRepeatedly(Return(timestamp(0x01, 0x02))); InSequence seq; - EXPECT_CALL(midi, notifyMIDIBLE(expected[0])); - EXPECT_CALL(midi, notifyMIDIBLE(expected[1])); + EXPECT_CALL(midi.backend, notifyMIDIBLE(expected[0])); + EXPECT_CALL(midi.backend, notifyMIDIBLE(expected[1])); midi.send(SysExMessage(sysex)); // First two packets should be sent immediately @@ -646,7 +689,7 @@ TEST(BluetoothMIDIInterface, sendLongSysEx) { // Third packet is sent after the timeout std::this_thread::sleep_for(timeout * 0.9); - EXPECT_CALL(midi, notifyMIDIBLE(expected[2])); + EXPECT_CALL(midi.backend, notifyMIDIBLE(expected[2])); std::this_thread::sleep_for(timeout * 0.2); Mock::VerifyAndClear(&midi); @@ -657,7 +700,7 @@ TEST(BluetoothMIDIInterface, sendSysExBufferFullPacket1) { std::chrono::milliseconds timeout {100}; BluetoothMIDI_Interface midi; midi.begin(); - midi.forceMinMTU(5 + 3); + midi.backend.forceMinMTU(5 + 3); midi.setTimeout(timeout); std::vector sysex = {0xF0, 0xF7}; @@ -685,12 +728,12 @@ TEST(BluetoothMIDIInterface, sendSysExBufferFullPacket1) { .WillOnce(Return(timestamp(0x01, 0x03))); InSequence seq; - EXPECT_CALL(midi, notifyMIDIBLE(expected[0])); - EXPECT_CALL(midi, notifyMIDIBLE(expected[1])); + EXPECT_CALL(midi.backend, notifyMIDIBLE(expected[0])); + EXPECT_CALL(midi.backend, notifyMIDIBLE(expected[1])); midi.sendNoteOn({0x14, Channel_2}, 0x15); midi.send(SysExMessage(sysex)); - midi.flush(); + midi.sendNow(); Mock::VerifyAndClear(&ArduinoMock::getInstance()); } @@ -699,7 +742,7 @@ TEST(BluetoothMIDIInterface, sendLongSysExFlush) { std::chrono::milliseconds timeout {100}; BluetoothMIDI_Interface midi; midi.begin(); - midi.forceMinMTU(5 + 3); + midi.backend.forceMinMTU(5 + 3); midi.setTimeout(timeout); std::vector sysex = { @@ -734,69 +777,17 @@ TEST(BluetoothMIDIInterface, sendLongSysExFlush) { .WillRepeatedly(Return(timestamp(0x01, 0x02))); InSequence seq; - EXPECT_CALL(midi, notifyMIDIBLE(expected[0])); - EXPECT_CALL(midi, notifyMIDIBLE(expected[1])); + EXPECT_CALL(midi.backend, notifyMIDIBLE(expected[0])); + EXPECT_CALL(midi.backend, notifyMIDIBLE(expected[1])); midi.send(SysExMessage(sysex)); // First two packets should be sent immediately Mock::VerifyAndClear(&midi); // Third packet is sent after flush - EXPECT_CALL(midi, notifyMIDIBLE(expected[2])); - midi.flush(); + EXPECT_CALL(midi.backend, notifyMIDIBLE(expected[2])); + midi.sendNow(); Mock::VerifyAndClear(&midi); Mock::VerifyAndClear(&ArduinoMock::getInstance()); } - -TEST(BluetoothMIDIInterface, sendLongSysExFlushStopSendingThread) { - std::chrono::milliseconds timeout {100}; - auto midi = std::make_unique(); - midi->begin(); - midi->forceMinMTU(5 + 3); - midi->setTimeout(timeout); - - std::vector sysex = { - 0xF0, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0xF7, - }; - - std::vector expected[] = { - { - 0x80 | 0x01, // header + timestamp msb - 0x80 | 0x02, // timestamp lsb - 0xF0, // SysEx start - 0x10, // data - 0x11, // data - }, - { - 0x80 | 0x01, // header + timestamp msb - 0x12, // data - 0x13, // data - 0x14, // data - 0x15, // data - }, - { - 0x80 | 0x01, // header + timestamp msb - 0x16, // data - 0x80 | 0x02, // timestamp lsb - 0xF7, // SysEx end - }, - }; - - EXPECT_CALL(ArduinoMock::getInstance(), millis()) - .Times(1) // For time stamp - .WillRepeatedly(Return(timestamp(0x01, 0x02))); - - InSequence seq; - EXPECT_CALL(*midi, notifyMIDIBLE(expected[0])); - EXPECT_CALL(*midi, notifyMIDIBLE(expected[1])); - EXPECT_CALL(*midi, notifyMIDIBLE(expected[2])); - - midi->send(SysExMessage(sysex)); - // First two packets should be sent immediately - // Third packet is sent when the MIDI interface is destroyed - midi->stopSendingThread(); - Mock::VerifyAndClear(midi.get()); - - Mock::VerifyAndClear(&ArduinoMock::getInstance()); -} diff --git a/test/examples-board-fqbns.yaml b/test/examples-board-fqbns.yaml index f0414a1a7a..cf32445045 100644 --- a/test/examples-board-fqbns.yaml +++ b/test/examples-board-fqbns.yaml @@ -1,4 +1,5 @@ esp32: esp32:esp32:esp32thing:FlashFreq=80,PartitionScheme=default,UploadSpeed=921600,DebugLevel=none +esp32-s3: esp32:esp32:esp32s3 esp8266: esp8266:esp8266:d1_mini:xtal=80,vt=flash,exception=disabled,stacksmash=disabled,ssl=all,mmu=3232,non32xfer=fast,eesz=4M2M,ip=lm2f,dbg=Disabled,lvl=None____,wipe=none,baud=921600 teensy 3.x: teensy:avr:teensy31:speed=96,usb=serialmidiaudio,opt=o2std,keys=en-us teensy 3.6: teensy:avr:teensy36:speed=180,usb=serialmidiaudio,opt=o2std,keys=en-us @@ -12,4 +13,5 @@ nano 33 iot: arduino:samd:nano_33_iot nano 33 ble: arduino:mbed_nano:nano33ble nano every: arduino:megaavr:nona4809:mode=off pi pico: arduino:mbed_rp2040:pico +pi pico w: rp2040:rp2040:rpipicow:ipbtstack=ipv4btcble uno r4: arduino:renesas_uno:minima \ No newline at end of file diff --git a/tools/arduino-cli.yaml b/tools/arduino-cli.yaml index 74186be77b..37429e75bf 100644 --- a/tools/arduino-cli.yaml +++ b/tools/arduino-cli.yaml @@ -3,3 +3,4 @@ board_manager: - https://arduino.esp8266.com/stable/package_esp8266com_index.json - https://espressif.github.io/arduino-esp32/package_esp32_index.json - https://www.pjrc.com/teensy/package_teensy_index.json + - https://github.com/earlephilhower/arduino-pico/releases/download/global/package_rp2040_index.json