-
Notifications
You must be signed in to change notification settings - Fork 141
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
11 changed files
with
535 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
Oops, something went wrong.