diff --git a/src/share/sleex/modules/settings/Bluetooth.qml b/src/share/sleex/modules/settings/Bluetooth.qml index b6d7fd24..947b690c 100644 --- a/src/share/sleex/modules/settings/Bluetooth.qml +++ b/src/share/sleex/modules/settings/Bluetooth.qml @@ -8,11 +8,11 @@ import Quickshell.Bluetooth import qs.services import qs.modules.common import qs.modules.common.widgets +import Sleex.Services ContentPage { forceWidth: true - function getDeviceIcon(modelData) { const icon = modelData?.icon ?? ""; @@ -32,6 +32,29 @@ ContentPage { } } + Timer { + id: refreshTimer + interval: 8000 + onTriggered: BluetoothService.refreshPairedDevices() + } + + function unpairDevice(address) { + BluetoothService.unpairDevice(address); + refreshTimer.start(); + } + + function connectDevice(device) { + if (device.paired) { + BluetoothService.connectDevice(device.address); + } else { + BluetoothService.pairDevice(device.address); + } + } + + function disconnectDevice(address) { + BluetoothService.disconnectDevice(address); + } + ContentSection { title: "Bluetooth settings" @@ -43,17 +66,16 @@ ContentPage { ConfigSwitch { text: "Enabled" - checked: Config.options.bar.showTitle + checked: BluetoothService.bluetoothEnabled onClicked: checked = !checked; onCheckedChanged: { - if (Bluetooth.defaultAdapter) - Bluetooth.defaultAdapter.enabled = checked; + BluetoothService.bluetoothEnabled = checked; } } ConfigSwitch { text: "Discoverable" - checked: Bluetooth.defaultAdapter.discoverable + checked: Bluetooth.defaultAdapter?.discoverable ?? false onClicked: checked = !checked; onCheckedChanged: { if (Bluetooth.defaultAdapter) @@ -68,7 +90,7 @@ ContentPage { StyledText { text: { - const devices = Bluetooth.devices.values; + const devices = BluetoothService.devices; let available = qsTr("%1 device%2 available").arg(devices.length).arg(devices.length === 1 ? "" : "s"); const connected = devices.filter(d => d.connected).length; if (connected > 0) @@ -79,36 +101,70 @@ ContentPage { font.pixelSize: Appearance.font.pixelSize.huge } - RippleButton { - id: discoverBtn + RowLayout { + spacing: 10 + + RippleButton { + id: discoverBtn - visible: Bluetooth.adapters.values.length > 0 + visible: Bluetooth.adapters.values.length > 0 - contentItem: Rectangle { - id: discoverBtnBody - radius: Appearance.rounding.full - color: Bluetooth.defaultAdapter?.discovering ? Appearance.m3colors.m3primary : Appearance.colors.colLayer2 - implicitWidth: height + contentItem: Rectangle { + id: discoverBtnBody + radius: Appearance.rounding.full + color: BluetoothService.discovering ? Appearance.m3colors.m3primary : Appearance.colors.colLayer2 + implicitWidth: height - MaterialSymbol { - id: scanIcon + MaterialSymbol { + id: scanIcon - anchors.centerIn: parent - text: "bluetooth_searching" - color: Bluetooth.defaultAdapter?.discovering ? Appearance.m3colors.m3onSecondary : Appearance.m3colors.m3onSecondaryContainer - fill: Bluetooth.defaultAdapter?.discovering ? 1 : 0 + anchors.centerIn: parent + text: "bluetooth_searching" + color: BluetoothService.discovering ? Appearance.m3colors.m3onSecondary : Appearance.m3colors.m3onSecondaryContainer + fill: BluetoothService.discovering ? 1 : 0 + } + } + + MouseArea { + id: discoverArea + anchors.fill: parent + hoverEnabled: true + onClicked: { + BluetoothService.discovering = !BluetoothService.discovering; + } + + StyledToolTip { + extraVisibleCondition: discoverArea.containsMouse + text: "Discover new devices" + } } } - MouseArea { - id: discoverArea - anchors.fill: parent - hoverEnabled: true - onClicked: Bluetooth.defaultAdapter.discovering = !Bluetooth.defaultAdapter.discovering + RippleButton { + id: refreshBtn + visible: Bluetooth.adapters.values.length > 0 + width: 40 + height: 40 - StyledToolTip { - extraVisibleCondition: discoverArea.containsMouse - text: "Discover new devices" + contentItem: MaterialSymbol { + anchors.centerIn: parent + text: "refresh" + color: Appearance.colors.colOnLayer2 + } + + MouseArea { + id: refreshArea + anchors.fill: parent + hoverEnabled: true + onClicked: { + BluetoothService.refreshPairedDevices(); + refreshTimer.restart(); + } + + StyledToolTip { + extraVisibleCondition: refreshArea.containsMouse + text: "Refresh device list" + } } } } @@ -135,7 +191,12 @@ ContentPage { Repeater { model: ScriptModel { values: { - let devices = [...Bluetooth.devices.values].sort((a, b) => (b.connected - a.connected) || (b.bonded - a.bonded)); + // Only show devices if Bluetooth is enabled + if (!BluetoothService.bluetoothEnabled) { + return []; + } + let devices = [...BluetoothService.devices].sort((a, b) => (b.connected - a.connected) || (b.paired - a.paired)); + if (deviceSearch.text.trim() !== "") { devices = devices.filter(d => d.name.toLowerCase().includes(deviceSearch.text.toLowerCase()) || d.address.toLowerCase().includes(deviceSearch.text.toLowerCase())); } @@ -146,8 +207,8 @@ ContentPage { RowLayout { id: device - required property BluetoothDevice modelData - readonly property bool loading: modelData.state === BluetoothDeviceState.Connecting || modelData.state === BluetoothDeviceState.Disconnecting + required property var modelData + readonly property bool loading: false // Placeholder if needed Layout.fillWidth: true spacing: 10 @@ -169,7 +230,7 @@ ContentPage { MaterialSymbol { anchors.centerIn: parent - text: getDeviceIcon(device.modelData) + text: device.modelData.icon font.pixelSize: Appearance.font.pixelSize.title color: Appearance.colors.colOnSecondaryContainer } @@ -185,32 +246,69 @@ ContentPage { color: Appearance.colors.colOnSecondaryContainer } StyledText { - text: device.modelData.address + (device.modelData.connected ? qsTr(" (Connected)") : device.modelData.bonded ? qsTr(" (Paired)") : "") + text: device.modelData.address + (device.modelData.connected ? qsTr(" (Connected)") : (device.modelData.paired ? qsTr(" (Paired)") : qsTr(" (Available)"))) font.pixelSize: Appearance.font.pixelSize.small color: Appearance.colors.colSubtext } } + RippleButton { + id: forgetButton + visible: device.modelData.paired + + width: 40 + height: 40 + buttonRadius: Appearance.rounding.normal + colBackground: "transparent" + colBackgroundHover: Appearance.colors.colSurfaceContainerHigh + hoverEnabled: true + + property bool processing: false + + contentItem: MaterialSymbol { + anchors.centerIn: parent + text: forgetButton.processing ? "hourglass_empty" : "link_off" + font.pixelSize: Appearance.font.pixelSize.normal + color: forgetButton.hovered ? Appearance.colors.colOnSurface : Appearance.colors.colOnSurfaceVariant + } + + onClicked: { + if (forgetButton.processing) return; + + forgetButton.processing = true; + unpairDevice(device.modelData.address); + + // Reset processing state after a delay + Qt.callLater(() => { + processingTimer.start(); + }); + } + + Timer { + id: processingTimer + interval: 5000 + onTriggered: forgetButton.processing = false + } + + StyledToolTip { + extraVisibleCondition: forgetButton.hovered + text: forgetButton.processing ? "Removing device..." : "Forget device" + } + } + StyledSwitch { scale: 0.80 Layout.fillWidth: false checked: device.modelData.connected - onClicked: device.modelData.connected = !device.modelData.connected - } - } - } - - - Loader { - asynchronous: true - active: device.modelData.bonded - sourceComponent: Item { - implicitWidth: connectBtn.implicitWidth - implicitHeight: connectBtn.implicitHeight - - MaterialSymbol { - anchors.centerIn: parent - text: "delete" + onClicked: { + if (checked) { + connectDevice(device.modelData); + } else { + disconnectDevice(device.modelData.address); + } + // Restore binding to ensure switch reflects actual state + checked = Qt.binding(function() { return device.modelData.connected; }); + } } } } diff --git a/src/share/sleex/plugins/src/Sleex/services/bluetooth.cpp b/src/share/sleex/plugins/src/Sleex/services/bluetooth.cpp index d7eb564b..f4171e6f 100644 --- a/src/share/sleex/plugins/src/Sleex/services/bluetooth.cpp +++ b/src/share/sleex/plugins/src/Sleex/services/bluetooth.cpp @@ -1,5 +1,86 @@ #include "bluetooth.hpp" +#include +#include +#include +namespace sleex::services { + +// BluetoothDevice Implementation +BluetoothDevice::BluetoothDevice(const QBluetoothDeviceInfo &info, QObject *parent) + : QObject(parent), m_info(info), m_hasInfo(true) +{ + m_name = info.name(); + m_address = info.address().toString(); +} + +BluetoothDevice::BluetoothDevice(const QString &name, const QString &address, QObject *parent) + : QObject(parent), m_name(name), m_address(address), m_hasInfo(false) +{ +} + +QString BluetoothDevice::icon() const { + if (!m_hasInfo) { + return "bluetooth_connected"; // Default icon + } + + // Map device class to icon name + QBluetoothDeviceInfo::MajorDeviceClass major = m_info.majorDeviceClass(); + quint8 minor = m_info.minorDeviceClass(); + + switch (major) { + case QBluetoothDeviceInfo::ComputerDevice: + return "computer"; + case QBluetoothDeviceInfo::PhoneDevice: + return "smartphone"; + case QBluetoothDeviceInfo::AudioVideoDevice: + if (minor == QBluetoothDeviceInfo::WearableHeadsetDevice || + minor == QBluetoothDeviceInfo::HandsFreeDevice) + return "headset"; + if (minor == QBluetoothDeviceInfo::Headphones) + return "headphones"; + if (minor == QBluetoothDeviceInfo::Loudspeaker) + return "speaker"; + return "audio_file"; // Generic audio + case QBluetoothDeviceInfo::PeripheralDevice: + if (minor == QBluetoothDeviceInfo::KeyboardPeripheral) + return "keyboard_alt"; + if (minor == QBluetoothDeviceInfo::PointingDevicePeripheral) + return "mouse"; + return "settings_input_component"; + case QBluetoothDeviceInfo::WearableDevice: + return "watch"; + default: + return "bluetooth_connected"; + } +} + +void BluetoothDevice::update(const QBluetoothDeviceInfo &info) { + m_info = info; + m_hasInfo = true; + + if (m_name != info.name()) { + m_name = info.name(); + emit nameChanged(); + } + emit iconChanged(); +} + +void BluetoothDevice::setConnected(bool connected) { + if (m_connected != connected) { + m_connected = connected; + emit connectedChanged(); + } +} + +void BluetoothDevice::setPaired(bool paired) { + if (m_paired != paired) { + m_paired = paired; + emit pairedChanged(); + } +} + + +// BluetoothService Implementation BluetoothService::BluetoothService(QObject *parent) : QObject(parent) { @@ -8,6 +89,8 @@ BluetoothService::BluetoothService(QObject *parent) if (!adapters.isEmpty()) { m_localDevice = new QBluetoothLocalDevice(adapters.first().address(), this); m_available = true; + m_enabled = (m_localDevice->hostMode() != QBluetoothLocalDevice::HostPoweredOff); + m_discovering = false; connect(m_localDevice, &QBluetoothLocalDevice::hostModeStateChanged, this, [this](QBluetoothLocalDevice::HostMode mode) { bool enabled = (mode != QBluetoothLocalDevice::HostPoweredOff); @@ -16,34 +99,344 @@ BluetoothService::BluetoothService(QObject *parent) emit bluetoothEnabledChanged(); } }); + + connect(m_localDevice, &QBluetoothLocalDevice::deviceConnected, this, [this](const QBluetoothAddress &address){ + updateDeviceConnectionState(address.toString(), true); + }); + + connect(m_localDevice, &QBluetoothLocalDevice::deviceDisconnected, this, [this](const QBluetoothAddress &address){ + updateDeviceConnectionState(address.toString(), false); + }); + + // Initialize discovery agent + m_discoveryAgent = new QBluetoothDeviceDiscoveryAgent(adapters.first().address(), this); + connect(m_discoveryAgent, &QBluetoothDeviceDiscoveryAgent::deviceDiscovered, this, &BluetoothService::addOrUpdateDevice); + connect(m_discoveryAgent, &QBluetoothDeviceDiscoveryAgent::finished, this, [this](){ + if (m_discovering) { + // Restart if we are supposed to be discovering + m_discoveryAgent->start(); + } + }); + } else { // No adapters at all m_available = false; } + + // Start bluetoothctl monitor + m_monitorProcess = new QProcess(this); + m_monitorProcess->setProcessChannelMode(QProcess::MergedChannels); + connect(m_monitorProcess, &QProcess::readyReadStandardOutput, this, &BluetoothService::parseMonitorOutput); + m_monitorProcess->start("bluetoothctl"); + + refreshPairedDevices(); + checkConnectedDevices(); +} + +void BluetoothService::setBluetoothEnabled(bool enabled) +{ + if (m_localDevice) { + if (enabled) { + m_localDevice->powerOn(); + } else { + m_localDevice->setHostMode(QBluetoothLocalDevice::HostPoweredOff); + } + } +} + +void BluetoothService::setDiscovering(bool discovering) +{ + if (m_discovering != discovering) { + m_discovering = discovering; + emit discoveringChanged(); + setDiscovery(discovering); + } +} + +void BluetoothService::connectDevice(const QString &address) +{ + QProcess::startDetached("bluetoothctl", {"connect", address}); +} + +void BluetoothService::disconnectDevice(const QString &address) +{ + QProcess::startDetached("bluetoothctl", {"disconnect", address}); +} + +void BluetoothService::pairDevice(const QString &address) +{ + if (m_monitorProcess && m_monitorProcess->state() == QProcess::Running) { + m_pendingPairAddress = address; + + // Start scanning via the interactive monitor + m_monitorProcess->write("scan on\n"); + + // Setup a fallback timer in case we don't see a discovery event + if (!m_pairTimeoutTimer) { + m_pairTimeoutTimer = new QTimer(this); + m_pairTimeoutTimer->setSingleShot(true); + connect(m_pairTimeoutTimer, &QTimer::timeout, this, [this]() { + if (!m_pendingPairAddress.isEmpty()) { + QString addr = m_pendingPairAddress; + m_pendingPairAddress.clear(); + + // Try to pair anyway + if (m_monitorProcess) { + m_monitorProcess->write(QString("pair %1\n").arg(addr).toUtf8()); + m_monitorProcess->write(QString("trust %1\n").arg(addr).toUtf8()); + m_monitorProcess->write(QString("connect %1\n").arg(addr).toUtf8()); + + // Turn off scan if we weren't discovering + if (!m_discovering) { + m_monitorProcess->write("scan off\n"); + } + } + } + }); + } + m_pairTimeoutTimer->start(5000); // 5 seconds timeout + } else { + // Fallback if monitor is dead (shouldn't happen) + QProcess::startDetached("bash", {"-c", QString("bluetoothctl pair %1 && bluetoothctl trust %1 && bluetoothctl connect %1").arg(address)}); + } +} + +void BluetoothService::unpairDevice(const QString &address) +{ + QProcess::startDetached("bluetoothctl", {"remove", address}); + // Refresh list after a short delay + QTimer::singleShot(1000, this, &BluetoothService::refreshPairedDevices); +} + +void BluetoothService::trustDevice(const QString &address) +{ + QProcess::startDetached("bluetoothctl", {"trust", address}); +} + +void BluetoothService::setDiscovery(bool discover) +{ + // Only use QBluetoothDeviceDiscoveryAgent for discovery to avoid conflicts + // bluetoothctl scan on/off is handled via monitor if needed for pairing + + if (m_discovering != discover) { + m_discovering = discover; + emit discoveringChanged(); + } + + if (m_discoveryAgent) { + if (discover) { + m_discoveryAgent->start(); + } else { + m_discoveryAgent->stop(); + } + } +} + +void BluetoothService::refreshPairedDevices() +{ + QProcess *process = new QProcess(this); + connect(process, &QProcess::finished, this, [this, process](int exitCode, QProcess::ExitStatus exitStatus) { + if (exitStatus == QProcess::NormalExit && exitCode == 0) { + QString output = process->readAllStandardOutput(); + QStringList lines = output.split('\n', Qt::SkipEmptyParts); + QStringList newPaired; + for (const QString &line : lines) { + // Output format: "Device " + QStringList parts = line.split(' ', Qt::SkipEmptyParts); + if (parts.size() >= 3 && parts[0] == "Device") { + QString address = parts[1]; + QString name = parts.mid(2).join(' '); + newPaired.append(address); + + // Check if device exists, if not create it + bool found = false; + for (QObject* obj : m_devices) { + BluetoothDevice* device = qobject_cast(obj); + if (device && device->address() == address) { + found = true; + break; + } + } + + if (!found) { + BluetoothDevice* device = new BluetoothDevice(name, address, this); + device->setPaired(true); + m_devices.append(device); + emit devicesChanged(); + } + } + } + + m_pairedAddresses = newPaired; + + // Update existing devices paired status + for (QObject* obj : m_devices) { + BluetoothDevice* device = qobject_cast(obj); + if (device) { + device->setPaired(m_pairedAddresses.contains(device->address())); + } + } + } + process->deleteLater(); + }); + process->start("bluetoothctl", {"devices", "Paired"}); +} + +void BluetoothService::updateDeviceConnectionState(const QString &address, bool connected) +{ + for (QObject* obj : m_devices) { + BluetoothDevice* device = qobject_cast(obj); + if (device && device->address() == address) { + device->setConnected(connected); + return; + } + } } -void BluetoothService::update() +void BluetoothService::checkConnectedDevices() { - if (!m_available || !m_localDevice) { - // Reset state if no adapter - if (m_enabled) { m_enabled = false; emit bluetoothEnabledChanged(); } - if (m_connected) { m_connected = false; emit bluetoothConnectedChanged(); } - if (!m_deviceName.isEmpty()) { m_deviceName.clear(); emit bluetoothDeviceNameChanged(); } - if (!m_deviceAddress.isEmpty()) { m_deviceAddress.clear(); emit bluetoothDeviceAddressChanged(); } - return; + if (!m_localDevice) return; + + QList connectedAddrs = m_localDevice->connectedDevices(); + QStringList connectedStrs; + for (const auto &addr : connectedAddrs) { + connectedStrs.append(addr.toString()); } + + for (QObject* obj : m_devices) { + BluetoothDevice* device = qobject_cast(obj); + if (device) { + device->setConnected(connectedStrs.contains(device->address())); + } + } +} - // Update enabled state - bool enabled = (m_localDevice->hostMode() != QBluetoothLocalDevice::HostPoweredOff); - if (m_enabled != enabled) { - m_enabled = enabled; - emit bluetoothEnabledChanged(); +void BluetoothService::addOrUpdateDevice(const QBluetoothDeviceInfo &info) +{ + QString address = info.address().toString(); + + // Check if device already exists + for (QObject* obj : m_devices) { + BluetoothDevice* device = qobject_cast(obj); + if (device && device->address() == address) { + device->update(info); + return; + } + } + + // New device + BluetoothDevice* device = new BluetoothDevice(info, this); + device->setPaired(m_pairedAddresses.contains(address)); + + // Check connection status + if (m_localDevice) { + QList connectedAddrs = m_localDevice->connectedDevices(); + device->setConnected(connectedAddrs.contains(info.address())); } + + m_devices.append(device); + emit devicesChanged(); +} - // If enabled, fetch connected device info (stubbed here, needs device discovery logic) - // For now, just reset connected state - if (!m_connected) { - m_connected = false; - emit bluetoothConnectedChanged(); +void BluetoothService::parseMonitorOutput() +{ + while (m_monitorProcess->canReadLine()) { + QString line = QString::fromUtf8(m_monitorProcess->readLine()).trimmed(); + + // Strip color codes if present (bluetoothctl uses them) + static QRegularExpression colorRegex("\x1B\\[[0-9;]*[mK]"); + line.remove(colorRegex); + + // Check for discovery of the pending device + if (!m_pendingPairAddress.isEmpty()) { + if (line.startsWith("[NEW] Device") || line.startsWith("[CHG] Device")) { + QStringList parts = line.split(' ', Qt::SkipEmptyParts); + if (parts.size() >= 3) { + QString address = parts[2]; + if (address == m_pendingPairAddress) { + // Found it! Pair immediately. + if (m_pairTimeoutTimer) m_pairTimeoutTimer->stop(); + m_pendingPairAddress.clear(); + + m_monitorProcess->write(QString("pair %1\n").arg(address).toUtf8()); + m_monitorProcess->write(QString("trust %1\n").arg(address).toUtf8()); + m_monitorProcess->write(QString("connect %1\n").arg(address).toUtf8()); + + // Turn off scan if we weren't discovering + if (!m_discovering) { + m_monitorProcess->write("scan off\n"); + } + } + } + } + } + + // Parse device state changes from bluetoothctl output + if (line.startsWith("[CHG] Device")) { + QStringList parts = line.split(' ', Qt::SkipEmptyParts); + if (parts.size() >= 5) { + QString address = parts[2]; + QString property = parts[3]; + QString value = parts[4]; + + if (property == "Connected:") { + bool isConnected = (value == "yes"); + bool found = false; + for (QObject* obj : m_devices) { + BluetoothDevice* device = qobject_cast(obj); + if (device && device->address() == address) { + device->setConnected(isConnected); + found = true; + break; + } + } + if (!found && isConnected) { + // Device connected but not in list? Add it. + BluetoothDevice* device = new BluetoothDevice(address, address, this); + device->setConnected(true); + device->setPaired(m_pairedAddresses.contains(address)); + m_devices.append(device); + emit devicesChanged(); + } + } else if (property == "Paired:") { + bool paired = (value == "yes"); + for (QObject* obj : m_devices) { + BluetoothDevice* device = qobject_cast(obj); + if (device && device->address() == address) { + device->setPaired(paired); + break; + } + } + if (paired && !m_pairedAddresses.contains(address)) { + m_pairedAddresses.append(address); + } else if (!paired) { + m_pairedAddresses.removeAll(address); + } + } + } + } else if (line.startsWith("[DEL] Device")) { + // Device removed from bluez (unpaired) + QStringList parts = line.split(' ', Qt::SkipEmptyParts); + if (parts.size() >= 3) { + QString address = parts[2]; + for (QObject* obj : m_devices) { + BluetoothDevice* device = qobject_cast(obj); + if (device && device->address() == address) { + device->setPaired(false); + device->setConnected(false); + break; + } + } + m_pairedAddresses.removeAll(address); + } + } else if (line.contains("Failed to connect")) { + // Handle connection failure + if (line.contains("In Progress") || line.contains("InProgress")) { + // Connection failed because it's already in progress, which is fine. + // We could add retry logic here if needed in the future. + } + } } } + +} // namespace sleex::services diff --git a/src/share/sleex/plugins/src/Sleex/services/bluetooth.hpp b/src/share/sleex/plugins/src/Sleex/services/bluetooth.hpp index 78fbaacd..54d07622 100644 --- a/src/share/sleex/plugins/src/Sleex/services/bluetooth.hpp +++ b/src/share/sleex/plugins/src/Sleex/services/bluetooth.hpp @@ -3,7 +3,52 @@ #include #include #include +#include #include +#include +#include + +namespace sleex::services { + +class BluetoothDevice : public QObject { + Q_OBJECT + QML_ELEMENT + QML_UNCREATABLE("Created by BluetoothService") + + Q_PROPERTY(QString name READ name NOTIFY nameChanged) + Q_PROPERTY(QString address READ address CONSTANT) + Q_PROPERTY(bool connected READ connected NOTIFY connectedChanged) + Q_PROPERTY(bool paired READ paired NOTIFY pairedChanged) + Q_PROPERTY(QString icon READ icon NOTIFY iconChanged) + +public: + explicit BluetoothDevice(const QBluetoothDeviceInfo &info, QObject *parent = nullptr); + explicit BluetoothDevice(const QString &name, const QString &address, QObject *parent = nullptr); + + QString name() const { return m_name; } + QString address() const { return m_address; } + bool connected() const { return m_connected; } + bool paired() const { return m_paired; } + QString icon() const; + + void update(const QBluetoothDeviceInfo &info); + void setConnected(bool connected); + void setPaired(bool paired); + +signals: + void nameChanged(); + void connectedChanged(); + void pairedChanged(); + void iconChanged(); + +private: + QBluetoothDeviceInfo m_info; + QString m_name; + QString m_address; + bool m_connected = false; + bool m_paired = false; + bool m_hasInfo = false; +}; class BluetoothService : public QObject { Q_OBJECT @@ -11,34 +56,52 @@ class BluetoothService : public QObject { QML_NAMED_ELEMENT(BluetoothService) Q_PROPERTY(bool bluetoothAvailable READ bluetoothAvailable NOTIFY bluetoothAvailableChanged FINAL) - Q_PROPERTY(bool bluetoothEnabled READ bluetoothEnabled NOTIFY bluetoothEnabledChanged FINAL) - Q_PROPERTY(bool bluetoothConnected READ bluetoothConnected NOTIFY bluetoothConnectedChanged FINAL) - Q_PROPERTY(QString bluetoothDeviceName READ bluetoothDeviceName NOTIFY bluetoothDeviceNameChanged FINAL) - Q_PROPERTY(QString bluetoothDeviceAddress READ bluetoothDeviceAddress NOTIFY bluetoothDeviceAddressChanged FINAL) + Q_PROPERTY(bool bluetoothEnabled READ bluetoothEnabled WRITE setBluetoothEnabled NOTIFY bluetoothEnabledChanged FINAL) + Q_PROPERTY(QList devices READ devices NOTIFY devicesChanged FINAL) + Q_PROPERTY(bool discovering READ discovering WRITE setDiscovering NOTIFY discoveringChanged FINAL) public: explicit BluetoothService(QObject *parent = nullptr); bool bluetoothAvailable() const { return m_available; } bool bluetoothEnabled() const { return m_enabled; } - bool bluetoothConnected() const { return m_connected; } - QString bluetoothDeviceName() const { return m_deviceName; } - QString bluetoothDeviceAddress() const { return m_deviceAddress; } + void setBluetoothEnabled(bool enabled); + + bool discovering() const { return m_discovering; } + void setDiscovering(bool discovering); + + QList devices() const { return m_devices; } - Q_INVOKABLE void update(); + Q_INVOKABLE void connectDevice(const QString &address); + Q_INVOKABLE void disconnectDevice(const QString &address); + Q_INVOKABLE void pairDevice(const QString &address); + Q_INVOKABLE void unpairDevice(const QString &address); + Q_INVOKABLE void trustDevice(const QString &address); + Q_INVOKABLE void setDiscovery(bool discover); + Q_INVOKABLE void refreshPairedDevices(); signals: void bluetoothAvailableChanged(); void bluetoothEnabledChanged(); - void bluetoothConnectedChanged(); - void bluetoothDeviceNameChanged(); - void bluetoothDeviceAddressChanged(); + void devicesChanged(); + void discoveringChanged(); private: + void addOrUpdateDevice(const QBluetoothDeviceInfo &info); + void updateDeviceConnectionState(const QString &address, bool connected); + void checkConnectedDevices(); + void parseMonitorOutput(); + QBluetoothLocalDevice *m_localDevice = nullptr; + QBluetoothDeviceDiscoveryAgent *m_discoveryAgent = nullptr; + QProcess *m_monitorProcess = nullptr; bool m_available = false; bool m_enabled = false; - bool m_connected = false; - QString m_deviceName; - QString m_deviceAddress; + bool m_discovering = false; + QList m_devices; + QStringList m_pairedAddresses; + QString m_pendingPairAddress; + QTimer *m_pairTimeoutTimer = nullptr; }; + +} // namespace sleex::services