QGroundControl
Ground Control Station for MAVLink Drones
Loading...
Searching...
No Matches
BluetoothConfiguration.cc
Go to the documentation of this file.
2
4#include "QGCNetworkHelper.h"
5
6#include <QtBluetooth/QBluetoothHostInfo>
7#include <QtBluetooth/QBluetoothUuid>
8#include <QtConcurrent/QtConcurrentRun>
9#include <QtCore/QCoreApplication>
10#include <QtCore/QPermissions>
11#include <QtCore/QTimer>
12#include <QtCore/QVariantMap>
13
14QGC_LOGGING_CATEGORY(BluetoothLinkLog, "Comms.BluetoothLink")
15QGC_LOGGING_CATEGORY(BluetoothLinkVerboseLog, "Comms.BluetoothLink:verbose")
16
17/*===========================================================================*/
18
19BluetoothConfiguration::BluetoothConfiguration(const QString &name, QObject *parent)
20 : LinkConfiguration(name, parent)
21{
22 qCDebug(BluetoothLinkLog) << this;
23}
24
25BluetoothConfiguration::BluetoothConfiguration(const BluetoothConfiguration *copy, QObject *parent)
26 : LinkConfiguration(copy, parent)
27 , _mode(copy->mode())
28 , _device(copy->device())
29 , _connectedRssi(copy->connectedRssi())
30 , _serviceUuid(copy->_serviceUuid)
31 , _readCharacteristicUuid(copy->readCharacteristicUuid())
32 , _writeCharacteristicUuid(copy->writeCharacteristicUuid())
33{
34 qCDebug(BluetoothLinkLog) << this;
35}
36
37BluetoothConfiguration::~BluetoothConfiguration()
38{
39 stopScan();
40
41 if (_adapterWatcher) {
42 _adapterWatcher->cancel();
43 _adapterWatcher->waitForFinished();
44 }
45
46 qCDebug(BluetoothLinkLog) << this;
47}
48
49bool BluetoothConfiguration::_createLocalDevice(const QBluetoothAddress &address)
50{
51 if (_localDevice) {
52 _localDevice->disconnect();
53 _localDevice->deleteLater();
54 _localDevice = nullptr;
55 }
56
57 _localDevice = new QBluetoothLocalDevice(address, this);
58
59 if (!_localDevice->isValid()) {
60 qCWarning(BluetoothLinkLog) << "Failed to initialize Bluetooth adapter:" << address.toString();
61 _localDevice->deleteLater();
62 _localDevice = nullptr;
63 return false;
64 }
65
66 _connectLocalDeviceSignals();
67
68 qCDebug(BluetoothLinkLog) << "Initialized Bluetooth adapter:" << _localDevice->name() << _localDevice->address().toString();
70 return true;
71}
72
73void BluetoothConfiguration::_connectLocalDeviceSignals()
74{
75 if (!_localDevice || !_localDevice->isValid()) {
76 return;
77 }
78
79 (void) connect(_localDevice.data(), &QBluetoothLocalDevice::hostModeStateChanged,
80 this, &BluetoothConfiguration::_onHostModeStateChanged);
81 (void) connect(_localDevice.data(), &QBluetoothLocalDevice::deviceConnected,
82 this, &BluetoothConfiguration::_onDeviceConnected);
83 (void) connect(_localDevice.data(), &QBluetoothLocalDevice::deviceDisconnected,
84 this, &BluetoothConfiguration::_onDeviceDisconnected);
85 (void) connect(_localDevice.data(), &QBluetoothLocalDevice::pairingFinished,
86 this, &BluetoothConfiguration::_onPairingFinished);
87 (void) connect(_localDevice.data(), &QBluetoothLocalDevice::errorOccurred,
88 this, &BluetoothConfiguration::_onLocalDeviceErrorOccurred);
89}
90
91void BluetoothConfiguration::_initDeviceDiscoveryAgent()
92{
93 if (!_deviceDiscoveryAgent) {
94 _deviceDiscoveryAgent = new QBluetoothDeviceDiscoveryAgent(this);
95
96 _deviceDiscoveryAgent->setLowEnergyDiscoveryTimeout(15000);
97
98 (void) connect(_deviceDiscoveryAgent.data(), &QBluetoothDeviceDiscoveryAgent::deviceDiscovered,
99 this, &BluetoothConfiguration::_deviceDiscovered);
100 (void) connect(_deviceDiscoveryAgent.data(), &QBluetoothDeviceDiscoveryAgent::canceled,
102 (void) connect(_deviceDiscoveryAgent.data(), &QBluetoothDeviceDiscoveryAgent::finished,
103 this, &BluetoothConfiguration::_onDiscoveryFinished);
104 (void) connect(_deviceDiscoveryAgent.data(), &QBluetoothDeviceDiscoveryAgent::errorOccurred,
105 this, &BluetoothConfiguration::_onDiscoveryErrorOccurred);
106
107 (void) connect(_deviceDiscoveryAgent.data(), &QBluetoothDeviceDiscoveryAgent::deviceUpdated,
108 this, &BluetoothConfiguration::_deviceUpdated);
109 }
110
111 if (!_localDevice) {
112 if (!_availableAdapters.isEmpty()) {
113 const QVariantMap adapter = _availableAdapters.first().toMap();
114 const QBluetoothAddress address(adapter.value("address").toString());
115 if (!address.isNull()) {
116 (void) _createLocalDevice(address);
117 }
118 } else {
119 _refreshAvailableAdaptersAsync();
120 }
121 }
122}
123
124void BluetoothConfiguration::_refreshAvailableAdaptersAsync()
125{
126 if (_adapterEnumerationInProgress) {
127 return;
128 }
129
130 _adapterEnumerationInProgress = true;
131
132 if (!_adapterWatcher) {
133 _adapterWatcher = new QFutureWatcher<QList<QBluetoothHostInfo>>(this);
134 (void) connect(_adapterWatcher.data(), &QFutureWatcher<QList<QBluetoothHostInfo>>::finished, this, [this]() {
135 _adapterEnumerationInProgress = false;
136 _updateAvailableAdapters(_adapterWatcher->future().result());
137 });
138 }
139
140 _adapterWatcher->setFuture(QtConcurrent::run([]() {
141 return QBluetoothLocalDevice::allDevices();
142 }));
143}
144
145void BluetoothConfiguration::_updateAvailableAdapters(const QList<QBluetoothHostInfo> &hosts)
146{
147 QVariantList adapters;
148 const QBluetoothAddress selectedAddress = (_localDevice && _localDevice->isValid())
149 ? _localDevice->address()
150 : QBluetoothAddress();
151
152 for (const QBluetoothHostInfo &host : hosts) {
153 QVariantMap adapter;
154 adapter["name"] = host.name();
155 adapter["address"] = host.address().toString();
156 adapter["selected"] = (!selectedAddress.isNull() && (host.address() == selectedAddress));
157 adapters.append(adapter);
158 }
159
160 if (_availableAdapters != adapters) {
161 _availableAdapters = adapters;
162 emit adapterStateChanged();
163 }
164}
165
166bool BluetoothConfiguration::_ensureLocalDevice()
167{
168 if (_localDevice && _localDevice->isValid()) {
169 return true;
170 }
171
172 // Prefer known adapter entries first.
173 for (const QVariant &entry : std::as_const(_availableAdapters)) {
174 const QBluetoothAddress address(entry.toMap().value("address").toString());
175 if (!address.isNull() && _createLocalDevice(address)) {
176 return true;
177 }
178 }
179
180 // Refresh synchronously as a fallback for platforms where async refresh may not have completed yet.
181 const QList<QBluetoothHostInfo> hosts = QBluetoothLocalDevice::allDevices();
182 if (!hosts.isEmpty()) {
183 _updateAvailableAdapters(hosts);
184 for (const QBluetoothHostInfo &host : hosts) {
185 if (!host.address().isNull() && _createLocalDevice(host.address())) {
186 return true;
187 }
188 }
189 }
190
191 // Some platforms may expose only the default adapter.
192 _localDevice = new QBluetoothLocalDevice(this);
193 if (_localDevice->isValid()) {
194 _connectLocalDeviceSignals();
195 emit adapterStateChanged();
196 return true;
197 }
198
199 _localDevice->deleteLater();
200 _localDevice = nullptr;
201 return false;
202}
203
204void BluetoothConfiguration::_deviceUpdated(const QBluetoothDeviceInfo &info, QBluetoothDeviceInfo::Fields updatedFields)
205{
206 qCDebug(BluetoothLinkLog) << "Device Updated:" << info.name() << "Fields:" << updatedFields;
207
208 if (!info.isValid() || (updatedFields == QBluetoothDeviceInfo::Field::None)) {
209 qCDebug(BluetoothLinkVerboseLog) << "Ignoring device update: invalid or no updated fields"
210 << info.name() << info.address().toString() << updatedFields;
211 return;
212 }
213
214 bool found = false;
215 for (QBluetoothDeviceInfo &dev : _deviceList) {
216 if (dev.address() == info.address()) {
217 found = true;
218 if (updatedFields & QBluetoothDeviceInfo::Field::RSSI) {
219 dev.setRssi(info.rssi());
220 }
221 if (updatedFields & QBluetoothDeviceInfo::Field::ManufacturerData) {
222 const QMultiHash<quint16, QByteArray> data = info.manufacturerData();
223 for (quint16 id : info.manufacturerIds()) {
224 dev.setManufacturerData(id, data.value(id));
225 }
226 }
227 if (updatedFields & QBluetoothDeviceInfo::Field::ServiceData) {
228 const QMultiHash<QBluetoothUuid, QByteArray> data = info.serviceData();
229 for (const QBluetoothUuid &uuid : info.serviceIds()) {
230 dev.setServiceData(uuid, data.value(uuid));
231 }
232 }
233 if (updatedFields & QBluetoothDeviceInfo::Field::All) {
234 dev = info;
235 }
236 _updateDeviceList();
237 if (_device.address() == dev.address()) {
238 emit selectedRssiChanged();
239 }
240 break;
241 }
242 }
243
244 if (!found && _isDeviceCompatibleWithMode(info)) {
245 _deviceList.append(info);
246 _updateDeviceList();
247 if (_device.address() == info.address()) {
248 emit selectedRssiChanged();
249 }
250 }
251}
252
253bool BluetoothConfiguration::_isDeviceCompatibleWithMode(const QBluetoothDeviceInfo &info) const
254{
255 // Some stacks temporarily report unknown core configuration while discovery is active.
256 // Treat unknown as compatible so devices appear during live scans.
257 if (info.coreConfigurations() == QBluetoothDeviceInfo::UnknownCoreConfiguration) {
258 return true;
259 }
260
261 if (_mode == BluetoothMode::ModeLowEnergy) {
262 return (info.coreConfigurations() & QBluetoothDeviceInfo::LowEnergyCoreConfiguration);
263 } else {
264 return (info.coreConfigurations() & QBluetoothDeviceInfo::BaseRateCoreConfiguration) ||
265 (info.coreConfigurations() & QBluetoothDeviceInfo::BaseRateAndLowEnergyCoreConfiguration);
266 }
267}
268
269void BluetoothConfiguration::setMode(BluetoothMode mode)
270{
271 if (_mode != mode) {
272 stopScan();
273 _mode = mode;
274 emit modeChanged();
275
276 _deviceList.clear();
277 _updateDeviceList();
278 }
279}
280
281void BluetoothConfiguration::copyFrom(const LinkConfiguration *source)
282{
283 LinkConfiguration::copyFrom(source);
284
285 const BluetoothConfiguration *bluetoothSource = qobject_cast<const BluetoothConfiguration*>(source);
286 if (!bluetoothSource) {
287 qCWarning(BluetoothLinkLog) << "Invalid source configuration type";
288 return;
289 }
290
291 if (_mode != bluetoothSource->mode()) {
292 _mode = bluetoothSource->mode();
293 emit modeChanged();
294 }
295
296 if (_device.address() != bluetoothSource->device().address()) {
297 _device = bluetoothSource->device();
298 emit deviceChanged();
299 }
300
301 if (_serviceUuid != bluetoothSource->_serviceUuid) {
302 _serviceUuid = bluetoothSource->_serviceUuid;
303 emit serviceUuidChanged();
304 }
305
306 if (_readCharacteristicUuid != bluetoothSource->readCharacteristicUuid()) {
307 _readCharacteristicUuid = bluetoothSource->readCharacteristicUuid();
308 emit readUuidChanged();
309 }
310
311 if (_writeCharacteristicUuid != bluetoothSource->writeCharacteristicUuid()) {
312 _writeCharacteristicUuid = bluetoothSource->writeCharacteristicUuid();
313 emit writeUuidChanged();
314 }
315
316 if (_connectedRssi != bluetoothSource->connectedRssi()) {
317 _connectedRssi = bluetoothSource->connectedRssi();
319 }
320}
321
322void BluetoothConfiguration::loadSettings(QSettings &settings, const QString &root)
323{
324 settings.beginGroup(root);
325
326 _mode = static_cast<BluetoothMode>(settings.value("mode", static_cast<int>(BluetoothMode::ModeClassic)).toInt());
327
328 const QString deviceName = settings.value("deviceName").toString();
329 const QBluetoothAddress address(settings.value("address").toString());
330
331 if (!deviceName.isEmpty() && !address.isNull()) {
332 _device = QBluetoothDeviceInfo(address, deviceName, 0);
333 }
334
335 _serviceUuid = QBluetoothUuid(settings.value("serviceUuid", _serviceUuid.toString()).toString());
336 _readCharacteristicUuid = QBluetoothUuid(settings.value("readCharUuid", _readCharacteristicUuid.toString()).toString());
337 _writeCharacteristicUuid = QBluetoothUuid(settings.value("writeCharUuid", _writeCharacteristicUuid.toString()).toString());
338
339 settings.endGroup();
340}
341
342void BluetoothConfiguration::saveSettings(QSettings &settings, const QString &root) const
343{
344 settings.beginGroup(root);
345
346 settings.setValue("mode", static_cast<int>(_mode));
347 settings.setValue("deviceName", _device.name());
348 settings.setValue("address", _device.address().toString());
349 settings.setValue("serviceUuid", _serviceUuid.toString());
350 settings.setValue("readCharUuid", _readCharacteristicUuid.toString());
351 settings.setValue("writeCharUuid", _writeCharacteristicUuid.toString());
352
353 settings.endGroup();
354}
355
356QString BluetoothConfiguration::settingsTitle() const
357{
358 if (isBluetoothAvailable()) {
359 return (_mode == BluetoothMode::ModeLowEnergy) ? tr("Bluetooth Low Energy Link Settings") : tr("Bluetooth Link Settings");
360 }
361 return tr("Bluetooth Not Available");
362}
363
364bool BluetoothConfiguration::isBluetoothAvailable()
365{
367}
368
369QVariantList BluetoothConfiguration::devicesModel() const
370{
371 return _devicesModelCache;
372}
373
374void BluetoothConfiguration::setConnectedRssi(qint16 rssi)
375{
376 if (_connectedRssi != rssi) {
377 _connectedRssi = rssi;
379 emit selectedRssiChanged();
380 }
381}
382
383qint16 BluetoothConfiguration::selectedRssi() const
384{
385 const QBluetoothAddress selAddr = _device.address();
386 if (!selAddr.isNull()) {
387 for (const QBluetoothDeviceInfo &dev : _deviceList) {
388 if (dev.address() == selAddr) {
389 return dev.rssi();
390 }
391 }
392 }
393 return 0;
394}
395
396void BluetoothConfiguration::setServiceUuid(const QString &uuid)
397{
398 const QBluetoothUuid newUuid(uuid);
399 if (!uuid.isEmpty() && newUuid.isNull()) {
400 emit errorOccurred(tr("Invalid service UUID format: %1").arg(uuid));
401 return;
402 }
403 if (_serviceUuid != newUuid) {
404 _serviceUuid = newUuid;
405 emit serviceUuidChanged();
406 }
407}
408
409void BluetoothConfiguration::setReadUuid(const QString &uuid)
410{
411 const QBluetoothUuid newUuid(uuid);
412 if (!uuid.isEmpty() && newUuid.isNull()) {
413 emit errorOccurred(tr("Invalid read characteristic UUID format: %1").arg(uuid));
414 return;
415 }
416 if (_readCharacteristicUuid != newUuid) {
417 _readCharacteristicUuid = newUuid;
418 emit readUuidChanged();
419 }
420}
421
422void BluetoothConfiguration::setWriteUuid(const QString &uuid)
423{
424 const QBluetoothUuid newUuid(uuid);
425 if (!uuid.isEmpty() && newUuid.isNull()) {
426 emit errorOccurred(tr("Invalid write characteristic UUID format: %1").arg(uuid));
427 return;
428 }
429 if (_writeCharacteristicUuid != newUuid) {
430 _writeCharacteristicUuid = newUuid;
431 emit writeUuidChanged();
432 }
433}
434
435void BluetoothConfiguration::startScan()
436{
437 _initDeviceDiscoveryAgent();
438
439 if (!_deviceDiscoveryAgent) {
440 return;
441 }
442
443 {
444 QBluetoothPermission permission;
445 permission.setCommunicationModes(QBluetoothPermission::Access);
446 const Qt::PermissionStatus status = QCoreApplication::instance()->checkPermission(permission);
447 if (status == Qt::PermissionStatus::Undetermined) {
448 QCoreApplication::instance()->requestPermission(permission, this, [this](const QPermission &perm) {
449 if (perm.status() == Qt::PermissionStatus::Granted) {
450 startScan();
451 } else {
452 errorOccurred(tr("Bluetooth Permission Denied"));
453 }
454 });
455 return;
456 } else if (status != Qt::PermissionStatus::Granted) {
457 emit errorOccurred(tr("Bluetooth Permission Denied"));
458 return;
459 }
460 }
461
462 if (_localDevice && _localDevice->isValid() && (_localDevice->hostMode() == QBluetoothLocalDevice::HostPoweredOff)) {
463 _localDevice->powerOn();
464 }
465
466 _deviceList.clear();
467 _updateDeviceList();
468
469 if (_mode == BluetoothMode::ModeLowEnergy) {
470 _deviceDiscoveryAgent->start(QBluetoothDeviceDiscoveryAgent::LowEnergyMethod);
471 } else {
472 _deviceDiscoveryAgent->start(QBluetoothDeviceDiscoveryAgent::ClassicMethod);
473 }
474
475 emit scanningChanged();
476}
477
478void BluetoothConfiguration::stopScan()
479{
480 if (_deviceDiscoveryAgent && scanning()) {
481 _deviceDiscoveryAgent->stop();
482 }
483}
484
485void BluetoothConfiguration::setDevice(const QString &name)
486{
487 for (const QBluetoothDeviceInfo &info : std::as_const(_deviceList)) {
488 if (info.name() == name) {
489 stopScan();
490 _applySelectedDevice(info);
491 return;
492 }
493 }
494}
495
496void BluetoothConfiguration::setDeviceByAddress(const QString &address)
497{
498 if (address.isEmpty()) {
499 return;
500 }
501 const QBluetoothAddress addr(address);
502 if (addr.isNull()) {
503 return;
504 }
505
506 for (const QBluetoothDeviceInfo &info : std::as_const(_deviceList)) {
507 if (info.address() == addr) {
508 stopScan();
509 _applySelectedDevice(info);
510 return;
511 }
512 }
513}
514
515void BluetoothConfiguration::_applySelectedDevice(const QBluetoothDeviceInfo &info)
516{
517 _device = info;
518
519 if (_mode == BluetoothMode::ModeLowEnergy) {
520 const QList<QBluetoothUuid> serviceUuids = info.serviceUuids();
521 if (!serviceUuids.isEmpty()) {
522 QBluetoothUuid serviceUuid = QBluetoothUuid();
523 for (const QBluetoothUuid &uuid : serviceUuids) {
524 if ((uuid == NORDIC_UART_SERVICE) || (uuid == TI_SENSORTAG_SERVICE)) {
525 serviceUuid = uuid;
526
527 if (uuid == NORDIC_UART_SERVICE) {
528 _readCharacteristicUuid = NORDIC_UART_RX_CHAR;
529 _writeCharacteristicUuid = NORDIC_UART_TX_CHAR;
530 } else {
531 _readCharacteristicUuid = TI_SENSORTAG_CHAR;
532 _writeCharacteristicUuid = TI_SENSORTAG_CHAR;
533 }
534 break;
535 }
536 }
537
538 if (serviceUuid.isNull()) {
539 _serviceUuid = serviceUuids.first();
540 _readCharacteristicUuid = QBluetoothUuid();
541 _writeCharacteristicUuid = QBluetoothUuid();
542 } else {
543 _serviceUuid = serviceUuid;
544 }
545
546 emit serviceUuidChanged();
547 emit readUuidChanged();
548 emit writeUuidChanged();
549 }
550 }
551
552 emit deviceChanged();
553}
554
555bool BluetoothConfiguration::scanning() const
556{
557 return _deviceDiscoveryAgent && _deviceDiscoveryAgent->isActive();
558}
559
560void BluetoothConfiguration::_requestHostMode(QBluetoothLocalDevice::HostMode mode)
561{
562 if (!_localDevice || !_localDevice->isValid()) {
563 emit errorOccurred(tr("Bluetooth adapter not available"));
564 return;
565 }
566
567 if (_localDevice->hostMode() == mode) {
568 return;
569 }
570
571 // BlueZ rejects overlapping setHostMode() calls while a previous one is pending.
572 if (_hostModeRequestPending) {
573 _queuedHostMode = mode;
574 _hasQueuedHostModeRequest = true;
575 if (_requestedHostMode != mode) {
576 qCDebug(BluetoothLinkLog) << "Host mode change already pending; queueing request"
577 << _requestedHostMode << "->" << mode;
578 }
579 return;
580 }
581
582 _requestedHostMode = mode;
583 _hasQueuedHostModeRequest = false;
584 _hostModeRequestPending = true;
585 _localDevice->setHostMode(mode);
586}
587
588void BluetoothConfiguration::_deviceDiscovered(const QBluetoothDeviceInfo &info)
589{
590 if (!info.isValid()) {
591 qCDebug(BluetoothLinkVerboseLog) << "Ignoring discovered device: invalid device info"
592 << info.name() << info.address().toString();
593 return;
594 }
595
596 if (!_isDeviceCompatibleWithMode(info)) {
597 qCDebug(BluetoothLinkVerboseLog) << "Ignoring discovered device: incompatible with mode"
598 << ((_mode == BluetoothMode::ModeLowEnergy) ? "BLE" : "Classic")
599 << info.name() << info.address().toString()
600 << "CoreCfg:" << info.coreConfigurations();
601 return;
602 }
603
604 int existingIndex = -1;
605 for (int i = 0; i < _deviceList.size(); ++i) {
606 if (_deviceList[i].address() == info.address()) {
607 existingIndex = i;
608 break;
609 }
610 }
611
612 if (existingIndex >= 0) {
613 _deviceList[existingIndex] = info;
614 _updateDeviceList();
615 return;
616 }
617
618 // BLE-only: collapse duplicates by common device name when private addresses rotate
619 if (_mode == BluetoothMode::ModeLowEnergy) {
620 int sameNameIndex = -1;
621 for (int i = 0; i < _deviceList.size(); ++i) {
622 if (_deviceList[i].name() == info.name()) {
623 sameNameIndex = i;
624 break;
625 }
626 }
627 if (sameNameIndex >= 0) {
628 const bool replace = (_deviceList[sameNameIndex].address() != info.address());
629 if (replace) {
630 _deviceList[sameNameIndex] = info;
631
632 // If the selected device is this BLE peripheral, track its latest private address.
633 if ((_device.name() == info.name()) && (_device.address() != info.address())) {
634 _device = info;
635 emit deviceChanged();
636 emit selectedRssiChanged();
637 }
638
639 _updateDeviceList();
640 }
641 return;
642 }
643 }
644
645 _deviceList.append(info);
646 _updateDeviceList();
647
648 qCDebug(BluetoothLinkLog) << ((_mode == BluetoothMode::ModeLowEnergy) ? "BLE" : "Classic")
649 << "device discovered:" << info.name()
650 << "Address:" << info.address().toString()
651 << "RSSI:" << info.rssi();
652}
653
654void BluetoothConfiguration::_onDiscoveryFinished()
655{
656 emit scanningChanged();
657 _updateDeviceList();
658}
659
660void BluetoothConfiguration::_updateDeviceList()
661{
662 QVariantList model;
663 for (const QBluetoothDeviceInfo &info : std::as_const(_deviceList)) {
664 QVariantMap device;
665 device["name"] = info.name();
666 device["address"] = info.address().toString();
667 if (info.rssi() != 0) {
668 device["rssi"] = info.rssi();
669 }
670 model.append(device);
671 }
672 if (_devicesModelCache != model) {
673 _devicesModelCache = model;
674 qCDebug(BluetoothLinkVerboseLog) << "Bluetooth devices model updated. Count:" << _devicesModelCache.size();
675 emit devicesModelChanged();
676 }
677}
678
679void BluetoothConfiguration::_onDiscoveryErrorOccurred(QBluetoothDeviceDiscoveryAgent::Error error)
680{
681 QString errorString;
682 if (_deviceDiscoveryAgent) {
683 errorString = _deviceDiscoveryAgent->errorString();
684 } else {
685 errorString = tr("Discovery error: %1").arg(error);
686 }
687
688 qCWarning(BluetoothLinkLog) << "Bluetooth discovery error:" << error << errorString;
690}
691
692void BluetoothConfiguration::_onHostModeStateChanged(QBluetoothLocalDevice::HostMode mode)
693{
694 _hostModeRequestPending = false;
695
696 QString modeString;
697 switch (mode) {
698 case QBluetoothLocalDevice::HostPoweredOff:
699 modeString = tr("Powered Off");
700 break;
701 case QBluetoothLocalDevice::HostConnectable:
702 modeString = tr("Connectable");
703 break;
704 case QBluetoothLocalDevice::HostDiscoverable:
705 modeString = tr("Discoverable");
706 break;
707 case QBluetoothLocalDevice::HostDiscoverableLimitedInquiry:
708 modeString = tr("Discoverable (Limited Inquiry)");
709 break;
710 }
711
712 qCDebug(BluetoothLinkLog) << "Bluetooth adapter mode changed to:" << modeString;
713 emit adapterStateChanged();
714
715 if (_hasQueuedHostModeRequest) {
716 const QBluetoothLocalDevice::HostMode queuedMode = _queuedHostMode;
717 _hasQueuedHostModeRequest = false;
718 if (queuedMode != mode) {
719 QTimer::singleShot(0, this, [this, queuedMode]() {
720 _requestHostMode(queuedMode);
721 });
722 }
723 }
724
725 if ((mode == QBluetoothLocalDevice::HostPoweredOff) && scanning()) {
726 stopScan();
727 emit errorOccurred(tr("Bluetooth adapter powered off"));
728 }
729}
730
731void BluetoothConfiguration::_onDeviceConnected(const QBluetoothAddress &address)
732{
733 qCDebug(BluetoothLinkLog) << "Device connected to adapter:" << address.toString();
734 for (const QBluetoothDeviceInfo &dev : std::as_const(_deviceList)) {
735 if (dev.address() == address) {
736 qCDebug(BluetoothLinkLog) << "Connected device:" << dev.name();
737 break;
738 }
739 }
740}
741
742void BluetoothConfiguration::_onDeviceDisconnected(const QBluetoothAddress &address)
743{
744 qCDebug(BluetoothLinkLog) << "Device disconnected from adapter:" << address.toString();
745 for (const QBluetoothDeviceInfo &dev : std::as_const(_deviceList)) {
746 if (dev.address() == address) {
747 qCDebug(BluetoothLinkLog) << "Disconnected device:" << dev.name();
748 break;
749 }
750 }
751}
752
753void BluetoothConfiguration::_onPairingFinished(const QBluetoothAddress &address, QBluetoothLocalDevice::Pairing pairing)
754{
755 QString pairingStatus;
756 switch (pairing) {
757 case QBluetoothLocalDevice::Unpaired:
758 pairingStatus = tr("Unpaired");
759 break;
760 case QBluetoothLocalDevice::Paired:
761 pairingStatus = tr("Paired");
762 break;
763 case QBluetoothLocalDevice::AuthorizedPaired:
764 pairingStatus = tr("Authorized Paired");
765 break;
766 }
767
768 qCDebug(BluetoothLinkLog) << "Pairing finished for device:" << address.toString() << "Status:" << pairingStatus;
769
770 QString deviceName;
771 for (const QBluetoothDeviceInfo &dev : std::as_const(_deviceList)) {
772 if (dev.address() == address) {
773 deviceName = dev.name();
774 break;
775 }
776 }
777
778 if (pairing == QBluetoothLocalDevice::Unpaired) {
779 const QString msg = deviceName.isEmpty()
780 ? tr("Device %1 unpaired").arg(address.toString())
781 : tr("Device %1 (%2) unpaired").arg(deviceName, address.toString());
782 qCInfo(BluetoothLinkLog) << msg;
783 } else {
784 const QString msg = deviceName.isEmpty()
785 ? tr("Device %1 paired successfully").arg(address.toString())
786 : tr("Device %1 (%2) paired successfully").arg(deviceName, address.toString());
787 qCInfo(BluetoothLinkLog) << msg;
788 }
789
790 _updateDeviceList();
792}
793
794void BluetoothConfiguration::_onLocalDeviceErrorOccurred(QBluetoothLocalDevice::Error error)
795{
796 QString errorString;
797 switch (error) {
798 case QBluetoothLocalDevice::PairingError:
799 errorString = tr("Pairing Error");
800 break;
801 case QBluetoothLocalDevice::MissingPermissionsError:
802 errorString = tr("Missing Bluetooth Permissions");
803 break;
804 case QBluetoothLocalDevice::UnknownError:
805 default:
806 errorString = tr("Unknown Bluetooth Adapter Error");
807 break;
808 }
809
810 qCWarning(BluetoothLinkLog) << "Local Bluetooth device error:" << error << errorString;
812}
813
814void BluetoothConfiguration::requestPairing(const QString &address)
815{
816 if (!_localDevice || !_localDevice->isValid()) {
817 emit errorOccurred(tr("Bluetooth adapter not available"));
818 return;
819 }
820
821 if (_mode != BluetoothMode::ModeClassic) {
822 emit errorOccurred(tr("Pairing is only supported for Classic Bluetooth"));
823 return;
824 }
825
826 const QBluetoothAddress addr(address);
827 if (addr.isNull()) {
828 emit errorOccurred(tr("Invalid Bluetooth address"));
829 return;
830 }
831
832 qCDebug(BluetoothLinkLog) << "Requesting pairing with device:" << address;
833 _localDevice->requestPairing(addr, QBluetoothLocalDevice::Paired);
834}
835
836void BluetoothConfiguration::removePairing(const QString &address)
837{
838 if (!_localDevice || !_localDevice->isValid()) {
839 emit errorOccurred(tr("Bluetooth adapter not available"));
840 return;
841 }
842
843 if (_mode != BluetoothMode::ModeClassic) {
844 emit errorOccurred(tr("Unpairing is only supported for Classic Bluetooth"));
845 return;
846 }
847
848 const QBluetoothAddress addr(address);
849 if (addr.isNull()) {
850 emit errorOccurred(tr("Invalid Bluetooth address"));
851 return;
852 }
853
854 qCDebug(BluetoothLinkLog) << "Removing pairing with device:" << address;
855 _localDevice->requestPairing(addr, QBluetoothLocalDevice::Unpaired);
856}
857
858QString BluetoothConfiguration::getPairingStatus(const QString &address) const
859{
860 if (!_localDevice || !_localDevice->isValid()) {
861 return tr("Adapter unavailable");
862 }
863
864 if (_mode != BluetoothMode::ModeClassic) {
865 return tr("N/A (BLE mode)");
866 }
867
868 const QBluetoothAddress addr(address);
869 if (addr.isNull()) {
870 return tr("Invalid address");
871 }
872
873 const QBluetoothLocalDevice::Pairing pairingStatus = _localDevice->pairingStatus(addr);
874 switch (pairingStatus) {
875 case QBluetoothLocalDevice::Unpaired:
876 return tr("Unpaired");
877 case QBluetoothLocalDevice::Paired:
878 return tr("Paired");
879 case QBluetoothLocalDevice::AuthorizedPaired:
880 return tr("Authorized Paired");
881 default:
882 return tr("Unknown");
883 }
884}
885
886bool BluetoothConfiguration::isPaired(const QString &address) const
887{
888 if (!_localDevice || !_localDevice->isValid() || _mode != BluetoothMode::ModeClassic) {
889 return false;
890 }
891
892 const QBluetoothAddress addr(address);
893 if (addr.isNull()) {
894 return false;
895 }
896
897 const QBluetoothLocalDevice::Pairing status = _localDevice->pairingStatus(addr);
898 return (status == QBluetoothLocalDevice::Paired || status == QBluetoothLocalDevice::AuthorizedPaired);
899}
900
901bool BluetoothConfiguration::isDiscoverable() const
902{
903 if (!_localDevice || !_localDevice->isValid()) {
904 return false;
905 }
906
907 const QBluetoothLocalDevice::HostMode mode = _localDevice->hostMode();
908 return (mode == QBluetoothLocalDevice::HostDiscoverable || mode == QBluetoothLocalDevice::HostDiscoverableLimitedInquiry);
909}
910
911bool BluetoothConfiguration::isAdapterAvailable() const
912{
913 return _localDevice && _localDevice->isValid();
914}
915
916QString BluetoothConfiguration::getAdapterAddress() const
917{
918 if (!_localDevice || !_localDevice->isValid()) {
919 return QString();
920 }
921 return _localDevice->address().toString();
922}
923
924QString BluetoothConfiguration::getAdapterName() const
925{
926 if (!_localDevice || !_localDevice->isValid()) {
927 return QString();
928 }
929 return _localDevice->name();
930}
931
932bool BluetoothConfiguration::isAdapterPoweredOn() const
933{
934 if (!_localDevice || !_localDevice->isValid()) {
935 return false;
936 }
937 return _localDevice->hostMode() != QBluetoothLocalDevice::HostPoweredOff;
938}
939
940QVariantList BluetoothConfiguration::getAllPairedDevices() const
941{
942 QVariantList pairedDevices;
943
944 if (!_localDevice || !_localDevice->isValid()) {
945 return pairedDevices;
946 }
947
948 const QList<QBluetoothAddress> connectedAddresses = _localDevice->connectedDevices();
949
950 for (const QBluetoothDeviceInfo &dev : std::as_const(_deviceList)) {
951 const QBluetoothLocalDevice::Pairing pairingStatus = _localDevice->pairingStatus(dev.address());
952 if (pairingStatus != QBluetoothLocalDevice::Unpaired) {
953 QVariantMap device;
954 device["name"] = dev.name();
955 device["address"] = dev.address().toString();
956 device["paired"] = true;
957
958 if (pairingStatus == QBluetoothLocalDevice::AuthorizedPaired) {
959 device["pairingStatus"] = tr("Authorized");
960 } else {
961 device["pairingStatus"] = tr("Paired");
962 }
963
964 device["connected"] = connectedAddresses.contains(dev.address());
965
966 if (dev.rssi() != 0) {
967 device["rssi"] = dev.rssi();
968 }
969
970 pairedDevices.append(device);
971 }
972 }
973
974 return pairedDevices;
975}
976
977QVariantList BluetoothConfiguration::getConnectedDevices() const
978{
979 QVariantList connectedDevices;
980
981 if (!_localDevice || !_localDevice->isValid()) {
982 return connectedDevices;
983 }
984
985 const QList<QBluetoothAddress> connectedAddresses = _localDevice->connectedDevices();
986
987 for (const QBluetoothAddress &addr : connectedAddresses) {
988 QVariantMap device;
989 device["address"] = addr.toString();
990 device["connected"] = true;
991
992 QString deviceName;
993 for (const QBluetoothDeviceInfo &dev : std::as_const(_deviceList)) {
994 if (dev.address() == addr) {
995 deviceName = dev.name();
996 if (dev.rssi() != 0) {
997 device["rssi"] = dev.rssi();
998 }
999 break;
1000 }
1001 }
1002
1003 device["name"] = deviceName.isEmpty() ? tr("Unknown Device") : deviceName;
1004 connectedDevices.append(device);
1005 }
1006
1007 return connectedDevices;
1008}
1009
1010void BluetoothConfiguration::powerOnAdapter()
1011{
1012 if (!_ensureLocalDevice()) {
1013 emit errorOccurred(tr("Bluetooth adapter not available"));
1014 _refreshAvailableAdaptersAsync();
1015 return;
1016 }
1017
1018 const bool wasPoweredOff = (_localDevice->hostMode() == QBluetoothLocalDevice::HostPoweredOff);
1019
1020 qCDebug(BluetoothLinkLog) << "Powering on Bluetooth adapter";
1021
1022 // powerOn() triggers an async host-mode change on BlueZ. Mark this as pending
1023 // so explicit setHostMode() requests are queued instead of rejected.
1024 if (wasPoweredOff) {
1025 _requestedHostMode = QBluetoothLocalDevice::HostConnectable;
1026 _hostModeRequestPending = true;
1027 }
1028
1029 _localDevice->powerOn();
1030
1031 // On some platforms powerOn can remain powered off; defer explicit mode request
1032 // to avoid colliding with BlueZ's in-flight state transition.
1033 if (wasPoweredOff && !_deferredPowerOnFixPending) {
1034 _deferredPowerOnFixPending = true;
1035 QTimer::singleShot(1500, this, [this]() {
1036 _deferredPowerOnFixPending = false;
1037 if (!_localDevice || !_localDevice->isValid()) {
1038 return;
1039 }
1040
1041 if (_localDevice->hostMode() == QBluetoothLocalDevice::HostPoweredOff) {
1042 _hostModeRequestPending = false;
1043 qCDebug(BluetoothLinkLog) << "Power-on fallback: requesting connectable host mode";
1044 _requestHostMode(QBluetoothLocalDevice::HostConnectable);
1045 } else if (_hostModeRequestPending) {
1046 // Some stacks update hostMode without emitting hostModeStateChanged.
1047 _hostModeRequestPending = false;
1048
1049 if (_hasQueuedHostModeRequest) {
1050 const QBluetoothLocalDevice::HostMode queuedMode = _queuedHostMode;
1051 _hasQueuedHostModeRequest = false;
1052 if (queuedMode != _localDevice->hostMode()) {
1053 _requestHostMode(queuedMode);
1054 }
1055 }
1056 }
1057 });
1058 }
1059
1060 emit adapterStateChanged();
1061}
1062
1063void BluetoothConfiguration::powerOffAdapter()
1064{
1065 if (!_localDevice || !_localDevice->isValid()) {
1066 emit errorOccurred(tr("Bluetooth adapter not available"));
1067 return;
1068 }
1069
1070 if (scanning()) {
1071 stopScan();
1072 }
1073
1074 if (_localDevice->hostMode() == QBluetoothLocalDevice::HostPoweredOff) {
1075 return;
1076 }
1077
1078 qCDebug(BluetoothLinkLog) << "Powering off Bluetooth adapter";
1079 _requestHostMode(QBluetoothLocalDevice::HostPoweredOff);
1080}
1081
1082void BluetoothConfiguration::setAdapterDiscoverable(bool discoverable)
1083{
1084 if (!_localDevice || !_localDevice->isValid()) {
1085 emit errorOccurred(tr("Bluetooth adapter not available"));
1086 return;
1087 }
1088
1089 if (discoverable) {
1090 if (_localDevice->hostMode() == QBluetoothLocalDevice::HostDiscoverable) {
1091 return;
1092 }
1093 qCDebug(BluetoothLinkLog) << "Making Bluetooth adapter discoverable";
1094 _requestHostMode(QBluetoothLocalDevice::HostDiscoverable);
1095 } else {
1096 if (_localDevice->hostMode() == QBluetoothLocalDevice::HostConnectable) {
1097 return;
1098 }
1099 qCDebug(BluetoothLinkLog) << "Making Bluetooth adapter connectable only";
1100 _requestHostMode(QBluetoothLocalDevice::HostConnectable);
1101 }
1102}
1103
1104QVariantList BluetoothConfiguration::getAllAvailableAdapters()
1105{
1106 if (_availableAdapters.isEmpty()) {
1107 _refreshAvailableAdaptersAsync();
1108 }
1109 return _availableAdapters;
1110}
1111
1112void BluetoothConfiguration::selectAdapter(const QString &address)
1113{
1114 const QBluetoothAddress addr(address);
1115 if (addr.isNull()) {
1116 emit errorOccurred(tr("Invalid adapter address"));
1117 return;
1118 }
1119
1120 if (!_availableAdapters.isEmpty()) {
1121 bool found = false;
1122 for (const QVariant &entry : std::as_const(_availableAdapters)) {
1123 const QVariantMap adapter = entry.toMap();
1124 if (QBluetoothAddress(adapter.value("address").toString()) == addr) {
1125 found = true;
1126 break;
1127 }
1128 }
1129 if (!found) {
1130 emit errorOccurred(tr("Adapter not found"));
1131 _refreshAvailableAdaptersAsync();
1132 return;
1133 }
1134 }
1135
1136 if (scanning()) {
1137 stopScan();
1138 }
1139
1140 if (!_createLocalDevice(addr)) {
1141 emit errorOccurred(tr("Failed to initialize adapter"));
1142 _refreshAvailableAdaptersAsync();
1143 return;
1144 }
1145
1146 bool selectionUpdated = false;
1147 for (QVariant &entry : _availableAdapters) {
1148 QVariantMap adapter = entry.toMap();
1149 if (adapter.isEmpty()) {
1150 continue;
1151 }
1152
1153 const bool selected = (QBluetoothAddress(adapter.value("address").toString()) == addr);
1154 if (adapter.value("selected").toBool() != selected) {
1155 adapter["selected"] = selected;
1156 entry = adapter;
1157 selectionUpdated = true;
1158 }
1159 }
1160
1161 if (_availableAdapters.isEmpty() && _localDevice && _localDevice->isValid()) {
1162 QVariantMap adapter;
1163 adapter["name"] = _localDevice->name();
1164 adapter["address"] = _localDevice->address().toString();
1165 adapter["selected"] = true;
1166 _availableAdapters.append(adapter);
1167 selectionUpdated = true;
1168 }
1169
1170 if (selectionUpdated) {
1171 emit adapterStateChanged();
1172 }
1173}
1174
1175QString BluetoothConfiguration::getHostMode() const
1176{
1177 if (!_localDevice || !_localDevice->isValid()) {
1178 return tr("Unavailable");
1179 }
1180
1181 switch (_localDevice->hostMode()) {
1182 case QBluetoothLocalDevice::HostPoweredOff:
1183 return tr("Powered Off");
1184 case QBluetoothLocalDevice::HostConnectable:
1185 return tr("Connectable");
1186 case QBluetoothLocalDevice::HostDiscoverable:
1187 return tr("Discoverable");
1188 case QBluetoothLocalDevice::HostDiscoverableLimitedInquiry:
1189 return tr("Discoverable (Limited)");
1190 default:
1191 return tr("Unknown");
1192 }
1193}
QString errorString
Error error
#define QGC_LOGGING_CATEGORY(name, categoryStr)
void errorOccurred(const QString &errorString)
Interface holding link specific settings.
bool isBluetoothAvailable()
Check if Bluetooth is available on this device.