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