Skip to content

Commit

Permalink
ArduinoBLE support
Browse files Browse the repository at this point in the history
  • Loading branch information
tttapa committed Jan 23, 2024
1 parent a6d2eef commit 196b5fc
Show file tree
Hide file tree
Showing 11 changed files with 535 additions and 8 deletions.
20 changes: 20 additions & 0 deletions src/MIDI_Interfaces/BLEMIDI/ArduinoBLE/midi.hpp
@@ -0,0 +1,20 @@
#pragma once

#include <MIDI_Interfaces/BLEMIDI/BLEAPI.hpp>

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"
119 changes: 119 additions & 0 deletions src/MIDI_Interfaces/BLEMIDI/ArduinoBLE/midi.ipp
@@ -0,0 +1,119 @@
#include <ArduinoBLE.h>

#include <AH/Error/Error.hpp>
#include <Settings/NamespaceSettings.hpp>

#include <MIDI_Interfaces/BLEMIDI/BLEAPI.hpp>

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(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(BLEDevice central) {
DEBUGREF("CS-BLEMIDI disconnected, central: " << central.address());
if (midi_instance)
midi_instance->handleDisconnect(BLEConnectionHandle {});
}

void on_write(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<uint16_t>(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(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
172 changes: 172 additions & 0 deletions src/MIDI_Interfaces/BLEMIDI/ArduinoBLEBackend.hpp
@@ -0,0 +1,172 @@
#pragma once

#include <AH/Error/Error.hpp>

#include "ArduinoBLE/midi.hpp"
#include "BLEAPI.hpp"
#include "MIDI_Interfaces/BLEMIDI/BLERingBuf.hpp"
#include "PollingMIDISender.hpp"
#include <MIDI_Parsers/BLEMIDIParser.hpp>
#include <MIDI_Parsers/SerialMIDI_Parser.hpp>

BEGIN_CS_NAMESPACE

/// ArduinoBLE backend intended to be plugged into
/// @ref GenericBLEMIDI_Interface.
class ArduinoBLEBackend : private PollingMIDISender<ArduinoBLEBackend>,
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 (!ble_buffer.push(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 data to be parsed.
BLERingBuf<1024> 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:
/// MIDI message variant type.
struct IncomingMIDIMessage {
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;

IncomingMIDIMessage() = default;
IncomingMIDIMessage(ChannelMessage message, uint16_t timestamp)
: eventType(MIDIReadEvent::CHANNEL_MESSAGE), message(message),
timestamp(timestamp) {}
IncomingMIDIMessage(SysCommonMessage message, uint16_t timestamp)
: eventType(MIDIReadEvent::SYSCOMMON_MESSAGE), message(message),
timestamp(timestamp) {}
IncomingMIDIMessage(RealTimeMessage message, uint16_t timestamp)
: eventType(MIDIReadEvent::REALTIME_MESSAGE), message(message),
timestamp(timestamp) {}
IncomingMIDIMessage(SysExMessage message, uint16_t timestamp)
: eventType(message.isLastChunk() ? MIDIReadEvent::SYSEX_MESSAGE
: MIDIReadEvent::SYSEX_CHUNK),
message(message), timestamp(timestamp) {}
};

/// 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();
// 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
}
}

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 = PollingMIDISender<ArduinoBLEBackend>;
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

0 comments on commit 196b5fc

Please sign in to comment.