// Copyright (C) 2016 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #include #include #include "qbluetoothdevicediscoveryagent.h" #include "qbluetoothdevicediscoveryagent_p.h" #include "qbluetoothaddress.h" #include "qbluetoothuuid.h" #include "bluez/bluez5_helper_p.h" #include "bluez/objectmanager_p.h" #include "bluez/adapter1_bluez5_p.h" #include "bluez/device1_bluez5_p.h" #include "bluez/properties_p.h" #include "bluez/bluetoothmanagement_p.h" QT_BEGIN_NAMESPACE Q_DECLARE_LOGGING_CATEGORY(QT_BT_BLUEZ) QBluetoothDeviceDiscoveryAgentPrivate::QBluetoothDeviceDiscoveryAgentPrivate( const QBluetoothAddress &deviceAdapter, QBluetoothDeviceDiscoveryAgent *parent) : adapterAddress(deviceAdapter), q_ptr(parent) { initializeBluez5(); manager = new OrgFreedesktopDBusObjectManagerInterface( QStringLiteral("org.bluez"), QStringLiteral("/"), QDBusConnection::systemBus(), parent); QObject::connect(manager, &OrgFreedesktopDBusObjectManagerInterface::InterfacesAdded, q_ptr, [this](const QDBusObjectPath &objectPath, InterfaceList interfacesAndProperties) { this->_q_InterfacesAdded(objectPath, interfacesAndProperties); }); // start private address monitoring BluetoothManagement::instance(); } QBluetoothDeviceDiscoveryAgentPrivate::~QBluetoothDeviceDiscoveryAgentPrivate() { delete adapter; } //TODO: Qt6 remove the pendingCancel/pendingStart logic as it is cumbersome. // It is a behavior change across all platforms and was initially done // for Bluez. The behavior should be similar to QBluetoothServiceDiscoveryAgent // PendingCancel creates issues whereby the agent is still shutting down // but isActive() below already returns false. This means the isActive() is // out of sync with the finished() and cancel() signal. bool QBluetoothDeviceDiscoveryAgentPrivate::isActive() const { if (pendingStart) return true; if (pendingCancel) return false; //TODO Qt6: remove pending[Cancel|Start] logic (see comment above) return adapter; } QBluetoothDeviceDiscoveryAgent::DiscoveryMethods QBluetoothDeviceDiscoveryAgent::supportedDiscoveryMethods() { return (ClassicMethod | LowEnergyMethod); } void QBluetoothDeviceDiscoveryAgentPrivate::start(QBluetoothDeviceDiscoveryAgent::DiscoveryMethods methods) { if (pendingCancel == true) { pendingStart = true; return; } lastError = QBluetoothDeviceDiscoveryAgent::NoError; errorString.clear(); discoveredDevices.clear(); devicesProperties.clear(); Q_Q(QBluetoothDeviceDiscoveryAgent); bool ok = false; const QString adapterPath = findAdapterForAddress(adapterAddress, &ok); if (!ok || adapterPath.isEmpty()) { qCWarning(QT_BT_BLUEZ) << "Cannot find Bluez 5 adapter for device search" << ok; lastError = QBluetoothDeviceDiscoveryAgent::InputOutputError; errorString = QBluetoothDeviceDiscoveryAgent::tr("Cannot find valid Bluetooth adapter."); q->errorOccurred(lastError); return; } adapter = new OrgBluezAdapter1Interface(QStringLiteral("org.bluez"), adapterPath, QDBusConnection::systemBus()); if (!adapter->powered()) { qCDebug(QT_BT_BLUEZ) << "Aborting device discovery due to offline Bluetooth Adapter"; lastError = QBluetoothDeviceDiscoveryAgent::PoweredOffError; errorString = QBluetoothDeviceDiscoveryAgent::tr("Device is powered off"); delete adapter; adapter = nullptr; emit q->errorOccurred(lastError); return; } QVariantMap map; if (methods == (QBluetoothDeviceDiscoveryAgent::LowEnergyMethod|QBluetoothDeviceDiscoveryAgent::ClassicMethod)) map.insert(QStringLiteral("Transport"), QStringLiteral("auto")); else if (methods & QBluetoothDeviceDiscoveryAgent::LowEnergyMethod) map.insert(QStringLiteral("Transport"), QStringLiteral("le")); else map.insert(QStringLiteral("Transport"), QStringLiteral("bredr")); // older BlueZ 5.x versions don't have this function // filterReply returns UnknownMethod which we ignore QDBusPendingReply<> filterReply = adapter->SetDiscoveryFilter(map); filterReply.waitForFinished(); if (filterReply.isError()) { if (filterReply.error().type() == QDBusError::Other && filterReply.error().name() == QStringLiteral("org.bluez.Error.Failed")) { qCDebug(QT_BT_BLUEZ) << "Discovery method" << methods << "not supported"; lastError = QBluetoothDeviceDiscoveryAgent::UnsupportedDiscoveryMethod; errorString = QBluetoothDeviceDiscoveryAgent::tr("One or more device discovery methods " "are not supported on this platform"); delete adapter; adapter = nullptr; emit q->errorOccurred(lastError); return; } else if (filterReply.error().type() != QDBusError::UnknownMethod) { qCDebug(QT_BT_BLUEZ) << "SetDiscoveryFilter failed:" << filterReply.error(); } } QtBluezDiscoveryManager::instance()->registerDiscoveryInterest(adapter->path()); QObject::connect(QtBluezDiscoveryManager::instance(), &QtBluezDiscoveryManager::discoveryInterrupted, q, [this](const QString &path){ this->_q_discoveryInterrupted(path); }); OrgFreedesktopDBusPropertiesInterface *prop = new OrgFreedesktopDBusPropertiesInterface( QStringLiteral("org.bluez"), QStringLiteral(""), QDBusConnection::systemBus()); QObject::connect(prop, &OrgFreedesktopDBusPropertiesInterface::PropertiesChanged, q, [this](const QString &interface, const QVariantMap &changedProperties, const QStringList &invalidatedProperties, const QDBusMessage &signal) { this->_q_PropertiesChanged(interface, signal.path(), changedProperties, invalidatedProperties); }); // remember what we have to cleanup propertyMonitors.append(prop); // collect initial set of information QDBusPendingReply reply = manager->GetManagedObjects(); reply.waitForFinished(); if (!reply.isError()) { ManagedObjectList managedObjectList = reply.value(); for (ManagedObjectList::const_iterator it = managedObjectList.constBegin(); it != managedObjectList.constEnd(); ++it) { const QDBusObjectPath &path = it.key(); const InterfaceList &ifaceList = it.value(); for (InterfaceList::const_iterator jt = ifaceList.constBegin(); jt != ifaceList.constEnd(); ++jt) { const QString &iface = jt.key(); if (iface == QStringLiteral("org.bluez.Device1")) { if (path.path().indexOf(adapter->path()) != 0) continue; //devices whose path doesn't start with same path we skip deviceFound(path.path(), jt.value()); if (!isActive()) // Can happen if stop() was called from a slot in user code. return; } } } } // wait interval and sum up what was found if (!discoveryTimer) { discoveryTimer = new QTimer(q); discoveryTimer->setSingleShot(true); QObject::connect(discoveryTimer, &QTimer::timeout, q, [this]() { this->_q_discoveryFinished(); }); } if (lowEnergySearchTimeout > 0) { // otherwise no timeout and stop() required discoveryTimer->setInterval(lowEnergySearchTimeout); discoveryTimer->start(); } } void QBluetoothDeviceDiscoveryAgentPrivate::stop() { if (!adapter) return; qCDebug(QT_BT_BLUEZ) << Q_FUNC_INFO; pendingCancel = true; pendingStart = false; _q_discoveryFinished(); } // Returns invalid QBluetoothDeviceInfo in case of error static QBluetoothDeviceInfo createDeviceInfoFromBluez5Device(const QVariantMap& properties) { const QBluetoothAddress btAddress(properties[QStringLiteral("Address")].toString()); if (btAddress.isNull()) return QBluetoothDeviceInfo(); const QString btName = properties[QStringLiteral("Alias")].toString(); quint32 btClass = properties[QStringLiteral("Class")].toUInt(); QBluetoothDeviceInfo deviceInfo(btAddress, btName, btClass); deviceInfo.setRssi(qvariant_cast(properties[QStringLiteral("RSSI")])); QList uuids; bool foundLikelyLowEnergyUuid = false; const QStringList foundUuids = qvariant_cast(properties[QStringLiteral("UUIDs")]); for (const auto &u: foundUuids) { const QBluetoothUuid id(u); if (id.isNull()) continue; if (!foundLikelyLowEnergyUuid) { //once we found one BTLE service we are done bool ok = false; quint16 shortId = id.toUInt16(&ok); quint16 genericAccessInt = static_cast(QBluetoothUuid::ServiceClassUuid::GenericAccess); if (ok && ((shortId & genericAccessInt) == genericAccessInt)) foundLikelyLowEnergyUuid = true; } uuids.append(id); } deviceInfo.setServiceUuids(uuids); if (!btClass) { deviceInfo.setCoreConfigurations(QBluetoothDeviceInfo::LowEnergyCoreConfiguration); } else { deviceInfo.setCoreConfigurations(QBluetoothDeviceInfo::BaseRateCoreConfiguration); if (foundLikelyLowEnergyUuid) deviceInfo.setCoreConfigurations(QBluetoothDeviceInfo::BaseRateAndLowEnergyCoreConfiguration); } const ManufacturerDataList deviceManufacturerData = qdbus_cast(properties[QStringLiteral("ManufacturerData")]); const QList keysManufacturer = deviceManufacturerData.keys(); for (quint16 key : keysManufacturer) deviceInfo.setManufacturerData( key, deviceManufacturerData.value(key).variant().toByteArray()); const ServiceDataList deviceServiceData = qdbus_cast(properties[QStringLiteral("ServiceData")]); const QList keysService = deviceServiceData.keys(); for (QString key : keysService) deviceInfo.setServiceData(QBluetoothUuid(key), deviceServiceData.value(key).variant().toByteArray()); return deviceInfo; } void QBluetoothDeviceDiscoveryAgentPrivate::deviceFound(const QString &devicePath, const QVariantMap &properties) { Q_Q(QBluetoothDeviceDiscoveryAgent); if (!q->isActive()) return; auto deviceAdapter = qvariant_cast(properties[QStringLiteral("Adapter")]); if (deviceAdapter.path() != adapter->path()) return; // read information QBluetoothDeviceInfo deviceInfo = createDeviceInfoFromBluez5Device(properties); if (!deviceInfo.isValid()) // no point reporting an empty address return; qCDebug(QT_BT_BLUEZ) << "Discovered: " << deviceInfo.name() << deviceInfo.address() << "Num UUIDs" << deviceInfo.serviceUuids().size() << "total device" << discoveredDevices.size() << "cached" << "RSSI" << deviceInfo.rssi() << "Num ManufacturerData" << deviceInfo.manufacturerData().size() << "Num ServiceData" << deviceInfo.serviceData().size(); // Cache the properties so we do not have to access dbus every time to get a value devicesProperties[devicePath] = properties; for (qsizetype i = 0; i < discoveredDevices.size(); ++i) { if (discoveredDevices[i].address() == deviceInfo.address()) { if (lowEnergySearchTimeout > 0 && discoveredDevices[i] == deviceInfo) { qCDebug(QT_BT_BLUEZ) << "Duplicate: " << deviceInfo.address(); return; } discoveredDevices.replace(i, deviceInfo); emit q->deviceDiscovered(deviceInfo); return; // this works if the list doesn't contain duplicates. Don't let it. } } discoveredDevices.append(deviceInfo); emit q->deviceDiscovered(deviceInfo); } void QBluetoothDeviceDiscoveryAgentPrivate::_q_InterfacesAdded(const QDBusObjectPath &object_path, InterfaceList interfaces_and_properties) { Q_Q(QBluetoothDeviceDiscoveryAgent); if (!q->isActive()) return; if (interfaces_and_properties.contains(QStringLiteral("org.bluez.Device1"))) { // device interfaces belonging to different adapter // will be filtered out by deviceFound(); deviceFound(object_path.path(), interfaces_and_properties[QStringLiteral("org.bluez.Device1")]); } } void QBluetoothDeviceDiscoveryAgentPrivate::_q_discoveryFinished() { Q_Q(QBluetoothDeviceDiscoveryAgent); if (discoveryTimer) discoveryTimer->stop(); QtBluezDiscoveryManager::instance()->disconnect(q); QtBluezDiscoveryManager::instance()->unregisterDiscoveryInterest(adapter->path()); qDeleteAll(propertyMonitors); propertyMonitors.clear(); delete adapter; adapter = nullptr; if (pendingCancel && !pendingStart) { pendingCancel = false; emit q->canceled(); } else if (pendingStart) { pendingStart = false; pendingCancel = false; start(QBluetoothDeviceDiscoveryAgent::ClassicMethod | QBluetoothDeviceDiscoveryAgent::LowEnergyMethod); } else { emit q->finished(); } } void QBluetoothDeviceDiscoveryAgentPrivate::_q_discoveryInterrupted(const QString &path) { Q_Q(QBluetoothDeviceDiscoveryAgent); if (!q->isActive()) return; if (path == adapter->path()) { qCWarning(QT_BT_BLUEZ) << "Device discovery aborted due to unexpected adapter changes from another process."; if (discoveryTimer) discoveryTimer->stop(); QtBluezDiscoveryManager::instance()->disconnect(q); // no need to call unregisterDiscoveryInterest since QtBluezDiscoveryManager // does this automatically when emitting discoveryInterrupted(QString) signal delete adapter; adapter = nullptr; errorString = QBluetoothDeviceDiscoveryAgent::tr("Bluetooth adapter error"); lastError = QBluetoothDeviceDiscoveryAgent::InputOutputError; emit q->errorOccurred(lastError); } } void QBluetoothDeviceDiscoveryAgentPrivate::_q_PropertiesChanged(const QString &interface, const QString &path, const QVariantMap &changed_properties, const QStringList &invalidated_properties) { Q_Q(QBluetoothDeviceDiscoveryAgent); if (interface != QStringLiteral("org.bluez.Device1")) return; if (!devicesProperties.contains(path)) return; // Update the cached properties before checking changed_properties for RSSI and ManufacturerData // so the cached properties are always up to date. QVariantMap & properties = devicesProperties[path]; for (QVariantMap::const_iterator it = changed_properties.constBegin(); it != changed_properties.constEnd(); ++it) { properties[it.key()] = it.value(); } for (const QString & property : invalidated_properties) properties.remove(property); const auto info = createDeviceInfoFromBluez5Device(properties); if (!info.isValid()) return; if (changed_properties.contains(QStringLiteral("RSSI")) || changed_properties.contains(QStringLiteral("ManufacturerData"))) { for (qsizetype i = 0; i < discoveredDevices.size(); ++i) { if (discoveredDevices[i].address() == info.address()) { QBluetoothDeviceInfo::Fields updatedFields = QBluetoothDeviceInfo::Field::None; if (changed_properties.contains(QStringLiteral("RSSI"))) { qCDebug(QT_BT_BLUEZ) << "Updating RSSI for" << info.address() << changed_properties.value(QStringLiteral("RSSI")); discoveredDevices[i].setRssi( changed_properties.value(QStringLiteral("RSSI")).toInt()); updatedFields.setFlag(QBluetoothDeviceInfo::Field::RSSI); } if (changed_properties.contains(QStringLiteral("ManufacturerData"))) { qCDebug(QT_BT_BLUEZ) << "Updating ManufacturerData for" << info.address(); ManufacturerDataList changedManufacturerData = qdbus_cast< ManufacturerDataList >(changed_properties.value(QStringLiteral("ManufacturerData"))); const QList keys = changedManufacturerData.keys(); bool wasNewValue = false; for (quint16 key : keys) { bool added = discoveredDevices[i].setManufacturerData(key, changedManufacturerData.value(key).variant().toByteArray()); wasNewValue = (wasNewValue || added); } if (wasNewValue) updatedFields.setFlag(QBluetoothDeviceInfo::Field::ManufacturerData); } if (lowEnergySearchTimeout > 0) { if (discoveredDevices[i] != info) { // field other than manufacturer or rssi changed if (discoveredDevices.at(i).name() == info.name()) { qCDebug(QT_BT_BLUEZ) << "Almost Duplicate " << info.address() << info.name() << "- replacing in place"; discoveredDevices.replace(i, info); emit q->deviceDiscovered(info); } } else { if (!updatedFields.testFlag(QBluetoothDeviceInfo::Field::None)) emit q->deviceUpdated(discoveredDevices[i], updatedFields); } return; } discoveredDevices.replace(i, info); emit q_ptr->deviceDiscovered(discoveredDevices[i]); if (!updatedFields.testFlag(QBluetoothDeviceInfo::Field::None)) emit q->deviceUpdated(discoveredDevices[i], updatedFields); return; } } } } QT_END_NAMESPACE