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 "qleadvertiser_bluez_p.h" |
5 | |
6 | #include "bluez/bluez_data_p.h" |
7 | #include "bluez/hcimanager_p.h" |
8 | #include "qbluetoothsocketbase_p.h" |
9 | |
10 | #include <QtCore/qloggingcategory.h> |
11 | |
12 | #include <cstring> |
13 | |
14 | QT_BEGIN_NAMESPACE |
15 | |
16 | Q_DECLARE_LOGGING_CATEGORY(QT_BT_BLUEZ) |
17 | |
18 | QLeAdvertiser::~QLeAdvertiser() |
19 | = default; |
20 | |
21 | struct AdvParams { |
22 | quint16 minInterval; |
23 | quint16 maxInterval; |
24 | quint8 type; |
25 | quint8 ownAddrType; |
26 | quint8 directAddrType; |
27 | bdaddr_t directAddr; |
28 | quint8 channelMap; |
29 | quint8 filterPolicy; |
30 | } __attribute__ ((packed)); |
31 | |
32 | struct AdvData { |
33 | quint8 length; |
34 | quint8 data[31]; |
35 | }; |
36 | |
37 | struct WhiteListParams { |
38 | quint8 addrType; |
39 | bdaddr_t addr; |
40 | }; |
41 | |
42 | |
43 | template <typename T> |
44 | static QByteArray byteArrayFromStruct(const T &data) |
45 | { |
46 | return QByteArray(reinterpret_cast<const char *>(&data), sizeof data); |
47 | } |
48 | |
49 | QLeAdvertiserBluez::QLeAdvertiserBluez(const QLowEnergyAdvertisingParameters ¶ms, |
50 | const QLowEnergyAdvertisingData &advertisingData, |
51 | const QLowEnergyAdvertisingData &scanResponseData, |
52 | std::shared_ptr<HciManager> hciManager, QObject *parent) |
53 | : QLeAdvertiser(params, advertisingData, scanResponseData, parent), m_hciManager(hciManager) |
54 | { |
55 | Q_ASSERT(m_hciManager); |
56 | connect(sender: m_hciManager.get(), signal: &HciManager::commandCompleted, context: this, |
57 | slot: &QLeAdvertiserBluez::handleCommandCompleted); |
58 | } |
59 | |
60 | QLeAdvertiserBluez::~QLeAdvertiserBluez() |
61 | { |
62 | disconnect(sender: m_hciManager.get(), signal: &HciManager::commandCompleted, receiver: this, |
63 | slot: &QLeAdvertiserBluez::handleCommandCompleted); |
64 | doStopAdvertising(); |
65 | } |
66 | |
67 | void QLeAdvertiserBluez::doStartAdvertising() |
68 | { |
69 | if (!m_hciManager->monitorEvent(event: HciManager::HciEvent::EVT_CMD_COMPLETE)) { |
70 | handleError(); |
71 | return; |
72 | } |
73 | |
74 | m_sendPowerLevel = advertisingData().includePowerLevel() |
75 | || scanResponseData().includePowerLevel(); |
76 | if (m_sendPowerLevel) |
77 | queueReadTxPowerLevelCommand(); |
78 | else |
79 | queueAdvertisingCommands(); |
80 | sendNextCommand(); |
81 | } |
82 | |
83 | void QLeAdvertiserBluez::doStopAdvertising() |
84 | { |
85 | toggleAdvertising(enable: false); |
86 | sendNextCommand(); |
87 | } |
88 | |
89 | void QLeAdvertiserBluez::queueCommand(QBluezConst::OpCodeCommandField ocf, const QByteArray &data) |
90 | { |
91 | m_pendingCommands << Command(ocf, data); |
92 | } |
93 | |
94 | void QLeAdvertiserBluez::sendNextCommand() |
95 | { |
96 | if (m_pendingCommands.isEmpty()) { |
97 | // TODO: Unmonitor event. |
98 | return; |
99 | } |
100 | const Command &c = m_pendingCommands.first(); |
101 | if (!m_hciManager->sendCommand(ogf: QBluezConst::OgfLinkControl, ocf: c.ocf, parameters: c.data)) { |
102 | handleError(); |
103 | return; |
104 | } |
105 | } |
106 | |
107 | void QLeAdvertiserBluez::queueAdvertisingCommands() |
108 | { |
109 | toggleAdvertising(enable: false); // Stop advertising first, in case it's currently active. |
110 | setWhiteList(); |
111 | setAdvertisingParams(); |
112 | setAdvertisingData(); |
113 | setScanResponseData(); |
114 | toggleAdvertising(enable: true); |
115 | } |
116 | |
117 | void QLeAdvertiserBluez::queueReadTxPowerLevelCommand() |
118 | { |
119 | // Spec v4.2, Vol 2, Part E, 7.8.6 |
120 | queueCommand(ocf: QBluezConst::OcfLeReadTxPowerLevel, data: QByteArray()); |
121 | } |
122 | |
123 | void QLeAdvertiserBluez::toggleAdvertising(bool enable) |
124 | { |
125 | // Spec v4.2, Vol 2, Part E, 7.8.9 |
126 | queueCommand(ocf: QBluezConst::OcfLeSetAdvEnable, data: QByteArray(1, enable)); |
127 | } |
128 | |
129 | void QLeAdvertiserBluez::setAdvertisingParams() |
130 | { |
131 | // Spec v4.2, Vol 2, Part E, 7.8.5 |
132 | // or Spec v5.3, Vol 4, Part E, 7.8.5 |
133 | AdvParams params; |
134 | static_assert(sizeof params == 15, "unexpected struct size" ); |
135 | using namespace std; |
136 | memset(s: ¶ms, c: 0, n: sizeof params); |
137 | setAdvertisingInterval(params); |
138 | params.type = parameters().mode(); |
139 | params.filterPolicy = parameters().filterPolicy(); |
140 | if (params.filterPolicy != QLowEnergyAdvertisingParameters::IgnoreWhiteList |
141 | && advertisingData().discoverability() == QLowEnergyAdvertisingData::DiscoverabilityLimited) { |
142 | qCWarning(QT_BT_BLUEZ) << "limited discoverability is incompatible with " |
143 | "using a white list; disabling filtering" ; |
144 | params.filterPolicy = QLowEnergyAdvertisingParameters::IgnoreWhiteList; |
145 | } |
146 | params.ownAddrType = QLowEnergyController::PublicAddress; // TODO: Make configurable. |
147 | |
148 | // TODO: For ADV_DIRECT_IND. |
149 | // params.directAddrType = xxx; |
150 | // params.direct_bdaddr = xxx; |
151 | |
152 | params.channelMap = 0x7; // All channels. |
153 | |
154 | const QByteArray paramsData = byteArrayFromStruct(data: params); |
155 | qCDebug(QT_BT_BLUEZ) << "advertising parameters:" << paramsData.toHex(); |
156 | queueCommand(ocf: QBluezConst::OcfLeSetAdvParams, data: paramsData); |
157 | } |
158 | |
159 | static quint16 forceIntoRange(quint16 val, quint16 min, quint16 max) |
160 | { |
161 | return qMin(a: qMax(a: val, b: min), b: max); |
162 | } |
163 | |
164 | void QLeAdvertiserBluez::setAdvertisingInterval(AdvParams ¶ms) |
165 | { |
166 | const double multiplier = 0.625; |
167 | const quint16 minVal = parameters().minimumInterval() / multiplier; |
168 | const quint16 maxVal = parameters().maximumInterval() / multiplier; |
169 | Q_ASSERT(minVal <= maxVal); |
170 | const quint16 specMinimum = |
171 | parameters().mode() == QLowEnergyAdvertisingParameters::AdvScanInd |
172 | || parameters().mode() == QLowEnergyAdvertisingParameters::AdvNonConnInd ? 0xa0 : 0x20; |
173 | const quint16 specMaximum = 0x4000; |
174 | params.minInterval = qToLittleEndian(source: forceIntoRange(val: minVal, min: specMinimum, max: specMaximum)); |
175 | params.maxInterval = qToLittleEndian(source: forceIntoRange(val: maxVal, min: specMinimum, max: specMaximum)); |
176 | Q_ASSERT(params.minInterval <= params.maxInterval); |
177 | } |
178 | |
179 | void QLeAdvertiserBluez::setPowerLevel(AdvData &advData) |
180 | { |
181 | if (m_sendPowerLevel) { |
182 | advData.data[advData.length++] = 2; |
183 | advData.data[advData.length++]= 0xa; |
184 | advData.data[advData.length++] = m_powerLevel; |
185 | } |
186 | } |
187 | |
188 | void QLeAdvertiserBluez::setFlags(AdvData &advData) |
189 | { |
190 | // TODO: Discoverability flags are incompatible with ADV_DIRECT_IND |
191 | quint8 flags = 0; |
192 | if (advertisingData().discoverability() == QLowEnergyAdvertisingData::DiscoverabilityLimited) |
193 | flags |= 0x1; |
194 | else if (advertisingData().discoverability() == QLowEnergyAdvertisingData::DiscoverabilityGeneral) |
195 | flags |= 0x2; |
196 | flags |= 0x4; // "BR/EDR not supported". Otherwise clients might try to connect over Bluetooth classic. |
197 | if (flags) { |
198 | advData.data[advData.length++] = 2; |
199 | advData.data[advData.length++] = 0x1; |
200 | advData.data[advData.length++] = flags; |
201 | } |
202 | } |
203 | |
204 | template<typename T> static quint8 servicesType(bool dataComplete); |
205 | template<> quint8 servicesType<quint16>(bool dataComplete) |
206 | { |
207 | return dataComplete ? 0x3 : 0x2; |
208 | } |
209 | template<> quint8 servicesType<quint32>(bool dataComplete) |
210 | { |
211 | return dataComplete ? 0x5 : 0x4; |
212 | } |
213 | template<> quint8 servicesType<QUuid::Id128Bytes>(bool dataComplete) |
214 | { |
215 | return dataComplete ? 0x7 : 0x6; |
216 | } |
217 | |
218 | template<typename T> |
219 | static void addServicesData(AdvData &data, const QList<T> &services) |
220 | { |
221 | if (services.isEmpty()) |
222 | return; |
223 | constexpr auto sizeofT = static_cast<int>(sizeof(T)); // signed is more convenient |
224 | const qsizetype spaceAvailable = sizeof data.data - data.length; |
225 | // Determine how many services will be set, space may limit the number |
226 | const qsizetype maxServices = (std::min)((spaceAvailable - 2) / sizeofT, services.size()); |
227 | if (maxServices <= 0) { |
228 | qCWarning(QT_BT_BLUEZ) << "services data does not fit into advertising data packet" ; |
229 | return; |
230 | } |
231 | const bool dataComplete = maxServices == services.size(); |
232 | if (!dataComplete) { |
233 | qCWarning(QT_BT_BLUEZ) << "only" << maxServices << "out of" << services.size() |
234 | << "services fit into the advertising data" ; |
235 | } |
236 | data.data[data.length++] = 1 + maxServices * sizeofT; |
237 | data.data[data.length++] = servicesType<T>(dataComplete); |
238 | for (qsizetype i = 0; i < maxServices; ++i) { |
239 | memcpy(data.data + data.length, &services.at(i), sizeofT); |
240 | data.length += sizeofT; |
241 | } |
242 | } |
243 | |
244 | void QLeAdvertiserBluez::setServicesData(const QLowEnergyAdvertisingData &src, AdvData &dest) |
245 | { |
246 | QList<quint16> services16; |
247 | QList<quint32> services32; |
248 | QList<QUuid::Id128Bytes> services128; |
249 | const QList<QBluetoothUuid> services = src.services(); |
250 | for (const QBluetoothUuid &service : services) { |
251 | bool ok; |
252 | const quint16 service16 = service.toUInt16(ok: &ok); |
253 | if (ok) { |
254 | services16 << qToLittleEndian(source: service16); |
255 | continue; |
256 | } |
257 | const quint32 service32 = service.toUInt32(ok: &ok); |
258 | if (ok) { |
259 | services32 << qToLittleEndian(source: service32); |
260 | continue; |
261 | } |
262 | |
263 | // QUuid::toBytes() is defaults to Big-Endian |
264 | services128 << service.toBytes(order: QSysInfo::LittleEndian); |
265 | } |
266 | addServicesData(data&: dest, services: services16); |
267 | addServicesData(data&: dest, services: services32); |
268 | addServicesData(data&: dest, services: services128); |
269 | } |
270 | |
271 | void QLeAdvertiserBluez::setManufacturerData(const QLowEnergyAdvertisingData &src, AdvData &dest) |
272 | { |
273 | if (src.manufacturerId() == QLowEnergyAdvertisingData::invalidManufacturerId()) |
274 | return; |
275 | |
276 | const QByteArray manufacturerData = src.manufacturerData(); |
277 | if (dest.length >= sizeof dest.data - 1 - 1 - 2 - manufacturerData.size()) { |
278 | qCWarning(QT_BT_BLUEZ) << "manufacturer data does not fit into advertising data packet" ; |
279 | return; |
280 | } |
281 | |
282 | dest.data[dest.length++] = manufacturerData.size() + 1 + 2; |
283 | dest.data[dest.length++] = 0xff; |
284 | putBtData(src: src.manufacturerId(), dst: dest.data + dest.length); |
285 | dest.length += sizeof(quint16); |
286 | std::memcpy(dest: dest.data + dest.length, src: manufacturerData.data(), n: manufacturerData.size()); |
287 | dest.length += manufacturerData.size(); |
288 | } |
289 | |
290 | void QLeAdvertiserBluez::setLocalNameData(const QLowEnergyAdvertisingData &src, AdvData &dest) |
291 | { |
292 | if (src.localName().isEmpty()) |
293 | return; |
294 | if (dest.length >= sizeof dest.data - 3) { |
295 | qCWarning(QT_BT_BLUEZ) << "local name does not fit into advertising data" ; |
296 | return; |
297 | } |
298 | |
299 | const QByteArray localNameUtf8 = src.localName().toUtf8(); |
300 | const qsizetype fullSize = localNameUtf8.size() + 1 + 1; |
301 | const qsizetype size = (std::min)(a: fullSize, b: qsizetype(sizeof dest.data - dest.length)); |
302 | const bool isComplete = size == fullSize; |
303 | dest.data[dest.length++] = size - 1; |
304 | const int dataType = isComplete ? 0x9 : 0x8; |
305 | dest.data[dest.length++] = dataType; |
306 | std::memcpy(dest: dest.data + dest.length, src: localNameUtf8, n: size - 2); |
307 | dest.length += size - 2; |
308 | } |
309 | |
310 | void QLeAdvertiserBluez::setData(bool isScanResponseData) |
311 | { |
312 | // Spec v4.2, Vol 3, Part C, 11 and Supplement, Part 1 |
313 | AdvData theData; |
314 | static_assert(sizeof theData == 32, "unexpected struct size" ); |
315 | theData.length = 0; |
316 | |
317 | const QLowEnergyAdvertisingData &sourceData = isScanResponseData |
318 | ? scanResponseData() : advertisingData(); |
319 | |
320 | if (const QByteArray rawData = sourceData.rawData(); !rawData.isEmpty()) { |
321 | theData.length = (std::min)(a: qsizetype(sizeof theData.data), b: rawData.size()); |
322 | std::memcpy(dest: theData.data, src: rawData.data(), n: theData.length); |
323 | } else { |
324 | if (sourceData.includePowerLevel()) |
325 | setPowerLevel(theData); |
326 | if (!isScanResponseData) |
327 | setFlags(theData); |
328 | |
329 | // Insert new constant-length data here. |
330 | |
331 | setLocalNameData(src: sourceData, dest&: theData); |
332 | setServicesData(src: sourceData, dest&: theData); |
333 | setManufacturerData(src: sourceData, dest&: theData); |
334 | } |
335 | |
336 | std::memset(s: theData.data + theData.length, c: 0, n: sizeof theData.data - theData.length); |
337 | const QByteArray dataToSend = byteArrayFromStruct(data: theData); |
338 | |
339 | if (!isScanResponseData) { |
340 | qCDebug(QT_BT_BLUEZ) << "advertising data:" << dataToSend.toHex(); |
341 | queueCommand(ocf: QBluezConst::OcfLeSetAdvData, data: dataToSend); |
342 | } else if ((parameters().mode() == QLowEnergyAdvertisingParameters::AdvScanInd |
343 | || parameters().mode() == QLowEnergyAdvertisingParameters::AdvInd) |
344 | && theData.length > 0) { |
345 | qCDebug(QT_BT_BLUEZ) << "scan response data:" << dataToSend.toHex(); |
346 | queueCommand(ocf: QBluezConst::OcfLeSetScanResponseData, data: dataToSend); |
347 | } |
348 | } |
349 | |
350 | void QLeAdvertiserBluez::setAdvertisingData() |
351 | { |
352 | // Spec v4.2, Vol 2, Part E, 7.8.7 |
353 | setData(false); |
354 | } |
355 | |
356 | void QLeAdvertiserBluez::setScanResponseData() |
357 | { |
358 | // Spec v4.2, Vol 2, Part E, 7.8.8 |
359 | setData(true); |
360 | } |
361 | |
362 | void QLeAdvertiserBluez::setWhiteList() |
363 | { |
364 | // Spec v4.2, Vol 2, Part E, 7.8.15-16 |
365 | if (parameters().filterPolicy() == QLowEnergyAdvertisingParameters::IgnoreWhiteList) |
366 | return; |
367 | queueCommand(ocf: QBluezConst::OcfLeClearWhiteList, data: QByteArray()); |
368 | const QList<QLowEnergyAdvertisingParameters::AddressInfo> whiteListInfos |
369 | = parameters().whiteList(); |
370 | for (const auto &addressInfo : whiteListInfos) { |
371 | WhiteListParams commandParam; |
372 | static_assert(sizeof commandParam == 7, "unexpected struct size" ); |
373 | commandParam.addrType = addressInfo.type; |
374 | convertAddress(from: addressInfo.address.toUInt64(), to&: commandParam.addr.b); |
375 | queueCommand(ocf: QBluezConst::OcfLeAddToWhiteList, data: byteArrayFromStruct(data: commandParam)); |
376 | } |
377 | } |
378 | |
379 | void QLeAdvertiserBluez::handleCommandCompleted(quint16 opCode, quint8 status, |
380 | const QByteArray &data) |
381 | { |
382 | if (m_pendingCommands.isEmpty()) |
383 | return; |
384 | const QBluezConst::OpCodeCommandField ocf = QBluezConst::OpCodeCommandField(ocfFromOpCode(opCode)); |
385 | const Command currentCmd = m_pendingCommands.first(); |
386 | if (currentCmd.ocf != ocf) |
387 | return; // Not one of our commands. |
388 | m_pendingCommands.takeFirst(); |
389 | if (status != 0) { |
390 | qCDebug(QT_BT_BLUEZ) << "command" << ocf |
391 | << "failed with status" << (HciManager::HciError)status |
392 | << "status code" << status; |
393 | if (ocf == QBluezConst::OcfLeSetAdvEnable && status == 0xc && currentCmd.data == QByteArray(1, '\0')) { |
394 | // we ignore OcfLeSetAdvEnable if it tries to disable an active advertisement |
395 | // it seems the platform often automatically turns off advertisements |
396 | // subsequently the explicit stopAdvertisement call fails when re-issued |
397 | qCDebug(QT_BT_BLUEZ) << "Advertising disable failed, ignoring" ; |
398 | sendNextCommand(); |
399 | return; |
400 | } |
401 | if (ocf == QBluezConst::OcfLeReadTxPowerLevel) { |
402 | qCDebug(QT_BT_BLUEZ) << "reading power level failed, leaving it out of the " |
403 | "advertising data" ; |
404 | m_sendPowerLevel = false; |
405 | } else { |
406 | handleError(); |
407 | return; |
408 | } |
409 | } else { |
410 | qCDebug(QT_BT_BLUEZ) << "command" << ocf << "executed successfully" ; |
411 | } |
412 | |
413 | switch (ocf) { |
414 | case QBluezConst::OcfLeReadTxPowerLevel: |
415 | if (m_sendPowerLevel) { |
416 | m_powerLevel = data.at(i: 0); |
417 | qCDebug(QT_BT_BLUEZ) << "TX power level is" << m_powerLevel; |
418 | } |
419 | queueAdvertisingCommands(); |
420 | break; |
421 | default: |
422 | break; |
423 | } |
424 | |
425 | sendNextCommand(); |
426 | } |
427 | |
428 | void QLeAdvertiserBluez::handleError() |
429 | { |
430 | m_pendingCommands.clear(); |
431 | // TODO: Unmonitor event |
432 | emit errorOccurred(); |
433 | } |
434 | |
435 | QT_END_NAMESPACE |
436 | |
437 | #include "moc_qleadvertiser_bluez_p.cpp" |
438 | |