Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qbluetoothdevicediscoveryagent_bluez.cpp
Go to the documentation of this file.
1// Copyright (C) 2016 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
4#include <QtCore/QLoggingCategory>
5
6#include <QtCore/qcoreapplication.h>
7
11#include "qbluetoothuuid.h"
12
13#include "bluez/bluez5_helper_p.h"
14#include "bluez/objectmanager_p.h"
15#include "bluez/adapter1_bluez5_p.h"
16#include "bluez/device1_bluez5_p.h"
17#include "bluez/properties_p.h"
18#include "bluez/bluetoothmanagement_p.h"
19
21
22Q_DECLARE_LOGGING_CATEGORY(QT_BT_BLUEZ)
23
24using namespace QtBluetoothPrivate; // for D-Bus wrappers
25
26QBluetoothDeviceDiscoveryAgentPrivate::QBluetoothDeviceDiscoveryAgentPrivate(
27 const QBluetoothAddress &deviceAdapter, QBluetoothDeviceDiscoveryAgent *parent) :
28 adapterAddress(deviceAdapter),
29 q_ptr(parent)
30{
31 initializeBluez5();
32 manager = new OrgFreedesktopDBusObjectManagerInterface(
33 QStringLiteral("org.bluez"),
34 QStringLiteral("/"),
35 QDBusConnection::systemBus(), parent);
36 QObject::connect(manager,
37 &OrgFreedesktopDBusObjectManagerInterface::InterfacesAdded,
38 q_ptr,
39 [this](const QDBusObjectPath &objectPath, InterfaceList interfacesAndProperties) {
40 this->_q_InterfacesAdded(objectPath, interfacesAndProperties);
41 });
42
43 // start private address monitoring
44 BluetoothManagement::instance();
45}
46
47QBluetoothDeviceDiscoveryAgentPrivate::~QBluetoothDeviceDiscoveryAgentPrivate()
48{
49 delete adapter;
50}
51
52//TODO: Qt6 remove the pendingCancel/pendingStart logic as it is cumbersome.
53// It is a behavior change across all platforms and was initially done
54// for Bluez. The behavior should be similar to QBluetoothServiceDiscoveryAgent
55// PendingCancel creates issues whereby the agent is still shutting down
56// but isActive() below already returns false. This means the isActive() is
57// out of sync with the finished() and cancel() signal.
58
59bool QBluetoothDeviceDiscoveryAgentPrivate::isActive() const
60{
61 if (pendingStart)
62 return true;
63 if (pendingCancel)
64 return false; //TODO Qt6: remove pending[Cancel|Start] logic (see comment above)
65
66 return adapter;
67}
68
69QBluetoothDeviceDiscoveryAgent::DiscoveryMethods QBluetoothDeviceDiscoveryAgent::supportedDiscoveryMethods()
70{
71 return (ClassicMethod | LowEnergyMethod);
72}
73
74void QBluetoothDeviceDiscoveryAgentPrivate::start(QBluetoothDeviceDiscoveryAgent::DiscoveryMethods methods)
75{
76 if (pendingCancel == true) {
77 pendingStart = true;
78 return;
79 }
80
81 lastError = QBluetoothDeviceDiscoveryAgent::NoError;
82 errorString.clear();
83 discoveredDevices.clear();
84 devicesProperties.clear();
85
86 Q_Q(QBluetoothDeviceDiscoveryAgent);
87
88 bool ok = false;
89 const QString adapterPath = findAdapterForAddress(adapterAddress, &ok);
90 if (!ok || adapterPath.isEmpty()) {
91 qCWarning(QT_BT_BLUEZ) << "Cannot find Bluez 5 adapter for device search" << ok;
92 lastError = QBluetoothDeviceDiscoveryAgent::InputOutputError;
93 errorString = QBluetoothDeviceDiscoveryAgent::tr("Cannot find valid Bluetooth adapter.");
94 q->errorOccurred(lastError);
95 return;
96 }
97
98 adapter = new OrgBluezAdapter1Interface(QStringLiteral("org.bluez"), adapterPath,
99 QDBusConnection::systemBus());
100
101 if (!adapter->powered()) {
102 qCDebug(QT_BT_BLUEZ) << "Aborting device discovery due to offline Bluetooth Adapter";
103 lastError = QBluetoothDeviceDiscoveryAgent::PoweredOffError;
104 errorString = QBluetoothDeviceDiscoveryAgent::tr("Device is powered off");
105 delete adapter;
106 adapter = nullptr;
107 emit q->errorOccurred(lastError);
108 return;
109 }
110
111 QVariantMap map;
112 if (methods == (QBluetoothDeviceDiscoveryAgent::LowEnergyMethod|QBluetoothDeviceDiscoveryAgent::ClassicMethod))
113 map.insert(QStringLiteral("Transport"), QStringLiteral("auto"));
114 else if (methods & QBluetoothDeviceDiscoveryAgent::LowEnergyMethod)
115 map.insert(QStringLiteral("Transport"), QStringLiteral("le"));
116 else
117 map.insert(QStringLiteral("Transport"), QStringLiteral("bredr"));
118
119 // older BlueZ 5.x versions don't have this function
120 // filterReply returns UnknownMethod which we ignore
121 QDBusPendingReply<> filterReply = adapter->SetDiscoveryFilter(map);
122 filterReply.waitForFinished();
123 if (filterReply.isError()) {
124 if (filterReply.error().type() == QDBusError::Other
125 && filterReply.error().name() == QStringLiteral("org.bluez.Error.Failed")) {
126 qCDebug(QT_BT_BLUEZ) << "Discovery method" << methods << "not supported";
127 lastError = QBluetoothDeviceDiscoveryAgent::UnsupportedDiscoveryMethod;
128 errorString = QBluetoothDeviceDiscoveryAgent::tr("One or more device discovery methods "
129 "are not supported on this platform");
130 delete adapter;
131 adapter = nullptr;
132 emit q->errorOccurred(lastError);
133 return;
134 } else if (filterReply.error().type() != QDBusError::UnknownMethod) {
135 qCDebug(QT_BT_BLUEZ) << "SetDiscoveryFilter failed:" << filterReply.error();
136 }
137 }
138
139 QtBluezDiscoveryManager::instance()->registerDiscoveryInterest(adapter->path());
140 QObject::connect(QtBluezDiscoveryManager::instance(), &QtBluezDiscoveryManager::discoveryInterrupted,
141 q, [this](const QString &path){
142 this->_q_discoveryInterrupted(path);
143 });
144 OrgFreedesktopDBusPropertiesInterface *prop = new OrgFreedesktopDBusPropertiesInterface(
145 QStringLiteral("org.bluez"), QStringLiteral(""), QDBusConnection::systemBus());
146 QObject::connect(prop, &OrgFreedesktopDBusPropertiesInterface::PropertiesChanged,
147 q, [this](const QString &interface, const QVariantMap &changedProperties,
148 const QStringList &invalidatedProperties,
149 const QDBusMessage &signal) {
150 this->_q_PropertiesChanged(interface, signal.path(), changedProperties, invalidatedProperties);
151 });
152
153 // remember what we have to cleanup
154 propertyMonitors.append(prop);
155
156 // collect initial set of information
157 QDBusPendingReply<ManagedObjectList> reply = manager->GetManagedObjects();
158 reply.waitForFinished();
159 if (!reply.isError()) {
160 ManagedObjectList managedObjectList = reply.value();
161 for (ManagedObjectList::const_iterator it = managedObjectList.constBegin(); it != managedObjectList.constEnd(); ++it) {
162 const QDBusObjectPath &path = it.key();
163 const InterfaceList &ifaceList = it.value();
164
165 for (InterfaceList::const_iterator jt = ifaceList.constBegin(); jt != ifaceList.constEnd(); ++jt) {
166 const QString &iface = jt.key();
167
168 if (iface == QStringLiteral("org.bluez.Device1")) {
169
170 if (path.path().indexOf(adapter->path()) != 0)
171 continue; //devices whose path doesn't start with same path we skip
172
173 deviceFound(path.path(), jt.value());
174 if (!isActive()) // Can happen if stop() was called from a slot in user code.
175 return;
176 }
177 }
178 }
179 }
180
181 // wait interval and sum up what was found
182 if (!discoveryTimer) {
183 discoveryTimer = new QTimer(q);
184 discoveryTimer->setSingleShot(true);
185 QObject::connect(discoveryTimer, &QTimer::timeout,
186 q, [this]() {
187 this->_q_discoveryFinished();
188 });
189 }
190
191 if (lowEnergySearchTimeout > 0) { // otherwise no timeout and stop() required
192 discoveryTimer->setInterval(lowEnergySearchTimeout);
193 discoveryTimer->start();
194 }
195}
196
197void QBluetoothDeviceDiscoveryAgentPrivate::stop()
198{
199 if (!adapter)
200 return;
201
202 qCDebug(QT_BT_BLUEZ) << Q_FUNC_INFO;
203 pendingCancel = true;
204 pendingStart = false;
205 _q_discoveryFinished();
206}
207
208// Returns invalid QBluetoothDeviceInfo in case of error
209static QBluetoothDeviceInfo createDeviceInfoFromBluez5Device(const QVariantMap& properties)
210{
211 const QBluetoothAddress btAddress(properties[QStringLiteral("Address")].toString());
212 if (btAddress.isNull())
213 return QBluetoothDeviceInfo();
214
215 const QString btName = properties[QStringLiteral("Alias")].toString();
216 quint32 btClass = properties[QStringLiteral("Class")].toUInt();
217
218 QBluetoothDeviceInfo deviceInfo(btAddress, btName, btClass);
219 deviceInfo.setRssi(qvariant_cast<short>(properties[QStringLiteral("RSSI")]));
220
221 QList<QBluetoothUuid> uuids;
222 bool foundLikelyLowEnergyUuid = false;
223 const QStringList foundUuids = qvariant_cast<QStringList>(properties[QStringLiteral("UUIDs")]);
224 for (const auto &u: foundUuids) {
225 const QBluetoothUuid id(u);
226 if (id.isNull())
227 continue;
228
229 if (!foundLikelyLowEnergyUuid) {
230 //once we found one BTLE service we are done
231 bool ok = false;
232 quint16 shortId = id.toUInt16(&ok);
233 quint16 genericAccessInt = static_cast<quint16>(QBluetoothUuid::ServiceClassUuid::GenericAccess);
234 if (ok && ((shortId & genericAccessInt) == genericAccessInt))
235 foundLikelyLowEnergyUuid = true;
236 }
237 uuids.append(id);
238 }
239 deviceInfo.setServiceUuids(uuids);
240
241 if (!btClass) {
242 deviceInfo.setCoreConfigurations(QBluetoothDeviceInfo::LowEnergyCoreConfiguration);
243 } else {
244 deviceInfo.setCoreConfigurations(QBluetoothDeviceInfo::BaseRateCoreConfiguration);
245 if (foundLikelyLowEnergyUuid)
246 deviceInfo.setCoreConfigurations(QBluetoothDeviceInfo::BaseRateAndLowEnergyCoreConfiguration);
247 }
248
249 const ManufacturerDataList deviceManufacturerData = qdbus_cast<ManufacturerDataList>(properties[QStringLiteral("ManufacturerData")]);
250 for (const auto &[key, value] : deviceManufacturerData.asKeyValueRange())
251 deviceInfo.setManufacturerData(key, value.variant().toByteArray());
252
253 const ServiceDataList deviceServiceData =
254 qdbus_cast<ServiceDataList>(properties[QStringLiteral("ServiceData")]);
255 for (const auto &[key, value] : deviceServiceData.asKeyValueRange())
256 deviceInfo.setServiceData(QBluetoothUuid(key), value.variant().toByteArray());
257
258 return deviceInfo;
259}
260
261void QBluetoothDeviceDiscoveryAgentPrivate::deviceFound(const QString &devicePath,
262 const QVariantMap &properties)
263{
264 Q_Q(QBluetoothDeviceDiscoveryAgent);
265
266 if (!q->isActive())
267 return;
268
269 auto deviceAdapter = qvariant_cast<QDBusObjectPath>(properties[QStringLiteral("Adapter")]);
270 if (deviceAdapter.path() != adapter->path())
271 return;
272
273 // read information
274 QBluetoothDeviceInfo deviceInfo = createDeviceInfoFromBluez5Device(properties);
275 if (!deviceInfo.isValid()) // no point reporting an empty address
276 return;
277
278 qCDebug(QT_BT_BLUEZ) << "Discovered: " << deviceInfo.name() << deviceInfo.address()
279 << "Num UUIDs" << deviceInfo.serviceUuids().size()
280 << "total device" << discoveredDevices.size() << "cached"
281 << "RSSI" << deviceInfo.rssi()
282 << "Num ManufacturerData" << deviceInfo.manufacturerData().size()
283 << "Num ServiceData" << deviceInfo.serviceData().size();
284
285 // Cache the properties so we do not have to access dbus every time to get a value
286 devicesProperties[devicePath] = properties;
287
288 for (qsizetype i = 0; i < discoveredDevices.size(); ++i) {
289 if (discoveredDevices[i].address() == deviceInfo.address()) {
290 if (lowEnergySearchTimeout > 0 && discoveredDevices[i] == deviceInfo) {
291 qCDebug(QT_BT_BLUEZ) << "Duplicate: " << deviceInfo.address();
292 return;
293 }
294 discoveredDevices.replace(i, deviceInfo);
295
296 emit q->deviceDiscovered(deviceInfo);
297 return; // this works if the list doesn't contain duplicates. Don't let it.
298 }
299 }
300
301 discoveredDevices.append(deviceInfo);
302 emit q->deviceDiscovered(deviceInfo);
303}
304
305void QBluetoothDeviceDiscoveryAgentPrivate::_q_InterfacesAdded(const QDBusObjectPath &object_path,
306 InterfaceList interfaces_and_properties)
307{
308 Q_Q(QBluetoothDeviceDiscoveryAgent);
309
310 if (!q->isActive())
311 return;
312
313 if (interfaces_and_properties.contains(QStringLiteral("org.bluez.Device1"))) {
314 // device interfaces belonging to different adapter
315 // will be filtered out by deviceFound();
316 deviceFound(object_path.path(),
317 interfaces_and_properties[QStringLiteral("org.bluez.Device1")]);
318 }
319}
320
321void QBluetoothDeviceDiscoveryAgentPrivate::_q_discoveryFinished()
322{
323 Q_Q(QBluetoothDeviceDiscoveryAgent);
324
325 if (discoveryTimer)
326 discoveryTimer->stop();
327
328 QtBluezDiscoveryManager::instance()->disconnect(q);
329 QtBluezDiscoveryManager::instance()->unregisterDiscoveryInterest(adapter->path());
330
331 qDeleteAll(propertyMonitors);
332 propertyMonitors.clear();
333
334 delete adapter;
335 adapter = nullptr;
336
337 if (pendingCancel && !pendingStart) {
338 pendingCancel = false;
339 emit q->canceled();
340 } else if (pendingStart) {
341 pendingStart = false;
342 pendingCancel = false;
343 start(QBluetoothDeviceDiscoveryAgent::ClassicMethod
344 | QBluetoothDeviceDiscoveryAgent::LowEnergyMethod);
345 } else {
346 emit q->finished();
347 }
348}
349
350void QBluetoothDeviceDiscoveryAgentPrivate::_q_discoveryInterrupted(const QString &path)
351{
352 Q_Q(QBluetoothDeviceDiscoveryAgent);
353
354 if (!q->isActive())
355 return;
356
357 if (path == adapter->path()) {
358 qCWarning(QT_BT_BLUEZ) << "Device discovery aborted due to unexpected adapter changes from another process.";
359
360 if (discoveryTimer)
361 discoveryTimer->stop();
362
363 QtBluezDiscoveryManager::instance()->disconnect(q);
364 // no need to call unregisterDiscoveryInterest since QtBluezDiscoveryManager
365 // does this automatically when emitting discoveryInterrupted(QString) signal
366
367 delete adapter;
368 adapter = nullptr;
369
370 errorString = QBluetoothDeviceDiscoveryAgent::tr("Bluetooth adapter error");
371 lastError = QBluetoothDeviceDiscoveryAgent::InputOutputError;
372 emit q->errorOccurred(lastError);
373 }
374}
375
376void QBluetoothDeviceDiscoveryAgentPrivate::_q_PropertiesChanged(const QString &interface,
377 const QString &path,
378 const QVariantMap &changed_properties,
379 const QStringList &invalidated_properties)
380{
381 Q_Q(QBluetoothDeviceDiscoveryAgent);
382 if (interface != QStringLiteral("org.bluez.Device1"))
383 return;
384
385 if (!devicesProperties.contains(path))
386 return;
387
388 // Update the cached properties before checking changed_properties for RSSI and ManufacturerData
389 // so the cached properties are always up to date.
390 QVariantMap & properties = devicesProperties[path];
391 for (QVariantMap::const_iterator it = changed_properties.constBegin();
392 it != changed_properties.constEnd(); ++it) {
393 properties[it.key()] = it.value();
394 }
395
396 for (const QString & property : invalidated_properties)
397 properties.remove(property);
398
399 const auto info = createDeviceInfoFromBluez5Device(properties);
400 if (!info.isValid())
401 return;
402
403 if (changed_properties.contains(QStringLiteral("RSSI"))
404 || changed_properties.contains(QStringLiteral("ManufacturerData"))) {
405
406 for (qsizetype i = 0; i < discoveredDevices.size(); ++i) {
407 if (discoveredDevices[i].address() == info.address()) {
408 QBluetoothDeviceInfo::Fields updatedFields = QBluetoothDeviceInfo::Field::None;
409 if (changed_properties.contains(QStringLiteral("RSSI"))) {
410 qCDebug(QT_BT_BLUEZ) << "Updating RSSI for" << info.address()
411 << changed_properties.value(QStringLiteral("RSSI"));
412 discoveredDevices[i].setRssi(
413 changed_properties.value(QStringLiteral("RSSI")).toInt());
414 updatedFields.setFlag(QBluetoothDeviceInfo::Field::RSSI);
415 }
416 if (changed_properties.contains(QStringLiteral("ManufacturerData"))) {
417 qCDebug(QT_BT_BLUEZ) << "Updating ManufacturerData for" << info.address();
418 ManufacturerDataList changedManufacturerData =
419 qdbus_cast< ManufacturerDataList >(changed_properties.value(QStringLiteral("ManufacturerData")));
420
421 const QList<quint16> keys = changedManufacturerData.keys();
422 bool wasNewValue = false;
423 for (quint16 key : keys) {
424 bool added = discoveredDevices[i].setManufacturerData(key, changedManufacturerData.value(key).variant().toByteArray());
425 wasNewValue = (wasNewValue || added);
426 }
427
428 if (wasNewValue)
429 updatedFields.setFlag(QBluetoothDeviceInfo::Field::ManufacturerData);
430 }
431
432 if (lowEnergySearchTimeout > 0) {
433 if (discoveredDevices[i] != info) { // field other than manufacturer or rssi changed
434 if (discoveredDevices.at(i).name() == info.name()) {
435 qCDebug(QT_BT_BLUEZ) << "Almost Duplicate " << info.address()
436 << info.name() << "- replacing in place";
437 discoveredDevices.replace(i, info);
438 emit q->deviceDiscovered(info);
439 }
440 } else {
441 if (!updatedFields.testFlag(QBluetoothDeviceInfo::Field::None))
442 emit q->deviceUpdated(discoveredDevices[i], updatedFields);
443 }
444
445 return;
446 }
447
448 discoveredDevices.replace(i, info);
449 emit q_ptr->deviceDiscovered(discoveredDevices[i]);
450
451 if (!updatedFields.testFlag(QBluetoothDeviceInfo::Field::None))
452 emit q->deviceUpdated(discoveredDevices[i], updatedFields);
453 return;
454 }
455 }
456 }
457}
458QT_END_NAMESPACE
QMap< QString, QDBusVariant > ServiceDataList
QMap< quint16, QDBusVariant > ManufacturerDataList
Combined button and popup list for selecting options.
static QBluetoothDeviceInfo createDeviceInfoFromBluez5Device(const QVariantMap &properties)