Skip to content

Commit 196b5fc

Browse files
committed
ArduinoBLE support
1 parent a6d2eef commit 196b5fc

11 files changed

+535
-8
lines changed
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
#pragma once
2+
3+
#include <MIDI_Interfaces/BLEMIDI/BLEAPI.hpp>
4+
5+
BEGIN_CS_NAMESPACE
6+
7+
namespace arduino_ble_midi {
8+
9+
bool init(MIDIBLEInstance &instance, BLESettings ble_settings);
10+
void poll();
11+
void notify(BLEDataView data);
12+
13+
} // namespace arduino_ble_midi
14+
15+
END_CS_NAMESPACE
16+
17+
// We cannot do this in a separate .cpp file, because the user might not have
18+
// the ArduinoBLE library installed, and the Arduino library dependency scanner
19+
// does not support __has_include.
20+
#include "midi.ipp"
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
#include <ArduinoBLE.h>
2+
3+
#include <AH/Error/Error.hpp>
4+
#include <Settings/NamespaceSettings.hpp>
5+
6+
#include <MIDI_Interfaces/BLEMIDI/BLEAPI.hpp>
7+
8+
BEGIN_CS_NAMESPACE
9+
10+
namespace arduino_ble_midi {
11+
12+
namespace {
13+
14+
BLEService midi_service {
15+
"03B80E5A-EDE8-4B33-A751-6CE34EC4C700",
16+
};
17+
BLECharacteristic midi_char {
18+
"7772E5DB-3868-4112-A1A9-F2669D106BF3",
19+
BLEWriteWithoutResponse | BLERead | BLENotify,
20+
512,
21+
false,
22+
};
23+
MIDIBLEInstance *midi_instance = nullptr;
24+
25+
bool is_midi_char(const BLECharacteristic &characteristic) {
26+
return strcasecmp(midi_char.uuid(), characteristic.uuid()) == 0;
27+
}
28+
29+
// Here I assume that all callbacks and handlers execute in the same task/thread
30+
// as the main program.
31+
32+
void on_connect(BLEDevice central) {
33+
DEBUGREF("CS-BLEMIDI connected, central: " << central.address());
34+
if (midi_instance) {
35+
midi_instance->handleConnect(BLEConnectionHandle {0});
36+
midi_instance->handleSubscribe(BLEConnectionHandle {0},
37+
BLECharacteristicHandle {0}, true);
38+
}
39+
}
40+
41+
void on_disconnect(BLEDevice central) {
42+
DEBUGREF("CS-BLEMIDI disconnected, central: " << central.address());
43+
if (midi_instance)
44+
midi_instance->handleDisconnect(BLEConnectionHandle {});
45+
}
46+
47+
void on_write(BLEDevice central, BLECharacteristic characteristic) {
48+
DEBUGREF(
49+
"CS-BLEMIDI write, central: "
50+
<< central.address() << ", char: " << characteristic.uuid()
51+
<< ", data: [" << characteristic.valueLength() << "] "
52+
<< AH::HexDump(characteristic.value(), characteristic.valueLength()));
53+
if (!is_midi_char(characteristic))
54+
return;
55+
if (!midi_instance)
56+
return;
57+
BLEDataView data {characteristic.value(),
58+
static_cast<uint16_t>(characteristic.valueLength())};
59+
auto data_gen = [data {data}]() mutable { return std::exchange(data, {}); };
60+
midi_instance->handleData(
61+
BLEConnectionHandle {0},
62+
BLEDataGenerator {compat::in_place, std::move(data_gen)},
63+
BLEDataLifetime::ConsumeImmediately);
64+
}
65+
66+
void on_read(BLEDevice central, BLECharacteristic characteristic) {
67+
DEBUGREF("CS-BLEMIDI read, central: " << central.address() << ", char: "
68+
<< characteristic.uuid());
69+
if (!is_midi_char(characteristic))
70+
return;
71+
characteristic.setValue(nullptr, 0);
72+
}
73+
74+
} // namespace
75+
76+
inline bool init(MIDIBLEInstance &instance, BLESettings ble_settings) {
77+
midi_instance = &instance;
78+
// Initialize the BLE hardware
79+
if (!BLE.begin()) {
80+
ERROR(F("Starting Bluetooth® Low Energy module failed!"), 0x7532);
81+
return false;
82+
}
83+
84+
// Set the local name and advertise the MIDI service
85+
BLE.setLocalName(ble_settings.device_name);
86+
BLE.setAdvertisedService(midi_service);
87+
// Note: advertising connection interval range not supported by ArduinoBLE
88+
89+
// Configure the MIDI service and characteristic
90+
midi_service.addCharacteristic(midi_char);
91+
BLE.addService(midi_service);
92+
93+
// Assign event handlers
94+
BLE.setEventHandler(BLEConnected, on_connect);
95+
BLE.setEventHandler(BLEDisconnected, on_disconnect);
96+
midi_char.setEventHandler(BLEWritten, on_write);
97+
midi_char.setEventHandler(BLERead, on_read);
98+
99+
// Start advertising
100+
BLE.advertise();
101+
102+
return true;
103+
}
104+
105+
inline void poll() {
106+
// poll for Bluetooth® Low Energy events
107+
BLE.poll();
108+
}
109+
110+
// TODO: there is currently no way in ArduinoBLE to request the MTU. So we
111+
// assume the tiny default of 23 bytes.
112+
113+
inline void notify(BLEDataView data) {
114+
midi_char.writeValue(data.data, data.length);
115+
}
116+
117+
} // namespace arduino_ble_midi
118+
119+
END_CS_NAMESPACE
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
#pragma once
2+
3+
#include <AH/Error/Error.hpp>
4+
5+
#include "ArduinoBLE/midi.hpp"
6+
#include "BLEAPI.hpp"
7+
#include "MIDI_Interfaces/BLEMIDI/BLERingBuf.hpp"
8+
#include "PollingMIDISender.hpp"
9+
#include <MIDI_Parsers/BLEMIDIParser.hpp>
10+
#include <MIDI_Parsers/SerialMIDI_Parser.hpp>
11+
12+
BEGIN_CS_NAMESPACE
13+
14+
/// ArduinoBLE backend intended to be plugged into
15+
/// @ref GenericBLEMIDI_Interface.
16+
class ArduinoBLEBackend : private PollingMIDISender<ArduinoBLEBackend>,
17+
private MIDIBLEInstance {
18+
private:
19+
// Callbacks from the ArduinoBLE stack.
20+
void handleConnect(BLEConnectionHandle) override { connected = true; }
21+
void handleDisconnect(BLEConnectionHandle) override {
22+
connected = subscribed = false;
23+
}
24+
void handleMTU(BLEConnectionHandle, uint16_t mtu) override {
25+
Sender::updateMTU(mtu);
26+
}
27+
void handleSubscribe(BLEConnectionHandle, BLECharacteristicHandle,
28+
bool notify) override {
29+
subscribed = notify;
30+
}
31+
void handleData(BLEConnectionHandle, BLEDataGenerator &&data,
32+
BLEDataLifetime) override {
33+
while (true) {
34+
BLEDataView packet = data();
35+
if (packet.length == 0) {
36+
break;
37+
} else if (!ble_buffer.push(packet)) {
38+
DEBUGREF(F("BLE packet dropped, size: ") << packet.length);
39+
break;
40+
}
41+
}
42+
}
43+
44+
private:
45+
/// Are we connected to a BLE Central?
46+
bool connected = false;
47+
/// Did the BLE Central subscribe to be notified for the MIDI characteristic?
48+
bool subscribed = false;
49+
/// Contains incoming data to be parsed.
50+
BLERingBuf<1024> ble_buffer {};
51+
/// Parses the (chunked) BLE packet obtained from @ref ble_buffer.
52+
BLEMIDIParser ble_parser {nullptr, 0};
53+
/// Parser for MIDI data extracted from the BLE packet by @ref ble_parser.
54+
SerialMIDI_Parser parser {false};
55+
56+
public:
57+
/// MIDI message variant type.
58+
struct IncomingMIDIMessage {
59+
MIDIReadEvent eventType = MIDIReadEvent::NO_MESSAGE;
60+
union Message {
61+
ChannelMessage channelmessage;
62+
SysCommonMessage syscommonmessage;
63+
RealTimeMessage realtimemessage;
64+
SysExMessage sysexmessage;
65+
66+
Message() : realtimemessage(0x00) {}
67+
Message(ChannelMessage msg) : channelmessage(msg) {}
68+
Message(SysCommonMessage msg) : syscommonmessage(msg) {}
69+
Message(RealTimeMessage msg) : realtimemessage(msg) {}
70+
Message(SysExMessage msg) : sysexmessage(msg) {}
71+
} message;
72+
uint16_t timestamp = 0xFFFF;
73+
74+
IncomingMIDIMessage() = default;
75+
IncomingMIDIMessage(ChannelMessage message, uint16_t timestamp)
76+
: eventType(MIDIReadEvent::CHANNEL_MESSAGE), message(message),
77+
timestamp(timestamp) {}
78+
IncomingMIDIMessage(SysCommonMessage message, uint16_t timestamp)
79+
: eventType(MIDIReadEvent::SYSCOMMON_MESSAGE), message(message),
80+
timestamp(timestamp) {}
81+
IncomingMIDIMessage(RealTimeMessage message, uint16_t timestamp)
82+
: eventType(MIDIReadEvent::REALTIME_MESSAGE), message(message),
83+
timestamp(timestamp) {}
84+
IncomingMIDIMessage(SysExMessage message, uint16_t timestamp)
85+
: eventType(message.isLastChunk() ? MIDIReadEvent::SYSEX_MESSAGE
86+
: MIDIReadEvent::SYSEX_CHUNK),
87+
message(message), timestamp(timestamp) {}
88+
};
89+
90+
/// Retrieve and remove a single incoming MIDI message from the buffer.
91+
bool popMessage(IncomingMIDIMessage &incomingMessage) {
92+
// This function is assumed to be polled regularly by the higher-level
93+
// MIDI_Interface, so we check the sender's timer here, and we poll
94+
// the ArduinoBLE library.
95+
auto lck = Sender::acquirePacket();
96+
Sender::releasePacketAndNotify(lck);
97+
arduino_ble_midi::poll();
98+
// Try reading a MIDI message from the parser
99+
auto try_read = [&] {
100+
MIDIReadEvent event = parser.pull(ble_parser);
101+
switch (event) {
102+
case MIDIReadEvent::CHANNEL_MESSAGE:
103+
incomingMessage = {parser.getChannelMessage(),
104+
ble_parser.getTimestamp()};
105+
return true;
106+
case MIDIReadEvent::SYSEX_CHUNK: // fallthrough
107+
case MIDIReadEvent::SYSEX_MESSAGE:
108+
incomingMessage = {parser.getSysExMessage(),
109+
ble_parser.getTimestamp()};
110+
return true;
111+
case MIDIReadEvent::REALTIME_MESSAGE:
112+
incomingMessage = {parser.getRealTimeMessage(),
113+
ble_parser.getTimestamp()};
114+
return true;
115+
case MIDIReadEvent::SYSCOMMON_MESSAGE:
116+
incomingMessage = {parser.getSysCommonMessage(),
117+
ble_parser.getTimestamp()};
118+
return true;
119+
case MIDIReadEvent::NO_MESSAGE: return false;
120+
default: break; // LCOV_EXCL_LINE
121+
}
122+
return false;
123+
};
124+
while (true) {
125+
// Try reading a MIDI message from the current buffer
126+
if (try_read())
127+
return true; // success, incomingMessage updated
128+
// Get the next chunk of the BLE packet (if available)
129+
BLEDataView chunk;
130+
auto popped = ble_buffer.pop(chunk);
131+
if (popped == BLEDataType::None)
132+
return false; // no more BLE data available
133+
else if (popped == BLEDataType::Continuation)
134+
ble_parser.extend(chunk.data, chunk.length); // same BLE packet
135+
else if (popped == BLEDataType::Packet)
136+
ble_parser = {chunk.data, chunk.length}; // new BLE packet
137+
}
138+
}
139+
140+
public:
141+
/// Initialize the BLE stack etc.
142+
void begin(BLESettings ble_settings) {
143+
arduino_ble_midi::init(*this, ble_settings);
144+
Sender::begin();
145+
}
146+
/// Deinitialize the BLE stack.
147+
/// @todo Not yet implemented.
148+
void end() {}
149+
/// Returns true if we are connected to a BLE Central device.
150+
bool isConnected() const { return connected; }
151+
152+
private:
153+
// Implement the interface for the BLE sender.
154+
using Sender = PollingMIDISender<ArduinoBLEBackend>;
155+
friend Sender;
156+
/// Send the given MIDI BLE packet.
157+
void sendData(BLEDataView data) {
158+
if (connected && subscribed)
159+
arduino_ble_midi::notify(data);
160+
}
161+
162+
public:
163+
// Expose the necessary BLE sender functions.
164+
using Sender::acquirePacket;
165+
using Sender::forceMinMTU;
166+
using Sender::getMinMTU;
167+
using Sender::releasePacketAndNotify;
168+
using Sender::sendNow;
169+
using Sender::setTimeout;
170+
};
171+
172+
END_CS_NAMESPACE

0 commit comments

Comments
 (0)