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