Revision control
Copy as Markdown
// Copyright (C) 2022 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 "qleadvertiser_bluezdbus_p.h"
#include "bluez/leadvertisement1adaptor_p.h"
#include "bluez/leadvertisingmanager1_p.h"
#include "bluez/bluez5_helper_p.h"
#include <QtCore/QtMinMax>
#include <QtCore/QLoggingCategory>
QT_BEGIN_NAMESPACE
Q_DECLARE_LOGGING_CATEGORY(QT_BT_BLUEZ)
using namespace Qt::StringLiterals;
using namespace QtBluetoothPrivate; // for D-Bus wrappers
// The advertisement dbus object path is freely definable, use as prefix
static constexpr auto advObjectPathTemplate{"/qt/btle/advertisement/%1%2/%3"_L1};
static constexpr auto bluezService{"org.bluez"_L1};
static constexpr auto bluezErrorFailed{"org.bluez.Error.Failed"_L1};
// From bluez API documentation
static constexpr auto advDataTXPower{"tx-power"_L1};
static constexpr auto advDataTypePeripheral{"peripheral"_L1};
static constexpr auto advDataTypeBroadcast{"broadcast"_L1};
static constexpr quint16 advDataMinIntervalMs{20};
static constexpr quint16 advDataMaxIntervalMs{10485};
QLeDBusAdvertiser::QLeDBusAdvertiser(const QLowEnergyAdvertisingParameters ¶ms,
const QLowEnergyAdvertisingData &advertisingData,
const QLowEnergyAdvertisingData &scanResponseData,
const QString &hostAdapterPath,
QObject* parent)
: QObject(parent),
m_advParams(params),
m_advData(advertisingData),
m_advObjectPath(QString(advObjectPathTemplate).
arg(sanitizeNameForDBus(QCoreApplication::applicationName())).
arg(QCoreApplication::applicationPid()).
arg(QRandomGenerator::global()->generate())),
m_advDataDBus(new OrgBluezLEAdvertisement1Adaptor(this)),
m_advManager(new OrgBluezLEAdvertisingManager1Interface(bluezService, hostAdapterPath,
QDBusConnection::systemBus(), this))
{
// Bluez DBus API doesn't allow distinguishing between advertisement and scan response data;
// consolidate the two if they differ.
// Union of service UUIDs:
if (scanResponseData.services() != advertisingData.services()) {
QList<QBluetoothUuid> services = advertisingData.services();
const auto scanResponseServices = scanResponseData.services();
for (const auto &service : scanResponseServices) {
if (!services.contains(service))
services.append(service);
}
m_advData.setServices(services);
}
// Scan response is given precedence with rest of the data
if (!scanResponseData.localName().isEmpty())
m_advData.setLocalName(scanResponseData.localName());
if (scanResponseData.manufacturerId() != QLowEnergyAdvertisingData::invalidManufacturerId()) {
m_advData.setManufacturerData(scanResponseData.manufacturerId(),
scanResponseData.manufacturerData());
}
if (scanResponseData.includePowerLevel())
m_advData.setIncludePowerLevel(true);
setDataForDBus();
}
QLeDBusAdvertiser::~QLeDBusAdvertiser()
{
stopAdvertising();
}
// This function parses the advertising data provided by the application and
// populates the dbus adaptor with it. DBus will ask the data from the adaptor when
// the advertisement is later registered (started)
void QLeDBusAdvertiser::setDataForDBus()
{
setAdvertisingParamsForDBus();
setAdvertisementDataForDBus();
}
void QLeDBusAdvertiser::setAdvertisingParamsForDBus()
{
// Whitelist and filter policy
if (!m_advParams.whiteList().isEmpty())
qCWarning(QT_BT_BLUEZ) << "White lists and filter policies not supported, ignoring";
// Legacy advertising mode mapped to GAP role (peripheral vs broadcast)
switch (m_advParams.mode())
{
case QLowEnergyAdvertisingParameters::AdvScanInd:
case QLowEnergyAdvertisingParameters::AdvNonConnInd:
m_advDataDBus->setType(advDataTypeBroadcast);
break;
case QLowEnergyAdvertisingParameters::AdvInd:
default:
m_advDataDBus->setType(advDataTypePeripheral);
}
// Advertisement interval (min max in milliseconds). Ensure the values fit the range bluez
// allows. The max >= min is guaranteed by QLowEnergyAdvertisingParameters::setInterval().
// Note: Bluez reads these values but at the time of this writing it marks this feature
// as 'experimental'
m_advDataDBus->setMinInterval(qBound(advDataMinIntervalMs,
quint16(m_advParams.minimumInterval()),
advDataMaxIntervalMs));
m_advDataDBus->setMaxInterval(qBound(advDataMinIntervalMs,
quint16(m_advParams.maximumInterval()),
advDataMaxIntervalMs));
}
void QLeDBusAdvertiser::setAdvertisementDataForDBus()
{
// We don't calculate the advertisement length to guard for too long advertisements.
// There isn't adequate control and visibility on the advertisement for that.
// - We don't know the max length (legacy or extended advertising)
// - Bluez may truncate some of the fields on its own, making calculus here imprecise
// - Scan response may or may not be used to offload some of the data
// Include the power level if requested and dbus supports it
const auto supportedIncludes = m_advManager->supportedIncludes();
if (m_advData.includePowerLevel() && supportedIncludes.contains(advDataTXPower))
m_advDataDBus->setIncludes({advDataTXPower});
// Set the application provided name (valid to be empty).
// For clarity: bluez also has "local-name" system include that could be set if no local
// name is provided. However that would require that the LocalName DBus property would
// not exist. Existing LocalName property when 'local-name' is included leads to an
// advertisement error.
m_advDataDBus->setLocalName(m_advData.localName());
// Service UUIDs
if (!m_advData.services().isEmpty()) {
QStringList serviceUUIDList;
const auto services = m_advData.services();
for (const auto &service : services)
serviceUUIDList << service.toString(QUuid::StringFormat::WithoutBraces);
m_advDataDBus->setServiceUUIDs(serviceUUIDList);
}
// Manufacturer data
if (m_advData.manufacturerId() != QLowEnergyAdvertisingData::invalidManufacturerId()) {
m_advDataDBus->setManufacturerData({
{m_advData.manufacturerId(), QDBusVariant(m_advData.manufacturerData())}});
}
// Discoverability
if (m_advDataDBus->type() == advDataTypePeripheral) {
m_advDataDBus->setDiscoverable(m_advData.discoverability()
!= QLowEnergyAdvertisingData::DiscoverabilityNone);
} else {
qCDebug(QT_BT_BLUEZ) << "Ignoring advertisement discoverability in broadcast mode";
}
// Raw data
if (!m_advData.rawData().isEmpty())
qCWarning(QT_BT_BLUEZ) << "Raw advertisement data not supported, ignoring";
}
void QLeDBusAdvertiser::startAdvertising()
{
qCDebug(QT_BT_BLUEZ) << "Start advertising" << m_advObjectPath << "on" << m_advManager->path();
if (m_advertising) {
qCWarning(QT_BT_BLUEZ) << "Start tried while already advertising";
return;
}
if (!QDBusConnection::systemBus().registerObject(m_advObjectPath, m_advDataDBus,
QDBusConnection::ExportAllContents)) {
qCWarning(QT_BT_BLUEZ) << "Advertisement dbus object registration failed";
emit errorOccurred();
return;
}
// Register the advertisement which starts the actual advertising.
// We use call watcher here instead of waitForFinished() because DBus will
// call back our advertisement object (to read data) in this same thread => would block
auto reply = m_advManager->RegisterAdvertisement(QDBusObjectPath(m_advObjectPath), {});
QDBusPendingCallWatcher* watcher = new QDBusPendingCallWatcher(reply, this);
QObject::connect(watcher, &QDBusPendingCallWatcher::finished, this,
[this](QDBusPendingCallWatcher* watcher){
QDBusPendingReply<> reply = *watcher;
if (reply.isError()) {
qCWarning(QT_BT_BLUEZ) << "Advertisement registration failed" << reply.error();
if (reply.error().name() == bluezErrorFailed)
qCDebug(QT_BT_BLUEZ) << "Advertisement could've been too large";
QDBusConnection::systemBus().unregisterObject(m_advObjectPath);
emit errorOccurred();
} else {
qCDebug(QT_BT_BLUEZ) << "Advertisement started successfully";
m_advertising = true;
}
watcher->deleteLater();
});
}
void QLeDBusAdvertiser::stopAdvertising()
{
if (!m_advertising)
return;
m_advertising = false;
auto reply = m_advManager->UnregisterAdvertisement(QDBusObjectPath(m_advObjectPath));
reply.waitForFinished();
if (reply.isError())
qCWarning(QT_BT_BLUEZ) << "Error in unregistering advertisement" << reply.error();
else
qCDebug(QT_BT_BLUEZ) << "Advertisement unregistered successfully";
QDBusConnection::systemBus().unregisterObject(m_advObjectPath);
}
// Called by Bluez when the advertisement has been removed (org.bluez.LEAdvertisement1.Release)
void QLeDBusAdvertiser::Release()
{
qCDebug(QT_BT_BLUEZ) << "Advertisement" << m_advObjectPath << "released"
<< (m_advertising ? "unexpectedly" : "");
if (m_advertising) {
// If we are advertising, it means the Release is unsolicited
// and handled as an advertisement error. No need to call UnregisterAdvertisement
m_advertising = false;
QDBusConnection::systemBus().unregisterObject(m_advObjectPath);
emit errorOccurred();
}
}
QT_END_NAMESPACE