OSSIA
Open Scenario System for Interactive Application
Loading...
Searching...
No Matches
qml_bluetooth.hpp
1#pragma once
2
3#if __has_include(<QBluetoothDeviceDiscoveryAgent>)
4#define OSSIA_HAS_BLUETOOTH 1
5
7
8#include <ossia-qt/protocols/utils.hpp>
9
10#include <QBluetoothDeviceDiscoveryAgent>
11#include <QBluetoothDeviceInfo>
12#include <QBluetoothSocket>
13#include <QBluetoothUuid>
14#include <QJSValue>
15#include <QObject>
16#include <QPointer>
17#include <QQmlEngine>
18#include <QVariant>
19#include <QLowEnergyCharacteristic>
20#include <QLowEnergyController>
21#include <QLowEnergyDescriptor>
22#include <QLowEnergyService>
23
24#include <nano_observer.hpp>
25
26#include <verdigris>
27
28namespace ossia::qt
29{
30
31static QBluetoothUuid bleUuidFromString(const QString& str)
32{
33 if(str.length() <= 4)
34 {
35 bool ok = false;
36 auto val = str.toUInt(&ok, 16);
37 if(ok)
38 return QBluetoothUuid{static_cast<quint16>(val)};
39 }
40 return QBluetoothUuid{QUuid::fromString(str)};
41}
42
43class qml_bluetooth_scanner
44 : public QObject
45 , public Nano::Observer
46{
47 W_OBJECT(qml_bluetooth_scanner)
48public:
49 qml_bluetooth_scanner()
50 {
51 m_agent = new QBluetoothDeviceDiscoveryAgent(this);
52
53 QObject::connect(
54 m_agent, &QBluetoothDeviceDiscoveryAgent::deviceDiscovered, this,
55 [this](const QBluetoothDeviceInfo& info) {
56 if(!onDeviceDiscovered.isCallable())
57 return;
58 auto* engine = qjsEngine(this);
59 if(!engine)
60 return;
61
62 auto dev = engine->newObject();
63 dev.setProperty("Name", info.name());
64 dev.setProperty("Address", info.address().toString());
65 dev.setProperty(
66 "DeviceUuid", info.deviceUuid().toString(QUuid::WithoutBraces));
67 dev.setProperty("RSSI", info.rssi());
68 dev.setProperty(
69 "IsBLE",
70 info.coreConfigurations().testFlag(
71 QBluetoothDeviceInfo::LowEnergyCoreConfiguration));
72 dev.setProperty(
73 "IsClassic",
74 info.coreConfigurations().testFlag(
75 QBluetoothDeviceInfo::BaseRateCoreConfiguration));
76 dev.setProperty(
77 "MajorDeviceClass",
78 static_cast<int>(info.majorDeviceClass()));
79 dev.setProperty("MinorDeviceClass", info.minorDeviceClass());
80
81 auto uuids = info.serviceUuids();
82 auto serviceArr = engine->newArray(uuids.size());
83 for(qsizetype i = 0; i < uuids.size(); ++i)
84 serviceArr.setProperty(i, uuids[i].toString(QUuid::WithoutBraces));
85 dev.setProperty("ServiceUuids", serviceArr);
86
87 auto mfData = info.manufacturerData();
88 if(!mfData.isEmpty())
89 {
90 auto mfObj = engine->newObject();
91 for(auto it = mfData.begin(); it != mfData.end(); ++it)
92 mfObj.setProperty(
93 QString::number(it.key()), engine->toScriptValue(it.value()));
94 dev.setProperty("ManufacturerData", mfObj);
95 }
96
97 onDeviceDiscovered.call({dev});
98 });
99
100 QObject::connect(
101 m_agent, &QBluetoothDeviceDiscoveryAgent::finished, this, [this]() {
102 if(onFinished.isCallable())
103 onFinished.call();
104 });
105
106 QObject::connect(
107 m_agent, &QBluetoothDeviceDiscoveryAgent::errorOccurred, this,
108 [this](QBluetoothDeviceDiscoveryAgent::Error) {
109 if(onError.isCallable())
110 onError.call({m_agent->errorString()});
111 });
112 }
113
114 ~qml_bluetooth_scanner() { stop(); }
115
116 void start()
117 {
118 m_agent->start(
119 QBluetoothDeviceDiscoveryAgent::ClassicMethod
120 | QBluetoothDeviceDiscoveryAgent::LowEnergyMethod);
121 }
122 W_SLOT(start)
123
124 void startBLE()
125 {
126 m_agent->start(QBluetoothDeviceDiscoveryAgent::LowEnergyMethod);
127 }
128 W_SLOT(startBLE)
129
130 void startClassic()
131 {
132 m_agent->start(QBluetoothDeviceDiscoveryAgent::ClassicMethod);
133 }
134 W_SLOT(startClassic)
135
136 void stop() { m_agent->stop(); }
137 W_SLOT(stop)
138
139 void setLowEnergyDiscoveryTimeout(int msTimeout)
140 {
141 m_agent->setLowEnergyDiscoveryTimeout(msTimeout);
142 }
143 W_SLOT(setLowEnergyDiscoveryTimeout)
144
145 QJSValue onDeviceDiscovered;
146 QJSValue onFinished;
147 QJSValue onError;
148
149private:
150 QBluetoothDeviceDiscoveryAgent* m_agent{};
151};
152
153class qml_bluetooth_socket
154 : public QObject
155 , public Nano::Observer
156{
157 W_OBJECT(qml_bluetooth_socket)
158public:
159 struct state
160 {
161 std::atomic_bool alive{true};
162 };
163
164 qml_bluetooth_socket(
165 const QBluetoothAddress& address, const QBluetoothUuid& serviceUuid)
166 : m_state{std::make_shared<state>()}
167 , m_address{address}
168 , m_serviceUuid{serviceUuid}
169 {
170 m_socket = new QBluetoothSocket(QBluetoothServiceInfo::RfcommProtocol, this);
171
172 auto st = m_state;
173 QObject::connect(
174 m_socket, &QBluetoothSocket::connected, this, [this, st]() {
175 if(!st->alive)
176 return;
177 m_is_open = true;
178 if(!onOpen.isCallable())
179 return;
180 auto* engine = qjsEngine(this);
181 if(!engine)
182 return;
183 onOpen.call({engine->newQObject(this)});
184 });
185
186 QObject::connect(
187 m_socket, &QBluetoothSocket::disconnected, this, [this, st]() {
188 if(!st->alive)
189 return;
190 m_is_open = false;
191 if(onClose.isCallable())
192 onClose.call();
193 });
194
195 QObject::connect(
196 m_socket, &QBluetoothSocket::readyRead, this, [this, st]() {
197 if(!st->alive)
198 return;
199 if(!onMessage.isCallable())
200 return;
201 auto data = m_socket->readAll();
202 auto* engine = qjsEngine(this);
203 if(engine)
204 onMessage.call({engine->toScriptValue(data)});
205 });
206
207 QObject::connect(
208 m_socket, &QBluetoothSocket::errorOccurred, this,
209 [this, st](QBluetoothSocket::SocketError) {
210 if(!st->alive)
211 return;
212 if(onError.isCallable())
213 onError.call({m_socket->errorString()});
214 });
215 }
216
217 ~qml_bluetooth_socket()
218 {
219 m_state->alive = false;
220 close();
221 }
222
223 void open() { m_socket->connectToService(m_address, m_serviceUuid); }
224
225 void write(QByteArray data)
226 {
227 if(m_is_open)
228 m_socket->write(data);
229 }
230 W_SLOT(write)
231
232 void close()
233 {
234 if(m_is_open)
235 {
236 m_is_open = false;
237 m_socket->disconnectFromService();
238 }
239 }
240 W_SLOT(close)
241
242 QJSValue onOpen;
243 QJSValue onClose;
244 QJSValue onError;
245 QJSValue onMessage;
246
247private:
248 std::shared_ptr<state> m_state;
249 QBluetoothSocket* m_socket{};
250 QBluetoothAddress m_address;
251 QBluetoothUuid m_serviceUuid;
252 std::atomic_bool m_is_open{false};
253};
254
255class qml_ble_service
256 : public QObject
257 , public Nano::Observer
258{
259 W_OBJECT(qml_ble_service)
260public:
261 explicit qml_ble_service(QLowEnergyService* service, QObject* parent)
262 : QObject{parent}
263 , m_service{service}
264 {
265 QObject::connect(
266 m_service, &QLowEnergyService::stateChanged, this,
267 [this](QLowEnergyService::ServiceState state) {
268 if(state != QLowEnergyService::RemoteServiceDiscovered)
269 return;
270 if(!onDetailsDiscovered.isCallable())
271 return;
272 auto* engine = qjsEngine(this);
273 if(!engine)
274 return;
275
276 auto chars = m_service->characteristics();
277 auto arr = engine->newArray(chars.size());
278 for(qsizetype i = 0; i < chars.size(); ++i)
279 {
280 auto obj = engine->newObject();
281 obj.setProperty(
282 "uuid", chars[i].uuid().toString(QUuid::WithoutBraces));
283 obj.setProperty("name", chars[i].name());
284 obj.setProperty("value", engine->toScriptValue(chars[i].value()));
285 obj.setProperty(
286 "properties", static_cast<int>(chars[i].properties()));
287 arr.setProperty(i, obj);
288 }
289 onDetailsDiscovered.call({arr});
290 });
291
292 QObject::connect(
293 m_service, &QLowEnergyService::characteristicChanged, this,
294 [this](const QLowEnergyCharacteristic& c, const QByteArray& value) {
295 if(!onCharacteristicChanged.isCallable())
296 return;
297 auto* engine = qjsEngine(this);
298 if(engine)
299 onCharacteristicChanged.call(
300 {c.uuid().toString(QUuid::WithoutBraces),
301 engine->toScriptValue(value)});
302 });
303
304 QObject::connect(
305 m_service, &QLowEnergyService::characteristicRead, this,
306 [this](const QLowEnergyCharacteristic& c, const QByteArray& value) {
307 if(!onCharacteristicRead.isCallable())
308 return;
309 auto* engine = qjsEngine(this);
310 if(engine)
311 onCharacteristicRead.call(
312 {c.uuid().toString(QUuid::WithoutBraces),
313 engine->toScriptValue(value)});
314 });
315
316 QObject::connect(
317 m_service, &QLowEnergyService::characteristicWritten, this,
318 [this](const QLowEnergyCharacteristic& c, const QByteArray& value) {
319 if(!onCharacteristicWritten.isCallable())
320 return;
321 auto* engine = qjsEngine(this);
322 if(engine)
323 onCharacteristicWritten.call(
324 {c.uuid().toString(QUuid::WithoutBraces),
325 engine->toScriptValue(value)});
326 });
327
328 QObject::connect(
329 m_service, &QLowEnergyService::errorOccurred, this,
330 [this](QLowEnergyService::ServiceError err) {
331 if(!onError.isCallable())
332 return;
333 QString msg;
334 switch(err)
335 {
336 case QLowEnergyService::NoError:
337 return;
338 case QLowEnergyService::OperationError:
339 msg = QStringLiteral("Operation error");
340 break;
341 case QLowEnergyService::CharacteristicWriteError:
342 msg = QStringLiteral("Characteristic write error");
343 break;
344 case QLowEnergyService::DescriptorWriteError:
345 msg = QStringLiteral("Descriptor write error");
346 break;
347 case QLowEnergyService::CharacteristicReadError:
348 msg = QStringLiteral("Characteristic read error");
349 break;
350 case QLowEnergyService::DescriptorReadError:
351 msg = QStringLiteral("Descriptor read error");
352 break;
353 default:
354 msg = QStringLiteral("Unknown service error");
355 break;
356 }
357 onError.call({msg});
358 });
359 }
360
361 QString uuid() const
362 {
363 return m_service->serviceUuid().toString(QUuid::WithoutBraces);
364 }
365 W_SLOT(uuid)
366
367 void discoverDetails() { m_service->discoverDetails(); }
368 W_SLOT(discoverDetails)
369
370 QJSValue characteristics()
371 {
372 auto* engine = qjsEngine(this);
373 if(!engine)
374 return {};
375
376 auto chars = m_service->characteristics();
377 auto arr = engine->newArray(chars.size());
378 for(qsizetype i = 0; i < chars.size(); ++i)
379 {
380 auto obj = engine->newObject();
381 obj.setProperty("uuid", chars[i].uuid().toString(QUuid::WithoutBraces));
382 obj.setProperty("name", chars[i].name());
383 obj.setProperty("value", engine->toScriptValue(chars[i].value()));
384 obj.setProperty("properties", static_cast<int>(chars[i].properties()));
385 arr.setProperty(i, obj);
386 }
387 return arr;
388 }
389 W_SLOT(characteristics)
390
391 void readCharacteristic(QString uuid)
392 {
393 auto c = m_service->characteristic(bleUuidFromString(uuid));
394 if(!c.isValid())
395 return report(QStringLiteral("no such characteristic: %1").arg(uuid));
396 m_service->readCharacteristic(c);
397 }
398 W_SLOT(readCharacteristic)
399
400 void writeCharacteristic(QString uuid, QByteArray value)
401 {
402 auto c = m_service->characteristic(bleUuidFromString(uuid));
403 if(!c.isValid())
404 return report(QStringLiteral("no such characteristic: %1").arg(uuid));
405 m_service->writeCharacteristic(c, value);
406 }
407 W_SLOT(writeCharacteristic)
408
409 void writeCharacteristicNoResponse(QString uuid, QByteArray value)
410 {
411 auto c = m_service->characteristic(bleUuidFromString(uuid));
412 if(!c.isValid())
413 return report(QStringLiteral("no such characteristic: %1").arg(uuid));
414 if(!c.properties().testFlag(QLowEnergyCharacteristic::WriteNoResponse))
415 report(QStringLiteral("%1 does not advertise WriteNoResponse").arg(uuid));
416 m_service->writeCharacteristic(
417 c, value, QLowEnergyService::WriteWithoutResponse);
418 }
419 W_SLOT(writeCharacteristicNoResponse)
420
421 void enableNotifications(QString uuid)
422 {
423 auto c = m_service->characteristic(bleUuidFromString(uuid));
424 if(!c.isValid())
425 return report(QStringLiteral("no such characteristic: %1").arg(uuid));
426 auto cccd = c.clientCharacteristicConfiguration();
427 if(!cccd.isValid())
428 return report(QStringLiteral(
429 "%1 has no CCCD descriptor; discoverDetails() must "
430 "have completed before subscribing")
431 .arg(uuid));
432
433 if(c.properties().testFlag(QLowEnergyCharacteristic::Indicate))
434 m_service->writeDescriptor(
435 cccd, QLowEnergyCharacteristic::CCCDEnableIndication);
436 else
437 m_service->writeDescriptor(
438 cccd, QLowEnergyCharacteristic::CCCDEnableNotification);
439 }
440 W_SLOT(enableNotifications)
441
442 void disableNotifications(QString uuid)
443 {
444 auto c = m_service->characteristic(bleUuidFromString(uuid));
445 if(!c.isValid())
446 return report(QStringLiteral("no such characteristic: %1").arg(uuid));
447 auto cccd = c.clientCharacteristicConfiguration();
448 if(cccd.isValid())
449 m_service->writeDescriptor(cccd, QLowEnergyCharacteristic::CCCDDisable);
450 }
451 W_SLOT(disableNotifications)
452
453 QJSValue onDetailsDiscovered;
454 QJSValue onCharacteristicRead;
455 QJSValue onCharacteristicWritten;
456 QJSValue onCharacteristicChanged;
457 QJSValue onError;
458
459 W_PROPERTY(QJSValue, onDetailsDiscovered MEMBER onDetailsDiscovered)
460 W_PROPERTY(QJSValue, onCharacteristicRead MEMBER onCharacteristicRead)
461 W_PROPERTY(QJSValue, onCharacteristicWritten MEMBER onCharacteristicWritten)
462 W_PROPERTY(QJSValue, onCharacteristicChanged MEMBER onCharacteristicChanged)
463 W_PROPERTY(QJSValue, onError MEMBER onError)
464
465private:
466 void report(const QString& msg)
467 {
468 ossia::logger().error("BLE service: {}", msg.toStdString());
469 if(onError.isCallable())
470 onError.call({msg});
471 }
472
473 QLowEnergyService* m_service{};
474};
475
476class qml_ble_controller
477 : public QObject
478 , public Nano::Observer
479{
480 W_OBJECT(qml_ble_controller)
481public:
482 struct state
483 {
484 std::atomic_bool alive{true};
485 };
486
487 explicit qml_ble_controller(const QBluetoothDeviceInfo& info)
488 : m_state{std::make_shared<state>()}
489 , m_deviceInfo{info}
490 {
491 m_controller = QLowEnergyController::createCentral(m_deviceInfo, this);
492
493 auto st = m_state;
494 QObject::connect(
495 m_controller, &QLowEnergyController::connected, this, [this, st]() {
496 if(!st->alive)
497 return;
498 m_is_open = true;
499 if(!onConnected.isCallable())
500 return;
501 auto* engine = qjsEngine(this);
502 if(!engine)
503 return;
504 onConnected.call({engine->newQObject(this)});
505 });
506
507 QObject::connect(
508 m_controller, &QLowEnergyController::disconnected, this, [this, st]() {
509 if(!st->alive)
510 return;
511 m_is_open = false;
512 if(onDisconnected.isCallable())
513 onDisconnected.call();
514 });
515
516 QObject::connect(
517 m_controller, &QLowEnergyController::serviceDiscovered, this,
518 [this, st](const QBluetoothUuid& uuid) {
519 if(!st->alive)
520 return;
521 if(onServiceDiscovered.isCallable())
522 onServiceDiscovered.call({uuid.toString(QUuid::WithoutBraces)});
523 });
524
525 QObject::connect(
526 m_controller, &QLowEnergyController::discoveryFinished, this,
527 [this, st]() {
528 if(!st->alive)
529 return;
530 if(!onDiscoveryFinished.isCallable())
531 return;
532 auto* engine = qjsEngine(this);
533 if(!engine)
534 return;
535 onDiscoveryFinished.call({engine->newQObject(this)});
536 });
537
538 QObject::connect(
539 m_controller, &QLowEnergyController::mtuChanged, this,
540 [this, st](int mtu) {
541 if(!st->alive)
542 return;
543 if(onMtuChanged.isCallable())
544 onMtuChanged.call({mtu});
545 });
546
547 QObject::connect(
548 m_controller, &QLowEnergyController::errorOccurred, this,
549 [this, st](QLowEnergyController::Error) {
550 if(!st->alive)
551 return;
552 if(onError.isCallable())
553 onError.call({m_controller->errorString()});
554 });
555 }
556
557 ~qml_ble_controller()
558 {
559 m_state->alive = false;
560 close();
561 }
562
563 void open() { m_controller->connectToDevice(); }
564
565 void close()
566 {
567 if(m_is_open)
568 {
569 m_is_open = false;
570 m_controller->disconnectFromDevice();
571 }
572 }
573 W_SLOT(close)
574
575 void discoverServices() { m_controller->discoverServices(); }
576 W_SLOT(discoverServices)
577
578 QObject* service(QString uuid)
579 {
580 auto serviceUuid = bleUuidFromString(uuid);
581 auto* svc = m_controller->createServiceObject(serviceUuid, this);
582 if(!svc)
583 return nullptr;
584
585 auto* wrapper = new qml_ble_service(svc, this);
586 auto* engine = qjsEngine(this);
587 if(engine)
588 engine->newQObject(wrapper);
589 return wrapper;
590 }
591 W_SLOT(service)
592
593 QJSValue services()
594 {
595 auto* engine = qjsEngine(this);
596 if(!engine)
597 return {};
598
599 auto uuids = m_controller->services();
600 auto arr = engine->newArray(uuids.size());
601 for(qsizetype i = 0; i < uuids.size(); ++i)
602 arr.setProperty(i, uuids[i].toString(QUuid::WithoutBraces));
603 return arr;
604 }
605 W_SLOT(services)
606
607 int mtu() const { return m_controller->mtu(); }
608 W_SLOT(mtu)
609
610 QString remoteName() const { return m_controller->remoteName(); }
611 W_SLOT(remoteName)
612
613 QJSValue onConnected;
614 QJSValue onDisconnected;
615 QJSValue onServiceDiscovered;
616 QJSValue onDiscoveryFinished;
617 QJSValue onMtuChanged;
618 QJSValue onError;
619
620private:
621 std::shared_ptr<state> m_state;
622 QLowEnergyController* m_controller{};
623 QBluetoothDeviceInfo m_deviceInfo;
624 std::atomic_bool m_is_open{false};
625};
626
627} // namespace ossia::qt
628
629#endif // __has_include(<QBluetoothDeviceDiscoveryAgent>)
Definition qml_device.cpp:43
spdlog::logger & logger() noexcept
Where the errors will be logged. Default is stderr.
Definition context.cpp:120