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 "bluezperipheralobjects_p.h" |
5 | |
6 | #include "propertiesadaptor_p.h" |
7 | #include "gattservice1adaptor_p.h" |
8 | #include "gattcharacteristic1adaptor_p.h" |
9 | #include "gattdescriptor1adaptor_p.h" |
10 | |
11 | #include <QtCore/QLoggingCategory> |
12 | #include <QtDBus/QDBusConnection> |
13 | |
14 | QT_BEGIN_NAMESPACE |
15 | |
16 | Q_DECLARE_LOGGING_CATEGORY(QT_BT_BLUEZ) |
17 | |
18 | using namespace Qt::StringLiterals; |
19 | |
20 | static constexpr auto characteristicPathTemplate{"%1/char%2"_L1 }; |
21 | static constexpr auto descriptorPathTemplate{"%1/desc%2"_L1 }; |
22 | static constexpr auto servicePathTemplate{"%1/service%2"_L1 }; |
23 | |
24 | // The interface names and error values are from BlueZ "gatt-api" documentation |
25 | static constexpr auto bluezServiceInterface{"org.bluez.GattService1"_L1 }; |
26 | static constexpr auto bluezCharacteristicInterface{"org.bluez.GattCharacteristic1"_L1 }; |
27 | static constexpr auto bluezDescriptorInterface{"org.bluez.GattDescriptor1"_L1 }; |
28 | |
29 | static constexpr auto bluezErrorInvalidValueLength{"org.bluez.Error.InvalidValueLength"_L1 }; |
30 | static constexpr auto bluezErrorInvalidOffset{"org.bluez.Error.InvalidOffset"_L1 }; |
31 | static constexpr auto bluezErrorNotAuthorized{"org.bluez.Error.NotAuthorized"_L1 }; |
32 | // Bluetooth Core v5.3, 3.2.9, Vol 3, Part F |
33 | static constexpr int maximumAttributeLength{512}; |
34 | |
35 | |
36 | QtBluezPeripheralGattObject::QtBluezPeripheralGattObject(const QString& objectPath, |
37 | const QString& uuid, QLowEnergyHandle handle, QObject* parent) |
38 | : QObject(parent), objectPath(objectPath), uuid(uuid), handle(handle), |
39 | propertiesAdaptor(new OrgFreedesktopDBusPropertiesAdaptor(this)) |
40 | {} |
41 | |
42 | QtBluezPeripheralGattObject::~QtBluezPeripheralGattObject() |
43 | { |
44 | unregisterObject(); |
45 | } |
46 | |
47 | bool QtBluezPeripheralGattObject::registerObject() |
48 | { |
49 | if (m_registered) |
50 | return true; |
51 | |
52 | if (QDBusConnection::systemBus().registerObject(path: objectPath, object: this)) { |
53 | qCDebug(QT_BT_BLUEZ) << "Registered object on DBus:" << objectPath << uuid; |
54 | m_registered = true; |
55 | return true; |
56 | } else { |
57 | qCWarning(QT_BT_BLUEZ) << "Failed to register object on DBus:" << objectPath << uuid; |
58 | return false; |
59 | } |
60 | } |
61 | |
62 | void QtBluezPeripheralGattObject::unregisterObject() |
63 | { |
64 | if (!m_registered) |
65 | return; |
66 | QDBusConnection::systemBus().unregisterObject(path: objectPath); |
67 | qCDebug(QT_BT_BLUEZ) << "Unregistered object on DBus:" << objectPath << uuid; |
68 | m_registered = false; |
69 | } |
70 | |
71 | void QtBluezPeripheralGattObject::accessEvent(const QVariantMap& options) |
72 | { |
73 | // Report this event for connection management purposes |
74 | const auto remoteDevice = options.value(key: "device"_L1 ).value<QDBusObjectPath>().path(); |
75 | if (!remoteDevice.isEmpty()) |
76 | emit remoteDeviceAccessEvent(remoteDeviceObjectPath: remoteDevice, mtu: options.value(key: "mtu"_L1 ).toUInt()); |
77 | } |
78 | |
79 | QtBluezPeripheralDescriptor::QtBluezPeripheralDescriptor( |
80 | const QLowEnergyDescriptorData& descriptorData, |
81 | const QString& characteristicPath, quint16 ordinal, |
82 | QLowEnergyHandle handle, QLowEnergyHandle characteristicHandle, |
83 | QObject* parent) |
84 | : QtBluezPeripheralGattObject(descriptorPathTemplate.arg(args: characteristicPath).arg(a: ordinal), |
85 | descriptorData.uuid().toString(mode: QUuid::WithoutBraces), handle, parent), |
86 | m_adaptor(new OrgBluezGattDescriptor1Adaptor(this)), |
87 | m_characteristicPath(characteristicPath), |
88 | m_characteristicHandle(characteristicHandle) |
89 | { |
90 | if (descriptorData.value().size() > maximumAttributeLength) { |
91 | qCWarning(QT_BT_BLUEZ) << "Descriptor value is too large, cropping it to" |
92 | << maximumAttributeLength; |
93 | m_value = descriptorData.value().sliced(pos: 0, n: maximumAttributeLength); |
94 | } else { |
95 | m_value = descriptorData.value(); |
96 | } |
97 | initializeFlags(data: descriptorData); |
98 | } |
99 | |
100 | InterfaceList QtBluezPeripheralDescriptor::properties() const |
101 | { |
102 | InterfaceList properties; |
103 | properties.insert(key: bluezDescriptorInterface, |
104 | value: { |
105 | {"UUID"_L1 , uuid}, |
106 | {"Characteristic"_L1 , QDBusObjectPath(m_characteristicPath)}, |
107 | {"Flags"_L1 , m_flags} |
108 | }); |
109 | return properties; |
110 | } |
111 | |
112 | // org.bluez.GattDescriptor1 |
113 | // This function is invoked when remote device reads the value |
114 | QByteArray QtBluezPeripheralDescriptor::ReadValue(const QVariantMap &options, QString& error) |
115 | { |
116 | accessEvent(options); |
117 | // Offset is set by Bluez when the value size is more than MTU size. |
118 | // Bluez deduces the value size from the first ReadValue. If the |
119 | // received data size is larger than MTU, Bluez will take the first MTU bytes and |
120 | // issue more ReadValue calls with the 'offset' set |
121 | const quint16 offset = options.value(key: "offset"_L1 ).toUInt(); |
122 | const quint16 mtu = options.value(key: "mtu"_L1 ).toUInt(); |
123 | |
124 | if (offset > m_value.length() - 1) { |
125 | qCWarning(QT_BT_BLUEZ) << "Invalid offset" << offset << ", value len:" << m_value.length(); |
126 | error = bluezErrorInvalidOffset; |
127 | return {}; |
128 | } |
129 | |
130 | if (offset > 0) |
131 | return m_value.mid(index: offset, len: mtu); |
132 | else |
133 | return m_value; |
134 | } |
135 | |
136 | // org.bluez.GattDescriptor1 |
137 | // This function is invoked when remote device writes a value |
138 | QString QtBluezPeripheralDescriptor::WriteValue(const QByteArray &value, |
139 | const QVariantMap &options) |
140 | { |
141 | accessEvent(options); |
142 | |
143 | if (options.value(key: "prepare-authorize"_L1 ).toBool()) { |
144 | // Qt API doesn't provide the means for application to authorize |
145 | qCWarning(QT_BT_BLUEZ) << "Descriptor write requires authorization." |
146 | << "The client device needs to be trusted beforehand" ; |
147 | return bluezErrorNotAuthorized; |
148 | } |
149 | |
150 | if (value.size() > maximumAttributeLength) { |
151 | qCWarning(QT_BT_BLUEZ) << "Descriptor value is too large:" << value.size(); |
152 | return bluezErrorInvalidValueLength; |
153 | } |
154 | m_value = value; |
155 | emit valueUpdatedByRemote(characteristicHandle: m_characteristicHandle, descriptorHandle: handle, value); |
156 | return {}; |
157 | } |
158 | |
159 | // This function is called when the value has been updated locally (server-side) |
160 | bool QtBluezPeripheralDescriptor::localValueUpdate(const QByteArray& value) |
161 | { |
162 | if (value.size() > maximumAttributeLength) { |
163 | qCWarning(QT_BT_BLUEZ) << "Descriptor value is too large:" << value.size(); |
164 | return false; |
165 | } |
166 | m_value = value; |
167 | return true; |
168 | } |
169 | |
170 | void QtBluezPeripheralDescriptor::initializeFlags(const QLowEnergyDescriptorData& data) |
171 | { |
172 | // Flag tokens are from org.bluez.GattDescriptor1 documentation |
173 | if (data.isReadable()) |
174 | m_flags.append(t: "read"_L1 ); |
175 | if (data.readConstraints() & QBluetooth::AttAccessConstraint::AttEncryptionRequired) |
176 | m_flags.append(t: "encrypt-read"_L1 ); |
177 | if (data.readConstraints() & QBluetooth::AttAccessConstraint::AttAuthenticationRequired) |
178 | m_flags.append(t: "encrypt-authenticated-read"_L1 ); |
179 | |
180 | if (data.isWritable()) |
181 | m_flags.append(t: "write"_L1 ); |
182 | if (data.writeConstraints() & QBluetooth::AttAccessConstraint::AttEncryptionRequired) |
183 | m_flags.append(t: "encrypt-write"_L1 ); |
184 | if (data.writeConstraints() & QBluetooth::AttAccessConstraint::AttAuthenticationRequired) |
185 | m_flags.append(t: "encrypt-authenticated-write"_L1 ); |
186 | |
187 | if (data.readConstraints() & QBluetooth::AttAccessConstraint::AttAuthorizationRequired |
188 | || data.writeConstraints() & QBluetooth::AttAccessConstraint::AttAuthorizationRequired) |
189 | m_flags.append(t: "authorize"_L1 ); |
190 | |
191 | if (m_flags.isEmpty()) { |
192 | qCWarning(QT_BT_BLUEZ) << "Descriptor property flags not set" << uuid |
193 | << "Peripheral may fail to register" ; |
194 | } |
195 | } |
196 | |
197 | QtBluezPeripheralCharacteristic::QtBluezPeripheralCharacteristic( |
198 | const QLowEnergyCharacteristicData& characteristicData, |
199 | const QString& servicePath, quint16 ordinal, |
200 | QLowEnergyHandle handle, QObject* parent) |
201 | : QtBluezPeripheralGattObject(characteristicPathTemplate.arg(args: servicePath).arg(a: ordinal), |
202 | characteristicData.uuid().toString(mode: QUuid::WithoutBraces), handle, parent), |
203 | m_adaptor(new OrgBluezGattCharacteristic1Adaptor(this)), |
204 | m_servicePath(servicePath), |
205 | m_minimumValueLength(std::min(a: characteristicData.minimumValueLength(), |
206 | b: maximumAttributeLength)), |
207 | m_maximumValueLength(std::min(a: characteristicData.maximumValueLength(), |
208 | b: maximumAttributeLength)) |
209 | { |
210 | initializeFlags(data: characteristicData); |
211 | initializeValue(value: characteristicData.value()); |
212 | } |
213 | |
214 | InterfaceList QtBluezPeripheralCharacteristic::properties() const |
215 | { |
216 | InterfaceList properties; |
217 | properties.insert(key: bluezCharacteristicInterface, |
218 | value: { |
219 | {"UUID"_L1 , uuid}, |
220 | {"Service"_L1 , QDBusObjectPath(m_servicePath)}, |
221 | {"Flags"_L1 , m_flags} |
222 | }); |
223 | return properties; |
224 | } |
225 | |
226 | // org.bluez.GattCharacteristic1 |
227 | // This function is invoked when remote device reads the value |
228 | QByteArray QtBluezPeripheralCharacteristic::ReadValue(const QVariantMap &options, QString& error) |
229 | { |
230 | accessEvent(options); |
231 | // Offset is set by Bluez when the value size is more than MTU size. |
232 | // Bluez deduces the value size from the first ReadValue. If the |
233 | // received data size is larger than MTU, Bluez will take the first MTU bytes and |
234 | // issue more ReadValue calls with the 'offset' set |
235 | const quint16 offset = options.value(key: "offset"_L1 ).toUInt(); |
236 | const quint16 mtu = options.value(key: "mtu"_L1 ).toUInt(); |
237 | |
238 | if (offset > m_value.length() - 1) { |
239 | qCWarning(QT_BT_BLUEZ) << "Invalid offset" << offset << ", value len:" << m_value.length(); |
240 | error = bluezErrorInvalidOffset; |
241 | return {}; |
242 | } |
243 | |
244 | if (offset > 0) |
245 | return m_value.mid(index: offset, len: mtu); |
246 | else |
247 | return m_value; |
248 | } |
249 | |
250 | // org.bluez.GattCharacteristic1 |
251 | // This function is invoked when remote device writes a value |
252 | QString QtBluezPeripheralCharacteristic::WriteValue(const QByteArray &value, |
253 | const QVariantMap &options) |
254 | { |
255 | accessEvent(options); |
256 | |
257 | if (options.value(key: "prepare-authorize"_L1 ).toBool()) { |
258 | // Qt API doesn't provide the means for application to authorize |
259 | qCWarning(QT_BT_BLUEZ) << "Characteristic write requires authorization." |
260 | << "The client device needs to be trusted beforehand" ; |
261 | return bluezErrorNotAuthorized; |
262 | } |
263 | |
264 | if (value.size() < m_minimumValueLength || value.size() > m_maximumValueLength) { |
265 | qCWarning(QT_BT_BLUEZ) << "Characteristic value has invalid length" << value.size() |
266 | << "min:" << m_minimumValueLength |
267 | << "max:" << m_maximumValueLength; |
268 | return bluezErrorInvalidValueLength; |
269 | } |
270 | m_value = value; |
271 | emit valueUpdatedByRemote(handle, value); |
272 | return {}; |
273 | } |
274 | |
275 | // This function is called when the value has been updated locally (server-side) |
276 | bool QtBluezPeripheralCharacteristic::localValueUpdate(const QByteArray& value) |
277 | { |
278 | if (value.size() < m_minimumValueLength || value.size() > m_maximumValueLength) { |
279 | qCWarning(QT_BT_BLUEZ) << "Characteristic value has invalid length" << value.size() |
280 | << "min:" << m_minimumValueLength |
281 | << "max:" << m_maximumValueLength; |
282 | return false; |
283 | } |
284 | m_value = value; |
285 | if (m_notifying) { |
286 | emit propertiesAdaptor->PropertiesChanged( |
287 | interface: bluezCharacteristicInterface, changed_properties: {{"Value"_L1 , m_value}}, invalidated_properties: {}); |
288 | } |
289 | return true; |
290 | } |
291 | |
292 | // org.bluez.GattCharacteristic1 |
293 | // These are called when remote client enables or disables NTF/IND |
294 | void QtBluezPeripheralCharacteristic::StartNotify() |
295 | { |
296 | qCDebug(QT_BT_BLUEZ) << "NTF or IND enabled for characteristic" << uuid; |
297 | m_notifying = true; |
298 | } |
299 | |
300 | void QtBluezPeripheralCharacteristic::StopNotify() |
301 | { |
302 | qCDebug(QT_BT_BLUEZ) << "NTF or IND disabled for characteristic" << uuid; |
303 | m_notifying = false; |
304 | } |
305 | |
306 | |
307 | void QtBluezPeripheralCharacteristic::initializeValue(const QByteArray& value) |
308 | { |
309 | const auto valueSize = value.size(); |
310 | if (valueSize < m_minimumValueLength || valueSize > m_maximumValueLength) { |
311 | qCWarning(QT_BT_BLUEZ) << "Characteristic value has invalid length" << valueSize |
312 | << "min:" << m_minimumValueLength |
313 | << "max:" << m_maximumValueLength; |
314 | m_value = QByteArray(m_minimumValueLength, 0); |
315 | } else { |
316 | m_value = value; |
317 | } |
318 | } |
319 | |
320 | void QtBluezPeripheralCharacteristic::initializeFlags(const QLowEnergyCharacteristicData& data) |
321 | { |
322 | // Flag tokens are from org.bluez.GattCharacteristic1 documentation |
323 | if (data.properties() & QLowEnergyCharacteristic::PropertyType::Broadcasting) |
324 | m_flags.append(t: "broadcast"_L1 ); |
325 | if (data.properties() & QLowEnergyCharacteristic::PropertyType::WriteNoResponse) |
326 | m_flags.append(t: "write-without-response"_L1 ); |
327 | if (data.properties() & QLowEnergyCharacteristic::PropertyType::Read) |
328 | m_flags.append(t: "read"_L1 ); |
329 | if (data.properties() & QLowEnergyCharacteristic::PropertyType::Write) |
330 | m_flags.append(t: "write"_L1 ); |
331 | if (data.properties() & QLowEnergyCharacteristic::PropertyType::Notify) |
332 | m_flags.append(t: "notify"_L1 ); |
333 | if (data.properties() & QLowEnergyCharacteristic::PropertyType::Indicate) |
334 | m_flags.append(t: "indicate"_L1 ); |
335 | if (data.properties() & QLowEnergyCharacteristic::PropertyType::WriteSigned) |
336 | m_flags.append(t: "authenticated-signed-writes"_L1 ); |
337 | if (data.properties() & QLowEnergyCharacteristic::PropertyType::ExtendedProperty) { |
338 | // If extended properties property is set, check if we have the descriptor |
339 | // describing them. Bluez will generate the actual descriptor based on these |
340 | // flags. For clarity: the 'extended-properties' token mentioned in the Bluez |
341 | // API is implied by these flags. |
342 | for (const auto& descriptor : data.descriptors()) { |
343 | // Core Bluetooth v5.3 Vol 3, Part G, 3.3.3.1 |
344 | if (descriptor.uuid() |
345 | == QBluetoothUuid::DescriptorType::CharacteristicExtendedProperties |
346 | && descriptor.value().size() == 2) { |
347 | const auto properties = descriptor.value().at(i: 0); |
348 | if (properties & 0x01) |
349 | m_flags.append(t: "reliable-write"_L1 ); |
350 | if (properties & 0x02) |
351 | m_flags.append(t: "writable-auxiliaries"_L1 ); |
352 | } |
353 | } |
354 | } |
355 | |
356 | if (data.readConstraints() & QBluetooth::AttAccessConstraint::AttEncryptionRequired) |
357 | m_flags.append(t: "encrypt-read"_L1 ); |
358 | if (data.readConstraints() & QBluetooth::AttAccessConstraint::AttAuthenticationRequired) |
359 | m_flags.append(t: "encrypt-authenticated-read"_L1 ); |
360 | if (data.writeConstraints() & QBluetooth::AttAccessConstraint::AttEncryptionRequired) |
361 | m_flags.append(t: "encrypt-write"_L1 ); |
362 | if (data.writeConstraints() & QBluetooth::AttAccessConstraint::AttAuthenticationRequired) |
363 | m_flags.append(t: "encrypt-authenticated-write"_L1 ); |
364 | |
365 | if (data.readConstraints() & QBluetooth::AttAccessConstraint::AttAuthorizationRequired |
366 | || data.writeConstraints() & QBluetooth::AttAccessConstraint::AttAuthorizationRequired) |
367 | m_flags.append(t: "authorize"_L1 ); |
368 | |
369 | if (m_flags.isEmpty()) { |
370 | qCWarning(QT_BT_BLUEZ) << "Characteristic property flags not set" << uuid |
371 | << "Peripheral may fail to register" ; |
372 | } |
373 | } |
374 | |
375 | |
376 | QtBluezPeripheralService::QtBluezPeripheralService(const QLowEnergyServiceData &serviceData, |
377 | const QString& applicationPath, quint16 ordinal, |
378 | QLowEnergyHandle handle, QObject* parent) |
379 | : QtBluezPeripheralGattObject(servicePathTemplate.arg(args: applicationPath).arg(a: ordinal), |
380 | serviceData.uuid().toString(mode: QUuid::WithoutBraces), handle, parent), |
381 | m_isPrimary(serviceData.type() == QLowEnergyServiceData::ServiceTypePrimary), |
382 | m_adaptor(new OrgBluezGattService1Adaptor(this)) |
383 | { |
384 | } |
385 | |
386 | void QtBluezPeripheralService::addIncludedService(const QString& objectPath) { |
387 | qCDebug(QT_BT_BLUEZ) << "Adding included service" << objectPath << "for" << uuid; |
388 | m_includedServices.append(t: QDBusObjectPath(objectPath)); |
389 | } |
390 | |
391 | InterfaceList QtBluezPeripheralService::properties() const { |
392 | InterfaceList interfaces; |
393 | interfaces.insert(key: bluezServiceInterface,value: { |
394 | {"UUID"_L1 , uuid}, |
395 | {"Primary"_L1 , m_isPrimary}, |
396 | {"Includes"_L1 , QVariant::fromValue(value: m_includedServices)} |
397 | }); |
398 | return interfaces; |
399 | }; |
400 | |
401 | QT_END_NAMESPACE |
402 | |
403 | #include "moc_bluezperipheralobjects_p.cpp" |
404 | |