diff --git a/src/Comms/BluetoothLink.cc b/src/Comms/BluetoothLink.cc index 2fa9c4efbc0..eecf846d291 100644 --- a/src/Comms/BluetoothLink.cc +++ b/src/Comms/BluetoothLink.cc @@ -9,379 +9,1790 @@ #include "BluetoothLink.h" -#include "DeviceInfo.h" -#include "QGCLoggingCategory.h" - +#include +#include #include #include #include +#include + +#include "QGCLoggingCategory.h" + +QGC_LOGGING_CATEGORY(BluetoothLinkLog, "Comms.BluetoothLink") + +/*===========================================================================*/ + +BluetoothConfiguration::BluetoothConfiguration(const QString &name, QObject *parent) + : LinkConfiguration(name, parent) +{ + qCDebug(BluetoothLinkLog) << this; + + _initDeviceDiscoveryAgent(); +} + +BluetoothConfiguration::BluetoothConfiguration(const BluetoothConfiguration *copy, QObject *parent) + : LinkConfiguration(copy, parent) + , _mode(copy->mode()) + , _device(copy->device()) + , _serviceUuid(copy->_serviceUuid) + , _readCharacteristicUuid(copy->readCharacteristicUuid()) + , _writeCharacteristicUuid(copy->writeCharacteristicUuid()) + , _connectedRssi(copy->connectedRssi()) +{ + qCDebug(BluetoothLinkLog) << this; + + BluetoothConfiguration::copyFrom(copy); + _initDeviceDiscoveryAgent(); +} + +BluetoothConfiguration::~BluetoothConfiguration() +{ + stopScan(); + + qCDebug(BluetoothLinkLog) << this; +} + +bool BluetoothConfiguration::_createLocalDevice(const QBluetoothAddress &address) +{ + // Delete old local device if exists + if (_localDevice) { + _localDevice->deleteLater(); + _localDevice = nullptr; + } + + // Create new local device with specified adapter + _localDevice = new QBluetoothLocalDevice(address, this); + + if (!_localDevice->isValid()) { + qCWarning(BluetoothLinkLog) << "Failed to initialize Bluetooth adapter:" << address.toString(); + _localDevice->deleteLater(); + _localDevice = nullptr; + return false; + } + + // Connect all signals + _connectLocalDeviceSignals(); + + qCDebug(BluetoothLinkLog) << "Initialized Bluetooth adapter:" << _localDevice->name() << _localDevice->address().toString(); + emit adapterStateChanged(); + return true; +} + +void BluetoothConfiguration::_connectLocalDeviceSignals() +{ + if (!_localDevice || !_localDevice->isValid()) { + return; + } + + // Monitor adapter host mode changes (powered on/off, discoverable, etc.) + (void) connect(_localDevice.data(), &QBluetoothLocalDevice::hostModeStateChanged, + this, &BluetoothConfiguration::_onHostModeStateChanged); + + // Monitor when devices connect/disconnect from the local adapter + (void) connect(_localDevice.data(), &QBluetoothLocalDevice::deviceConnected, + this, &BluetoothConfiguration::_onDeviceConnected); + (void) connect(_localDevice.data(), &QBluetoothLocalDevice::deviceDisconnected, + this, &BluetoothConfiguration::_onDeviceDisconnected); + + // Monitor pairing events for Classic Bluetooth + (void) connect(_localDevice.data(), &QBluetoothLocalDevice::pairingFinished, + this, &BluetoothConfiguration::_onPairingFinished); + + // Monitor local device errors + (void) connect(_localDevice.data(), &QBluetoothLocalDevice::errorOccurred, + this, &BluetoothConfiguration::_onLocalDeviceErrorOccurred); +} + +void BluetoothConfiguration::_initDeviceDiscoveryAgent() +{ + if (!_deviceDiscoveryAgent) { + _deviceDiscoveryAgent = new QBluetoothDeviceDiscoveryAgent(this); + } + + // Create local device instance for adapter state monitoring/power control + if (!_localDevice) { + const QList hosts = QBluetoothLocalDevice::allDevices(); + if (!hosts.isEmpty()) { + _createLocalDevice(hosts.first().address()); + } + } + + // Configure discovery timeout for BLE + _deviceDiscoveryAgent->setLowEnergyDiscoveryTimeout(15000); + + (void) connect(_deviceDiscoveryAgent.data(), &QBluetoothDeviceDiscoveryAgent::deviceDiscovered, + this, &BluetoothConfiguration::_deviceDiscovered); + (void) connect(_deviceDiscoveryAgent.data(), &QBluetoothDeviceDiscoveryAgent::canceled, + this, &BluetoothConfiguration::scanningChanged); + (void) connect(_deviceDiscoveryAgent.data(), &QBluetoothDeviceDiscoveryAgent::finished, + this, &BluetoothConfiguration::_onDiscoveryFinished); + (void) connect(_deviceDiscoveryAgent.data(), &QBluetoothDeviceDiscoveryAgent::errorOccurred, + this, &BluetoothConfiguration::_onDiscoveryErrorOccurred); + + // Receive RSSI/manufacturer/service updates during discovery + (void) connect(_deviceDiscoveryAgent.data(), &QBluetoothDeviceDiscoveryAgent::deviceUpdated, + this, &BluetoothConfiguration::_deviceUpdated); +} + +void BluetoothConfiguration::_deviceUpdated(const QBluetoothDeviceInfo &info, QBluetoothDeviceInfo::Fields updatedFields) +{ + qCDebug(BluetoothLinkLog) << "Device Updated:" << info.name() << "Fields:" << updatedFields; + + if (!info.isValid() || (updatedFields == QBluetoothDeviceInfo::Field::None)) { + return; + } + + bool found = false; + for (QBluetoothDeviceInfo &dev : _deviceList) { + if (dev.address() == info.address()) { + found = true; + if (updatedFields & QBluetoothDeviceInfo::Field::RSSI) { + dev.setRssi(info.rssi()); + } + if (updatedFields & QBluetoothDeviceInfo::Field::ManufacturerData) { + const QMultiHash data = info.manufacturerData(); + for (quint16 id : info.manufacturerIds()) { + dev.setManufacturerData(id, data.value(id)); + } + } + if (updatedFields & QBluetoothDeviceInfo::Field::ServiceData) { + const QMultiHash data = info.serviceData(); + for (const QBluetoothUuid &uuid : info.serviceIds()) { + dev.setServiceData(uuid, data.value(uuid)); + } + } + if (updatedFields & QBluetoothDeviceInfo::Field::All) { + dev = info; + } + emit devicesModelChanged(); + if (_device.address() == dev.address()) { + emit selectedRssiChanged(); + } + break; + } + } + + // If device isn't in list yet, add it on update when we have a usable signal (BLE only) + if (!found) { + if (_mode == BluetoothMode::ModeLowEnergy) { + if (info.rssi() != 0 && + (info.coreConfigurations() & QBluetoothDeviceInfo::LowEnergyCoreConfiguration)) { + _deviceList.append(info); + _nameList.append(info.name()); + emit nameListChanged(); + emit devicesModelChanged(); + if (_device.address() == info.address()) { + emit selectedRssiChanged(); + } + } + } else { + if ((info.coreConfigurations() & QBluetoothDeviceInfo::BaseRateCoreConfiguration) || + (info.coreConfigurations() & QBluetoothDeviceInfo::BaseRateAndLowEnergyCoreConfiguration)) { + _deviceList.append(info); + _nameList.append(info.name()); + emit nameListChanged(); + emit devicesModelChanged(); + if (_device.address() == info.address()) { + emit selectedRssiChanged(); + } + } + } + } +} + +void BluetoothConfiguration::setMode(BluetoothMode mode) +{ + if (_mode != mode) { + // Abort any ongoing discovery when switching modes + stopScan(); + _mode = mode; + emit modeChanged(); + + // Clear device list when switching modes + _deviceList.clear(); + _nameList.clear(); + emit nameListChanged(); + emit devicesModelChanged(); + } +} + +void BluetoothConfiguration::copyFrom(const LinkConfiguration *source) +{ + LinkConfiguration::copyFrom(source); + + const BluetoothConfiguration *const bluetoothSource = qobject_cast(source); + Q_ASSERT(bluetoothSource); + + _mode = bluetoothSource->mode(); + _device = bluetoothSource->device(); + _serviceUuid = bluetoothSource->_serviceUuid; + _readCharacteristicUuid = bluetoothSource->readCharacteristicUuid(); + _writeCharacteristicUuid = bluetoothSource->writeCharacteristicUuid(); + _connectedRssi = bluetoothSource->connectedRssi(); + + emit modeChanged(); + emit deviceChanged(); + emit serviceUuidChanged(); + emit readUuidChanged(); + emit writeUuidChanged(); + emit connectedRssiChanged(); +} + +void BluetoothConfiguration::loadSettings(QSettings &settings, const QString &root) +{ + settings.beginGroup(root); + + _mode = static_cast(settings.value("mode", static_cast(BluetoothMode::ModeClassic)).toInt()); + + const QString deviceName = settings.value("deviceName").toString(); + const QBluetoothAddress address(settings.value("address").toString()); + + if (!deviceName.isEmpty() && !address.isNull()) { + _device = QBluetoothDeviceInfo(address, deviceName, 0); + } + + _serviceUuid = QBluetoothUuid(settings.value("serviceUuid", _serviceUuid.toString()).toString()); + _readCharacteristicUuid = QBluetoothUuid(settings.value("readCharUuid", _readCharacteristicUuid.toString()).toString()); + _writeCharacteristicUuid = QBluetoothUuid(settings.value("writeCharUuid", _writeCharacteristicUuid.toString()).toString()); + + settings.endGroup(); +} + +void BluetoothConfiguration::saveSettings(QSettings &settings, const QString &root) const +{ + settings.beginGroup(root); + + settings.setValue("mode", static_cast(_mode)); + settings.setValue("deviceName", _device.name()); + settings.setValue("address", _device.address().toString()); + settings.setValue("serviceUuid", _serviceUuid.toString()); + settings.setValue("readCharUuid", _readCharacteristicUuid.toString()); + settings.setValue("writeCharUuid", _writeCharacteristicUuid.toString()); + + settings.endGroup(); +} + +QString BluetoothConfiguration::settingsTitle() const +{ + if (isBluetoothAvailable()) { + return (_mode == BluetoothMode::ModeLowEnergy) ? tr("Bluetooth Low Energy Link Settings") : tr("Bluetooth Link Settings"); + } + return tr("Bluetooth Not Available"); +} + +bool BluetoothConfiguration::isBluetoothAvailable() +{ + const QList devices = QBluetoothLocalDevice::allDevices(); + return !devices.isEmpty(); +} + +QVariantList BluetoothConfiguration::devicesModel() const +{ + QVariantList model; + for (const QBluetoothDeviceInfo &info : _deviceList) { + QVariantMap device; + device["name"] = info.name(); + device["address"] = info.address().toString(); + // Only report RSSI if non-zero. Many platforms return 0 when unknown/unavailable. + if (info.rssi() != 0) { + device["rssi"] = info.rssi(); + } + model.append(device); + } + return model; +} + +void BluetoothConfiguration::setConnectedRssi(qint16 rssi) +{ + if (_connectedRssi != rssi) { + _connectedRssi = rssi; + emit connectedRssiChanged(); + emit selectedRssiChanged(); + } +} + +qint16 BluetoothConfiguration::selectedRssi() const +{ + const QBluetoothAddress selAddr = _device.address(); + if (!selAddr.isNull()) { + for (const QBluetoothDeviceInfo &dev : _deviceList) { + if (dev.address() == selAddr) { + return dev.rssi(); + } + } + } + return 0; +} + +void BluetoothConfiguration::setServiceUuid(const QString &uuid) +{ + const QBluetoothUuid newUuid(uuid); + if (_serviceUuid != newUuid) { + _serviceUuid = newUuid; + emit serviceUuidChanged(); + } +} + +void BluetoothConfiguration::setReadUuid(const QString &uuid) +{ + const QBluetoothUuid newUuid(uuid); + if (_readCharacteristicUuid != newUuid) { + _readCharacteristicUuid = newUuid; + emit readUuidChanged(); + } +} + +void BluetoothConfiguration::setWriteUuid(const QString &uuid) +{ + const QBluetoothUuid newUuid(uuid); + if (_writeCharacteristicUuid != newUuid) { + _writeCharacteristicUuid = newUuid; + emit writeUuidChanged(); + } +} + +void BluetoothConfiguration::startScan() +{ + if (!_deviceDiscoveryAgent) { + return; + } + + // Ensure permission before starting discovery + { + QBluetoothPermission permission; + permission.setCommunicationModes(QBluetoothPermission::Access); + const Qt::PermissionStatus status = QCoreApplication::instance()->checkPermission(permission); + if (status == Qt::PermissionStatus::Undetermined) { + QCoreApplication::instance()->requestPermission(permission, this, [this](const QPermission &perm) { + if (perm.status() == Qt::PermissionStatus::Granted) { + startScan(); + } else { + errorOccurred(tr("Bluetooth Permission Denied")); + } + }); + return; + } else if (status != Qt::PermissionStatus::Granted) { + emit errorOccurred(tr("Bluetooth Permission Denied")); + return; + } + } + + // Ensure adapter is powered on if available + if (_localDevice && _localDevice->isValid() && (_localDevice->hostMode() == QBluetoothLocalDevice::HostPoweredOff)) { + _localDevice->powerOn(); + } + + _deviceList.clear(); + _nameList.clear(); + emit nameListChanged(); + emit devicesModelChanged(); + + // Start discovery based on mode + if (_mode == BluetoothMode::ModeLowEnergy) { + _deviceDiscoveryAgent->start(QBluetoothDeviceDiscoveryAgent::LowEnergyMethod); + } else { + _deviceDiscoveryAgent->start(QBluetoothDeviceDiscoveryAgent::ClassicMethod); + } + + emit scanningChanged(); +} + +void BluetoothConfiguration::stopScan() +{ + if (_deviceDiscoveryAgent && scanning()) { + _deviceDiscoveryAgent->stop(); + } +} + +void BluetoothConfiguration::setDevice(const QString &name) +{ + for (const QBluetoothDeviceInfo &info : std::as_const(_deviceList)) { + if (info.name() == name) { + // Stop scanning once a device is selected + stopScan(); + _applySelectedDevice(info); + return; + } + } +} + +void BluetoothConfiguration::setDeviceByAddress(const QString &address) +{ + if (address.isEmpty()) { + return; + } + const QBluetoothAddress addr(address); + if (addr.isNull()) { + return; + } + + for (const QBluetoothDeviceInfo &info : std::as_const(_deviceList)) { + if (info.address() == addr) { + // Stop scanning once a device is selected + stopScan(); + _applySelectedDevice(info); + return; + } + } +} + +void BluetoothConfiguration::_applySelectedDevice(const QBluetoothDeviceInfo &info) +{ + _device = info; + + // For BLE devices, try to extract service/characteristic UUIDs + if (_mode == BluetoothMode::ModeLowEnergy) { + const QList serviceUuids = info.serviceUuids(); + if (!serviceUuids.isEmpty()) { + _serviceUuid = QBluetoothUuid(); + for (const QBluetoothUuid &uuid : serviceUuids) { + if ((uuid == NORDIC_UART_SERVICE) || (uuid == TI_SENSORTAG_SERVICE)) { + _serviceUuid = uuid; + + if (uuid == NORDIC_UART_SERVICE) { + _readCharacteristicUuid = NORDIC_UART_RX_CHAR; + _writeCharacteristicUuid = NORDIC_UART_TX_CHAR; + } else { + _readCharacteristicUuid = TI_SENSORTAG_CHAR; + _writeCharacteristicUuid = TI_SENSORTAG_CHAR; + } + break; + } + } + + // If no known service, use first available and allow auto-detect characteristics + if (_serviceUuid.isNull()) { + _serviceUuid = serviceUuids.first(); + _readCharacteristicUuid = QBluetoothUuid(); + _writeCharacteristicUuid = QBluetoothUuid(); + } + + emit serviceUuidChanged(); + emit readUuidChanged(); + emit writeUuidChanged(); + } + } + + emit deviceChanged(); +} + +bool BluetoothConfiguration::scanning() const +{ + return _deviceDiscoveryAgent && _deviceDiscoveryAgent->isActive(); +} + +void BluetoothConfiguration::_deviceDiscovered(const QBluetoothDeviceInfo &info) +{ + if (info.name().isEmpty() || !info.isValid()) { + return; + } + + // Filter out entries with unknown/invalid RSSI. These often come from cached results + // and cause turned-off devices to appear with no signal. + if (info.rssi() == 0) { + return; + } + + // Filter based on mode + if (_mode == BluetoothMode::ModeLowEnergy) { + if (!(info.coreConfigurations() & QBluetoothDeviceInfo::LowEnergyCoreConfiguration)) { + return; // Skip non-BLE devices + } + } else { + if (!(info.coreConfigurations() & QBluetoothDeviceInfo::BaseRateCoreConfiguration) && + !(info.coreConfigurations() & QBluetoothDeviceInfo::BaseRateAndLowEnergyCoreConfiguration)) { + return; // Skip non-classic devices + } + } + + // Check if device already in list by address + int existingIndex = -1; + for (int i = 0; i < _deviceList.size(); ++i) { + if (_deviceList[i].address() == info.address()) { + existingIndex = i; + break; + } + } + + if (existingIndex >= 0) { + // Update stored info to capture RSSI/name changes + _deviceList[existingIndex] = info; + emit devicesModelChanged(); + return; + } + + // BLE-only: collapse duplicates by common device name when private addresses rotate + if (_mode == BluetoothMode::ModeLowEnergy) { + int sameNameIndex = -1; + for (int i = 0; i < _deviceList.size(); ++i) { + if (_deviceList[i].name() == info.name()) { + sameNameIndex = i; + break; + } + } + if (sameNameIndex >= 0) { + const bool replace = (_deviceList[sameNameIndex].rssi() == 0 && info.rssi() != 0); + if (replace) { + _deviceList[sameNameIndex] = info; + emit devicesModelChanged(); + } + return; + } + } + + _deviceList.append(info); + _nameList.append(info.name()); + emit nameListChanged(); + emit devicesModelChanged(); + + qCDebug(BluetoothLinkLog) << ((_mode == BluetoothMode::ModeLowEnergy) ? "BLE" : "Classic") + << "device discovered:" << info.name() + << "Address:" << info.address().toString() + << "RSSI:" << info.rssi(); +} + +void BluetoothConfiguration::_onDiscoveryFinished() +{ + emit scanningChanged(); + _updateDeviceList(); +} + +void BluetoothConfiguration::_updateDeviceList() +{ + // Update name list from device list + _nameList.clear(); + for (const QBluetoothDeviceInfo &info : std::as_const(_deviceList)) { + _nameList.append(info.name()); + } + emit nameListChanged(); + emit devicesModelChanged(); +} + +void BluetoothConfiguration::_onDiscoveryErrorOccurred(QBluetoothDeviceDiscoveryAgent::Error error) +{ + QString errorString; + if (_deviceDiscoveryAgent) { + errorString = _deviceDiscoveryAgent->errorString(); + } else { + errorString = tr("Discovery error: %1").arg(error); + } + + qCWarning(BluetoothLinkLog) << "Bluetooth discovery error:" << error << errorString; + emit errorOccurred(errorString); +} + +void BluetoothConfiguration::_onHostModeStateChanged(QBluetoothLocalDevice::HostMode mode) +{ + QString modeString; + switch (mode) { + case QBluetoothLocalDevice::HostPoweredOff: + modeString = tr("Powered Off"); + break; + case QBluetoothLocalDevice::HostConnectable: + modeString = tr("Connectable"); + break; + case QBluetoothLocalDevice::HostDiscoverable: + modeString = tr("Discoverable"); + break; + case QBluetoothLocalDevice::HostDiscoverableLimitedInquiry: + modeString = tr("Discoverable (Limited Inquiry)"); + break; + } + + qCDebug(BluetoothLinkLog) << "Bluetooth adapter mode changed to:" << modeString; + + // Notify UI of adapter state change + emit adapterStateChanged(); + + // If adapter is powered off during scanning, stop the scan + if ((mode == QBluetoothLocalDevice::HostPoweredOff) && scanning()) { + stopScan(); + emit errorOccurred(tr("Bluetooth adapter powered off")); + } +} + +void BluetoothConfiguration::_onDeviceConnected(const QBluetoothAddress &address) +{ + qCDebug(BluetoothLinkLog) << "Device connected to adapter:" << address.toString(); + + // Update device info in list if it exists + for (const QBluetoothDeviceInfo &dev : std::as_const(_deviceList)) { + if (dev.address() == address) { + qCDebug(BluetoothLinkLog) << "Connected device:" << dev.name(); + break; + } + } +} + +void BluetoothConfiguration::_onDeviceDisconnected(const QBluetoothAddress &address) +{ + qCDebug(BluetoothLinkLog) << "Device disconnected from adapter:" << address.toString(); + + // Update device info in list if it exists + for (const QBluetoothDeviceInfo &dev : std::as_const(_deviceList)) { + if (dev.address() == address) { + qCDebug(BluetoothLinkLog) << "Disconnected device:" << dev.name(); + break; + } + } +} + +void BluetoothConfiguration::_onPairingFinished(const QBluetoothAddress &address, QBluetoothLocalDevice::Pairing pairing) +{ + QString pairingStatus; + switch (pairing) { + case QBluetoothLocalDevice::Unpaired: + pairingStatus = tr("Unpaired"); + break; + case QBluetoothLocalDevice::Paired: + pairingStatus = tr("Paired"); + break; + case QBluetoothLocalDevice::AuthorizedPaired: + pairingStatus = tr("Authorized Paired"); + break; + } + + qCDebug(BluetoothLinkLog) << "Pairing finished for device:" << address.toString() << "Status:" << pairingStatus; + + // Find device name if available + QString deviceName; + for (const QBluetoothDeviceInfo &dev : std::as_const(_deviceList)) { + if (dev.address() == address) { + deviceName = dev.name(); + break; + } + } + + // Notify user of pairing result + if (pairing == QBluetoothLocalDevice::Unpaired) { + const QString msg = deviceName.isEmpty() + ? tr("Device %1 unpaired").arg(address.toString()) + : tr("Device %1 (%2) unpaired").arg(deviceName, address.toString()); + qCInfo(BluetoothLinkLog) << msg; + } else { + const QString msg = deviceName.isEmpty() + ? tr("Device %1 paired successfully").arg(address.toString()) + : tr("Device %1 (%2) paired successfully").arg(deviceName, address.toString()); + qCInfo(BluetoothLinkLog) << msg; + } + + // Trigger UI update to reflect new pairing status + emit devicesModelChanged(); +} + +void BluetoothConfiguration::_onLocalDeviceErrorOccurred(QBluetoothLocalDevice::Error error) +{ + QString errorString; + switch (error) { + case QBluetoothLocalDevice::PairingError: + errorString = tr("Pairing Error"); + break; + case QBluetoothLocalDevice::MissingPermissionsError: + errorString = tr("Missing Bluetooth Permissions"); + break; + case QBluetoothLocalDevice::UnknownError: + default: + errorString = tr("Unknown Bluetooth Adapter Error"); + break; + } + + qCWarning(BluetoothLinkLog) << "Local Bluetooth device error:" << error << errorString; + emit errorOccurred(errorString); +} + +void BluetoothConfiguration::requestPairing(const QString &address) +{ + if (!_localDevice || !_localDevice->isValid()) { + emit errorOccurred(tr("Bluetooth adapter not available")); + return; + } + + if (_mode != BluetoothMode::ModeClassic) { + emit errorOccurred(tr("Pairing is only supported for Classic Bluetooth")); + return; + } + + const QBluetoothAddress addr(address); + if (addr.isNull()) { + emit errorOccurred(tr("Invalid Bluetooth address")); + return; + } + + qCDebug(BluetoothLinkLog) << "Requesting pairing with device:" << address; + _localDevice->requestPairing(addr, QBluetoothLocalDevice::Paired); +} + +void BluetoothConfiguration::removePairing(const QString &address) +{ + if (!_localDevice || !_localDevice->isValid()) { + emit errorOccurred(tr("Bluetooth adapter not available")); + return; + } + + if (_mode != BluetoothMode::ModeClassic) { + emit errorOccurred(tr("Unpairing is only supported for Classic Bluetooth")); + return; + } + + const QBluetoothAddress addr(address); + if (addr.isNull()) { + emit errorOccurred(tr("Invalid Bluetooth address")); + return; + } + + qCDebug(BluetoothLinkLog) << "Removing pairing with device:" << address; + _localDevice->requestPairing(addr, QBluetoothLocalDevice::Unpaired); +} + +QString BluetoothConfiguration::getPairingStatus(const QString &address) const +{ + if (!_localDevice || !_localDevice->isValid()) { + return tr("Adapter unavailable"); + } + + if (_mode != BluetoothMode::ModeClassic) { + return tr("N/A (BLE mode)"); + } + + const QBluetoothAddress addr(address); + if (addr.isNull()) { + return tr("Invalid address"); + } + + const QBluetoothLocalDevice::Pairing pairingStatus = _localDevice->pairingStatus(addr); + switch (pairingStatus) { + case QBluetoothLocalDevice::Unpaired: + return tr("Unpaired"); + case QBluetoothLocalDevice::Paired: + return tr("Paired"); + case QBluetoothLocalDevice::AuthorizedPaired: + return tr("Authorized Paired"); + default: + return tr("Unknown"); + } +} + +bool BluetoothConfiguration::isAdapterAvailable() const +{ + return _localDevice && _localDevice->isValid(); +} + +QString BluetoothConfiguration::getAdapterAddress() const +{ + if (!_localDevice || !_localDevice->isValid()) { + return QString(); + } + return _localDevice->address().toString(); +} + +QString BluetoothConfiguration::getAdapterName() const +{ + if (!_localDevice || !_localDevice->isValid()) { + return QString(); + } + return _localDevice->name(); +} + +bool BluetoothConfiguration::isAdapterPoweredOn() const +{ + if (!_localDevice || !_localDevice->isValid()) { + return false; + } + return _localDevice->hostMode() != QBluetoothLocalDevice::HostPoweredOff; +} + +QVariantList BluetoothConfiguration::getAllPairedDevices() const +{ + QVariantList pairedDevices; + + if (!_localDevice || !_localDevice->isValid()) { + return pairedDevices; + } + + const QList pairedAddresses = _localDevice->connectedDevices(); + + // Get all devices from the local device's paired list + for (const QBluetoothDeviceInfo &dev : std::as_const(_deviceList)) { + const QBluetoothLocalDevice::Pairing pairingStatus = _localDevice->pairingStatus(dev.address()); + if (pairingStatus != QBluetoothLocalDevice::Unpaired) { + QVariantMap device; + device["name"] = dev.name(); + device["address"] = dev.address().toString(); + device["paired"] = true; + + // Add pairing status + if (pairingStatus == QBluetoothLocalDevice::AuthorizedPaired) { + device["pairingStatus"] = tr("Authorized"); + } else { + device["pairingStatus"] = tr("Paired"); + } + + // Check if currently connected + device["connected"] = pairedAddresses.contains(dev.address()); + + if (dev.rssi() != 0) { + device["rssi"] = dev.rssi(); + } + + pairedDevices.append(device); + } + } + + return pairedDevices; +} + +QVariantList BluetoothConfiguration::getConnectedDevices() const +{ + QVariantList connectedDevices; + + if (!_localDevice || !_localDevice->isValid()) { + return connectedDevices; + } + + const QList connectedAddresses = _localDevice->connectedDevices(); + + for (const QBluetoothAddress &addr : connectedAddresses) { + QVariantMap device; + device["address"] = addr.toString(); + device["connected"] = true; + + // Try to find device name from our device list + QString deviceName; + for (const QBluetoothDeviceInfo &dev : std::as_const(_deviceList)) { + if (dev.address() == addr) { + deviceName = dev.name(); + if (dev.rssi() != 0) { + device["rssi"] = dev.rssi(); + } + break; + } + } + + device["name"] = deviceName.isEmpty() ? tr("Unknown Device") : deviceName; + connectedDevices.append(device); + } + + return connectedDevices; +} + +void BluetoothConfiguration::powerOnAdapter() +{ + if (!_localDevice || !_localDevice->isValid()) { + emit errorOccurred(tr("Bluetooth adapter not available")); + return; + } + + qCDebug(BluetoothLinkLog) << "Powering on Bluetooth adapter"; + _localDevice->powerOn(); +} + +void BluetoothConfiguration::powerOffAdapter() +{ + if (!_localDevice || !_localDevice->isValid()) { + emit errorOccurred(tr("Bluetooth adapter not available")); + return; + } + + // Stop any ongoing scan before powering off + if (scanning()) { + stopScan(); + } + + qCDebug(BluetoothLinkLog) << "Powering off Bluetooth adapter"; + _localDevice->setHostMode(QBluetoothLocalDevice::HostPoweredOff); +} + +void BluetoothConfiguration::setAdapterDiscoverable(bool discoverable) +{ + if (!_localDevice || !_localDevice->isValid()) { + emit errorOccurred(tr("Bluetooth adapter not available")); + return; + } + + if (discoverable) { + qCDebug(BluetoothLinkLog) << "Making Bluetooth adapter discoverable"; + _localDevice->setHostMode(QBluetoothLocalDevice::HostDiscoverable); + } else { + qCDebug(BluetoothLinkLog) << "Making Bluetooth adapter connectable only"; + _localDevice->setHostMode(QBluetoothLocalDevice::HostConnectable); + } +} + +QVariantList BluetoothConfiguration::getAllAvailableAdapters() const +{ + QVariantList adapters; + const QList hosts = QBluetoothLocalDevice::allDevices(); + + for (const QBluetoothHostInfo &host : hosts) { + QVariantMap adapter; + adapter["name"] = host.name(); + adapter["address"] = host.address().toString(); + + // Check if this is the currently selected adapter + if (_localDevice && _localDevice->isValid()) { + adapter["selected"] = (host.address() == _localDevice->address()); + } else { + adapter["selected"] = false; + } + + adapters.append(adapter); + } + + return adapters; +} + +void BluetoothConfiguration::selectAdapter(const QString &address) +{ + const QBluetoothAddress addr(address); + if (addr.isNull()) { + emit errorOccurred(tr("Invalid adapter address")); + return; + } + + // Check if the adapter exists + const QList hosts = QBluetoothLocalDevice::allDevices(); + bool found = false; + for (const QBluetoothHostInfo &host : hosts) { + if (host.address() == addr) { + found = true; + break; + } + } + + if (!found) { + emit errorOccurred(tr("Adapter not found")); + return; + } + + // Stop any ongoing scan before switching adapters + if (scanning()) { + stopScan(); + } + + // Create new local device with selected adapter (deletes old one if exists) + if (!_createLocalDevice(addr)) { + emit errorOccurred(tr("Failed to initialize adapter")); + } +} + +QString BluetoothConfiguration::getHostMode() const +{ + if (!_localDevice || !_localDevice->isValid()) { + return tr("Unavailable"); + } + + switch (_localDevice->hostMode()) { + case QBluetoothLocalDevice::HostPoweredOff: + return tr("Powered Off"); + case QBluetoothLocalDevice::HostConnectable: + return tr("Connectable"); + case QBluetoothLocalDevice::HostDiscoverable: + return tr("Discoverable"); + case QBluetoothLocalDevice::HostDiscoverableLimitedInquiry: + return tr("Discoverable (Limited)"); + default: + return tr("Unknown"); + } +} + +/*===========================================================================*/ + +BluetoothWorker::BluetoothWorker(const BluetoothConfiguration *config, QObject *parent) + : QObject(parent) + , _config(config) + , _reconnectTimer(new QTimer(this)) +{ + qCDebug(BluetoothLinkLog) << this; + + _reconnectTimer->setInterval(5000); + _reconnectTimer->setSingleShot(true); + (void) connect(_reconnectTimer.data(), &QTimer::timeout, this, &BluetoothWorker::_reconnectTimeout); + + // Periodic RSSI polling for BLE connections + _rssiTimer = new QTimer(this); + _rssiTimer->setInterval(3000); + _rssiTimer->setSingleShot(false); + // Connect once; start/stop occurs on connect/disconnect + (void) connect(_rssiTimer.data(), &QTimer::timeout, this, [this]() { + if (_controller && (_controller->state() == QLowEnergyController::ConnectedState)) { + _controller->readRssi(); + } + }); +} + +BluetoothWorker::~BluetoothWorker() +{ + _intentionalDisconnect = true; + disconnectLink(); + qCDebug(BluetoothLinkLog) << this; +} + +bool BluetoothWorker::isConnected() const +{ + if (_config->mode() == BluetoothConfiguration::BluetoothMode::ModeLowEnergy) { + return ((_controller && (_controller->state() == QLowEnergyController::ConnectedState)) && + (_service && (_service->state() == QLowEnergyService::RemoteServiceDiscovered)) && + _writeCharacteristic.isValid()); + } else { + return (_socket && _socket->isOpen() && + (_socket->state() == QBluetoothSocket::SocketState::ConnectedState)); + } +} + +void BluetoothWorker::setupConnection() +{ + if (_config->mode() == BluetoothConfiguration::BluetoothMode::ModeLowEnergy) { + _setupBleController(); + } else { + _setupClassicSocket(); + } +} + +void BluetoothWorker::_setupClassicSocket() +{ + Q_ASSERT(!_socket); + _socket = new QBluetoothSocket(QBluetoothServiceInfo::RfcommProtocol, this); + + (void) connect(_socket.data(), &QBluetoothSocket::connected, this, &BluetoothWorker::_onSocketConnected); + (void) connect(_socket.data(), &QBluetoothSocket::disconnected, this, &BluetoothWorker::_onSocketDisconnected); + (void) connect(_socket.data(), &QBluetoothSocket::readyRead, this, &BluetoothWorker::_onSocketReadyRead); + (void) connect(_socket.data(), &QBluetoothSocket::errorOccurred, this, &BluetoothWorker::_onSocketErrorOccurred); + + if (BluetoothLinkLog().isDebugEnabled()) { + (void) connect(_socket.data(), &QBluetoothSocket::bytesWritten, this, &BluetoothWorker::_onSocketBytesWritten); + + // Capture this as context ensures auto-disconnect on destruction + (void) connect(_socket.data(), &QBluetoothSocket::stateChanged, this, + [this](QBluetoothSocket::SocketState state) { + qCDebug(BluetoothLinkLog) << "Bluetooth Socket State Changed:" << state; + }); + } +} + +void BluetoothWorker::_setupBleController() +{ + Q_ASSERT(!_controller); + + _controller = QLowEnergyController::createCentral(_config->device(), this); + + if (!_controller) { + emit errorOccurred(tr("Failed to create BLE controller")); + return; + } + + (void) connect(_controller.data(), &QLowEnergyController::connected, this, &BluetoothWorker::_onControllerConnected); + (void) connect(_controller.data(), &QLowEnergyController::disconnected, this, &BluetoothWorker::_onControllerDisconnected); + (void) connect(_controller.data(), &QLowEnergyController::errorOccurred, this, &BluetoothWorker::_onControllerErrorOccurred); + (void) connect(_controller.data(), &QLowEnergyController::serviceDiscovered, this, &BluetoothWorker::_onServiceDiscovered); + (void) connect(_controller.data(), &QLowEnergyController::discoveryFinished, this, &BluetoothWorker::_onServiceDiscoveryFinished); + + // Capture this; receiver context auto-disconnects safely + (void) connect(_controller.data(), &QLowEnergyController::mtuChanged, this, + [this](int mtu) { + _mtu = mtu; + qCDebug(BluetoothLinkLog) << "MTU changed to:" << mtu; + }); + (void) connect(_controller.data(), &QLowEnergyController::rssiRead, this, + [this](qint16 rssi) { + _rssi = rssi; + emit rssiUpdated(rssi); + }); + + if (BluetoothLinkLog().isDebugEnabled()) { + (void) connect(_controller.data(), &QLowEnergyController::stateChanged, this, + [](QLowEnergyController::ControllerState state) { + qCDebug(BluetoothLinkLog) << "BLE Controller State Changed:" << state; + }); + + (void) connect(_controller.data(), &QLowEnergyController::connectionUpdated, this, + [](const QLowEnergyConnectionParameters ¶ms) { + qCDebug(BluetoothLinkLog) << "BLE connection updated: min(ms)" << params.minimumInterval() + << "max(ms)" << params.maximumInterval() + << "latency" << params.latency() + << "supervision(ms)" << params.supervisionTimeout(); + }); + } +} + +void BluetoothWorker::connectLink() +{ + _intentionalDisconnect = false; + + if (isConnected()) { + qCWarning(BluetoothLinkLog) << "Already connected to" << _config->device().name(); + return; + } + + qCDebug(BluetoothLinkLog) << "Attempting to connect to" << _config->device().name() + << "Mode:" << ((_config->mode() == BluetoothConfiguration::BluetoothMode::ModeLowEnergy) ? "BLE" : "Classic"); + + if (_config->mode() == BluetoothConfiguration::BluetoothMode::ModeLowEnergy) { + if (!_controller) { + _setupBleController(); + } + + if (!_controller) { + emit errorOccurred(tr("BLE controller not available")); + return; + } -QGC_LOGGING_CATEGORY(BluetoothLinkLog, "Comms.BluetoothLink") + _controller->setRemoteAddressType(QLowEnergyController::PublicAddress); + _controller->connectToDevice(); + } else { + if (!_socket) { + _setupClassicSocket(); + } -/*===========================================================================*/ + if (!_socket) { + emit errorOccurred(tr("Socket not available")); + return; + } -BluetoothConfiguration::BluetoothConfiguration(const QString &name, QObject *parent) - : LinkConfiguration(name, parent) - , _deviceDiscoveryAgent(new QBluetoothDeviceDiscoveryAgent(this)) + static const QBluetoothUuid uuid = QBluetoothUuid(QBluetoothUuid::ServiceClassUuid::SerialPort); + _socket->connectToService(_config->device().address(), uuid); + } +} + +void BluetoothWorker::disconnectLink() { - // qCDebug(BluetoothLinkLog) << Q_FUNC_INFO << this; + _intentionalDisconnect = true; - _initDeviceDiscoveryAgent(); + if (_reconnectTimer) { + _reconnectTimer->stop(); + _reconnectTimer->setInterval(5000); // Reset to initial interval + } + _reconnectAttempts = 0; // Reset reconnection attempts + + if (_config->mode() == BluetoothConfiguration::BluetoothMode::ModeLowEnergy) { + if (_service) { + _service->deleteLater(); + _service = nullptr; + } + + if (_controller) { + _controller->disconnectFromDevice(); + } + } else { + if (_socket) { + _socket->disconnectFromService(); + } + } } -BluetoothConfiguration::BluetoothConfiguration(const BluetoothConfiguration *copy, QObject *parent) - : LinkConfiguration(copy, parent) - , _device(copy->device()) - , _deviceDiscoveryAgent(new QBluetoothDeviceDiscoveryAgent(this)) +void BluetoothWorker::writeData(const QByteArray &data) { - // qCDebug(BluetoothLinkLog) << Q_FUNC_INFO << this; + if (data.isEmpty()) { + emit errorOccurred(tr("Data to Send is Empty")); + return; + } - BluetoothConfiguration::copyFrom(copy); + if (!isConnected()) { + emit errorOccurred(tr("Device is not connected")); + return; + } - _initDeviceDiscoveryAgent(); + if (_config->mode() == BluetoothConfiguration::BluetoothMode::ModeLowEnergy) { + _writeBleData(data); + } else { + _writeClassicData(data); + } } -BluetoothConfiguration::~BluetoothConfiguration() +void BluetoothWorker::_writeClassicData(const QByteArray &data) { - stopScan(); + if (!_socket || !_socket->isWritable()) { + emit errorOccurred(tr("Socket is not writable")); + return; + } + + qint64 totalBytesWritten = 0; + while (totalBytesWritten < data.size()) { + const qint64 bytesWritten = _socket->write(data.constData() + totalBytesWritten, + data.size() - totalBytesWritten); + if (bytesWritten == -1) { + emit errorOccurred(tr("Write failed: %1").arg(_socket->errorString())); + return; + } else if (bytesWritten == 0) { + emit errorOccurred(tr("Write returned 0 bytes")); + return; + } + totalBytesWritten += bytesWritten; + } - // qCDebug(BluetoothLinkLog) << Q_FUNC_INFO << this; + emit dataSent(data.first(totalBytesWritten)); } -void BluetoothConfiguration::_initDeviceDiscoveryAgent() +void BluetoothWorker::_writeBleData(const QByteArray &data) { - (void) connect(_deviceDiscoveryAgent, &QBluetoothDeviceDiscoveryAgent::deviceDiscovered, this, &BluetoothConfiguration::_deviceDiscovered); - (void) connect(_deviceDiscoveryAgent, &QBluetoothDeviceDiscoveryAgent::canceled, this, &BluetoothConfiguration::scanningChanged); - (void) connect(_deviceDiscoveryAgent, &QBluetoothDeviceDiscoveryAgent::finished, this, &BluetoothConfiguration::scanningChanged); - (void) connect(_deviceDiscoveryAgent, &QBluetoothDeviceDiscoveryAgent::errorOccurred, this, &BluetoothConfiguration::_onSocketErrorOccurred); + if (!_writeCharacteristic.isValid()) { + emit errorOccurred(tr("Write characteristic is not valid")); + return; + } - if (BluetoothLinkLog().isDebugEnabled()) { - (void) connect(_deviceDiscoveryAgent, &QBluetoothDeviceDiscoveryAgent::deviceUpdated, this, [this](const QBluetoothDeviceInfo &info, QBluetoothDeviceInfo::Fields updatedFields) { - qCDebug(BluetoothLinkLog) << "Device Updated"; - }); + if (!_service) { + emit errorOccurred(tr("BLE service not available")); + return; + } + + // BLE has packet size limitations (typically 20-512 bytes depending on MTU) + // ATT MTU payload is (MTU - 3). If we don't know MTU yet, assume minimum + const int effectiveMtu = (_mtu > 3) ? (_mtu - 3) : BLE_MIN_PACKET_SIZE; + const int packetSize = qBound(BLE_MIN_PACKET_SIZE, effectiveMtu, BLE_MAX_PACKET_SIZE); + + const bool useWriteWithoutResponse = (_writeCharacteristic.properties() & QLowEnergyCharacteristic::WriteNoResponse); + + // For WriteWithoutResponse, send all chunks immediately (no flow control needed) + if (useWriteWithoutResponse) { + for (int i = 0; i < data.size(); i += packetSize) { + const QByteArray chunk = data.mid(i, packetSize); + _service->writeCharacteristic(_writeCharacteristic, chunk, QLowEnergyService::WriteWithoutResponse); + } + emit dataSent(data); + } else { + // For WriteWithResponse, use queue-based approach to wait for confirmations + _currentBleWrite = data; + for (int i = 0; i < data.size(); i += packetSize) { + _bleWriteQueue.enqueue(data.mid(i, packetSize)); + } + + // Start sending the first chunk if not already writing + if (!_bleWriteInProgress) { + _processNextBleWrite(); + } } } -void BluetoothConfiguration::copyFrom(const LinkConfiguration *source) +void BluetoothWorker::_processNextBleWrite() { - Q_ASSERT(source); - LinkConfiguration::copyFrom(source); + if (_bleWriteQueue.isEmpty()) { + _bleWriteInProgress = false; + // All chunks sent successfully, emit the complete data + if (!_currentBleWrite.isEmpty()) { + emit dataSent(_currentBleWrite); + _currentBleWrite.clear(); + } + return; + } - const BluetoothConfiguration *const bluetoothSource = qobject_cast(source); - Q_ASSERT(bluetoothSource); + if (!_service || !_writeCharacteristic.isValid()) { + _clearBleWriteQueue(); + return; + } - _device = bluetoothSource->device(); - emit deviceChanged(); + _bleWriteInProgress = true; + const QByteArray chunk = _bleWriteQueue.dequeue(); + _service->writeCharacteristic(_writeCharacteristic, chunk); } -void BluetoothConfiguration::loadSettings(QSettings &settings, const QString &root) +void BluetoothWorker::_clearBleWriteQueue() { - settings.beginGroup(root); + _bleWriteQueue.clear(); + _currentBleWrite.clear(); + _bleWriteInProgress = false; +} - _device.name = settings.value("deviceName", _device.name).toString(); -#ifdef Q_OS_IOS - _device.uuid = QUuid(settings.value("uuid", _device.uuid.toString()).toString()); -#else - _device.address = QBluetoothAddress(settings.value("address", _device.address.toString()).toString()); -#endif +// Classic Bluetooth slots +void BluetoothWorker::_onSocketConnected() +{ + qCDebug(BluetoothLinkLog) << "Socket connected to device:" << _config->device().name(); + _connected = true; + _reconnectAttempts = 0; // Reset on successful connection + emit connected(); +} - settings.endGroup(); +void BluetoothWorker::_onSocketDisconnected() +{ + qCDebug(BluetoothLinkLog) << "Socket disconnected from device:" << _config->device().name(); + _connected = false; + emit disconnected(); + + if (!_intentionalDisconnect.load() && _socket && _reconnectTimer) { + qCDebug(BluetoothLinkLog) << "Starting reconnect timer"; + _reconnectTimer->start(); + } } -void BluetoothConfiguration::saveSettings(QSettings &settings, const QString &root) const +void BluetoothWorker::_onSocketReadyRead() { - settings.beginGroup(root); + if (!_socket) { + return; + } - settings.setValue("deviceName", _device.name); -#ifdef Q_OS_IOS - settings.setValue("uuid", _device.uuid.toString()); -#else - settings.setValue("address", _device.address.toString()); -#endif + const QByteArray data = _socket->readAll(); + if (!data.isEmpty()) { + emit dataReceived(data); + } +} - settings.endGroup(); +void BluetoothWorker::_onSocketBytesWritten(qint64 bytes) +{ + qCDebug(BluetoothLinkLog) << _config->device().name() << "Wrote" << bytes << "bytes"; } -QString BluetoothConfiguration::settingsTitle() const +void BluetoothWorker::_onSocketErrorOccurred(QBluetoothSocket::SocketError socketError) { - if (QGCDeviceInfo::isBluetoothAvailable()) { - return tr("Bluetooth Link Settings"); + QString errorString; + if (_socket) { + errorString = _socket->errorString(); + } else { + errorString = tr("Socket error: Null Socket"); } - return tr("Bluetooth Not Available"); + qCWarning(BluetoothLinkLog) << "Socket error:" << socketError << errorString; + emit errorOccurred(errorString); + + // If we never successfully connected, emit disconnected to reset UI state + if (!_connected.load()) { + qCDebug(BluetoothLinkLog) << "Connection attempt failed, emitting disconnected signal"; + emit disconnected(); + } + + // Attempt classic service discovery as a fallback only for service-related errors + // Don't try discovery for device availability issues, connection drops, or permission errors + if (_config && (_config->mode() == BluetoothConfiguration::BluetoothMode::ModeClassic)) { + if ((socketError == QBluetoothSocket::SocketError::ServiceNotFoundError) || + (socketError == QBluetoothSocket::SocketError::UnsupportedProtocolError)) { + qCDebug(BluetoothLinkLog) << "Service-related error, attempting fallback discovery"; + _startClassicServiceDiscovery(); + } + } } -void BluetoothConfiguration::startScan() +void BluetoothWorker::_startClassicServiceDiscovery() { - _deviceList.clear(); + if (!_config) { + return; + } - _nameList.clear(); - emit nameListChanged(); + if (_classicDiscovery && _classicDiscovery->isActive()) { + return; // already running + } - _deviceDiscoveryAgent->start(); - emit scanningChanged(); + if (!_classicDiscovery) { + _classicDiscovery = new QBluetoothServiceDiscoveryAgent(_config->device().address(), this); + (void) connect(_classicDiscovery.data(), &QBluetoothServiceDiscoveryAgent::serviceDiscovered, + this, &BluetoothWorker::_onClassicServiceDiscovered); + (void) connect(_classicDiscovery.data(), &QBluetoothServiceDiscoveryAgent::finished, + this, &BluetoothWorker::_onClassicServiceDiscoveryFinished); + (void) connect(_classicDiscovery.data(), &QBluetoothServiceDiscoveryAgent::canceled, + this, &BluetoothWorker::_onClassicServiceDiscoveryCanceled); + (void) connect(_classicDiscovery.data(), &QBluetoothServiceDiscoveryAgent::errorOccurred, + this, &BluetoothWorker::_onClassicServiceDiscoveryError); + } + + qCDebug(BluetoothLinkLog) << "Starting classic service discovery on" << _config->device().name(); + _classicDiscoveredService = QBluetoothServiceInfo(); + // Discover all available services; we'll pick a suitable RFCOMM service below + _classicDiscovery->start(QBluetoothServiceDiscoveryAgent::FullDiscovery); } -void BluetoothConfiguration::stopScan() const +void BluetoothWorker::_onClassicServiceDiscovered(const QBluetoothServiceInfo &serviceInfo) { - if (scanning()) { - _deviceDiscoveryAgent->stop(); + qCDebug(BluetoothLinkLog) << "Classic service discovered: UUIDs=" << serviceInfo.serviceClassUuids() + << "RFCOMM channel=" << serviceInfo.serverChannel() + << "L2CAP PSM=" << serviceInfo.protocolServiceMultiplexer(); + + // Prefer Serial Port Profile, otherwise accept any service exposing an RFCOMM server channel + const QList serviceUuids = serviceInfo.serviceClassUuids(); + const bool isSerial = serviceUuids.contains(QBluetoothUuid(QBluetoothUuid::ServiceClassUuid::SerialPort)); + const bool hasRfcommChannel = serviceInfo.serverChannel() > 0; + + if (isSerial || (hasRfcommChannel && !_classicDiscoveredService.isValid())) { + _classicDiscoveredService = serviceInfo; } } -void BluetoothConfiguration::setDevice(const QString &name) +void BluetoothWorker::_onClassicServiceDiscoveryFinished() { - for (const BluetoothData &data: _deviceList) { - if (data.name == name) { - _device = data; - emit deviceChanged(); - return; - } + if (!_classicDiscoveredService.isValid()) { + qCWarning(BluetoothLinkLog) << "No suitable classic service found"; + return; } + + if (!_socket) { + _setupClassicSocket(); + } + + qCDebug(BluetoothLinkLog) << "Connecting using discovered service"; + _socket->connectToService(_classicDiscoveredService); } -bool BluetoothConfiguration::scanning() const +void BluetoothWorker::_onClassicServiceDiscoveryCanceled() { - return _deviceDiscoveryAgent->isActive(); + qCDebug(BluetoothLinkLog) << "Classic service discovery canceled"; } -void BluetoothConfiguration::_deviceDiscovered(const QBluetoothDeviceInfo &info) +void BluetoothWorker::_onClassicServiceDiscoveryError(QBluetoothServiceDiscoveryAgent::Error error) +{ + const QString e = _classicDiscovery ? _classicDiscovery->errorString() : tr("Service discovery error: %1").arg(error); + qCWarning(BluetoothLinkLog) << e; + emit errorOccurred(e); +} + +// BLE slots +void BluetoothWorker::_onControllerConnected() { - if (!info.name().isEmpty() && info.isValid()) { - BluetoothData data; - data.name = info.name(); -#ifdef Q_OS_IOS - data.uuid = info.deviceUuid(); -#else - data.address = info.address(); -#endif + qCDebug(BluetoothLinkLog) << "BLE Controller connected to device:" << _config->device().name(); - if (!_deviceList.contains(data)) { - _deviceList.append(data); - _nameList.append(data.name); - emit nameListChanged(); + if (_controller) { + _controller->discoverServices(); + if (_rssiTimer) { + _rssiTimer->start(); } } } -void BluetoothConfiguration::_onSocketErrorOccurred(QBluetoothDeviceDiscoveryAgent::Error error) +void BluetoothWorker::_onControllerDisconnected() { - const QString errorString = _deviceDiscoveryAgent->errorString(); - qCWarning(BluetoothLinkLog) << "Bluetooth Configuration error:" << error << errorString; - emit errorOccurred(errorString); -} + qCDebug(BluetoothLinkLog) << "BLE Controller disconnected from device:" << _config->device().name(); -/*===========================================================================*/ + if (_rssiTimer) { + _rssiTimer->stop(); + _rssi = 0; + emit rssiUpdated(_rssi); + } -BluetoothWorker::BluetoothWorker(const BluetoothConfiguration *config, QObject *parent) - : QObject(parent) - , _config(config) + // Clear any pending write queue + _clearBleWriteQueue(); + + if (_service) { + _service->deleteLater(); + _service = nullptr; + } + + _readCharacteristic = QLowEnergyCharacteristic(); + _writeCharacteristic = QLowEnergyCharacteristic(); + _connected = false; + + emit disconnected(); + + if (!_intentionalDisconnect.load() && _controller && _reconnectTimer) { + qCDebug(BluetoothLinkLog) << "Starting reconnect timer"; + _reconnectTimer->start(); + } +} + +void BluetoothWorker::_onControllerErrorOccurred(QLowEnergyController::Error error) { - // qCDebug(BluetoothLinkLog) << Q_FUNC_INFO << this; + QString errorString; + if (_controller) { + errorString = _controller->errorString(); + } else { + errorString = tr("Controller error: %1").arg(error); + } + + qCWarning(BluetoothLinkLog) << "BLE Controller error:" << error << errorString; + emit errorOccurred(errorString); + + // If we never successfully connected, emit disconnected to reset UI state + if (!_connected.load()) { + qCDebug(BluetoothLinkLog) << "Connection attempt failed, emitting disconnected signal"; + emit disconnected(); + } } -BluetoothWorker::~BluetoothWorker() +void BluetoothWorker::_onServiceDiscovered(const QBluetoothUuid &uuid) { - disconnectLink(); + qCDebug(BluetoothLinkLog) << "Service discovered:" << uuid.toString(); - // qCDebug(BluetoothLinkLog) << Q_FUNC_INFO << this; + // Check if this is our target service + const QBluetoothUuid targetUuid = QBluetoothUuid(_config->serviceUuid()); + if ((uuid == targetUuid) || (targetUuid.isNull())) { + _setupBleService(); + } } -bool BluetoothWorker::isConnected() const +void BluetoothWorker::_onServiceDiscoveryFinished() { - return (_socket && _socket->isOpen() && (_socket->state() == QBluetoothSocket::SocketState::ConnectedState)); + qCDebug(BluetoothLinkLog) << "Service discovery finished"; + + if (!_service && _controller) { + // If no specific service was found, try to use the first available service + const QList services = _controller->services(); + if (!services.isEmpty()) { + qCDebug(BluetoothLinkLog) << "Using first available service"; + _setupBleService(); + } else { + emit errorOccurred(tr("No services found on BLE device")); + } + } } -void BluetoothWorker::setupSocket() +void BluetoothWorker::_setupBleService() { - Q_ASSERT(!_socket); - _socket = new QBluetoothSocket(QBluetoothServiceInfo::RfcommProtocol, this); + if (!_controller) { + emit errorOccurred(tr("Controller not available")); + return; + } -#ifdef Q_OS_IOS - Q_ASSERT(!_serviceDiscoveryAgent); - _serviceDiscoveryAgent = new QBluetoothServiceDiscoveryAgent(this); -#endif + QBluetoothUuid serviceUuid = QBluetoothUuid(_config->serviceUuid()); - (void) connect(_socket, &QBluetoothSocket::connected, this, &BluetoothWorker::_onSocketConnected); - (void) connect(_socket, &QBluetoothSocket::disconnected, this, &BluetoothWorker::_onSocketDisconnected); - (void) connect(_socket, &QBluetoothSocket::readyRead, this, &BluetoothWorker::_onSocketReadyRead); - (void) connect(_socket, &QBluetoothSocket::errorOccurred, this, &BluetoothWorker::_onSocketErrorOccurred); + if (serviceUuid.isNull()) { + const QList services = _controller->services(); + if (services.isEmpty()) { + emit errorOccurred(tr("No services available")); + return; + } -#ifdef Q_OS_IOS - (void) connect(_serviceDiscoveryAgent, &QBluetoothServiceDiscoveryAgent::serviceDiscovered, this, &BluetoothWorker::_serviceDiscovered); - (void) connect(_serviceDiscoveryAgent, &QBluetoothServiceDiscoveryAgent::finished, this, &BluetoothWorker::_discoveryFinished); - (void) connect(_serviceDiscoveryAgent, &QBluetoothServiceDiscoveryAgent::canceled, this, &BluetoothWorker::_discoveryFinished); - (void) connect(_serviceDiscoveryAgent, &QBluetoothServiceDiscoveryAgent::errorOccurred, this, &BluetoothWorker::_onServiceErrorOccurred); -#endif + // Prefer known UART services when auto-selecting + if (services.contains(BluetoothConfiguration::NORDIC_UART_SERVICE)) { + serviceUuid = BluetoothConfiguration::NORDIC_UART_SERVICE; + qCDebug(BluetoothLinkLog) << "Auto-selected Nordic UART service"; + } else if (services.contains(BluetoothConfiguration::TI_SENSORTAG_SERVICE)) { + serviceUuid = BluetoothConfiguration::TI_SENSORTAG_SERVICE; + qCDebug(BluetoothLinkLog) << "Auto-selected TI SensorTag service"; + } else { + // Use first available service - will validate characteristics later + serviceUuid = services.first(); + qCDebug(BluetoothLinkLog) << "Auto-selected first available service:" << serviceUuid.toString(); + } + } - if (BluetoothLinkLog().isDebugEnabled()) { - // (void) connect(_socket, &QBluetoothSocket::bytesWritten, this, &BluetoothWorker::_onSocketBytesWritten); + _service = _controller->createServiceObject(serviceUuid, this); - (void) QObject::connect(_socket, &QBluetoothSocket::stateChanged, this, [](QBluetoothSocket::SocketState state) { - qCDebug(BluetoothLinkLog) << "Bluetooth State Changed:" << state; - }); + if (!_service) { + emit errorOccurred(tr("Failed to create service object")); + return; } + + (void) connect(_service.data(), &QLowEnergyService::stateChanged, this, &BluetoothWorker::_onServiceStateChanged); + (void) connect(_service.data(), &QLowEnergyService::characteristicChanged, this, &BluetoothWorker::_onCharacteristicChanged); + (void) connect(_service.data(), &QLowEnergyService::characteristicRead, this, &BluetoothWorker::_onCharacteristicRead); + (void) connect(_service.data(), &QLowEnergyService::characteristicWritten, this, &BluetoothWorker::_onCharacteristicWritten); + (void) connect(_service.data(), &QLowEnergyService::descriptorRead, this, &BluetoothWorker::_onDescriptorRead); + (void) connect(_service.data(), &QLowEnergyService::descriptorWritten, this, &BluetoothWorker::_onDescriptorWritten); + (void) connect(_service.data(), &QLowEnergyService::errorOccurred, this, &BluetoothWorker::_onServiceError); + + _discoverServiceDetails(); } -void BluetoothWorker::connectLink() +void BluetoothWorker::_discoverServiceDetails() { - if (isConnected()) { - qCWarning(BluetoothLinkLog) << "Already connected to" << _config->device().name; + if (!_service) { return; } - qCDebug(BluetoothLinkLog) << "Attempting to connect to" << _config->device().name; - -#ifdef Q_OS_IOS - if (_serviceDiscoveryAgent && _serviceDiscoveryAgent->isActive()) { - _serviceDiscoveryAgent->start(); - } -#else - static constexpr QBluetoothUuid uuid = QBluetoothUuid(QBluetoothUuid::ServiceClassUuid::SerialPort); - _socket->connectToService(_config->device().address, uuid); -#endif + qCDebug(BluetoothLinkLog) << "Discovering service details"; + _service->discoverDetails(); } -void BluetoothWorker::disconnectLink() +void BluetoothWorker::_findCharacteristics() { - if (!isConnected()) { - qCWarning(BluetoothLinkLog) << "Already disconnected from device:" << _config->device().name; + if (!_service) { return; } - qCDebug(BluetoothLinkLog) << "Attempting to disconnect from device:" << _config->device().name; + const QList characteristics = _service->characteristics(); + + // Try to match by UUID first if specified + const QBluetoothUuid readUuid = _config->readCharacteristicUuid(); + const QBluetoothUuid writeUuid = _config->writeCharacteristicUuid(); + + for (const QLowEnergyCharacteristic &c : characteristics) { + // Match read characteristic + if (!readUuid.isNull() && (c.uuid() == readUuid)) { + _readCharacteristic = c; + qCDebug(BluetoothLinkLog) << "Read characteristic found by UUID:" << c.uuid().toString(); + } + // Match write characteristic + if (!writeUuid.isNull() && (c.uuid() == writeUuid)) { + _writeCharacteristic = c; + qCDebug(BluetoothLinkLog) << "Write characteristic found by UUID:" << c.uuid().toString(); + } + } -#ifdef Q_OS_IOS - if (_serviceDiscoveryAgent && _serviceDiscoveryAgent->isActive()) { - _serviceDiscoveryAgent->stop(); + // Auto-detect if not found by UUID + if (!_readCharacteristic.isValid() || !_writeCharacteristic.isValid()) { + for (const QLowEnergyCharacteristic &c : characteristics) { + // Auto-detect read characteristic + if (!_readCharacteristic.isValid() && + (c.properties() & (QLowEnergyCharacteristic::Read | QLowEnergyCharacteristic::Notify))) { + _readCharacteristic = c; + qCDebug(BluetoothLinkLog) << "Read characteristic auto-detected:" << c.uuid().toString(); + } + + // Auto-detect write characteristic + if (!_writeCharacteristic.isValid() && + (c.properties() & (QLowEnergyCharacteristic::Write | QLowEnergyCharacteristic::WriteNoResponse))) { + _writeCharacteristic = c; + qCDebug(BluetoothLinkLog) << "Write characteristic auto-detected:" << c.uuid().toString(); + } + } } -#endif - _socket->disconnectFromService(); } -void BluetoothWorker::writeData(const QByteArray &data) +void BluetoothWorker::_onServiceStateChanged(QLowEnergyService::ServiceState state) { - if (data.isEmpty()) { - emit errorOccurred(tr("Data to Send is Empty")); - return; - } + qCDebug(BluetoothLinkLog) << "Service state changed:" << state; - if (!isConnected()) { - emit errorOccurred(tr("Socket is not connected")); - return; + if (state == QLowEnergyService::RemoteServiceDiscovered) { + if (!_service) { + return; + } + + _findCharacteristics(); + + if (!_writeCharacteristic.isValid()) { + emit errorOccurred(tr("Write characteristic not found")); + return; + } + + // Enable notifications if available + if (_readCharacteristic.isValid()) { + _enableNotifications(); + } + + // Request optimized connection parameters for better throughput + // MTU is negotiated automatically by the BLE stack (we monitor via mtuChanged signal) + if (_controller) { + QLowEnergyConnectionParameters params; + // Request faster connection interval for lower latency (7.5ms - 15ms) + params.setIntervalRange(6, 12); // Units of 1.25ms + params.setLatency(0); // No latency + params.setSupervisionTimeout(500); // 5 seconds + _controller->requestConnectionUpdate(params); + } + + _connected = true; + _reconnectAttempts = 0; // Reset on successful connection + emit connected(); } +} - if (!_socket->isWritable()) { - emit errorOccurred(tr("Socket is not Writable")); +void BluetoothWorker::_enableNotifications() +{ + if (!_service || !_readCharacteristic.isValid()) { return; } - qint64 totalBytesWritten = 0; - while (totalBytesWritten < data.size()) { - const qint64 bytesWritten = _socket->write(data.constData() + totalBytesWritten, data.size() - totalBytesWritten); - if (bytesWritten == -1) { - emit errorOccurred(tr("Could Not Send Data - Write Failed: %1").arg(_socket->errorString())); - return; - } else if (bytesWritten == 0) { - emit errorOccurred(tr("Could Not Send Data - Write Returned 0 Bytes")); - return; + const QLowEnergyDescriptor notificationDescriptor = _readCharacteristic.descriptor( + QBluetoothUuid::DescriptorType::ClientCharacteristicConfiguration); + + if (notificationDescriptor.isValid()) { + // Enable notifications (0x0100) or indications (0x0200) based on characteristic properties + QByteArray value; + if (_readCharacteristic.properties() & QLowEnergyCharacteristic::Notify) { + value = QByteArray::fromHex("0100"); + qCDebug(BluetoothLinkLog) << "Enabling notifications for read characteristic"; + } else if (_readCharacteristic.properties() & QLowEnergyCharacteristic::Indicate) { + value = QByteArray::fromHex("0200"); + qCDebug(BluetoothLinkLog) << "Enabling indications for read characteristic"; } - totalBytesWritten += bytesWritten; - } - const QByteArray sent = data.first(totalBytesWritten); - emit dataSent(sent); + if (!value.isEmpty()) { + _service->writeDescriptor(notificationDescriptor, value); + } + } } -void BluetoothWorker::_onSocketConnected() +void BluetoothWorker::_onCharacteristicChanged(const QLowEnergyCharacteristic &characteristic, + const QByteArray &value) { - qCDebug(BluetoothLinkLog) << "Socket connected to device:" << _config->device().name; - emit connected(); + if ((characteristic.uuid() == _readCharacteristic.uuid()) && !value.isEmpty()) { + emit dataReceived(value); + } } -void BluetoothWorker::_onSocketDisconnected() +void BluetoothWorker::_onCharacteristicRead(const QLowEnergyCharacteristic &characteristic, + const QByteArray &value) { - qCDebug(BluetoothLinkLog) << "Socket disconnected from device:" << _config->device().name; - emit disconnected(); + qCDebug(BluetoothLinkLog) << "Characteristic read:" << characteristic.uuid().toString() + << "Value length:" << value.size(); } -void BluetoothWorker::_onSocketReadyRead() +void BluetoothWorker::_onCharacteristicWritten(const QLowEnergyCharacteristic &characteristic, + const QByteArray &value) { - const QByteArray data = _socket->readAll(); - if (!data.isEmpty()) { - // qCDebug(BluetoothLinkLog) << "_onSocketReadyRead:" << data.size(); - emit dataReceived(data); + Q_UNUSED(value) + + // Process next chunk in write queue if this was our write characteristic + if (characteristic.uuid() == _writeCharacteristic.uuid()) { + _processNextBleWrite(); } } -void BluetoothWorker::_onSocketBytesWritten(qint64 bytes) +void BluetoothWorker::_onDescriptorRead(const QLowEnergyDescriptor &descriptor, + const QByteArray &value) { - qCDebug(BluetoothLinkLog) << _config->device().name << "Wrote" << bytes << "bytes"; + qCDebug(BluetoothLinkLog) << "Descriptor read:" << descriptor.uuid().toString() + << "Value:" << value.toHex(); } -void BluetoothWorker::_onSocketErrorOccurred(QBluetoothSocket::SocketError socketError) +void BluetoothWorker::_onDescriptorWritten(const QLowEnergyDescriptor &descriptor, + const QByteArray &value) { - const QString errorString = _socket->errorString(); - qCWarning(BluetoothLinkLog) << "Socket error:" << socketError << errorString; - emit errorOccurred(errorString); + qCDebug(BluetoothLinkLog) << "Descriptor written:" << descriptor.uuid().toString() + << "Value:" << value.toHex(); } -#ifdef Q_OS_IOS -void BluetoothWorker::_onServiceErrorOccurred(QBluetoothServiceDiscoveryAgent::Error error) +void BluetoothWorker::_onServiceError(QLowEnergyService::ServiceError error) { - const QString errorString = _serviceDiscoveryAgent->errorString(); - qCWarning(BluetoothLinkLog) << "Socket error:" << error << errorString; + const QString errorString = tr("Service error: %1").arg(error); + qCWarning(BluetoothLinkLog) << "BLE Service error:" << error; emit errorOccurred(errorString); + + // If we never successfully connected, emit disconnected to reset UI state + if (!_connected.load()) { + qCDebug(BluetoothLinkLog) << "Connection attempt failed, emitting disconnected signal"; + emit disconnected(); + } + + if (!_intentionalDisconnect.load() && _reconnectTimer) { + _reconnectTimer->start(); + } } -void BluetoothWorker::_serviceDiscovered(const QBluetoothServiceInfo &info) +void BluetoothWorker::_reconnectTimeout() { - if (isConnected()) { - qCWarning(BluetoothLinkLog) << "Already connected to" << _config->device().name; + if (_intentionalDisconnect.load()) { return; } - if (info.device().name().isEmpty() || !info.isValid()) { + if (_reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) { + qCWarning(BluetoothLinkLog) << "Max reconnection attempts (" << MAX_RECONNECT_ATTEMPTS << ") reached. Giving up."; + emit errorOccurred(tr("Max reconnection attempts reached")); return; } - if ((_config->device().uuid == info.device().deviceUuid()) && (_config->device().name == info.device().name)) { - _socket->connectToService(info); - } -} + _reconnectAttempts++; + qCDebug(BluetoothLinkLog) << "Attempting to reconnect (attempt" << _reconnectAttempts << "of" << MAX_RECONNECT_ATTEMPTS << ")"; -void BluetoothWorker::_discoveryFinished() -{ - if (!isConnected()) { - emit errorOccurred(QStringLiteral("Discovery Error: Could Not Locate Device!")); + // Exponential backoff: 5s, 10s, 20s, 40s, ... (capped at 60s) + const int nextInterval = qMin(5000 * (1 << (_reconnectAttempts - 1)), 60000); + if (_reconnectTimer) { + _reconnectTimer->setInterval(nextInterval); } + + connectLink(); } -#endif /*===========================================================================*/ BluetoothLink::BluetoothLink(SharedLinkConfigurationPtr &config, QObject *parent) : LinkInterface(config, parent) - , _bluetoothConfig(qobject_cast(config.get())) + , _bluetoothConfig(qobject_cast(config.get())) , _worker(new BluetoothWorker(_bluetoothConfig)) , _workerThread(new QThread(this)) { - // qCDebug(BluetoothLinkLog) << Q_FUNC_INFO << this; + qCDebug(BluetoothLinkLog) << this; _checkPermission(); - _workerThread->setObjectName(QStringLiteral("Bluetooth_%1").arg(_bluetoothConfig->name())); + const QString threadName = (_bluetoothConfig->mode() == BluetoothConfiguration::BluetoothMode::ModeLowEnergy) + ? QStringLiteral("BLE_%1") : QStringLiteral("Bluetooth_%1"); + _workerThread->setObjectName(threadName.arg(_bluetoothConfig->name())); - _worker->moveToThread(_workerThread); + _worker->moveToThread(_workerThread.data()); - (void) connect(_workerThread, &QThread::started, _worker, &BluetoothWorker::setupSocket); - (void) connect(_workerThread, &QThread::finished, _worker, &QObject::deleteLater); + (void) connect(_workerThread.data(), &QThread::started, _worker.data(), &BluetoothWorker::setupConnection); + (void) connect(_workerThread.data(), &QThread::finished, _worker.data(), &QObject::deleteLater); - (void) connect(_worker, &BluetoothWorker::connected, this, &BluetoothLink::_onConnected, Qt::QueuedConnection); - (void) connect(_worker, &BluetoothWorker::disconnected, this, &BluetoothLink::_onDisconnected, Qt::QueuedConnection); - (void) connect(_worker, &BluetoothWorker::errorOccurred, this, &BluetoothLink::_onErrorOccurred, Qt::QueuedConnection); - (void) connect(_worker, &BluetoothWorker::dataReceived, this, &BluetoothLink::_onDataReceived, Qt::QueuedConnection); - (void) connect(_worker, &BluetoothWorker::dataSent, this, &BluetoothLink::_onDataSent, Qt::QueuedConnection); + (void) connect(_worker.data(), &BluetoothWorker::connected, this, &BluetoothLink::_onConnected, Qt::QueuedConnection); + (void) connect(_worker.data(), &BluetoothWorker::disconnected, this, &BluetoothLink::_onDisconnected, Qt::QueuedConnection); + (void) connect(_worker.data(), &BluetoothWorker::errorOccurred, this, &BluetoothLink::_onErrorOccurred, Qt::QueuedConnection); + (void) connect(_worker.data(), &BluetoothWorker::dataReceived, this, &BluetoothLink::_onDataReceived, Qt::QueuedConnection); + (void) connect(_worker.data(), &BluetoothWorker::dataSent, this, &BluetoothLink::_onDataSent, Qt::QueuedConnection); + (void) connect(_worker.data(), &BluetoothWorker::rssiUpdated, this, &BluetoothLink::_onRssiUpdated, Qt::QueuedConnection); (void) connect(_bluetoothConfig, &BluetoothConfiguration::errorOccurred, this, &BluetoothLink::_onErrorOccurred); @@ -392,27 +1803,42 @@ BluetoothLink::~BluetoothLink() { BluetoothLink::disconnect(); - _workerThread->quit(); - if (!_workerThread->wait()) { - qCWarning(BluetoothLinkLog) << "Failed to wait for Bluetooth Thread to close"; + if (_workerThread) { + _workerThread->quit(); + if (!_workerThread->wait(5000)) { + qCWarning(BluetoothLinkLog) << "Failed to wait for Bluetooth Thread to close"; + _workerThread->terminate(); + if (!_workerThread->wait(1000)) { + qCCritical(BluetoothLinkLog) << "Failed to terminate Bluetooth Thread"; + } + } } - // qCDebug(BluetoothLinkLog) << Q_FUNC_INFO << this; + qCDebug(BluetoothLinkLog) << this; } bool BluetoothLink::isConnected() const { - return _worker->isConnected(); + return _worker ? _worker->isConnected() : false; } bool BluetoothLink::_connect() { - return QMetaObject::invokeMethod(_worker, "connectLink", Qt::QueuedConnection); + if (!_worker) { + return false; + } + // Stop any ongoing discovery to avoid contention while connecting + if (_bluetoothConfig) { + _bluetoothConfig->stopScan(); + } + return QMetaObject::invokeMethod(_worker.data(), "connectLink", Qt::QueuedConnection); } void BluetoothLink::disconnect() { - (void) QMetaObject::invokeMethod(_worker, "disconnectLink", Qt::QueuedConnection); + if (_worker) { + (void) QMetaObject::invokeMethod(_worker.data(), "disconnectLink", Qt::QueuedConnection); + } } void BluetoothLink::_onConnected() @@ -427,8 +1853,14 @@ void BluetoothLink::_onDisconnected() void BluetoothLink::_onErrorOccurred(const QString &errorString) { + const QString linkType = (_bluetoothConfig->mode() == BluetoothConfiguration::BluetoothMode::ModeLowEnergy) + ? tr("Bluetooth Low Energy") : tr("Bluetooth"); + qCWarning(BluetoothLinkLog) << "Communication error:" << errorString; - emit communicationError(tr("Bluetooth Link Error"), tr("Link %1: (Device: %2) %3").arg(_bluetoothConfig->name(), _bluetoothConfig->device().name, errorString)); + emit communicationError(tr("%1 Link Error").arg(linkType), + tr("Link %1: (Device: %2) %3").arg(_bluetoothConfig->name(), + _bluetoothConfig->device().name(), + errorString)); } void BluetoothLink::_onDataReceived(const QByteArray &data) @@ -441,9 +1873,16 @@ void BluetoothLink::_onDataSent(const QByteArray &data) emit bytesSent(this, data); } -void BluetoothLink::_writeBytes(const QByteArray& bytes) +void BluetoothLink::_onRssiUpdated(qint16 rssi) { - (void) QMetaObject::invokeMethod(_worker, "writeData", Qt::QueuedConnection, Q_ARG(QByteArray, bytes)); + _bluetoothConfig->setConnectedRssi(rssi); +} + +void BluetoothLink::_writeBytes(const QByteArray &bytes) +{ + if (_worker) { + (void) QMetaObject::invokeMethod(_worker.data(), "writeData", Qt::QueuedConnection, Q_ARG(QByteArray, bytes)); + } } void BluetoothLink::_checkPermission() @@ -465,7 +1904,7 @@ void BluetoothLink::_handlePermissionStatus(Qt::PermissionStatus permissionStatu { if (permissionStatus != Qt::PermissionStatus::Granted) { qCWarning(BluetoothLinkLog) << "Bluetooth Permission Denied"; - _onErrorOccurred("Bluetooth Permission Denied"); + _onErrorOccurred(tr("Bluetooth Permission Denied")); _onDisconnected(); } else { qCDebug(BluetoothLinkLog) << "Bluetooth Permission Granted"; diff --git a/src/Comms/BluetoothLink.h b/src/Comms/BluetoothLink.h index f72d31bfa51..cdc50993239 100644 --- a/src/Comms/BluetoothLink.h +++ b/src/Comms/BluetoothLink.h @@ -9,18 +9,26 @@ #pragma once +#include + #include #include #include -#ifdef Q_OS_IOS -#include -#endif -#include #include -#include +#include +#include +#include +#include +#include +#include #include #include +#include +#include #include +#include +#include +#include #include "LinkConfiguration.h" #include "LinkInterface.h" @@ -31,71 +39,62 @@ Q_DECLARE_LOGGING_CATEGORY(BluetoothLinkLog) /*===========================================================================*/ -struct BluetoothData -{ - BluetoothData() = default; -#ifdef Q_OS_IOS - BluetoothData(const QString &name_, QBluetoothUuid uuid_) - : uuid(uuid_) -#else - BluetoothData(const QString &name_, QBluetoothAddress address_) - : address(address_) -#endif - , name(name_) - {} - - BluetoothData(const BluetoothData &other) - { - *this = other; - } - - bool operator==(const BluetoothData &other) const - { -#ifdef Q_OS_IOS - return ((name == other.name) && (uuid == other.uuid)); -#else - return ((name == other.name) && (address == other.address)); -#endif - } - - BluetoothData &operator=(const BluetoothData &other) - { - name = other.name; -#ifdef Q_OS_IOS - uuid = other.uuid; -#else - address = other.address; -#endif - return *this; - } - - QString name; -#ifdef Q_OS_IOS - QBluetoothUuid uuid; -#else - QBluetoothAddress address; -#endif -}; - -/*===========================================================================*/ - class BluetoothConfiguration : public LinkConfiguration { Q_OBJECT - - Q_PROPERTY(QString deviceName READ deviceName NOTIFY deviceChanged) - Q_PROPERTY(QString address READ address NOTIFY deviceChanged) - Q_PROPERTY(QStringList nameList READ nameList NOTIFY nameListChanged) - Q_PROPERTY(bool scanning READ scanning NOTIFY scanningChanged) + QML_ELEMENT + QML_UNCREATABLE("") + Q_PROPERTY(BluetoothMode mode READ mode WRITE setMode NOTIFY modeChanged) + Q_PROPERTY(QString deviceName READ deviceName NOTIFY deviceChanged) + Q_PROPERTY(QString address READ address NOTIFY deviceChanged) + Q_PROPERTY(QStringList nameList READ nameList NOTIFY nameListChanged) + Q_PROPERTY(QVariantList devicesModel READ devicesModel NOTIFY devicesModelChanged) + Q_PROPERTY(bool scanning READ scanning NOTIFY scanningChanged) + Q_PROPERTY(QString serviceUuid READ serviceUuid WRITE setServiceUuid NOTIFY serviceUuidChanged) + Q_PROPERTY(QString readUuid READ readUuid WRITE setReadUuid NOTIFY readUuidChanged) + Q_PROPERTY(QString writeUuid READ writeUuid WRITE setWriteUuid NOTIFY writeUuidChanged) + Q_PROPERTY(qint16 connectedRssi READ connectedRssi NOTIFY connectedRssiChanged) + Q_PROPERTY(qint16 selectedRssi READ selectedRssi NOTIFY selectedRssiChanged) + Q_PROPERTY(bool adapterAvailable READ isAdapterAvailable NOTIFY adapterStateChanged) + Q_PROPERTY(bool adapterPoweredOn READ isAdapterPoweredOn NOTIFY adapterStateChanged) + Q_PROPERTY(QString adapterName READ getAdapterName NOTIFY adapterStateChanged) + Q_PROPERTY(QString adapterAddress READ getAdapterAddress NOTIFY adapterStateChanged) + Q_PROPERTY(QString hostMode READ getHostMode NOTIFY adapterStateChanged) public: explicit BluetoothConfiguration(const QString &name, QObject *parent = nullptr); explicit BluetoothConfiguration(const BluetoothConfiguration *copy, QObject *parent = nullptr); - virtual ~BluetoothConfiguration(); + ~BluetoothConfiguration() override; + + enum class BluetoothMode { + ModeClassic, + ModeLowEnergy + }; + Q_ENUM(BluetoothMode) Q_INVOKABLE void startScan(); - Q_INVOKABLE void stopScan() const; + Q_INVOKABLE void stopScan(); Q_INVOKABLE void setDevice(const QString &name); + Q_INVOKABLE void setDeviceByAddress(const QString &address); + + // Pairing support (Classic Bluetooth only) + Q_INVOKABLE void requestPairing(const QString &address); + Q_INVOKABLE void removePairing(const QString &address); + Q_INVOKABLE QString getPairingStatus(const QString &address) const; + + // Adapter information + Q_INVOKABLE bool isAdapterAvailable() const; + Q_INVOKABLE QString getAdapterAddress() const; + Q_INVOKABLE QString getAdapterName() const; + Q_INVOKABLE bool isAdapterPoweredOn() const; + Q_INVOKABLE QVariantList getAllPairedDevices() const; + Q_INVOKABLE QVariantList getConnectedDevices() const; + Q_INVOKABLE void powerOnAdapter(); + Q_INVOKABLE void powerOffAdapter(); + Q_INVOKABLE void setAdapterDiscoverable(bool discoverable); + Q_INVOKABLE QVariantList getAllAvailableAdapters() const; + Q_INVOKABLE void selectAdapter(const QString &address); + Q_INVOKABLE QString getHostMode() const; LinkType type() const override { return LinkConfiguration::TypeBluetooth; } void copyFrom(const LinkConfiguration *source) override; @@ -104,33 +103,85 @@ class BluetoothConfiguration : public LinkConfiguration QString settingsURL() const override { return QStringLiteral("BluetoothSettings.qml"); } QString settingsTitle() const override; - BluetoothData device() const { return _device; } - QString deviceName() const { return _device.name; } -#ifdef Q_OS_IOS - QString address() const { return QString(); }; -#else - QString address() const { return _device.address.toString(); }; -#endif - QStringList nameList() const { return _nameList; } + BluetoothMode mode() const { return _mode; } + void setMode(BluetoothMode mode); + + const QBluetoothDeviceInfo& device() const { return _device; } + QString deviceName() const { return _device.name(); } + QString address() const { return _device.address().toString(); } + const QStringList& nameList() const { return _nameList; } + QVariantList devicesModel() const; bool scanning() const; + qint16 connectedRssi() const { return _connectedRssi; } + void setConnectedRssi(qint16 rssi); + qint16 selectedRssi() const; + + // BLE specific settings + QString serviceUuid() const { return _serviceUuid.toString(); } + void setServiceUuid(const QString &uuid); + QString readUuid() const { return _readCharacteristicUuid.toString(); } + void setReadUuid(const QString &uuid); + QString writeUuid() const { return _writeCharacteristicUuid.toString(); } + void setWriteUuid(const QString &uuid); + + const QBluetoothUuid& readCharacteristicUuid() const { return _readCharacteristicUuid; } + const QBluetoothUuid& writeCharacteristicUuid() const { return _writeCharacteristicUuid; } + + static bool isBluetoothAvailable(); + + // Known BLE UART-like service UUIDs + static inline const QBluetoothUuid NORDIC_UART_SERVICE{QStringLiteral("6e400001-b5a3-f393-e0a9-e50e24dcca9e")}; + static inline const QBluetoothUuid NORDIC_UART_RX_CHAR{QStringLiteral("6e400003-b5a3-f393-e0a9-e50e24dcca9e")}; + static inline const QBluetoothUuid NORDIC_UART_TX_CHAR{QStringLiteral("6e400002-b5a3-f393-e0a9-e50e24dcca9e")}; + static inline const QBluetoothUuid TI_SENSORTAG_SERVICE{QStringLiteral("0000ffe0-0000-1000-8000-00805f9b34fb")}; + static inline const QBluetoothUuid TI_SENSORTAG_CHAR{QStringLiteral("0000ffe1-0000-1000-8000-00805f9b34fb")}; signals: + void modeChanged(); void deviceChanged(); void nameListChanged(); + void devicesModelChanged(); void scanningChanged(); + void serviceUuidChanged(); + void readUuidChanged(); + void writeUuidChanged(); + void connectedRssiChanged(); + void selectedRssiChanged(); void errorOccurred(const QString &errorString); + void adapterStateChanged(); private slots: void _deviceDiscovered(const QBluetoothDeviceInfo &info); - void _onSocketErrorOccurred(QBluetoothDeviceDiscoveryAgent::Error error); + void _onDiscoveryErrorOccurred(QBluetoothDeviceDiscoveryAgent::Error error); + void _onDiscoveryFinished(); + void _deviceUpdated(const QBluetoothDeviceInfo &info, QBluetoothDeviceInfo::Fields updatedFields); + + // QBluetoothLocalDevice slots + void _onHostModeStateChanged(QBluetoothLocalDevice::HostMode mode); + void _onDeviceConnected(const QBluetoothAddress &address); + void _onDeviceDisconnected(const QBluetoothAddress &address); + void _onPairingFinished(const QBluetoothAddress &address, QBluetoothLocalDevice::Pairing pairing); + void _onLocalDeviceErrorOccurred(QBluetoothLocalDevice::Error error); private: void _initDeviceDiscoveryAgent(); - - QList _deviceList; - BluetoothData _device{}; + void _updateDeviceList(); + void _applySelectedDevice(const QBluetoothDeviceInfo &info); + bool _createLocalDevice(const QBluetoothAddress &address); + void _connectLocalDeviceSignals(); + + BluetoothMode _mode = BluetoothMode::ModeClassic; + QBluetoothDeviceInfo _device; + QList _deviceList; QStringList _nameList; - QBluetoothDeviceDiscoveryAgent *_deviceDiscoveryAgent = nullptr; + QPointer _deviceDiscoveryAgent; + QPointer _localDevice; + qint16 _connectedRssi = 0; + + // BLE specific - Default to Nordic UART Service + QBluetoothUuid _serviceUuid{NORDIC_UART_SERVICE}; + QBluetoothUuid _readCharacteristicUuid{NORDIC_UART_RX_CHAR}; + QBluetoothUuid _writeCharacteristicUuid{NORDIC_UART_TX_CHAR}; }; /*===========================================================================*/ @@ -141,7 +192,7 @@ class BluetoothWorker : public QObject public: explicit BluetoothWorker(const BluetoothConfiguration *config, QObject *parent = nullptr); - ~BluetoothWorker(); + ~BluetoothWorker() override; bool isConnected() const; @@ -151,31 +202,85 @@ class BluetoothWorker : public QObject void errorOccurred(const QString &errorString); void dataReceived(const QByteArray &data); void dataSent(const QByteArray &data); + void rssiUpdated(qint16 rssi); public slots: - void setupSocket(); + void setupConnection(); void connectLink(); void disconnectLink(); void writeData(const QByteArray &data); private slots: + // Classic Bluetooth slots void _onSocketConnected(); void _onSocketDisconnected(); void _onSocketReadyRead(); void _onSocketBytesWritten(qint64 bytes); void _onSocketErrorOccurred(QBluetoothSocket::SocketError socketError); -#ifdef Q_OS_IOS - void _onServiceErrorOccurred(QBluetoothServiceDiscoveryAgent::Error error); - void _serviceDiscovered(const QBluetoothServiceInfo &info); - void _discoveryFinished(); -#endif + void _onClassicServiceDiscovered(const QBluetoothServiceInfo &serviceInfo); + void _onClassicServiceDiscoveryFinished(); + void _onClassicServiceDiscoveryCanceled(); + void _onClassicServiceDiscoveryError(QBluetoothServiceDiscoveryAgent::Error error); + + // BLE slots + void _onControllerConnected(); + void _onControllerDisconnected(); + void _onControllerErrorOccurred(QLowEnergyController::Error error); + void _onServiceDiscovered(const QBluetoothUuid &uuid); + void _onServiceDiscoveryFinished(); + void _onServiceStateChanged(QLowEnergyService::ServiceState state); + void _onCharacteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &value); + void _onCharacteristicRead(const QLowEnergyCharacteristic &characteristic, const QByteArray &value); + void _onCharacteristicWritten(const QLowEnergyCharacteristic &characteristic, const QByteArray &value); + void _onDescriptorRead(const QLowEnergyDescriptor &descriptor, const QByteArray &value); + void _onDescriptorWritten(const QLowEnergyDescriptor &descriptor, const QByteArray &value); + void _onServiceError(QLowEnergyService::ServiceError error); + + // Common slots + void _reconnectTimeout(); private: + void _setupClassicSocket(); + void _startClassicServiceDiscovery(); + void _setupBleController(); + void _setupBleService(); + void _discoverServiceDetails(); + void _enableNotifications(); + void _writeBleData(const QByteArray &data); + void _writeClassicData(const QByteArray &data); + void _findCharacteristics(); + void _processNextBleWrite(); + void _clearBleWriteQueue(); + const BluetoothConfiguration *_config = nullptr; - QBluetoothSocket *_socket = nullptr; -#ifdef Q_OS_IOS - QBluetoothServiceDiscoveryAgent *_serviceDiscoveryAgent = nullptr; -#endif + + // Classic Bluetooth + QPointer _socket; + + // BLE + QPointer _controller; + QPointer _service; + QLowEnergyCharacteristic _readCharacteristic; + QLowEnergyCharacteristic _writeCharacteristic; + int _mtu = 23; // default ATT MTU + qint16 _rssi = 0; + QPointer _rssiTimer; + QQueue _bleWriteQueue; + QByteArray _currentBleWrite; + bool _bleWriteInProgress = false; + + // Common + QPointer _reconnectTimer; + std::atomic _intentionalDisconnect{false}; + std::atomic _connected{false}; + int _reconnectAttempts = 0; + static constexpr int MAX_RECONNECT_ATTEMPTS = 10; + QPointer _classicDiscovery; + QBluetoothServiceInfo _classicDiscoveredService; + + // BLE packet size constraints + static constexpr int BLE_MIN_PACKET_SIZE = 20; + static constexpr int BLE_MAX_PACKET_SIZE = 512; }; /*===========================================================================*/ @@ -186,7 +291,7 @@ class BluetoothLink : public LinkInterface public: explicit BluetoothLink(SharedLinkConfigurationPtr &config, QObject *parent = nullptr); - virtual ~BluetoothLink(); + ~BluetoothLink() override; bool isConnected() const override; void disconnect() override; @@ -198,13 +303,14 @@ private slots: void _onErrorOccurred(const QString &errorString); void _onDataReceived(const QByteArray &data); void _onDataSent(const QByteArray &data); + void _onRssiUpdated(qint16 rssi); private: bool _connect() override; void _checkPermission(); void _handlePermissionStatus(Qt::PermissionStatus permissionStatus); - const BluetoothConfiguration *_bluetoothConfig = nullptr; - BluetoothWorker *_worker = nullptr; - QThread *_workerThread = nullptr; + BluetoothConfiguration *_bluetoothConfig = nullptr; + QPointer _worker; + QPointer _workerThread; }; diff --git a/src/Comms/CMakeLists.txt b/src/Comms/CMakeLists.txt index 62ce9307cc1..3c2961148ff 100644 --- a/src/Comms/CMakeLists.txt +++ b/src/Comms/CMakeLists.txt @@ -57,6 +57,7 @@ if(QGC_ENABLE_BLUETOOTH) BluetoothLink.cc BluetoothLink.h ) + target_compile_definitions(${CMAKE_PROJECT_NAME} PRIVATE QGC_ENABLE_BLUETOOTH) endif() diff --git a/src/Comms/LinkConfiguration.cc b/src/Comms/LinkConfiguration.cc index 422b5aa1817..fbaf1c5d61b 100644 --- a/src/Comms/LinkConfiguration.cc +++ b/src/Comms/LinkConfiguration.cc @@ -8,6 +8,7 @@ ****************************************************************************/ #include "LinkConfiguration.h" +#include "QGCLoggingCategory.h" #ifndef QGC_NO_SERIAL_LINK #include "SerialLink.h" #endif @@ -24,11 +25,13 @@ #include "AirLinkLink.h" #endif +QGC_LOGGING_CATEGORY(LinkConfigurationLog, "qgc.comms.linkconfiguration") + LinkConfiguration::LinkConfiguration(const QString &name, QObject *parent) : QObject(parent) , _name(name) { - // qCDebug(AudioOutputLog) << Q_FUNC_INFO << this; + qCDebug(LinkConfigurationLog) << this; } LinkConfiguration::LinkConfiguration(const LinkConfiguration *copy, QObject *parent) @@ -39,14 +42,14 @@ LinkConfiguration::LinkConfiguration(const LinkConfiguration *copy, QObject *par , _autoConnect(copy->isAutoConnect()) , _highLatency(copy->isHighLatency()) { - // qCDebug(AudioOutputLog) << Q_FUNC_INFO << this; + qCDebug(LinkConfigurationLog) << this; Q_ASSERT(!_name.isEmpty()); } LinkConfiguration::~LinkConfiguration() { - // qCDebug(AudioOutputLog) << Q_FUNC_INFO << this; + qCDebug(LinkConfigurationLog) << this; } void LinkConfiguration::copyFrom(const LinkConfiguration *source) diff --git a/src/Comms/LinkConfiguration.h b/src/Comms/LinkConfiguration.h index fb122f72964..01cb1775b0c 100644 --- a/src/Comms/LinkConfiguration.h +++ b/src/Comms/LinkConfiguration.h @@ -9,12 +9,15 @@ #pragma once +#include #include #include #include class LinkInterface; +Q_DECLARE_LOGGING_CATEGORY(LinkConfigurationLog) + /// Interface holding link specific settings. class LinkConfiguration : public QObject { diff --git a/src/Comms/LinkManager.cc b/src/Comms/LinkManager.cc index d88449e38b5..d6fc21ece61 100644 --- a/src/Comms/LinkManager.cc +++ b/src/Comms/LinkManager.cc @@ -8,7 +8,6 @@ ****************************************************************************/ #include "LinkManager.h" -#include "DeviceInfo.h" #include "LogReplayLink.h" #include "MAVLinkProtocol.h" #include "MultiVehicleManager.h" @@ -655,7 +654,11 @@ void LinkManager::_removeConfiguration(const LinkConfiguration *config) bool LinkManager::isBluetoothAvailable() { - return QGCDeviceInfo::isBluetoothAvailable(); +#ifdef QGC_ENABLE_BLUETOOTH + return BluetoothConfiguration::isBluetoothAvailable(); +#else + return false; +#endif } bool LinkManager::containsLink(const LinkInterface *link) const diff --git a/src/UI/AppSettings/BluetoothSettings.qml b/src/UI/AppSettings/BluetoothSettings.qml index 95d47c4718b..3e9e6f709da 100644 --- a/src/UI/AppSettings/BluetoothSettings.qml +++ b/src/UI/AppSettings/BluetoothSettings.qml @@ -14,65 +14,343 @@ import QtQuick.Layouts import QGroundControl import QGroundControl.Controls + ColumnLayout { + spacing: _rowSpacing + function saveSettings() { } -ColumnLayout { - spacing: _rowSpacing + // UUID validation regex: accepts empty (auto-detect), standard UUID (with/without hyphens), or short 4-digit UUID + readonly property var uuidValidator: RegularExpressionValidator { + regularExpression: /^$|^(0x)?[0-9a-fA-F]{4}$|^[0-9a-fA-F]{32}$|^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/ + } + + // Property to track whether selected device is paired + property bool paired: false + + // Update pairing status when device list or selection changes + Connections { + target: subEditConfig + function onDevicesModelChanged() { + updatePairingStatus() + } + function onDeviceChanged() { + updatePairingStatus() + } + function onModeChanged() { + updatePairingStatus() + } + } + + // Helper function to update pairing status for selected device + function updatePairingStatus() { + if (subEditConfig.mode === BluetoothConfiguration.BluetoothMode.ModeClassic && subEditConfig.address !== "") { + paired = subEditConfig.getPairingStatus(subEditConfig.address) !== qsTr("Unpaired") + } else { + paired = false + } + } + + // Initialize pairing status on load + Component.onCompleted: updatePairingStatus() - function saveSettings() { - // No need + //-- Bluetooth Adapter Status -- + SectionHeader { + Layout.fillWidth: true + text: qsTr("Bluetooth Adapter") } GridLayout { - columns: 2 - columnSpacing: _colSpacing - rowSpacing: _rowSpacing + columns: 2 + columnSpacing: _colSpacing + rowSpacing: _rowSpacing + Layout.fillWidth: true - QGCLabel { text: qsTr("Device") } + QGCLabel { + text: qsTr("Adapter") + visible: subEditConfig.adapterAvailable + } QGCLabel { Layout.preferredWidth: _secondColumnWidth - text: subEditConfig.deviceName + text: subEditConfig.adapterName + " (" + subEditConfig.adapterAddress + ")" + visible: subEditConfig.adapterAvailable } - QGCLabel { text: qsTr("Address") } + QGCLabel { + text: qsTr("Status") + visible: subEditConfig.adapterAvailable + } QGCLabel { Layout.preferredWidth: _secondColumnWidth - text: subEditConfig.address + text: subEditConfig.hostMode + visible: subEditConfig.adapterAvailable + color: subEditConfig.adapterPoweredOn ? qgcPal.text : qgcPal.warningText + } + + QGCLabel { + text: qsTr("Bluetooth adapter unavailable") + visible: !subEditConfig.adapterAvailable + color: qgcPal.warningText + Layout.columnSpan: 2 + } + } + + GridLayout { + columns: 2 + columnSpacing: _colSpacing + rowSpacing: _rowSpacing + Layout.fillWidth: true + visible: subEditConfig.adapterAvailable + + QGCCheckBox { + text: qsTr("Powered On") + checked: subEditConfig.adapterPoweredOn + onClicked: { + if (checked) { + subEditConfig.powerOnAdapter() + } else { + subEditConfig.powerOffAdapter() + } + } + } + + QGCCheckBox { + text: qsTr("Discoverable") + checked: subEditConfig.hostMode === qsTr("Discoverable") || subEditConfig.hostMode === qsTr("Discoverable (Limited)") + enabled: subEditConfig.adapterPoweredOn + onClicked: subEditConfig.setAdapterDiscoverable(checked) } } - QGCLabel { text: qsTr("Bluetooth Devices") } + //-- Connection Settings -- + SectionHeader { + Layout.fillWidth: true + text: qsTr("Connection") + } - Repeater { - model: subEditConfig.nameList + GridLayout { + columns: 2 + columnSpacing: _colSpacing + rowSpacing: _rowSpacing + Layout.fillWidth: true - delegate: QGCButton { - text: modelData + QGCLabel { text: qsTr("Mode") } + RowLayout { Layout.preferredWidth: _secondColumnWidth - autoExclusive: true + spacing: _colSpacing + + QGCRadioButton { + text: qsTr("Classic") + checked: subEditConfig.mode === BluetoothConfiguration.BluetoothMode.ModeClassic + onClicked: subEditConfig.mode = BluetoothConfiguration.BluetoothMode.ModeClassic + } + + QGCRadioButton { + text: qsTr("BLE") + checked: subEditConfig.mode === BluetoothConfiguration.BluetoothMode.ModeLowEnergy + onClicked: subEditConfig.mode = BluetoothConfiguration.BluetoothMode.ModeLowEnergy + } + } + + QGCLabel { text: qsTr("Selected Device") } + QGCLabel { + Layout.preferredWidth: _secondColumnWidth + text: subEditConfig.deviceName || qsTr("None") + } + + QGCLabel { text: qsTr("Device Address") } + QGCLabel { + Layout.preferredWidth: _secondColumnWidth + text: subEditConfig.address || qsTr("N/A") + } + + // Classic Bluetooth Pairing + QGCLabel { + text: qsTr("Pairing") + visible: subEditConfig.mode === BluetoothConfiguration.BluetoothMode.ModeClassic && subEditConfig.address !== "" + } + RowLayout { + Layout.preferredWidth: _secondColumnWidth + visible: subEditConfig.mode === BluetoothConfiguration.BluetoothMode.ModeClassic && subEditConfig.address !== "" + spacing: _colSpacing - onClicked: { - checked = true - if (modelData !== "") - subEditConfig.setDevice(modelData) + QGCLabel { + text: (paired, subEditConfig.getPairingStatus(subEditConfig.address)) + Layout.fillWidth: true } + + QGCButton { + text: paired ? qsTr("Unpair") : qsTr("Pair") + onClicked: { + if (paired) { + subEditConfig.removePairing(subEditConfig.address) + } else { + subEditConfig.requestPairing(subEditConfig.address) + } + } + } + } + + // BLE Signal Strength + QGCLabel { + text: qsTr("Signal Strength") + visible: subEditConfig.mode === BluetoothConfiguration.BluetoothMode.ModeLowEnergy && + (((typeof subEditConfig.connectedRssi === "number") && subEditConfig.connectedRssi !== 0) || + ((typeof subEditConfig.selectedRssi === "number") && subEditConfig.selectedRssi !== 0)) + } + QGCLabel { + Layout.preferredWidth: _secondColumnWidth + readonly property bool hasConnected: (typeof subEditConfig.connectedRssi === "number") && subEditConfig.connectedRssi !== 0 + readonly property bool hasSelected: (typeof subEditConfig.selectedRssi === "number") && subEditConfig.selectedRssi !== 0 + visible: subEditConfig.mode === BluetoothConfiguration.BluetoothMode.ModeLowEnergy && (hasConnected || hasSelected) + text: hasConnected ? qsTr("%1 dBm (Connected)").arg(subEditConfig.connectedRssi) + : qsTr("%1 dBm (Last Scan)").arg(subEditConfig.selectedRssi) + color: hasConnected ? qgcPal.text : qgcPal.buttonText + } + } + + //-- BLE Configuration -- + SectionHeader { + Layout.fillWidth: true + text: qsTr("BLE Configuration") + visible: subEditConfig.mode === BluetoothConfiguration.BluetoothMode.ModeLowEnergy + } + + GridLayout { + columns: 2 + columnSpacing: _colSpacing + rowSpacing: _rowSpacing + Layout.fillWidth: true + visible: subEditConfig.mode === BluetoothConfiguration.BluetoothMode.ModeLowEnergy + + QGCLabel { text: qsTr("Service UUID") } + QGCTextField { + Layout.preferredWidth: _secondColumnWidth + text: subEditConfig.serviceUuid + placeholderText: qsTr("Auto-detect") + validator: uuidValidator + onTextChanged: subEditConfig.serviceUuid = text + } + + QGCLabel { + text: qsTr("RX Characteristic") + visible: typeof subEditConfig.readUuid !== "undefined" } + QGCTextField { + Layout.preferredWidth: _secondColumnWidth + text: typeof subEditConfig.readUuid !== "undefined" ? subEditConfig.readUuid : "" + placeholderText: qsTr("Auto-detect") + visible: typeof subEditConfig.readUuid !== "undefined" + validator: uuidValidator + onTextChanged: { if (typeof subEditConfig.readUuid !== "undefined") subEditConfig.readUuid = text } + } + + QGCLabel { + text: qsTr("TX Characteristic") + visible: typeof subEditConfig.writeUuid !== "undefined" + } + QGCTextField { + Layout.preferredWidth: _secondColumnWidth + text: typeof subEditConfig.writeUuid !== "undefined" ? subEditConfig.writeUuid : "" + placeholderText: qsTr("Auto-detect") + visible: typeof subEditConfig.writeUuid !== "undefined" + validator: uuidValidator + onTextChanged: { if (typeof subEditConfig.writeUuid !== "undefined") subEditConfig.writeUuid = text } + } + } + + QGCLabel { + text: qsTr("Common Service UUIDs:\n • Nordic UART: 6e400001-b5a3-f393-e0a9-e50e24dcca9e\n • TI SensorTag: 0000ffe0-0000-1000-8000-00805f9b34fb") + visible: subEditConfig.mode === BluetoothConfiguration.BluetoothMode.ModeLowEnergy + wrapMode: Text.WordWrap + Layout.fillWidth: true + font.pointSize: ScreenTools.smallFontPointSize + color: qgcPal.buttonText + } + + //-- Available Devices -- + SectionHeader { + Layout.fillWidth: true + text: subEditConfig.mode === BluetoothConfiguration.BluetoothMode.ModeLowEnergy ? qsTr("Available BLE Devices") : qsTr("Available Devices") } - RowLayout { - Layout.alignment: Qt.AlignCenter - spacing: _colSpacing + ScrollView { + Layout.fillWidth: true + Layout.preferredHeight: Math.min(repeaterColumn.height, ScreenTools.defaultFontPixelHeight * 12) + clip: true - QGCButton { - text: qsTr("Scan") - enabled: !subEditConfig.scanning - onClicked: subEditConfig.startScan() + Column { + id: repeaterColumn + width: parent.width + spacing: ScreenTools.defaultFontPixelHeight * 0.5 + + Repeater { + model: subEditConfig.devicesModel && subEditConfig.devicesModel.length + ? subEditConfig.devicesModel + : subEditConfig.nameList + + delegate: QGCButton { + readonly property var _dev: (typeof modelData === "object") ? modelData : ({ name: modelData }) + readonly property string deviceName: _dev.name || "" + readonly property string address: _dev.address || "" + readonly property bool hasRssi: (typeof _dev.rssi === "number") && _dev.rssi !== 0 + readonly property int rssi: hasRssi ? _dev.rssi : 0 + // Use paired as dependency to force re-evaluation when any pairing changes + readonly property bool isPaired: (paired === paired) && + subEditConfig.mode === BluetoothConfiguration.BluetoothMode.ModeClassic && + address !== "" && + subEditConfig.getPairingStatus(address) !== qsTr("Unpaired") + + text: { + let displayText = deviceName + if (hasRssi) { + displayText += " (" + rssi + " dBm)" + } + if (isPaired) { + displayText += " 🔗" + } + return displayText + } + width: _secondColumnWidth + checkable: true + autoExclusive: true + checked: (address !== "") ? (address === subEditConfig.address) : (deviceName === subEditConfig.deviceName) + + onClicked: { + if (address !== "") { + subEditConfig.setDeviceByAddress(address) + } else if (deviceName !== "") { + subEditConfig.setDevice(deviceName) + } + } + } + } + + QGCLabel { + text: qsTr("No devices found") + visible: (subEditConfig.devicesModel ? subEditConfig.devicesModel.length === 0 : subEditConfig.nameList.length === 0) && !subEditConfig.scanning + width: _secondColumnWidth + horizontalAlignment: Text.AlignHCenter + color: qgcPal.warningText + } + + BusyIndicator { + visible: subEditConfig.scanning + running: visible + width: _secondColumnWidth + height: ScreenTools.defaultFontPixelHeight * 2 + } } + } - QGCButton { - text: qsTr("Stop") - enabled: subEditConfig.scanning - onClicked: subEditConfig.stopScan() + QGCButton { + Layout.alignment: Qt.AlignHCenter + text: subEditConfig.scanning ? qsTr("Stop Scan") : qsTr("Scan for Devices") + onClicked: { + if (subEditConfig.scanning) { + subEditConfig.stopScan() + } else { + subEditConfig.startScan() + } } } } diff --git a/src/UI/AppSettings/LinkSettings.qml b/src/UI/AppSettings/LinkSettings.qml index 40dbf66087f..4fae378df77 100644 --- a/src/UI/AppSettings/LinkSettings.qml +++ b/src/UI/AppSettings/LinkSettings.qml @@ -271,13 +271,20 @@ SettingsPage { Loader { id: linkSettingsLoader - source: subEditConfig.settingsURL + source: editingConfig && editingConfig.settingsURL ? editingConfig.settingsURL : "" + asynchronous: true property var subEditConfig: editingConfig property int _firstColumnWidth: ScreenTools.defaultFontPixelWidth * 12 property int _secondColumnWidth: ScreenTools.defaultFontPixelWidth * 30 property int _rowSpacing: ScreenTools.defaultFontPixelHeight / 2 property int _colSpacing: ScreenTools.defaultFontPixelWidth / 2 + + onStatusChanged: { + if (status === Loader.Error) { + console.warn("Failed to load link settings page:", source, errorString) + } + } } } } diff --git a/src/Utilities/DeviceInfo.cc b/src/Utilities/DeviceInfo.cc index ae206fe9ee3..d4f269e346a 100644 --- a/src/Utilities/DeviceInfo.cc +++ b/src/Utilities/DeviceInfo.cc @@ -12,9 +12,6 @@ #include #include -#ifdef QGC_ENABLE_BLUETOOTH -# include -#endif QGC_LOGGING_CATEGORY(QGCDeviceInfoLog, "Utilities.QGCDeviceInfo") @@ -47,16 +44,6 @@ bool isNetworkEthernet() return (QNetworkInformation::instance()->transportMedium() == QNetworkInformation::TransportMedium::Ethernet); } -bool isBluetoothAvailable() -{ - #ifdef QGC_ENABLE_BLUETOOTH - const QList devices = QBluetoothLocalDevice::allDevices(); - return !devices.isEmpty(); - #else - return false; - #endif -} - //////////////////////////////////////////////////////////////////// Q_APPLICATION_STATIC(QGCAmbientTemperature, s_ambientTemperature); diff --git a/src/Utilities/DeviceInfo.h b/src/Utilities/DeviceInfo.h index 15cd8948f1c..913c383ae0b 100644 --- a/src/Utilities/DeviceInfo.h +++ b/src/Utilities/DeviceInfo.h @@ -21,7 +21,6 @@ namespace QGCDeviceInfo { bool isInternetAvailable(); -bool isBluetoothAvailable(); bool isNetworkEthernet(); ////////////////////////////////////////////////////////////////////