diff options
Diffstat (limited to 'examples/bluetooth/lowenergyscanner')
18 files changed, 1050 insertions, 0 deletions
diff --git a/examples/bluetooth/lowenergyscanner/Scanner/Characteristics.qml b/examples/bluetooth/lowenergyscanner/Scanner/Characteristics.qml new file mode 100644 index 000000000..bd3ccbfcb --- /dev/null +++ b/examples/bluetooth/lowenergyscanner/Scanner/Characteristics.qml @@ -0,0 +1,121 @@ +// Copyright (C) 2013 BlackBerry Limited. All rights reserved. +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +pragma ComponentBehavior: Bound +import QtQuick + +Rectangle { + id: characteristicsPage + + signal showServices + signal showDevices + + width: 300 + height: 600 + + Header { + id: header + anchors.top: parent.top + headerText: "Characteristics list" + } + + Dialog { + id: info + anchors.centerIn: parent + visible: true + dialogText: "Scanning for characteristics..." + } + + Connections { + target: Device + function oncharacteristics_pdated() { + menu.menuText = "Back" + if (characteristicview.count === 0) { + info.dialogText = "No characteristic found" + info.busyImage = false + } else { + info.visible = false + info.busyImage = true + } + } + + function onDisconnected() { + characteristicsPage.showDevices() + } + } + + ListView { + id: characteristicview + width: parent.width + clip: true + + anchors.top: header.bottom + anchors.bottom: menu.top + model: Device.characteristicList + + delegate: Rectangle { + required property var modelData + id: box + height: 300 + width: characteristicview.width + color: "lightsteelblue" + border.width: 2 + border.color: "black" + radius: 5 + + Label { + id: characteristicName + textContent: box.modelData.characteristic_name + anchors.top: parent.top + anchors.topMargin: 5 + } + + Label { + id: characteristicUuid + font.pointSize: characteristicName.font.pointSize * 0.7 + textContent: box.modelData.characteristic_uuid + anchors.top: characteristicName.bottom + anchors.topMargin: 5 + } + + Label { + id: characteristicValue + font.pointSize: characteristicName.font.pointSize * 0.7 + textContent: ("Value: " + box.modelData.characteristic_value) + anchors.bottom: characteristicHandle.top + horizontalAlignment: Text.AlignHCenter + anchors.topMargin: 5 + } + + Label { + id: characteristicHandle + font.pointSize: characteristicName.font.pointSize * 0.7 + textContent: ("Handlers: " + box.modelData.characteristic_handle) + anchors.bottom: characteristicPermission.top + anchors.topMargin: 5 + } + + Label { + id: characteristicPermission + font.pointSize: characteristicName.font.pointSize * 0.7 + textContent: box.modelData.characteristic_permission + anchors.bottom: parent.bottom + anchors.topMargin: 5 + anchors.bottomMargin: 5 + } + } + } + + Menu { + id: menu + anchors.bottom: parent.bottom + menuWidth: parent.width + menuText: Device.update + menuHeight: (parent.height / 6) + onButtonClick: { + characteristicsPage.showServices() + Device.update = "Back" + } + } +} diff --git a/examples/bluetooth/lowenergyscanner/Scanner/Devices.qml b/examples/bluetooth/lowenergyscanner/Scanner/Devices.qml new file mode 100644 index 000000000..6e5e85a52 --- /dev/null +++ b/examples/bluetooth/lowenergyscanner/Scanner/Devices.qml @@ -0,0 +1,121 @@ +// Copyright (C) 2013 BlackBerry Limited. All rights reserved. +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +pragma ComponentBehavior: Bound +import QtQuick + +Rectangle { + id: devicesPage + + property bool deviceState: Device.state + signal showServices + + width: 300 + height: 600 + + onDeviceStateChanged: { + if (!Device.state) + info.visible = false + } + + Header { + id: header + anchors.top: parent.top + headerText: { + if (Device.state) + return "Discovering" + + if (Device.devices_list.length > 0) + return "Select a device" + + return "Start Discovery" + } + } + + Dialog { + id: info + anchors.centerIn: parent + visible: false + } + + ListView { + id: theListView + width: parent.width + clip: true + + anchors.top: header.bottom + anchors.bottom: connectToggle.top + model: Device.devices_list + + delegate: Rectangle { + required property var modelData + id: box + height: 100 + width: theListView.width + color: "lightsteelblue" + border.width: 2 + border.color: "black" + radius: 5 + + MouseArea { + anchors.fill: parent + onClicked: { + Device.scan_services(box.modelData.device_address) + showServices() + } + } + + Label { + id: deviceName + textContent: box.modelData.device_name + anchors.top: parent.top + anchors.topMargin: 5 + } + + Label { + id: deviceAddress + textContent: box.modelData.device_address + font.pointSize: deviceName.font.pointSize * 0.7 + anchors.bottom: box.bottom + anchors.bottomMargin: 5 + } + } + } + + Menu { + id: connectToggle + + menuWidth: parent.width + anchors.bottom: menu.top + menuText: { + visible = Device.devices_list.length > 0 + if (Device.use_random_address) + return "Address type: Random" + else + return "Address type: Public" + } + + onButtonClick: Device.use_random_address = !Device.use_random_address + } + + Menu { + id: menu + anchors.bottom: parent.bottom + menuWidth: parent.width + menuHeight: (parent.height / 6) + menuText: Device.update + onButtonClick: { + if (!Device.state) { + Device.start_device_discovery() + // if start_device_discovery() failed Device.state is not set + if (Device.state) { + info.dialogText = "Searching..." + info.visible = true + } + } else { + Device.stop_device_discovery() + } + } + } +} diff --git a/examples/bluetooth/lowenergyscanner/Scanner/Dialog.qml b/examples/bluetooth/lowenergyscanner/Scanner/Dialog.qml new file mode 100644 index 000000000..75e82642a --- /dev/null +++ b/examples/bluetooth/lowenergyscanner/Scanner/Dialog.qml @@ -0,0 +1,48 @@ +// Copyright (C) 2013 BlackBerry Limited. All rights reserved. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick + +Rectangle { + id: dialog + width: parent.width / 3 * 2 + height: dialogTextId.height + background.height + 20 + z: 50 + property string dialogText: "" + property bool busyImage: true + border.width: 1 + border.color: "#363636" + radius: 10 + + Text { + id: dialogTextId + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + anchors.topMargin: 10 + + elide: Text.ElideMiddle + text: dialog.dialogText + color: "#363636" + wrapMode: Text.Wrap + } + + Image { + id: background + + width: 20 + height: 20 + anchors.top: dialogTextId.bottom + anchors.horizontalCenter: dialogTextId.horizontalCenter + visible: parent.busyImage + source: "assets/busy_dark.png" + fillMode: Image.PreserveAspectFit + NumberAnimation on rotation { + duration: 3000 + from: 0 + to: 360 + loops: Animation.Infinite + } + } +} diff --git a/examples/bluetooth/lowenergyscanner/Scanner/Header.qml b/examples/bluetooth/lowenergyscanner/Scanner/Header.qml new file mode 100644 index 000000000..c95385dd3 --- /dev/null +++ b/examples/bluetooth/lowenergyscanner/Scanner/Header.qml @@ -0,0 +1,25 @@ +// Copyright (C) 2013 BlackBerry Limited. All rights reserved. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick + +Rectangle { + id: header + width: parent.width + height: 70 + border.width: 1 + border.color: "#363636" + radius: 5 + property string headerText: "" + + Text { + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + anchors.fill: parent + text: header.headerText + font.bold: true + font.pointSize: 20 + elide: Text.ElideMiddle + color: "#363636" + } +} diff --git a/examples/bluetooth/lowenergyscanner/Scanner/Label.qml b/examples/bluetooth/lowenergyscanner/Scanner/Label.qml new file mode 100644 index 000000000..e31156740 --- /dev/null +++ b/examples/bluetooth/lowenergyscanner/Scanner/Label.qml @@ -0,0 +1,16 @@ +// Copyright (C) 2013 BlackBerry Limited. All rights reserved. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick + +Text { + property string textContent: "" + font.pointSize: 20 + anchors.horizontalCenter: parent.horizontalCenter + color: "#363636" + horizontalAlignment: Text.AlignHCenter + elide: Text.ElideMiddle + width: parent.width + wrapMode: Text.Wrap + text: textContent +} diff --git a/examples/bluetooth/lowenergyscanner/Scanner/Main.qml b/examples/bluetooth/lowenergyscanner/Scanner/Main.qml new file mode 100644 index 000000000..88600bace --- /dev/null +++ b/examples/bluetooth/lowenergyscanner/Scanner/Main.qml @@ -0,0 +1,31 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import QtQuick.Layouts + +Window { + id: main + + width: 300 + height: 600 + visible: true + + StackLayout { + id: pagesLayout + anchors.fill: parent + currentIndex: 0 + + Devices { + onShowServices: pagesLayout.currentIndex = 1 + } + Services { + onShowDevices: pagesLayout.currentIndex = 0 + onShowCharacteristics: pagesLayout.currentIndex = 2 + } + Characteristics { + onShowDevices: pagesLayout.currentIndex = 0 + onShowServices: pagesLayout.currentIndex = 1 + } + } +} diff --git a/examples/bluetooth/lowenergyscanner/Scanner/Menu.qml b/examples/bluetooth/lowenergyscanner/Scanner/Menu.qml new file mode 100644 index 000000000..ef69c895e --- /dev/null +++ b/examples/bluetooth/lowenergyscanner/Scanner/Menu.qml @@ -0,0 +1,55 @@ +// Copyright (C) 2013 BlackBerry Limited. All rights reserved. +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick + +Rectangle { + id: menu + + property real menuWidth: 100 + property real menuHeight: 50 + property string menuText: "Search" + signal buttonClick + + height: menuHeight + width: menuWidth + + Rectangle { + id: search + width: parent.width + height: parent.height + anchors.centerIn: parent + color: "#363636" + border.width: 1 + border.color: "#E3E3E3" + radius: 5 + Text { + id: searchText + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + anchors.fill: parent + text: menu.menuText + elide: Text.ElideMiddle + color: "#E3E3E3" + wrapMode: Text.WordWrap + } + + MouseArea { + anchors.fill: parent + onPressed: { + search.width = search.width - 7 + search.height = search.height - 5 + } + + onReleased: { + search.width = search.width + 7 + search.height = search.height + 5 + } + + onClicked: { + menu.buttonClick() + } + } + } +} diff --git a/examples/bluetooth/lowenergyscanner/Scanner/Services.qml b/examples/bluetooth/lowenergyscanner/Scanner/Services.qml new file mode 100644 index 000000000..70326242e --- /dev/null +++ b/examples/bluetooth/lowenergyscanner/Scanner/Services.qml @@ -0,0 +1,115 @@ +// Copyright (C) 2013 BlackBerry Limited. All rights reserved. +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +pragma ComponentBehavior: Bound +import QtQuick + +Rectangle { + id: servicesPage + + signal showCharacteristics + signal showDevices + + width: 300 + height: 600 + + Component.onCompleted: { + // Loading this page may take longer than QLEController + // stopping with an error, go back and readjust this view + // based on controller errors + if (Device.controller_error) { + info.visible = false + menu.menuText = Device.update + } + } + + Header { + id: header + anchors.top: parent.top + headerText: "Services list" + } + + Dialog { + id: info + anchors.centerIn: parent + visible: true + dialogText: "Scanning for services..." + } + + Connections { + target: Device + function onservices_updated() { + if (servicesview.count === 0) + info.dialogText = "No services found" + else + info.visible = false + } + + function ondisconnected() { + servicesPage.showDevices() + } + } + + ListView { + id: servicesview + width: parent.width + anchors.top: header.bottom + anchors.bottom: menu.top + model: Device.servicesList + clip: true + + delegate: Rectangle { + required property var modelData + id: box + height: 100 + color: "lightsteelblue" + border.width: 2 + border.color: "black" + radius: 5 + width: servicesview.width + + MouseArea { + anchors.fill: parent + onClicked: { + Device.connectToService(box.modelData.service_uuid) + servicesPage.showCharacteristics() + } + } + + Label { + id: serviceName + textContent: box.modelData.service_name + anchors.top: parent.top + anchors.topMargin: 5 + } + + Label { + textContent: box.modelData.service_type + font.pointSize: serviceName.font.pointSize * 0.5 + anchors.top: serviceName.bottom + } + + Label { + id: serviceUuid + font.pointSize: serviceName.font.pointSize * 0.5 + textContent: box.modelData.service_uuid + anchors.bottom: box.bottom + anchors.bottomMargin: 5 + } + } + } + + Menu { + id: menu + anchors.bottom: parent.bottom + menuWidth: parent.width + menuText: Device.update + menuHeight: (parent.height / 6) + onButtonClick: { + Device.disconnect_from_device() + servicesPage.showDevices() + Device.update = "Search" + } + } +} diff --git a/examples/bluetooth/lowenergyscanner/Scanner/assets/busy_dark.png b/examples/bluetooth/lowenergyscanner/Scanner/assets/busy_dark.png Binary files differnew file mode 100644 index 000000000..3a1059531 --- /dev/null +++ b/examples/bluetooth/lowenergyscanner/Scanner/assets/busy_dark.png diff --git a/examples/bluetooth/lowenergyscanner/Scanner/qmldir b/examples/bluetooth/lowenergyscanner/Scanner/qmldir new file mode 100644 index 000000000..0adf6fb19 --- /dev/null +++ b/examples/bluetooth/lowenergyscanner/Scanner/qmldir @@ -0,0 +1,10 @@ +module Scanner +typeinfo scanner.qmltypes +Characteristics 1.0 Characteristics.qml +Devices 1.0 Devices.qml +Dialog 1.0 Dialog.qml +Header 1.0 Header.qml +Label 1.0 Label.qml +Main 1.0 Main.qml +Menu 1.0 Menu.qml +Services 1.0 Services.qml diff --git a/examples/bluetooth/lowenergyscanner/characteristicinfo.py b/examples/bluetooth/lowenergyscanner/characteristicinfo.py new file mode 100644 index 000000000..42bde8753 --- /dev/null +++ b/examples/bluetooth/lowenergyscanner/characteristicinfo.py @@ -0,0 +1,87 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +from PySide6.QtCore import QObject, Property, Signal +from PySide6.QtBluetooth import QLowEnergyCharacteristic, QBluetoothUuid + + +class CharacteristicInfo(QObject): + + characteristic_changed = Signal() + + def __init__(self, characteristic=None) -> None: + super().__init__() + self._characteristic = characteristic + + @Property(str, notify=characteristic_changed) + def characteristic_name(self): + if not self.characteristic: + raise Exception("characteristic unset") + name = self.characteristic.name() + if name: + return name + + for descriptor in self.characteristic.descriptors(): + if descriptor.type() == QBluetoothUuid.DescriptorType.CharacteristicUserDescription: + name = descriptor.value() + break + + if not name: + name = "Unknown" + + return name + + @Property(str, notify=characteristic_changed) + def characteristic_uuid(self): + uuid = self.characteristic.uuid() + result16, success16 = uuid.toUInt16() + if success16: + return f"0x{result16:x}" + + result32, sucess32 = uuid.toUInt32() + if sucess32: + return f"0x{result32:x}" + + return uuid.toString().replace('{', '').replace('}', '') + + @Property(str, notify=characteristic_changed) + def characteristic_value(self): + # Show raw string first and hex value below + a = self.characteristic.value() + if not a: + return "<none>" + + result = f"{str(a)}\n{str(a.toHex())}" + return result + + @Property(str, notify=characteristic_changed) + def characteristic_permission(self): + properties = "( " + permission = self.characteristic.properties() + if (permission & QLowEnergyCharacteristic.Read): + properties += " Read" + if (permission & QLowEnergyCharacteristic.Write): + properties += " Write" + if (permission & QLowEnergyCharacteristic.Notify): + properties += " Notify" + if (permission & QLowEnergyCharacteristic.Indicate): + properties += " Indicate" + if (permission & QLowEnergyCharacteristic.ExtendedProperty): + properties += " ExtendedProperty" + if (permission & QLowEnergyCharacteristic.Broadcasting): + properties += " Broadcast" + if (permission & QLowEnergyCharacteristic.WriteNoResponse): + properties += " WriteNoResp" + if (permission & QLowEnergyCharacteristic.WriteSigned): + properties += " WriteSigned" + properties += " )" + return properties + + @property + def characteristic(self): + return self._characteristic + + @characteristic.setter + def characteristic(self, characteristic): + self._characteristic = characteristic + self.characteristic_changed.emit() diff --git a/examples/bluetooth/lowenergyscanner/device.py b/examples/bluetooth/lowenergyscanner/device.py new file mode 100644 index 000000000..09108cf69 --- /dev/null +++ b/examples/bluetooth/lowenergyscanner/device.py @@ -0,0 +1,278 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause +import warnings +from PySide6.QtBluetooth import (QBluetoothDeviceDiscoveryAgent, QLowEnergyController, + QBluetoothDeviceInfo, QBluetoothUuid, QLowEnergyService) +from PySide6.QtCore import QObject, Property, Signal, Slot, QTimer, QMetaObject, Qt +from PySide6.QtQml import QmlElement, QmlSingleton + +from deviceinfo import DeviceInfo +from serviceinfo import ServiceInfo +from characteristicinfo import CharacteristicInfo + +QML_IMPORT_NAME = "Scanner" +QML_IMPORT_MAJOR_VERSION = 1 + + +@QmlElement +@QmlSingleton +class Device(QObject): + + devices_updated = Signal() + services_updated = Signal() + characteristic_updated = Signal() + update_changed = Signal() + state_changed = Signal() + disconnected = Signal() + random_address_changed = Signal() + + def __init__(self, parent=None) -> None: + super().__init__(parent) + self.devices = [] + self._services = [] + self._characteristics = [] + self._previousAddress = "" + self._message = "" + self.currentDevice = DeviceInfo() + self.connected = False + self.controller: QLowEnergyController = None + self._deviceScanState = False + self.random_address = False + self.discovery_agent = QBluetoothDeviceDiscoveryAgent() + self.discovery_agent.setLowEnergyDiscoveryTimeout(25000) + self.discovery_agent.deviceDiscovered.connect(self.add_device) + self.discovery_agent.errorOccurred.connect(self.device_scan_error) + self.discovery_agent.finished.connect(self.device_scan_finished) + self.update = "Search" + + @Property("QVariant", notify=devices_updated) + def devices_list(self): + return self.devices + + @Property("QVariant", notify=services_updated) + def services_list(self): + return self._services + + @Property("QVariant", notify=characteristic_updated) + def characteristic_list(self): + return self._characteristics + + @Property(str, notify=update_changed) + def update(self): + return self._message + + @update.setter + def update(self, message): + self._message = message + self.update_changed.emit() + + @Property(bool, notify=random_address_changed) + def use_random_address(self): + return self.random_address + + @use_random_address.setter + def use_random_address(self, newValue): + self.random_address = newValue + self.random_address_changed.emit() + + @Property(bool, notify=state_changed) + def state(self): + return self._deviceScanState + + @Property(bool) + def controller_error(self): + return self.controller and (self.controller.error() != QLowEnergyController.NoError) + + @Slot() + def start_device_discovery(self): + self.devices.clear() + self.devices_updated.emit() + self.update = "Scanning for devices ..." + self.discovery_agent.start(QBluetoothDeviceDiscoveryAgent.LowEnergyMethod) + + if self.discovery_agent.isActive(): + self._deviceScanState = True + self.state_changed.emit() + + @Slot(str) + def scan_services(self, address): + # We need the current device for service discovery. + for device in self.devices: + if device.device_address == address: + self.currentDevice.set_device(device.get_device()) + break + + if not self.currentDevice.get_device().isValid(): + warnings.warn("Not a valid device") + return + + self._characteristics.clear() + self.characteristic_updated.emit() + self._services.clear() + self.services_updated.emit() + + self.update = "Back\n(Connecting to device...)" + + if self.controller and (self._previousAddress != self.currentDevice.device_address): + self.controller.disconnectFromDevice() + del self.controller + self.controller = None + + if not self.controller: + self.controller = QLowEnergyController.createCentral(self.currentDevice.get_device()) + self.controller.connected.connect(self.device_connected) + self.controller.errorOccurred.connect(self.error_received) + self.controller.disconnected.connect(self.device_disconnected) + self.controller.serviceDiscovered.connect(self.add_low_energy_service) + self.controller.discoveryFinished.connect(self.services_scan_done) + + if self.random_address: + self.controller.setRemoteAddressType(QLowEnergyController.RandomAddress) + else: + self.controller.setRemoteAddressType(QLowEnergyController.PublicAddress) + self.controller.connectToDevice() + + self._previousAddress = self.currentDevice.device_address + + @Slot(str) + def connect_to_service(self, uuid): + service: QLowEnergyService = None + for serviceInfo in self._services: + if not serviceInfo: + continue + + if serviceInfo.service_uuid == uuid: + service = serviceInfo.service + break + + if not service: + return + + self._characteristics.clear() + self.characteristic_updated.emit() + + if service.state() == QLowEnergyService.RemoteService: + service.state_changed.connect(self.service_details_discovered) + service.discoverDetails() + self.update = "Back\n(Discovering details...)" + return + + # discovery already done + chars = service.characteristics() + for ch in chars: + cInfo = CharacteristicInfo(ch) + self._characteristics.append(cInfo) + + QTimer.singleShot(0, self.characteristic_updated) + + @Slot() + def disconnect_from_device(self): + # UI always expects disconnect() signal when calling this signal + # TODO what is really needed is to extend state() to a multi value + # and thus allowing UI to keep track of controller progress in addition to + # device scan progress + + if self.controller.state() != QLowEnergyController.UnconnectedState: + self.controller.disconnectFromDevice() + else: + self.device_disconnected() + + @Slot(QBluetoothDeviceInfo) + def add_device(self, info): + if info.coreConfigurations() & QBluetoothDeviceInfo.LowEnergyCoreConfiguration: + self.update = "Last device added: " + info.name() + + @Slot() + def device_scan_finished(self): + foundDevices = self.discovery_agent.discoveredDevices() + for nextDevice in foundDevices: + if nextDevice.coreConfigurations() & QBluetoothDeviceInfo.LowEnergyCoreConfiguration: + device = DeviceInfo(nextDevice) + self.devices.append(device) + + self.devices_updated.emit() + self._deviceScanState = False + self.state_changed.emit() + if not self.devices: + self.update = "No Low Energy devices found..." + else: + self.update = "Done! Scan Again!" + + @Slot("QBluetoothDeviceDiscovertAgent::Error") + def device_scan_error(self, error): + if error == QBluetoothDeviceDiscoveryAgent.PoweredOffError: + self.update = ( + "The Bluetooth adaptor is powered off, power it on before doing discovery." + ) + elif error == QBluetoothDeviceDiscoveryAgent.InputOutputError: + self.update = "Writing or reading from the device resulted in an error." + else: + qme = self.discovery_agent.metaObject().enumerator( + self.discovery_agent.metaObject().indexOfEnumerator("Error") + ) + self.update = f"Error: {qme.valueToKey(error)}" + + self._deviceScanState = False + self.devices_updated.emit() + self.state_changed.emit() + + @Slot(QBluetoothUuid) + def add_low_energy_service(self, service_uuid): + service = self.controller.createServiceObject(service_uuid) + if not service: + warnings.warn("Cannot create service from uuid") + return + + serv = ServiceInfo(service) + self._services.append(serv) + self.services_updated.emit() + + @Slot() + def device_connected(self): + self.update = "Back\n(Discovering services...)" + self.connected = True + self.controller.discoverServices() + + @Slot("QLowEnergyController::Error") + def error_received(self, error): + warnings.warn(f"Error: {self.controller.errorString()}") + self.update = f"Back\n({self.controller.errorString()})" + + @Slot() + def services_scan_done(self): + self.update = "Back\n(Service scan done!)" + # force UI in case we didn't find anything + if not self._services: + self.services_updated.emit() + + @Slot() + def device_disconnected(self): + warnings.warn("Disconnect from Device") + self.disconnected.emit() + + @Slot("QLowEnergyService::ServiceState") + def service_details_discovered(self, newState): + if newState != QLowEnergyService.RemoteServiceDiscovered: + # do not hang in "Scanning for characteristics" mode forever + # in case the service discovery failed + # We have to queue the signal up to give UI time to even enter + # the above mode + if newState != QLowEnergyService.RemoteServiceDiscovering: + QMetaObject.invokeMethod(self.characteristic_updated, Qt.QueuedConnection) + return + + service = self.sender() + if not service: + return + + chars = service.characteristics() + for ch in chars: + cInfo = CharacteristicInfo(ch) + self._characteristics.append(cInfo) + + self.characteristic_updated.emit() + + @Slot() + def stop_device_discovery(self): + if self.discovery_agent.isActive(): + self.discovery_agent.stop() diff --git a/examples/bluetooth/lowenergyscanner/deviceinfo.py b/examples/bluetooth/lowenergyscanner/deviceinfo.py new file mode 100644 index 000000000..35a568821 --- /dev/null +++ b/examples/bluetooth/lowenergyscanner/deviceinfo.py @@ -0,0 +1,34 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import sys + +from PySide6.QtCore import QObject, Property, Signal +from PySide6.QtBluetooth import QBluetoothDeviceInfo + + +class DeviceInfo(QObject): + + device_changed = Signal() + + def __init__(self, d: QBluetoothDeviceInfo = None) -> None: + super().__init__() + self._device = d + + @Property(str, notify=device_changed) + def device_name(self): + return self._device.name() + + @Property(str, notify=device_changed) + def device_address(self): + if sys.platform == "darwin": + return self._device.deviceUuid().toString() + + return self._device.address().toString() + + def get_device(self): + return self._device + + def set_device(self, device): + self._device = device + self.device_changed.emit() diff --git a/examples/bluetooth/lowenergyscanner/doc/lowenergyscanner.png b/examples/bluetooth/lowenergyscanner/doc/lowenergyscanner.png Binary files differnew file mode 100644 index 000000000..29f41deb4 --- /dev/null +++ b/examples/bluetooth/lowenergyscanner/doc/lowenergyscanner.png diff --git a/examples/bluetooth/lowenergyscanner/doc/lowenergyscanner.rst b/examples/bluetooth/lowenergyscanner/doc/lowenergyscanner.rst new file mode 100644 index 000000000..a0c574350 --- /dev/null +++ b/examples/bluetooth/lowenergyscanner/doc/lowenergyscanner.rst @@ -0,0 +1,11 @@ +Bluetooth Low Energy Scanner Example +==================================== + +.. tags:: Android + +A Python application that demonstrates the analogous example in Qt +`Bluetooth Low Energy Scanner <https://doc.qt.io/qt-6/qtbluetooth-lowenergyscanner-example.html>`_ + +.. image:: lowenergyscanner.png + :width: 400 + :alt: lowenergyscanner screenshot diff --git a/examples/bluetooth/lowenergyscanner/lowenergyscanner.pyproject b/examples/bluetooth/lowenergyscanner/lowenergyscanner.pyproject new file mode 100644 index 000000000..7e0cfa3d9 --- /dev/null +++ b/examples/bluetooth/lowenergyscanner/lowenergyscanner.pyproject @@ -0,0 +1,7 @@ +{ + "files": ["main.py", "device.py", "deviceinfo.py", "serviceinfo.py", "characteristicinfo.py", + "Scanner/Main.qml", "Scanner/Menu.qml","Scanner/Header.qml", + "Scanner/Characteristics.qml", "Scanner/Dialog.qml", "Scanner/Services.qml", + "Scanner/Label.qml", "Scanner/Devices.qml", "Scanner/assets/busy_dark.png", + "Scanner/qmldir"] +} diff --git a/examples/bluetooth/lowenergyscanner/main.py b/examples/bluetooth/lowenergyscanner/main.py new file mode 100644 index 000000000..ec12f99e7 --- /dev/null +++ b/examples/bluetooth/lowenergyscanner/main.py @@ -0,0 +1,27 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +"""PySide6 port of the bluetooth/lowenergyscanner example from Qt v6.x""" + + +import sys + +from PySide6.QtCore import QCoreApplication +from PySide6.QtGui import QGuiApplication +from PySide6.QtQml import QQmlApplicationEngine + +from device import Device # noqa: F401 +from pathlib import Path + +if __name__ == '__main__': + app = QGuiApplication(sys.argv) + engine = QQmlApplicationEngine() + engine.addImportPath(Path(__file__).parent) + engine.loadFromModule("Scanner", "Main") + + if not engine.rootObjects(): + sys.exit(-1) + + ex = QCoreApplication.exec() + del engine + sys.exit(ex) diff --git a/examples/bluetooth/lowenergyscanner/serviceinfo.py b/examples/bluetooth/lowenergyscanner/serviceinfo.py new file mode 100644 index 000000000..cddffe663 --- /dev/null +++ b/examples/bluetooth/lowenergyscanner/serviceinfo.py @@ -0,0 +1,64 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +from PySide6.QtCore import QObject, Property, Signal +from PySide6.QtBluetooth import QLowEnergyService + + +class ServiceInfo(QObject): + + service_changed = Signal() + + def __init__(self, service: QLowEnergyService) -> None: + super().__init__() + self._service = service + self.service.setParent(self) + + @Property(str, notify=service_changed) + def service_name(self): + if not self.service: + return "" + + return self.service.service_name() + + @Property(str, notify=service_changed) + def service_type(self): + if not self.service: + return "" + + result = "" + if (self.service.type() & QLowEnergyService.PrimaryService): + result += "primary" + else: + result += "secondary" + + if (self.service.type() & QLowEnergyService.IncludedService): + result += " included" + + result = '<' + result + '>' + + return result + + @Property(str, notify=service_changed) + def service_uuid(self): + if not self.service: + return "" + + uuid = self.service.service_uuid() + result16, success16 = uuid.toUInt16() + if success16: + return f"0x{result16:x}" + + result32, sucesss32 = uuid.toUInt32() + if sucesss32: + return f"0x{result32:x}" + + return uuid.toString().replace('{', '').replace('}', '') + + @property + def service(self): + return self._service + + @service.setter + def service(self, service): + self._service = service |