549 lines
15 KiB
C++
549 lines
15 KiB
C++
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
|
*
|
|
* Copyright (C) 2013 - 2024, nymea GmbH
|
|
* Copyright (C) 2024 - 2025, chargebyte austria GmbH
|
|
*
|
|
* This file is part of nymea-plugins.
|
|
*
|
|
* nymea-plugins is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* nymea-plugins is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
* General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with nymea-plugins. If not, see <https://www.gnu.org/licenses/>.
|
|
*
|
|
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
|
|
|
#include "bluetoothdevice.h"
|
|
|
|
#include <QDBusPendingReply>
|
|
|
|
BluetoothDevice::State BluetoothDevice::state() const
|
|
{
|
|
return m_state;
|
|
}
|
|
|
|
QString BluetoothDevice::name() const
|
|
{
|
|
return m_name;
|
|
}
|
|
|
|
QBluetoothAddress BluetoothDevice::address() const
|
|
{
|
|
return m_address;
|
|
}
|
|
|
|
QString BluetoothDevice::iconName() const
|
|
{
|
|
return m_iconName;
|
|
}
|
|
|
|
QBluetoothHostInfo BluetoothDevice::hostInfo() const
|
|
{
|
|
return m_hostInfo;
|
|
}
|
|
|
|
QString BluetoothDevice::alias() const
|
|
{
|
|
return m_alias;
|
|
}
|
|
|
|
bool BluetoothDevice::setAlias(const QString &alias)
|
|
{
|
|
if (!m_deviceInterface->isValid())
|
|
return false;
|
|
|
|
return m_deviceInterface->setProperty("Alias", QVariant(alias));
|
|
}
|
|
|
|
QString BluetoothDevice::modalias() const
|
|
{
|
|
return m_modalias;
|
|
}
|
|
|
|
quint32 BluetoothDevice::deviceClass() const
|
|
{
|
|
return m_deviceClass;
|
|
}
|
|
|
|
quint16 BluetoothDevice::appearance() const
|
|
{
|
|
return m_appearance;
|
|
}
|
|
|
|
qint16 BluetoothDevice::rssi() const
|
|
{
|
|
return m_rssi;
|
|
}
|
|
|
|
qint16 BluetoothDevice::txPower() const
|
|
{
|
|
return m_txPower;
|
|
}
|
|
|
|
QList<QBluetoothUuid> BluetoothDevice::uuids() const
|
|
{
|
|
return m_uuids;
|
|
}
|
|
|
|
bool BluetoothDevice::paired() const
|
|
{
|
|
return m_paired;
|
|
}
|
|
|
|
bool BluetoothDevice::connected() const
|
|
{
|
|
return m_connected;
|
|
}
|
|
|
|
bool BluetoothDevice::trusted() const
|
|
{
|
|
return m_trusted;
|
|
}
|
|
|
|
bool BluetoothDevice::setTrusted(const bool &trusted)
|
|
{
|
|
if (!m_deviceInterface->isValid())
|
|
return false;
|
|
|
|
return m_deviceInterface->setProperty("Trusted", QVariant(trusted));
|
|
}
|
|
|
|
bool BluetoothDevice::blocked() const
|
|
{
|
|
return m_blocked;
|
|
}
|
|
|
|
bool BluetoothDevice::setBlocked(const bool &blocked)
|
|
{
|
|
if (!m_deviceInterface->isValid())
|
|
return false;
|
|
|
|
return m_deviceInterface->setProperty("Blocked", QVariant(blocked));
|
|
}
|
|
|
|
bool BluetoothDevice::legacyPairing() const
|
|
{
|
|
return m_legacyPairing;
|
|
}
|
|
|
|
bool BluetoothDevice::servicesResolved() const
|
|
{
|
|
return m_servicesResolved;
|
|
}
|
|
|
|
QList<BluetoothGattService *> BluetoothDevice::services() const
|
|
{
|
|
return m_services;
|
|
}
|
|
|
|
bool BluetoothDevice::hasService(const QBluetoothUuid &serviceUuid)
|
|
{
|
|
foreach (BluetoothGattService *service, m_services) {
|
|
if (service->uuid() == serviceUuid) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
BluetoothGattService *BluetoothDevice::getService(const QBluetoothUuid &serviceUuid)
|
|
{
|
|
foreach (BluetoothGattService *service, m_services) {
|
|
if (service->uuid() == serviceUuid) {
|
|
return service;
|
|
}
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
BluetoothDevice::BluetoothDevice(const QDBusObjectPath &path, const QVariantMap &properties, QObject *parent) :
|
|
QObject(parent),
|
|
m_path(path),
|
|
m_state(Disconnected),
|
|
m_deviceClass(0),
|
|
m_appearance(0),
|
|
m_rssi(0),
|
|
m_txPower(0),
|
|
m_paired(false),
|
|
m_connected(false),
|
|
m_trusted(false),
|
|
m_blocked(false),
|
|
m_legacyPairing(false),
|
|
m_servicesResolved(false),
|
|
m_connectWatcher(nullptr),
|
|
m_disconnectWatcher(nullptr),
|
|
m_pairingWatcher(nullptr)
|
|
{
|
|
// Check DBus connection
|
|
if (!QDBusConnection::systemBus().isConnected()) {
|
|
qCWarning(dcBluez()) << "System DBus not connected.";
|
|
return;
|
|
}
|
|
|
|
m_deviceInterface = new QDBusInterface(orgBluez, m_path.path(), orgBluezDevice1, QDBusConnection::systemBus(), this);
|
|
if (!m_deviceInterface->isValid()) {
|
|
qCWarning(dcBluez()) << "Invalid DBus thing interface for" << m_path.path();
|
|
return;
|
|
}
|
|
|
|
QDBusConnection::systemBus().connect(orgBluez, m_path.path(), "org.freedesktop.DBus.Properties", "PropertiesChanged", this, SLOT(onPropertiesChanged(QString,QVariantMap,QStringList)));
|
|
|
|
processProperties(properties);
|
|
|
|
// Set initial state
|
|
evaluateCurrentState();
|
|
}
|
|
|
|
BluetoothDevice::~BluetoothDevice()
|
|
{
|
|
|
|
}
|
|
|
|
void BluetoothDevice::processProperties(const QVariantMap &properties)
|
|
{
|
|
foreach (const QString &propertyName, properties.keys()) {
|
|
if (propertyName == "Name") {
|
|
m_name = properties.value(propertyName).toString();
|
|
m_hostInfo.setName(m_name);
|
|
} else if (propertyName == "Address") {
|
|
m_address = QBluetoothAddress(properties.value(propertyName).toString());
|
|
m_hostInfo.setAddress(m_address);
|
|
} else if (propertyName == "Icon") {
|
|
m_iconName = properties.value(propertyName).toString();
|
|
} else if (propertyName == "Alias") {
|
|
setAliasInternally(properties.value(propertyName).toString());
|
|
} else if (propertyName == "Modalias") {
|
|
m_modalias = properties.value(propertyName).toString();
|
|
} else if (propertyName == "Class") {
|
|
m_deviceClass = properties.value(propertyName).toUInt();
|
|
} else if (propertyName == "Appearance") {
|
|
m_appearance = properties.value(propertyName).toUInt();
|
|
} else if (propertyName == "RSSI") {
|
|
setRssiInternally(properties.value(propertyName).toInt());
|
|
} else if (propertyName == "TxPower") {
|
|
setTxPowerInternally(properties.value(propertyName).toInt());
|
|
} else if (propertyName == "UUIDs") {
|
|
QStringList uuidStrings = properties.value(propertyName).toStringList();
|
|
m_uuids.clear();
|
|
foreach (const QString &uuidString, uuidStrings) {
|
|
QBluetoothUuid uuid = QBluetoothUuid(QUuid(uuidString));
|
|
m_uuids.append(uuid);
|
|
}
|
|
} else if (propertyName == "Paired") {
|
|
setPairedInternally(properties.value(propertyName).toBool());
|
|
} else if (propertyName == "Connected") {
|
|
setConnectedInternally(properties.value(propertyName).toBool());
|
|
} else if (propertyName == "Trusted") {
|
|
setTrustedInternally(properties.value(propertyName).toBool());
|
|
} else if (propertyName == "Blocked") {
|
|
setBlockedInternally(properties.value(propertyName).toBool());
|
|
} else if (propertyName == "LegacyPairing") {
|
|
m_legacyPairing = properties.value(propertyName).toBool();
|
|
} else if (propertyName == "ServicesResolved") {
|
|
setServicesResolvedInternally(properties.value(propertyName).toBool());
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
void BluetoothDevice::evaluateCurrentState()
|
|
{
|
|
if (!connected()) {
|
|
setStateInternally(Disconnected);
|
|
} else if (connected() && servicesResolved()) {
|
|
setStateInternally(Discovered);
|
|
}
|
|
}
|
|
|
|
void BluetoothDevice::addServiceInternally(const QDBusObjectPath &path, const QVariantMap &properties)
|
|
{
|
|
if (hasService(path))
|
|
return;
|
|
|
|
BluetoothGattService *service = new BluetoothGattService(path, properties, this);
|
|
m_services.append(service);
|
|
qCDebug(dcBluez()) << "[+]" << service;
|
|
}
|
|
|
|
bool BluetoothDevice::hasService(const QDBusObjectPath &path)
|
|
{
|
|
foreach (BluetoothGattService *service, m_services) {
|
|
if (service->m_path == path) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
BluetoothGattService *BluetoothDevice::getService(const QDBusObjectPath &path)
|
|
{
|
|
foreach (BluetoothGattService *service, m_services) {
|
|
if (service->m_path == path) {
|
|
return service;
|
|
}
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
void BluetoothDevice::setStateInternally(const BluetoothDevice::State &state)
|
|
{
|
|
if (m_state != state) {
|
|
m_state = state;
|
|
emit stateChanged(m_state);
|
|
}
|
|
}
|
|
|
|
void BluetoothDevice::setAliasInternally(const QString &alias)
|
|
{
|
|
if (m_alias != alias) {
|
|
m_alias = alias;
|
|
emit aliasChanged(m_alias);
|
|
}
|
|
}
|
|
|
|
void BluetoothDevice::setRssiInternally(const qint16 &rssi)
|
|
{
|
|
if (m_rssi != rssi) {
|
|
m_rssi = rssi;
|
|
emit rssiChanged(m_rssi);
|
|
}
|
|
}
|
|
|
|
void BluetoothDevice::setTxPowerInternally(const qint16 &txPower)
|
|
{
|
|
if (m_txPower != txPower) {
|
|
m_txPower = txPower;
|
|
emit txPowerChanged(m_txPower);
|
|
}
|
|
}
|
|
|
|
void BluetoothDevice::setPairedInternally(const bool &paired)
|
|
{
|
|
if (m_paired != paired) {
|
|
m_paired = paired;
|
|
emit pairedChanged(m_paired);
|
|
|
|
// Paired, if services not resolved
|
|
if (!m_servicesResolved) {
|
|
setStateInternally(Discovering);
|
|
} else {
|
|
setStateInternally(Discovered);
|
|
}
|
|
}
|
|
}
|
|
|
|
void BluetoothDevice::setConnectedInternally(const bool &connected)
|
|
{
|
|
if (m_connected != connected) {
|
|
m_connected = connected;
|
|
emit connectedChanged(m_connected);
|
|
|
|
if (m_connected) {
|
|
setStateInternally(Connected);
|
|
|
|
if (!m_servicesResolved)
|
|
setStateInternally(Discovering);
|
|
|
|
} else {
|
|
setStateInternally(Disconnected);
|
|
}
|
|
}
|
|
}
|
|
|
|
void BluetoothDevice::setTrustedInternally(const bool &trusted)
|
|
{
|
|
if (m_trusted != trusted) {
|
|
m_trusted = trusted;
|
|
emit trustedChanged(m_trusted);
|
|
}
|
|
}
|
|
|
|
void BluetoothDevice::setBlockedInternally(const bool &blocked)
|
|
{
|
|
if (m_blocked != blocked) {
|
|
m_blocked = blocked;
|
|
emit blockedChanged(m_blocked);
|
|
}
|
|
}
|
|
|
|
void BluetoothDevice::setServicesResolvedInternally(const bool &servicesResolved)
|
|
{
|
|
if (m_servicesResolved != servicesResolved) {
|
|
m_servicesResolved = servicesResolved;
|
|
emit servicesResolvedChanged(m_servicesResolved);
|
|
|
|
if (m_servicesResolved)
|
|
setStateInternally(Discovered);
|
|
|
|
// Note: if unresolved, the thing is about to disconnect
|
|
}
|
|
}
|
|
|
|
void BluetoothDevice::onPropertiesChanged(const QString &interface, const QVariantMap &changedProperties, const QStringList &invalidatedProperties)
|
|
{
|
|
if (interface != orgBluezDevice1)
|
|
return;
|
|
|
|
qCDebug(dcBluez()) << "BluetoothDevice:" << m_name << m_address << "properties changed" << interface << changedProperties << invalidatedProperties;
|
|
processProperties(changedProperties);
|
|
}
|
|
|
|
void BluetoothDevice::onConnectDeviceFinished(QDBusPendingCallWatcher *call)
|
|
{
|
|
QDBusPendingReply<void> reply = *call;
|
|
if (reply.isError()) {
|
|
setStateInternally(Disconnected);
|
|
qCWarning(dcBluez()) << "Could not connect device" << m_address.toString() << reply.error().name() << reply.error().message();
|
|
}
|
|
|
|
call->deleteLater();
|
|
m_connectWatcher = nullptr;
|
|
}
|
|
|
|
void BluetoothDevice::onDisconnectDeviceFinished(QDBusPendingCallWatcher *call)
|
|
{
|
|
QDBusPendingReply<void> reply = *call;
|
|
if (reply.isError())
|
|
qCWarning(dcBluez()) << "Could not disconnect device" << m_address.toString() << reply.error().name() << reply.error().message();
|
|
|
|
evaluateCurrentState();
|
|
|
|
call->deleteLater();
|
|
m_disconnectWatcher = nullptr;
|
|
}
|
|
|
|
void BluetoothDevice::onPairingFinished(QDBusPendingCallWatcher *call)
|
|
{
|
|
QDBusPendingReply<void> reply = *call;
|
|
if (reply.isError())
|
|
qCWarning(dcBluez()) << "Could not pair device" << m_address.toString() << reply.error().name() << reply.error().message();
|
|
|
|
evaluateCurrentState();
|
|
|
|
call->deleteLater();
|
|
m_pairingWatcher = nullptr;
|
|
}
|
|
|
|
void BluetoothDevice::onCancelPairingFinished(QDBusPendingCallWatcher *call)
|
|
{
|
|
QDBusPendingReply<void> reply = *call;
|
|
if (reply.isError())
|
|
qCWarning(dcBluez()) << "Could not cancel pairing" << m_address.toString() << reply.error().name() << reply.error().message();
|
|
|
|
evaluateCurrentState();
|
|
|
|
call->deleteLater();
|
|
}
|
|
|
|
bool BluetoothDevice::connectDevice()
|
|
{
|
|
if (!m_deviceInterface->isValid()) {
|
|
qCWarning(dcBluez()) << "Invalid DBus thing interface for" << m_path.path();
|
|
return false;
|
|
}
|
|
|
|
if (connected() || state() == Connecting || m_connectWatcher)
|
|
return true;
|
|
|
|
setStateInternally(Connecting);
|
|
|
|
QDBusPendingCall connectingCall = m_deviceInterface->asyncCall("Connect");
|
|
m_connectWatcher = new QDBusPendingCallWatcher(connectingCall, this);
|
|
connect(m_connectWatcher, &QDBusPendingCallWatcher::finished, this, &BluetoothDevice::onConnectDeviceFinished);
|
|
return true;
|
|
}
|
|
|
|
bool BluetoothDevice::disconnectDevice()
|
|
{
|
|
if (!m_deviceInterface->isValid()) {
|
|
qCWarning(dcBluez()) << "Invalid DBus thing interface for" << m_path.path();
|
|
return false;
|
|
}
|
|
|
|
if (!connected() || state() == Disconnecting || m_disconnectWatcher)
|
|
return true;
|
|
|
|
setStateInternally(Disconnecting);
|
|
|
|
QDBusPendingCall disconnectingCall = m_deviceInterface->asyncCall("Disconnect");
|
|
m_disconnectWatcher = new QDBusPendingCallWatcher(disconnectingCall, this);
|
|
connect(m_disconnectWatcher, &QDBusPendingCallWatcher::finished, this, &BluetoothDevice::onDisconnectDeviceFinished);
|
|
return true;
|
|
}
|
|
|
|
bool BluetoothDevice::disconnectDeviceBlocking()
|
|
{
|
|
if (!m_deviceInterface->isValid()) {
|
|
qCWarning(dcBluez()) << "Invalid DBus thing interface for" << m_path.path();
|
|
return false;
|
|
}
|
|
|
|
if (!connected() || state() == Disconnecting)
|
|
return true;
|
|
|
|
qCWarning(dcBluez()) << "Disconnecting blocking" << this;
|
|
QDBusPendingReply<void> reply = m_deviceInterface->call("Disconnect");
|
|
reply.waitForFinished();
|
|
if(reply.isError()) {
|
|
qCWarning(dcBluez()) << reply.error();
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool BluetoothDevice::requestPairing()
|
|
{
|
|
if (!m_deviceInterface->isValid()) {
|
|
qCWarning(dcBluez()) << "Invalid DBus thing interface for" << m_path.path();
|
|
return false;
|
|
}
|
|
|
|
if (paired() || state() == Pairing || m_pairingWatcher)
|
|
return true;
|
|
|
|
setStateInternally(Pairing);
|
|
|
|
QDBusPendingCall pairCall = m_deviceInterface->asyncCall("Pair");
|
|
m_pairingWatcher = new QDBusPendingCallWatcher(pairCall, this);
|
|
connect(m_pairingWatcher, &QDBusPendingCallWatcher::finished, this, &BluetoothDevice::onPairingFinished);
|
|
return true;
|
|
}
|
|
|
|
bool BluetoothDevice::cancelPairingRequest()
|
|
{
|
|
if (!m_deviceInterface->isValid()) {
|
|
qCWarning(dcBluez()) << "Invalid DBus thing interface for" << m_path.path();
|
|
return false;
|
|
}
|
|
|
|
if (!paired() || state() == Unpairing)
|
|
return true;
|
|
|
|
setStateInternally(Unpairing);
|
|
|
|
QDBusPendingCall cancelPairingCall = m_deviceInterface->asyncCall("CancelPairing");
|
|
QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(cancelPairingCall, this);
|
|
connect(watcher, &QDBusPendingCallWatcher::finished, this, &BluetoothDevice::onPairingFinished);
|
|
return true;
|
|
}
|
|
|
|
QDebug operator<<(QDebug debug, BluetoothDevice *thing)
|
|
{
|
|
debug.noquote().nospace() << "BluetoothDevice(" << thing->name() << ", " << thing->address() << ") ";
|
|
return debug;
|
|
}
|