// SPDX-License-Identifier: LGPL-3.0-or-later /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Copyright (C) 2013 - 2024, nymea GmbH * Copyright (C) 2024 - 2025, chargebyte austria GmbH * * This file is part of libnymea-networkmanager. * * libnymea-networkmanager is free software: you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public License * as published by the Free Software Foundation, either version 3 * of the License, or (at your option) any later version. * * libnymea-networkmanager 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with libnymea-networkmanager. If not, see . * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ /*! \class BluetoothServer \brief Represents a bluetooth LE server for network-manager remote configuration. \inmodule nymea-networkmanager \ingroup networkmanager-bluetooth */ #include "bluetoothserver.h" #include "../networkmanager.h" #include "../networkmanagerutils.h" #include "bluetoothuuids.h" #include #include #include BluetoothServer::BluetoothServer(NetworkManager *networkManager) : QObject(networkManager), m_networkManager(networkManager) { } BluetoothServer::~BluetoothServer() { qCDebug(dcNetworkManagerBluetoothServer()) << "Destroy bluetooth server."; if (m_controller) m_controller->stopAdvertising(); if (m_localDevice) m_localDevice->setHostMode(QBluetoothLocalDevice::HostConnectable); } QString BluetoothServer::advertiseName() const { return m_advertiseName; } void BluetoothServer::setAdvertiseName(const QString &advertiseName, bool forceFullName) { m_advertiseName = advertiseName; m_forceFullName = forceFullName; } QString BluetoothServer::modelName() const { return m_modelName; } void BluetoothServer::setModelName(const QString &modelName) { m_modelName = modelName; } QString BluetoothServer::softwareVersion() const { return m_softwareVersion; } void BluetoothServer::setSoftwareVersion(const QString &softwareVersion) { m_softwareVersion = softwareVersion; } QString BluetoothServer::hardwareVersion() const { return m_hardwareVersion; } void BluetoothServer::setHardwareVersion(const QString &hardwareVersion) { m_hardwareVersion = hardwareVersion; } QString BluetoothServer::serialNumber() const { return m_serialNumber; } void BluetoothServer::setSerialNumber(const QString &serialNumber) { m_serialNumber = serialNumber; } bool BluetoothServer::running() const { return m_running; } bool BluetoothServer::connected() const { return m_connected; } QLowEnergyServiceData BluetoothServer::deviceInformationServiceData() { QLowEnergyServiceData serviceData; serviceData.setType(QLowEnergyServiceData::ServiceTypePrimary); #if QT_VERSION >= QT_VERSION_CHECK(6,0,0) serviceData.setUuid(QBluetoothUuid::ServiceClassUuid::DeviceInformation); #else serviceData.setUuid(QBluetoothUuid::DeviceInformation); #endif // Model number string 0x2a24 QLowEnergyCharacteristicData modelNumberCharData; #if QT_VERSION >= QT_VERSION_CHECK(6,0,0) modelNumberCharData.setUuid(QBluetoothUuid::CharacteristicType::ModelNumberString); #else modelNumberCharData.setUuid(QBluetoothUuid::ModelNumberString); #endif if (m_modelName.isEmpty()) { modelNumberCharData.setValue(QString("N.A.").toUtf8()); } else { modelNumberCharData.setValue(m_modelName.toUtf8()); } modelNumberCharData.setProperties(QLowEnergyCharacteristic::Read); serviceData.addCharacteristic(modelNumberCharData); // Serial number string 0x2a25 QLowEnergyCharacteristicData serialNumberCharData; #if QT_VERSION >= QT_VERSION_CHECK(6,0,0) serialNumberCharData.setUuid(QBluetoothUuid::CharacteristicType::SerialNumberString); #else serialNumberCharData.setUuid(QBluetoothUuid::SerialNumberString); #endif if (m_serialNumber.isNull()) { // Note: if no serialnumber specified use the system uuid from /etc/machine-id qCDebug(dcNetworkManagerBluetoothServer()) << "Serial number not specified. Using system uuid from /etc/machine-id as serialnumber."; m_serialNumber = readMachineId().toString(); } serialNumberCharData.setValue(m_serialNumber.toUtf8()); serialNumberCharData.setProperties(QLowEnergyCharacteristic::Read); serviceData.addCharacteristic(serialNumberCharData); // Firmware revision string 0x2a26 QLowEnergyCharacteristicData firmwareRevisionCharData; #if QT_VERSION >= QT_VERSION_CHECK(6,0,0) firmwareRevisionCharData.setUuid(QBluetoothUuid::CharacteristicType::FirmwareRevisionString); #else firmwareRevisionCharData.setUuid(QBluetoothUuid::FirmwareRevisionString); #endif firmwareRevisionCharData.setValue(QString(VERSION_STRING).toUtf8()); firmwareRevisionCharData.setProperties(QLowEnergyCharacteristic::Read); serviceData.addCharacteristic(firmwareRevisionCharData); // Hardware revision string 0x2a27 QLowEnergyCharacteristicData hardwareRevisionCharData; #if QT_VERSION >= QT_VERSION_CHECK(6,0,0) hardwareRevisionCharData.setUuid(QBluetoothUuid::CharacteristicType::HardwareRevisionString); #else hardwareRevisionCharData.setUuid(QBluetoothUuid::HardwareRevisionString); #endif hardwareRevisionCharData.setValue(m_hardwareVersion.toUtf8()); hardwareRevisionCharData.setProperties(QLowEnergyCharacteristic::Read); serviceData.addCharacteristic(hardwareRevisionCharData); // Software revision string 0x2a28 QLowEnergyCharacteristicData softwareRevisionCharData; #if QT_VERSION >= QT_VERSION_CHECK(6,0,0) softwareRevisionCharData.setUuid(QBluetoothUuid::CharacteristicType::SoftwareRevisionString); #else softwareRevisionCharData.setUuid(QBluetoothUuid::SoftwareRevisionString); #endif softwareRevisionCharData.setValue(m_softwareVersion.toUtf8()); softwareRevisionCharData.setProperties(QLowEnergyCharacteristic::Read); serviceData.addCharacteristic(softwareRevisionCharData); // Manufacturer name string 0x2a29 QLowEnergyCharacteristicData manufacturerNameCharData; #if QT_VERSION >= QT_VERSION_CHECK(6,0,0) manufacturerNameCharData.setUuid(QBluetoothUuid::CharacteristicType::ManufacturerNameString); #else manufacturerNameCharData.setUuid(QBluetoothUuid::ManufacturerNameString); #endif manufacturerNameCharData.setValue(QString("nymea GmbH").toUtf8()); manufacturerNameCharData.setProperties(QLowEnergyCharacteristic::Read); serviceData.addCharacteristic(manufacturerNameCharData); return serviceData; } QLowEnergyServiceData BluetoothServer::genericAccessServiceData() { QLowEnergyServiceData serviceData; serviceData.setType(QLowEnergyServiceData::ServiceTypePrimary); #if QT_VERSION >= QT_VERSION_CHECK(6,0,0) serviceData.setUuid(QBluetoothUuid::ServiceClassUuid::GenericAccess); #else serviceData.setUuid(QBluetoothUuid::GenericAccess); #endif // Device name 0x2a00 QLowEnergyCharacteristicData nameCharData; #if QT_VERSION >= QT_VERSION_CHECK(6,0,0) nameCharData.setUuid(QBluetoothUuid::CharacteristicType::DeviceName); #else nameCharData.setUuid(QBluetoothUuid::DeviceName); #endif if (m_advertiseName.isNull()) { qCWarning(dcNetworkManagerBluetoothServer()) << "Advertise name not specified. Using system host name as device name."; m_advertiseName = QSysInfo::machineHostName(); } nameCharData.setValue(m_advertiseName.toUtf8()); nameCharData.setProperties(QLowEnergyCharacteristic::Read); serviceData.addCharacteristic(nameCharData); // Appearance 0x2a01 QLowEnergyCharacteristicData appearanceCharData; #if QT_VERSION >= QT_VERSION_CHECK(6,0,0) appearanceCharData.setUuid(QBluetoothUuid::CharacteristicType::Appearance); #else appearanceCharData.setUuid(QBluetoothUuid::Appearance); #endif appearanceCharData.setValue(QByteArray(4, 0)); appearanceCharData.setProperties(QLowEnergyCharacteristic::Read); serviceData.addCharacteristic(appearanceCharData); // Peripheral Privacy Flag 0x2a02 QLowEnergyCharacteristicData privacyFlagCharData; #if QT_VERSION >= QT_VERSION_CHECK(6,0,0) privacyFlagCharData.setUuid(QBluetoothUuid::CharacteristicType::PeripheralPrivacyFlag); #else privacyFlagCharData.setUuid(QBluetoothUuid::PeripheralPrivacyFlag); #endif privacyFlagCharData.setValue(QByteArray(2, 0)); privacyFlagCharData.setProperties(QLowEnergyCharacteristic::Read | QLowEnergyCharacteristic::Write); serviceData.addCharacteristic(privacyFlagCharData); // Reconnection Address 0x2a03 QLowEnergyCharacteristicData reconnectionAddressCharData; #if QT_VERSION >= QT_VERSION_CHECK(6,0,0) reconnectionAddressCharData.setUuid(QBluetoothUuid::CharacteristicType::ReconnectionAddress); #else reconnectionAddressCharData.setUuid(QBluetoothUuid::ReconnectionAddress); #endif reconnectionAddressCharData.setValue(QByteArray()); reconnectionAddressCharData.setProperties(QLowEnergyCharacteristic::Write); serviceData.addCharacteristic(reconnectionAddressCharData); return serviceData; } QLowEnergyServiceData BluetoothServer::genericAttributeServiceData() { QLowEnergyServiceData serviceData; serviceData.setType(QLowEnergyServiceData::ServiceTypePrimary); #if QT_VERSION >= QT_VERSION_CHECK(6,0,0) serviceData.setUuid(QBluetoothUuid::ServiceClassUuid::GenericAttribute); #else serviceData.setUuid(QBluetoothUuid::GenericAttribute); #endif QLowEnergyCharacteristicData charData; #if QT_VERSION >= QT_VERSION_CHECK(6,0,0) charData.setUuid(QBluetoothUuid::CharacteristicType::ServiceChanged); #else charData.setUuid(QBluetoothUuid::ServiceChanged); #endif charData.setProperties(QLowEnergyCharacteristic::Indicate); serviceData.addCharacteristic(charData); return serviceData; } void BluetoothServer::setRunning(bool running) { if (m_running == running) return; qCDebug(dcNetworkManagerBluetoothServer()) << "Set running" << running; m_running = running; emit runningChanged(m_running); } void BluetoothServer::setConnected(bool connected) { if (m_connected == connected) return; qCDebug(dcNetworkManagerBluetoothServer()) << "Set connected" << connected; m_connected = connected; emit connectedChanged(m_connected); } QUuid BluetoothServer::readMachineId() { QUuid systemUuid; QFile systemUuidFile("/etc/machine-id"); if (systemUuidFile.open(QFile::ReadOnly)) { QString tmpId = QString::fromLatin1(systemUuidFile.readAll()).trimmed(); tmpId.insert(8, "-"); tmpId.insert(13, "-"); tmpId.insert(18, "-"); tmpId.insert(23, "-"); systemUuid = QUuid(tmpId); } else { qCWarning(dcNetworkManagerBluetoothServer()) << "Failed to open /etc/machine-id for reading the system uuid as device information serialnumber."; } systemUuidFile.close(); return systemUuid; } void BluetoothServer::onHostModeStateChanged(const QBluetoothLocalDevice::HostMode mode) { switch (mode) { case QBluetoothLocalDevice::HostConnectable: qCDebug(dcNetworkManagerBluetoothServer()) << "Bluetooth host in connectable mode."; break; case QBluetoothLocalDevice::HostDiscoverable: qCDebug(dcNetworkManagerBluetoothServer()) << "Bluetooth host in discoverable mode."; break; case QBluetoothLocalDevice::HostPoweredOff: qCDebug(dcNetworkManagerBluetoothServer()) << "Bluetooth host in power off mode."; setRunning(false); break; case QBluetoothLocalDevice::HostDiscoverableLimitedInquiry: qCDebug(dcNetworkManagerBluetoothServer()) << "Bluetooth host in discoverable limited inquiry mode."; break; } } void BluetoothServer::onDeviceConnected(const QBluetoothAddress &address) { qCDebug(dcNetworkManagerBluetoothServer()) << "Device connected" << address.toString(); } void BluetoothServer::onDeviceDisconnected(const QBluetoothAddress &address) { qCDebug(dcNetworkManagerBluetoothServer()) << "Device disconnected" << address.toString(); setConnected(false); } void BluetoothServer::onError(QLowEnergyController::Error error) { qCWarning(dcNetworkManagerBluetoothServer()) << "Bluetooth error occurred:" << error << m_controller->errorString(); } void BluetoothServer::onConnected() { qCDebug(dcNetworkManagerBluetoothServer()) << "Client connected" << m_controller->remoteName() << m_controller->remoteAddress(); setConnected(true); } void BluetoothServer::onDisconnected() { qCDebug(dcNetworkManagerBluetoothServer()) << "Client disconnected"; setConnected(false); stop(); } void BluetoothServer::onControllerStateChanged(QLowEnergyController::ControllerState state) { switch (state) { case QLowEnergyController::UnconnectedState: qCDebug(dcNetworkManagerBluetoothServer()) << "Controller state disonnected."; setConnected(false); setRunning(false); break; case QLowEnergyController::ConnectingState: qCDebug(dcNetworkManagerBluetoothServer()) << "Controller state connecting..."; setConnected(false); break; case QLowEnergyController::ConnectedState: qCDebug(dcNetworkManagerBluetoothServer()) << "Controller state connected." << m_controller->remoteName() << m_controller->remoteAddress(); setConnected(true); break; case QLowEnergyController::DiscoveringState: qCDebug(dcNetworkManagerBluetoothServer()) << "Controller state discovering..."; break; case QLowEnergyController::DiscoveredState: qCDebug(dcNetworkManagerBluetoothServer()) << "Controller state discovered."; break; case QLowEnergyController::ClosingState: qCDebug(dcNetworkManagerBluetoothServer()) << "Controller state closing..."; break; case QLowEnergyController::AdvertisingState: qCDebug(dcNetworkManagerBluetoothServer()) << "Controller state advertising..."; setRunning(true); break; } } void BluetoothServer::characteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &value) { qCDebug(dcNetworkManagerBluetoothServer()) << "Service characteristic changed" << characteristic.uuid() << value; } void BluetoothServer::characteristicRead(const QLowEnergyCharacteristic &characteristic, const QByteArray &value) { qCDebug(dcNetworkManagerBluetoothServer()) << "Service characteristic read" << characteristic.uuid() << value; } void BluetoothServer::characteristicWritten(const QLowEnergyCharacteristic &characteristic, const QByteArray &value) { qCDebug(dcNetworkManagerBluetoothServer()) << "Service characteristic written" << characteristic.uuid() << value; } void BluetoothServer::descriptorRead(const QLowEnergyDescriptor &descriptor, const QByteArray &value) { qCDebug(dcNetworkManagerBluetoothServer()) << "Descriptor read" << descriptor.uuid() << value; } void BluetoothServer::descriptorWritten(const QLowEnergyDescriptor &descriptor, const QByteArray &value) { qCDebug(dcNetworkManagerBluetoothServer()) << "Descriptor written" << descriptor.uuid() << value; } void BluetoothServer::serviceError(QLowEnergyService::ServiceError error) { QString errorString; switch (error) { case QLowEnergyService::NoError: errorString = "No error"; break; case QLowEnergyService::OperationError: errorString = "Operation error"; break; case QLowEnergyService::CharacteristicReadError: errorString = "Characteristic read error"; break; case QLowEnergyService::CharacteristicWriteError: errorString = "Characteristic write error"; break; case QLowEnergyService::DescriptorReadError: errorString = "Descriptor read error"; break; case QLowEnergyService::DescriptorWriteError: errorString = "Descriptor write error"; break; case QLowEnergyService::UnknownError: errorString = "Unknown error"; break; } qCWarning(dcNetworkManagerBluetoothServer()) << "Service error:" << errorString; } void BluetoothServer::start() { if (running()) { qCDebug(dcNetworkManagerBluetoothServer()) << "Start Bluetooth server called but the server is already running. Doing nothing."; return; } if (connected()) { qCDebug(dcNetworkManagerBluetoothServer()) << "Start Bluetooth server called but the server is running and a client is connected. Doing nothing."; return; } qCDebug(dcNetworkManagerBluetoothServer()) << "-------------------------------------"; qCDebug(dcNetworkManagerBluetoothServer()) << "Starting bluetooth server..."; qCDebug(dcNetworkManagerBluetoothServer()) << "-------------------------------------"; // Local bluetooth device m_localDevice = new QBluetoothLocalDevice(this); if (!m_localDevice->isValid()) { qCWarning(dcNetworkManagerBluetoothServer()) << "Local bluetooth device is not valid."; delete m_localDevice; m_localDevice = nullptr; return; } connect(m_localDevice, &QBluetoothLocalDevice::hostModeStateChanged, this, &BluetoothServer::onHostModeStateChanged); connect(m_localDevice, &QBluetoothLocalDevice::deviceConnected, this, &BluetoothServer::onDeviceConnected); connect(m_localDevice, &QBluetoothLocalDevice::deviceDisconnected, this, &BluetoothServer::onDeviceDisconnected); qCDebug(dcNetworkManagerBluetoothServer()) << "Local device" << m_localDevice->name() << m_localDevice->address().toString(); m_localDevice->setHostMode(QBluetoothLocalDevice::HostDiscoverable); m_localDevice->powerOn(); // Bluetooth low energy periperal controller m_controller = QLowEnergyController::createPeripheral(this); connect(m_controller, &QLowEnergyController::stateChanged, this, &BluetoothServer::onControllerStateChanged); connect(m_controller, &QLowEnergyController::connected, this, &BluetoothServer::onConnected); connect(m_controller, &QLowEnergyController::disconnected, this, &BluetoothServer::onDisconnected); #if QT_VERSION >= QT_VERSION_CHECK(6,0,0) connect(m_controller, &QLowEnergyController::errorOccurred, this, &BluetoothServer::onError); #else connect(m_controller, static_cast (&QLowEnergyController::error), this, &BluetoothServer::onError); #endif // Note: https://www.bluetooth.com/specifications/gatt/services m_deviceInfoService = m_controller->addService(deviceInformationServiceData(), m_controller); m_genericAccessService = m_controller->addService(genericAccessServiceData(), m_controller); m_genericAttributeService = m_controller->addService(genericAttributeServiceData(), m_controller); // Create custom services m_networkService = new NetworkService(m_controller->addService(NetworkService::serviceData(m_networkManager), m_controller), m_networkManager, m_controller); m_wirelessService = new WirelessService(m_controller->addService(WirelessService::serviceData(m_networkManager), m_controller), m_networkManager, m_controller); QLowEnergyAdvertisingData advertisingData; advertisingData.setDiscoverability(QLowEnergyAdvertisingData::DiscoverabilityGeneral); // Setting the wireless service UUID to the advertise data. // Given that nymea-networkmanager doesn't have a registered service UUID (yet), we need // to write the full UUID which takes up most of the advertise data space. Because of // this, we can only fit 8 character device name and nothing else. // Registering a service UUID with the BT SIG would allow to use shortened uuids and leave // more space for device name, multiple uuids, or other info (in that case we'd likely also have manufacturer // data to add here). // Anyhow, for now, 8 characters device name it is with the single wireless service UUID. advertisingData.setServices({wirelessServiceUuid}); if (m_forceFullName || m_advertiseName.length() <= 8) { advertisingData.setLocalName(m_advertiseName); } else { qCWarning(dcNetworkManagerBluetoothServer()) << "Truncating local host name to" << m_advertiseName.left(8) << "(maximum length of 8 characters exceeded)"; advertisingData.setLocalName(m_advertiseName.left(8)); } // Note: start advertising in 100 ms interval, this makes the device better discoverable on certain phones QLowEnergyAdvertisingParameters advertisingParameters; advertisingParameters.setInterval(100, 100); qCDebug(dcNetworkManagerBluetoothServer()) << "Start advertising" << m_advertiseName << m_localDevice->address().toString(); m_controller->startAdvertising(advertisingParameters, advertisingData, advertisingData); // Note: setRunning(true) will be called when the service is really advertising, see onControllerStateChanged() } void BluetoothServer::stop() { // Prevent printing the stop message twice in case of different shutdown reasons if (!m_controller && !m_localDevice) return; qCDebug(dcNetworkManagerBluetoothServer()) << "-------------------------------------"; qCDebug(dcNetworkManagerBluetoothServer()) << "Stopping bluetooth server."; qCDebug(dcNetworkManagerBluetoothServer()) << "-------------------------------------"; if (m_controller) { qCDebug(dcNetworkManagerBluetoothServer()) << "Stop advertising."; m_controller->stopAdvertising(); m_controller->deleteLater(); m_controller = nullptr; } if (m_localDevice) { qCDebug(dcNetworkManagerBluetoothServer()) << "Set host mode to connectable."; m_localDevice->setHostMode(QBluetoothLocalDevice::HostConnectable); m_localDevice->deleteLater(); m_localDevice = nullptr; } // Let the events set the state connected and running }