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