1// Copyright (C) 2022 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 "qleadvertiser_bluezdbus_p.h"
5#include "bluez/leadvertisement1adaptor_p.h"
6#include "bluez/leadvertisingmanager1_p.h"
7#include "bluez/bluez5_helper_p.h"
8
9#include <QtCore/QtMinMax>
10#include <QtCore/QLoggingCategory>
11
12QT_BEGIN_NAMESPACE
13
14Q_DECLARE_LOGGING_CATEGORY(QT_BT_BLUEZ)
15
16using namespace Qt::StringLiterals;
17using namespace QtBluetoothPrivate; // for D-Bus wrappers
18
19// The advertisement dbus object path is freely definable, use as prefix
20static constexpr auto advObjectPathTemplate{"/qt/btle/advertisement/%1%2/%3"_L1};
21static constexpr auto bluezService{"org.bluez"_L1};
22static constexpr auto bluezErrorFailed{"org.bluez.Error.Failed"_L1};
23
24// From bluez API documentation
25static constexpr auto advDataTXPower{"tx-power"_L1};
26static constexpr auto advDataTypePeripheral{"peripheral"_L1};
27static constexpr auto advDataTypeBroadcast{"broadcast"_L1};
28static constexpr quint16 advDataMinIntervalMs{20};
29static constexpr quint16 advDataMaxIntervalMs{10485};
30
31
32QLeDBusAdvertiser::QLeDBusAdvertiser(const QLowEnergyAdvertisingParameters &params,
33 const QLowEnergyAdvertisingData &advertisingData,
34 const QLowEnergyAdvertisingData &scanResponseData,
35 const QString &hostAdapterPath,
36 QObject* parent)
37 : QObject(parent),
38 m_advParams(params),
39 m_advData(advertisingData),
40 m_advObjectPath(QString(advObjectPathTemplate).
41 arg(a: sanitizeNameForDBus(text: QCoreApplication::applicationName())).
42 arg(a: QCoreApplication::applicationPid()).
43 arg(a: QRandomGenerator::global()->generate())),
44 m_advDataDBus(new OrgBluezLEAdvertisement1Adaptor(this)),
45 m_advManager(new OrgBluezLEAdvertisingManager1Interface(bluezService, hostAdapterPath,
46 QDBusConnection::systemBus(), this))
47{
48 // Bluez DBus API doesn't allow distinguishing between advertisement and scan response data;
49 // consolidate the two if they differ.
50 // Union of service UUIDs:
51 if (scanResponseData.services() != advertisingData.services()) {
52 QList<QBluetoothUuid> services = advertisingData.services();
53 for (const auto &service: scanResponseData.services()) {
54 if (!services.contains(t: service))
55 services.append(t: service);
56 }
57 m_advData.setServices(services);
58 }
59 // Scan response is given precedence with rest of the data
60 if (!scanResponseData.localName().isEmpty())
61 m_advData.setLocalName(scanResponseData.localName());
62 if (scanResponseData.manufacturerId() != QLowEnergyAdvertisingData::invalidManufacturerId()) {
63 m_advData.setManufacturerData(id: scanResponseData.manufacturerId(),
64 data: scanResponseData.manufacturerData());
65 }
66 if (scanResponseData.includePowerLevel())
67 m_advData.setIncludePowerLevel(true);
68
69 setDataForDBus();
70}
71
72QLeDBusAdvertiser::~QLeDBusAdvertiser()
73{
74 stopAdvertising();
75}
76
77// This function parses the advertising data provided by the application and
78// populates the dbus adaptor with it. DBus will ask the data from the adaptor when
79// the advertisement is later registered (started)
80void QLeDBusAdvertiser::setDataForDBus()
81{
82 setAdvertisingParamsForDBus();
83 setAdvertisementDataForDBus();
84}
85
86void QLeDBusAdvertiser::setAdvertisingParamsForDBus()
87{
88 // Whitelist and filter policy
89 if (!m_advParams.whiteList().isEmpty())
90 qCWarning(QT_BT_BLUEZ) << "White lists and filter policies not supported, ignoring";
91
92 // Legacy advertising mode mapped to GAP role (peripheral vs broadcast)
93 switch (m_advParams.mode())
94 {
95 case QLowEnergyAdvertisingParameters::AdvScanInd:
96 case QLowEnergyAdvertisingParameters::AdvNonConnInd:
97 m_advDataDBus->setType(advDataTypeBroadcast);
98 break;
99 case QLowEnergyAdvertisingParameters::AdvInd:
100 default:
101 m_advDataDBus->setType(advDataTypePeripheral);
102 }
103
104 // Advertisement interval (min max in milliseconds). Ensure the values fit the range bluez
105 // allows. The max >= min is guaranteed by QLowEnergyAdvertisingParameters::setInterval().
106 // Note: Bluez reads these values but at the time of this writing it marks this feature
107 // as 'experimental'
108 m_advDataDBus->setMinInterval(qBound(min: advDataMinIntervalMs,
109 val: quint16(m_advParams.minimumInterval()),
110 max: advDataMaxIntervalMs));
111 m_advDataDBus->setMaxInterval(qBound(min: advDataMinIntervalMs,
112 val: quint16(m_advParams.maximumInterval()),
113 max: advDataMaxIntervalMs));
114}
115
116void QLeDBusAdvertiser::setAdvertisementDataForDBus()
117{
118 // We don't calculate the advertisement length to guard for too long advertisements.
119 // There isn't adequate control and visibility on the advertisement for that.
120 // - We don't know the max length (legacy or extended advertising)
121 // - Bluez may truncate some of the fields on its own, making calculus here imprecise
122 // - Scan response may or may not be used to offload some of the data
123
124 // Include the power level if requested and dbus supports it
125 const auto supportedIncludes = m_advManager->supportedIncludes();
126 if (m_advData.includePowerLevel() && supportedIncludes.contains(str: advDataTXPower))
127 m_advDataDBus->setIncludes({advDataTXPower});
128
129 // Set the application provided name (valid to be empty).
130 // For clarity: bluez also has "local-name" system include that could be set if no local
131 // name is provided. However that would require that the LocalName DBus property would
132 // not exist. Existing LocalName property when 'local-name' is included leads to an
133 // advertisement error.
134 m_advDataDBus->setLocalName(m_advData.localName());
135
136 // Service UUIDs
137 if (!m_advData.services().isEmpty()) {
138 QStringList serviceUUIDList;
139 for (const auto& service: m_advData.services())
140 serviceUUIDList << service.toString(mode: QUuid::StringFormat::WithoutBraces);
141 m_advDataDBus->setServiceUUIDs(serviceUUIDList);
142 }
143
144 // Manufacturer data
145 if (m_advData.manufacturerId() != QLowEnergyAdvertisingData::invalidManufacturerId()) {
146 m_advDataDBus->setManufacturerData({
147 {m_advData.manufacturerId(), QDBusVariant(m_advData.manufacturerData())}});
148 }
149
150 // Discoverability
151 if (m_advDataDBus->type() == advDataTypePeripheral) {
152 m_advDataDBus->setDiscoverable(m_advData.discoverability()
153 != QLowEnergyAdvertisingData::DiscoverabilityNone);
154 } else {
155 qCDebug(QT_BT_BLUEZ) << "Ignoring advertisement discoverability in broadcast mode";
156 }
157
158 // Raw data
159 if (!m_advData.rawData().isEmpty())
160 qCWarning(QT_BT_BLUEZ) << "Raw advertisement data not supported, ignoring";
161}
162
163void QLeDBusAdvertiser::startAdvertising()
164{
165 qCDebug(QT_BT_BLUEZ) << "Start advertising" << m_advObjectPath << "on" << m_advManager->path();
166 if (m_advertising) {
167 qCWarning(QT_BT_BLUEZ) << "Start tried while already advertising";
168 return;
169 }
170
171 if (!QDBusConnection::systemBus().registerObject(path: m_advObjectPath, object: m_advDataDBus,
172 options: QDBusConnection::ExportAllContents)) {
173 qCWarning(QT_BT_BLUEZ) << "Advertisement dbus object registration failed";
174 emit errorOccurred();
175 return;
176 }
177
178 // Register the advertisement which starts the actual advertising.
179 // We use call watcher here instead of waitForFinished() because DBus will
180 // call back our advertisement object (to read data) in this same thread => would block
181 auto reply = m_advManager->RegisterAdvertisement(advertisement: QDBusObjectPath(m_advObjectPath), options: {});
182 QDBusPendingCallWatcher* watcher = new QDBusPendingCallWatcher(reply, this);
183
184 QObject::connect(sender: watcher, signal: &QDBusPendingCallWatcher::finished, context: this,
185 slot: [this](QDBusPendingCallWatcher* watcher){
186 QDBusPendingReply<> reply = *watcher;
187 if (reply.isError()) {
188 qCWarning(QT_BT_BLUEZ) << "Advertisement registration failed" << reply.error();
189 if (reply.error().name() == bluezErrorFailed)
190 qCDebug(QT_BT_BLUEZ) << "Advertisement could've been too large";
191 QDBusConnection::systemBus().unregisterObject(path: m_advObjectPath);
192 emit errorOccurred();
193 } else {
194 qCDebug(QT_BT_BLUEZ) << "Advertisement started successfully";
195 m_advertising = true;
196 }
197 watcher->deleteLater();
198 });
199}
200
201void QLeDBusAdvertiser::stopAdvertising()
202{
203 if (!m_advertising)
204 return;
205
206 m_advertising = false;
207 auto reply = m_advManager->UnregisterAdvertisement(advertisement: QDBusObjectPath(m_advObjectPath));
208 reply.waitForFinished();
209 if (reply.isError())
210 qCWarning(QT_BT_BLUEZ) << "Error in unregistering advertisement" << reply.error();
211 else
212 qCDebug(QT_BT_BLUEZ) << "Advertisement unregistered successfully";
213 QDBusConnection::systemBus().unregisterObject(path: m_advObjectPath);
214}
215
216// Called by Bluez when the advertisement has been removed (org.bluez.LEAdvertisement1.Release)
217void QLeDBusAdvertiser::Release()
218{
219 qCDebug(QT_BT_BLUEZ) << "Advertisement" << m_advObjectPath << "released"
220 << (m_advertising ? "unexpectedly" : "");
221 if (m_advertising) {
222 // If we are advertising, it means the Release is unsolicited
223 // and handled as an advertisement error. No need to call UnregisterAdvertisement
224 m_advertising = false;
225 QDBusConnection::systemBus().unregisterObject(path: m_advObjectPath);
226 emit errorOccurred();
227 }
228}
229
230QT_END_NAMESPACE
231

source code of qtconnectivity/src/bluetooth/qleadvertiser_bluezdbus.cpp