diff options
author | Shyamnath Premnadh <Shyamnath.Premnadh@qt.io> | 2023-01-12 17:02:04 +0100 |
---|---|---|
committer | Shyamnath Premnadh <Shyamnath.Premnadh@qt.io> | 2023-02-22 10:58:48 +0100 |
commit | 9a5a0310d73fddf88848ed73949237af255d828f (patch) | |
tree | 12e674a1f3707c752ba6e2a683f961cb663ab562 /examples | |
parent | 4a3f337d0154105f924e2e076d8f2cb63b3aca8e (diff) |
example: Add Low Energy Scanner - QtBluetooth
Task-number: PYSIDE-841
Task-number: PYSIDE-2194
Change-Id: I54dee9d9504c20c39742781ca9cb1d176568af86
Reviewed-by: Friedemann Kleint <Friedemann.Kleint@qt.io>
Reviewed-by: Cristian Maureira-Fredes <cristian.maureira-fredes@qt.io>
Diffstat (limited to 'examples')
18 files changed, 1809 insertions, 0 deletions
diff --git a/examples/bluetooth/lowenergyscanner/assets/Characteristics.qml b/examples/bluetooth/lowenergyscanner/assets/Characteristics.qml new file mode 100644 index 000000000..ae2f12ffc --- /dev/null +++ b/examples/bluetooth/lowenergyscanner/assets/Characteristics.qml @@ -0,0 +1,114 @@ +// 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 2.0 + +Rectangle { + 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 oncharacteristic_updated() { + menu.menuText = "Back" + if (characteristicview.count === 0) { + info.dialogText = "No characteristic found" + info.busyImage = false + } else { + info.visible = false + info.busyImage = true + } + } + + function onDisconnected() { + pageLoader.source = "qrc:/assets/main.qml" + } + } + + ListView { + id: characteristicview + width: parent.width + clip: true + + anchors.top: header.bottom + anchors.bottom: menu.top + model: device.characteristic_list + + delegate: Rectangle { + id: characteristicbox + height:300 + width: characteristicview.width + color: "lightsteelblue" + border.width: 2 + border.color: "black" + radius: 5 + + Label { + id: characteristic_name + textContent: modelData.characteristic_name + anchors.top: parent.top + anchors.topMargin: 5 + } + + Label { + id: characteristic_uuid + font.pointSize: characteristic_name.font.pointSize*0.7 + textContent: modelData.characteristic_uuid + anchors.top: characteristic_name.bottom + anchors.topMargin: 5 + } + + Label { + id: characteristic_value + font.pointSize: characteristic_name.font.pointSize*0.7 + textContent: ("Value: " + modelData.characteristic_value) + anchors.bottom: characteristicHandle.top + horizontalAlignment: Text.AlignHCenter + anchors.topMargin: 5 + } + + Label { + id: characteristicHandle + font.pointSize: characteristic_name.font.pointSize*0.7 + textContent: ("Handlers: " + modelData.characteristicHandle) + anchors.bottom: characteristic_permission.top + anchors.topMargin: 5 + } + + Label { + id: characteristic_permission + font.pointSize: characteristic_name.font.pointSize*0.7 + textContent: 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: { + pageLoader.source = "qrc:/assets/Services.qml" + device.update = "Back" + } + } +} diff --git a/examples/bluetooth/lowenergyscanner/assets/Dialog.qml b/examples/bluetooth/lowenergyscanner/assets/Dialog.qml new file mode 100644 index 000000000..bfe4eca19 --- /dev/null +++ b/examples/bluetooth/lowenergyscanner/assets/Dialog.qml @@ -0,0 +1,42 @@ +// Copyright (C) 2013 BlackBerry Limited. All rights reserved. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick 2.0 + +Rectangle { + 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: 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: "qrc:/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/assets/Header.qml b/examples/bluetooth/lowenergyscanner/assets/Header.qml new file mode 100644 index 000000000..51649be05 --- /dev/null +++ b/examples/bluetooth/lowenergyscanner/assets/Header.qml @@ -0,0 +1,24 @@ +// Copyright (C) 2013 BlackBerry Limited. All rights reserved. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick 2.0 + +Rectangle { + 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: headerText + font.bold: true + font.pointSize: 20 + elide: Text.ElideMiddle + color: "#363636" + } +} diff --git a/examples/bluetooth/lowenergyscanner/assets/Label.qml b/examples/bluetooth/lowenergyscanner/assets/Label.qml new file mode 100644 index 000000000..664aa9f45 --- /dev/null +++ b/examples/bluetooth/lowenergyscanner/assets/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 2.0 + +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/assets/Menu.qml b/examples/bluetooth/lowenergyscanner/assets/Menu.qml new file mode 100644 index 000000000..a7cd153c2 --- /dev/null +++ b/examples/bluetooth/lowenergyscanner/assets/Menu.qml @@ -0,0 +1,54 @@ +// 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 2.0 + +Rectangle { + + 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: 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: { + buttonClick() + } + } + } +} diff --git a/examples/bluetooth/lowenergyscanner/assets/Services.qml b/examples/bluetooth/lowenergyscanner/assets/Services.qml new file mode 100644 index 000000000..5b33bdcdf --- /dev/null +++ b/examples/bluetooth/lowenergyscanner/assets/Services.qml @@ -0,0 +1,111 @@ +// 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 2.0 + +Rectangle { + 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() { + pageLoader.source = "qrc:/assets/main.qml" + } + } + + ListView { + id: servicesview + width: parent.width + anchors.top: header.bottom + anchors.bottom: menu.top + model: device.services_list + clip: true + + delegate: Rectangle { + id: servicebox + height:100 + color: "lightsteelblue" + border.width: 2 + border.color: "black" + radius: 5 + width: servicesview.width + Component.onCompleted: { + info.visible = false + } + + MouseArea { + anchors.fill: parent + onClicked: { + pageLoader.source = "qrc:/assets/Characteristics.qml"; + device.connect_to_service(modelData.service_uuid); + } + } + + Label { + id: service_name + textContent: modelData.service_name + anchors.top: parent.top + anchors.topMargin: 5 + } + + Label { + textContent: modelData.service_type + font.pointSize: service_name.font.pointSize * 0.5 + anchors.top: service_name.bottom + } + + Label { + id: service_uuid + font.pointSize: service_name.font.pointSize * 0.5 + textContent: modelData.service_uuid + anchors.bottom: servicebox.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() + pageLoader.source = "qrc:/assets/main.qml" + device.update = "Search" + } + } +} diff --git a/examples/bluetooth/lowenergyscanner/assets/busy_dark.png b/examples/bluetooth/lowenergyscanner/assets/busy_dark.png Binary files differnew file mode 100644 index 000000000..3a1059531 --- /dev/null +++ b/examples/bluetooth/lowenergyscanner/assets/busy_dark.png diff --git a/examples/bluetooth/lowenergyscanner/assets/main.qml b/examples/bluetooth/lowenergyscanner/assets/main.qml new file mode 100644 index 000000000..01bf63cd8 --- /dev/null +++ b/examples/bluetooth/lowenergyscanner/assets/main.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 + +import QtQuick + +Rectangle { + id: back + width: 300 + height: 600 + property bool deviceState: device.state + onDevicestate_changed: { + if (!device.state) + info.visible = false; + } + + Header { + id: header + anchors.top: parent.top + headerText: "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 { + id: box + height:100 + width: theListView.width + color: "lightsteelblue" + border.width: 2 + border.color: "black" + radius: 5 + + Component.onCompleted: { + info.visible = false; + header.headerText = "Select a device"; + } + + MouseArea { + anchors.fill: parent + onClicked: { + device.scan_services(modelData.device_address); + pageLoader.source = "qrc:/assets/Services.qml" + } + } + + Label { + id: device_name + textContent: modelData.device_name + anchors.top: parent.top + anchors.topMargin: 5 + } + + Label { + id: device_address + textContent: modelData.device_address + font.pointSize: device_name.font.pointSize*0.7 + anchors.bottom: box.bottom + anchors.bottomMargin: 5 + } + } + } + + Menu { + id: connectToggle + + menuWidth: parent.width + anchors.bottom: menu.top + menuText: { if (device.devices_list.length) + visible = true + else + visible = false + if (device.use_random_address) + "Address type: Random" + else + "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: { + device.start_device_discovery(); + // if start_device_discovery() failed device.state is not set + if (device.state) { + info.dialogText = "Searching..."; + info.visible = true; + } + } + } + + Loader { + id: pageLoader + anchors.fill: parent + } +} diff --git a/examples/bluetooth/lowenergyscanner/characteristicinfo.py b/examples/bluetooth/lowenergyscanner/characteristicinfo.py new file mode 100644 index 000000000..a0e9df77e --- /dev/null +++ b/examples/bluetooth/lowenergyscanner/characteristicinfo.py @@ -0,0 +1,88 @@ +# 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..d4a2300bc --- /dev/null +++ b/examples/bluetooth/lowenergyscanner/device.py @@ -0,0 +1,268 @@ +# 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 deviceinfo import DeviceInfo +from serviceinfo import ServiceInfo +from characteristicinfo import CharacteristicInfo + + +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.disconnect_from_device() + 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.disconnect_from_device() + 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() + diff --git a/examples/bluetooth/lowenergyscanner/deviceinfo.py b/examples/bluetooth/lowenergyscanner/deviceinfo.py new file mode 100644 index 000000000..edcbef89d --- /dev/null +++ b/examples/bluetooth/lowenergyscanner/deviceinfo.py @@ -0,0 +1,35 @@ +# 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..842b0f713 --- /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..920b11587 --- /dev/null +++ b/examples/bluetooth/lowenergyscanner/doc/lowenergyscanner.rst @@ -0,0 +1,9 @@ +Bluetooth Low Energy Scanner Example +==================================== + +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..31fc9a651 --- /dev/null +++ b/examples/bluetooth/lowenergyscanner/lowenergyscanner.pyproject @@ -0,0 +1,6 @@ +{ + "files": ["main.py", "device.py", "deviceinfo.py", "serviceinfo.py", "characteristicinfo.py", + "assets/main.qml", "assets/Menu.qml","assets/Header.qml", + "assets/Characteristics.qml", "assets/Dialog.qml", "assets/Services.qml", + "assets/Label.qml", "assets/busy_dark.png", "resources.qrc"] +} diff --git a/examples/bluetooth/lowenergyscanner/main.py b/examples/bluetooth/lowenergyscanner/main.py new file mode 100644 index 000000000..58eb3175b --- /dev/null +++ b/examples/bluetooth/lowenergyscanner/main.py @@ -0,0 +1,33 @@ +# 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 +import os + +from PySide6.QtCore import QUrl +from PySide6.QtGui import QGuiApplication +from PySide6.QtQuick import QQuickView + +from device import Device +from pathlib import Path + +import rc_resources + +if __name__ == '__main__': + app = QGuiApplication(sys.argv) + d = Device() + view = QQuickView() + view.rootContext().setContextProperty("device", d) + src_dir = Path(__file__).resolve().parent + view.engine().addImportPath(os.fspath(src_dir)) + view.engine().quit.connect(view.close) + view.setSource(QUrl.fromLocalFile(":/assets/main.qml")) + view.setResizeMode(QQuickView.SizeRootObjectToView) + view.show() + res = app.exec() + del view + sys.exit(res) + diff --git a/examples/bluetooth/lowenergyscanner/rc_resources.py b/examples/bluetooth/lowenergyscanner/rc_resources.py new file mode 100644 index 000000000..f6e1b10fe --- /dev/null +++ b/examples/bluetooth/lowenergyscanner/rc_resources.py @@ -0,0 +1,816 @@ +# Resource object code (Python 3) +# Created by: object code +# Created by: The Resource Compiler for Qt version 6.4.0 +# WARNING! All changes made in this file will be lost! + +from PySide6 import QtCore + +qt_resource_data = b"\ +\x00\x00\x04j\ +\x89\ +PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ +\x00\x002\x00\x00\x002\x08\x06\x00\x00\x00\x1e?\x88\xb1\ +\x00\x00\x041IDATx\xda\xe5\x99\xcbn\xd3@\ +\x14\x86s-\x15\x97RJ\xa1\x04\x0a\x0amQ!U\ +Q\xa4\xa8\xb9\xd2\xc8I\xec\x19;qD\x81E\xc5\x8a\ +\xcb\x96E_\x01\xd8\xc2\x1e\xb1B\xac\xd8t\xc9\x1e\x89\ +'\xe0\x09x\x15\x98\xdf\xf1H\xd3\xc9\xf8\x96\xd8i\x0a\ +G\xfa\xa5\xc8v\x9d\xf9\xfc\x9fs\xe6\xc4M\xa5\x12\x8c\ +\xbei~y<\xb0\xff@\xf8\x9c:\x8b\xa1k\xbav\ +\xe0B@\xf8Lz\xe4\xe0\xcc\x81\xf4\x89\xf9V\x06\xc1\ +\xb1\xb9X\x5c\xadV[\xb7M\xf3\xe7\xd0\xb4~\xdb\xd4\ +\xfc\xc5\x9ez=F\x904SFPr1 \xf4\x03\ + \xb8\x00\xb3\xb5\xb5\xb5\x14\x13HFR:1\x10\xcb\ +0\xbe\x89 #\x18z\x14\x03HZ\x01\x92\x9c+X\ +\xb4\x0c\x02!\xe5\xa6\x04\xc9\xcc\xd4\x11\xa4\x11\xaf\x11Q\ +\x03\xd3\xfc<\x05Hf\xa6n\xf0 \xbd\x9e\xa1rE\ +.\xfc\xbea\xbe\x0c\x01\x92\x9e\xb9\x1b\x81\xb5\xc2\x9c\x0a\ +\xdaG\x14 \xa7\xe3\x06\x8f^\xafWR\xb9b\xe9\xf4\ +U\x04\x90\xe4\x0a\xbc\xf9\xb0y\xdd6\x8c7\x03J\xdf\ +\xd9\xc4:\x5c[[\xbb\x10\xb6\x15\xcb\xedX\x09\xc2\xd2\ +m\xca\x02\xc759\xa6\xac\xef\xb5\xb6e}d\x9d\xe9\ +\x98\xabO\xe9'\xda\xedV=\x0b\x9f-\xdc\xcb\x15\xad\ +\xae\x15e\x10\x1c\x9b\xd0\x0d\x07\xa0R\xa9\xe4\xb9\x5c \ +\xcf\xf6z\xac\x12\x1c\xaa\xd7\xeb\xc5\xb1Za\x8b\xf6\xdb\ +W0[\xb1\xbf\xfd\x01\xc1\xa1\x80\xb4J{\xec/'\ +\x00$\x18\x0f\x10b}\xf5\x82q\xc4\xd2NN7\x9b\ +\xf6\xbf\xfbu\xaf\x08\x1b\xa0\x1cY/\x80@\x90N\xab\ +\xb3\x13\x08\xc3\xce\xb3\x16l\x9fL1z\x04\x85\x84\x10\ +a\xd2\x0a'2A\x00.\x84\x7fc\xc0\x13G\xa1\xfb\ +\xc2@\xac\x9e\xfc\x9a\xc1\x84\x91\x9b\x1a@\xd5\xc1\x9c\xee\ +\xe5\xef\xcea\x8c\x10\x99\x00\x80\xdcT\x1b%\xd2\x0d\xdd\ +\xeb\xb4@\xa6\x06P\x8c&\xb6\x5c?\x80\x8c\x11$=\ +u\x1a\x85\x8dB\xa1p\xde\x05:\x8c\x19B\xd5\xcd\xfe\ +\xe3hW\xdbw\xd9\xfc\xd4\xb0\xba\xa4\xeb'\x5cS.\ +\x97\x97g\xb0\xa4\xb4;\x86\xe4Bh\xe4\x9e\xa9\xebO\ +\x07\x86\xf5>\x8a\xda\xed\xf6\x83\x04!\xb2\xc5bq1\ +\x8a\x9cz\x8a\x0a\xe1H\xb7^'\x08\xb2\x10\x15\x04\xfa\ +w@\xe60\xb52\x13\xa5\x16/\xf6\xa0B\x9f\xe3b\ +\xcf\xfe\xd7\x9dzAgie\x18Fyoo\xefF\ +\x12\xf7\xdf\xde\xde\xbe\xe4\x0e\xa3\xc9l\x8a\x00\x18\x9a\xe6\ +s\x93\x90\x17\x5c1\xc3,\xb4Z\xad\x9b\x5c\x1a\xbbw\ +\xac\xd35\x16\xcb\x86\xc6g\x22\x00\x17\x9c\x89\xeb{\xe0\ +\x84\x08\xc2\x85)\x1c\x90\x13\xdf\xb8T*]\xa4\x94v\ +T\x00\xb3\x04\xe1\xa2\x8d\xc6J\xd4\xc2^\xc0\x02\xfd\x00\ + \xcb0\x86\xec\xda|\x9cmw\x7f\x7f\xff\x9a\x1f\x0c\ +\x04\xe0\xc0\xfaA\x1a\xc9u \x0b\xe7\xf5\x93\xfbH\x1e\ +\xfb\x0a\xc4\xfa\xf9j\x94IZ\xabh\xab\x90\xf8\xa4Q\ +\x17\xa8\x0f?\x18mT\x9b\xde\xe9\x16\x04at:-\ +\xf9\x06N\xfa\x19\xc6\x13\xae00\xce\xcf\x01M\xbb\xcf\ +\xa5i\x9a\xfc\x86&\xa3\x95\xb5\xe5\x100\xea\xf0\x840\ +M\xb6^\xba2653\x17D\x08H\xdc\xf5ww\ +w\xaf\xd0Ng\x07\xc2g~\x1c.\x88 PcT\ +\x03r\xe4\xab\xd5\xeaU/\x18\xbfw\xbaC\x11\x00\xdd\ +\x8a\x10r\xdb\xe3\xf2|\x9f\x10[\x06\xe9v\xbb\x9b\xee\ +\xdb\x95s\xba\xae\xd7E\xe1\x18\xcea\xd12\x88\xd1n\ +\xdf\xf3*f\x8c!\xe8^\x22\x04\xea\xc9\xb7S!}\ +\xe0@PG\x22\x1dR\x91!\x00\xc6\x1b\x00\xbeH\x06\ +a\xc7\x0a|T\xc7\xc2e\x18\xe1\xbcgg\x83CH\ +\xbbXF\x93\xca\xc6\xc6e\x19Bt\xc3I\x9fZm\ +]\x06\xd1\x84\x7f\x08\xa9\x5c\x81X\xb1,\xcelNa\ +\x13\xf3\xa31\x10\xe6\xa2xM\x10\x08\xa2\xdbln\xca\ + z\xabug&\x10\xb0_\xe5\x86\xdc\xad\xc2\x80\xc8\ +\xdd\x8b\xcb\xdd+\x12v\x83=\xf91\x10\xe6\x90|]\ +\x18\x10\xc7\x95F\xe3\x96\x0c\x02\xa7\x92}I\xa1h\xb7\ +\x10\x9e\xec\xa4 h\x0e*W\xdc\x8drv\xb5\xe1\xf5\ +k\x11)\x18\x12D\xb9\xaf$Z+r\xcb\x15\xdb\xad\ +\x1c\xb5Zm),\x88\xaa\x1d\x07\xb5\xe2\xa9\xdf4\xf2\ +\x91\x04\x10~\xe3HD\x10\xe7\xde\x1c\xc6\x1dY\xf2\xa9\ +y\x88\xa8 s\x1b*\x10\x1c;\x93?\xf0\xd1F9\ +D\xd2-\xf5/\xf7\x98Zhx\x8a\xa0e\x00\x00\x00\ +\x00IEND\xaeB`\x82\ +\x00\x00\x0b\xb0\ +/\ +/ Copyright (C) \ +2013 BlackBerry \ +Limited. All rig\ +hts reserved.\x0a//\ + Copyright (C) 2\ +023 The Qt Compa\ +ny Ltd.\x0a// SPDX-\ +License-Identifi\ +er: LicenseRef-Q\ +t-Commercial OR \ +BSD-3-Clause\x0a\x0aim\ +port QtQuick 2.0\ +\x0a\x0aRectangle {\x0a \ + width: 300\x0a \ + height: 600\x0a\x0a \ + Component.onCo\ +mpleted: {\x0a \ + // Loading th\ +is page may take\ + longer than QLE\ +Controller\x0a \ + // stopping w\ +ith an error, go\ + back and readju\ +st this view\x0a \ + // based on\ + controller erro\ +rs\x0a if (d\ +evice.controller\ +_error) {\x0a \ + info.visib\ +le = false;\x0a \ + menu.men\ +uText = device.u\ +pdate\x0a }\x0a\ + }\x0a\x0a Heade\ +r {\x0a id: \ +header\x0a a\ +nchors.top: pare\ +nt.top\x0a h\ +eaderText: \x22Serv\ +ices list\x22\x0a }\ +\x0a\x0a Dialog {\x0a \ + id: info\x0a\ + anchors.\ +centerIn: parent\ +\x0a visible\ +: true\x0a d\ +ialogText: \x22Scan\ +ning for service\ +s...\x22;\x0a }\x0a\x0a \ + Connections {\x0a\ + target: \ +device\x0a f\ +unction onservic\ +es_updated() {\x0a \ + if (s\ +ervicesview.coun\ +t === 0)\x0a \ + info.di\ +alogText = \x22No s\ +ervices found\x22\x0a \ + else\x0a\ + \ +info.visible = f\ +alse;\x0a }\x0a\ +\x0a functio\ +n onDisconnected\ +() {\x0a \ + pageLoader.sour\ +ce = \x22qrc:/asset\ +s/main.qml\x22\x0a \ + }\x0a }\x0a\x0a \ + ListView {\x0a \ + id: services\ +view\x0a wid\ +th: parent.width\ +\x0a anchors\ +.top: header.bot\ +tom\x0a anch\ +ors.bottom: menu\ +.top\x0a mod\ +el: device.servi\ +ces_list\x0a \ + clip: true\x0a\x0a \ + delegate: R\ +ectangle {\x0a \ + id: servi\ +cebox\x0a \ + height:100\x0a \ + color: \ +\x22lightsteelblue\x22\ +\x0a bor\ +der.width: 2\x0a \ + border.\ +color: \x22black\x22\x0a \ + radiu\ +s: 5\x0a \ + width: services\ +view.width\x0a \ + Component\ +.onCompleted: {\x0a\ + \ +info.visible = f\ +alse\x0a \ + }\x0a\x0a \ +MouseArea {\x0a \ + anch\ +ors.fill: parent\ +\x0a \ + onClicked: {\x0a \ + \ + pageLoader.sou\ +rce = \x22qrc:/asse\ +ts/Characteristi\ +cs.qml\x22;\x0a \ + dev\ +ice.connect_to_s\ +ervice(modelData\ +.service_uuid);\x0a\ + \ +}\x0a }\x0a\ +\x0a Lab\ +el {\x0a \ + id: service\ +_name\x0a \ + textConten\ +t: modelData.ser\ +vice_name\x0a \ + anchor\ +s.top: parent.to\ +p\x0a \ + anchors.topMar\ +gin: 5\x0a \ + }\x0a\x0a \ + Label {\x0a \ + textCo\ +ntent: modelData\ +.service_type\x0a \ + fo\ +nt.pointSize: se\ +rvice_name.font.\ +pointSize * 0.5\x0a\ + \ +anchors.top: ser\ +vice_name.bottom\ +\x0a }\x0a\x0a\ + Labe\ +l {\x0a \ + id: service_\ +uuid\x0a \ + font.pointS\ +ize: service_nam\ +e.font.pointSize\ + * 0.5\x0a \ + textConte\ +nt: modelData.se\ +rvice_uuid\x0a \ + ancho\ +rs.bottom: servi\ +cebox.bottom\x0a \ + anc\ +hors.bottomMargi\ +n: 5\x0a \ + }\x0a }\x0a \ + }\x0a\x0a Menu {\x0a \ + id: menu\x0a\ + anchors.\ +bottom: parent.b\ +ottom\x0a me\ +nuWidth: parent.\ +width\x0a me\ +nuText: device.u\ +pdate\x0a me\ +nuHeight: (paren\ +t.height/6)\x0a \ + onButtonClic\ +k: {\x0a \ + device.disconne\ +ct_from_device()\ +\x0a pag\ +eLoader.source =\ + \x22qrc:/assets/ma\ +in.qml\x22\x0a \ + device.updat\ +e = \x22Search\x22\x0a \ + }\x0a }\x0a}\x0a\ +\x00\x00\x01\xb5\ +/\ +/ Copyright (C) \ +2013 BlackBerry \ +Limited. All rig\ +hts reserved.\x0a//\ + SPDX-License-Id\ +entifier: Licens\ +eRef-Qt-Commerci\ +al OR BSD-3-Clau\ +se\x0a\x0aimport QtQui\ +ck 2.0\x0a\x0aText {\x0a \ + property stri\ +ng textContent: \ +\x22\x22\x0a font.poin\ +tSize: 20\x0a an\ +chors.horizontal\ +Center: parent.h\ +orizontalCenter\x0a\ + color: \x22#363\ +636\x22\x0a horizon\ +talAlignment: Te\ +xt.AlignHCenter\x0a\ + elide: Text.\ +ElideMiddle\x0a \ +width: parent.wi\ +dth\x0a wrapMode\ +: Text.Wrap\x0a \ +text: textConten\ +t\x0a}\x0a\ +\x00\x00\x04\x8a\ +/\ +/ Copyright (C) \ +2013 BlackBerry \ +Limited. All rig\ +hts reserved.\x0a//\ + SPDX-License-Id\ +entifier: Licens\ +eRef-Qt-Commerci\ +al OR BSD-3-Clau\ +se\x0a\x0aimport QtQui\ +ck 2.0\x0a\x0aRectangl\ +e {\x0a width: p\ +arent.width/3*2\x0a\ + height: dial\ +ogTextId.height \ ++ background.hei\ +ght + 20\x0a z: \ +50\x0a property \ +string dialogTex\ +t: \x22\x22\x0a proper\ +ty bool busyImag\ +e: true\x0a bord\ +er.width: 1\x0a \ +border.color: \x22#\ +363636\x22\x0a radi\ +us: 10\x0a\x0a Text\ + {\x0a id: d\ +ialogTextId\x0a \ + horizontalAl\ +ignment: Text.Al\ +ignHCenter\x0a \ + verticalAlign\ +ment: Text.Align\ +VCenter\x0a \ +anchors.horizont\ +alCenter: parent\ +.horizontalCente\ +r\x0a anchor\ +s.top: parent.to\ +p;\x0a ancho\ +rs.topMargin: 10\ +\x0a\x0a elide:\ + Text.ElideMiddl\ +e\x0a text: \ +dialogText\x0a \ + color: \x22#3636\ +36\x22\x0a wrap\ +Mode: Text.Wrap\x0a\ + }\x0a\x0a Image\ + {\x0a id: b\ +ackground\x0a\x0a \ + width:20\x0a \ + height:20\x0a \ + anchors.to\ +p: dialogTextId.\ +bottom\x0a a\ +nchors.horizonta\ +lCenter: dialogT\ +extId.horizontal\ +Center\x0a v\ +isible: parent.b\ +usyImage\x0a \ + source: \x22qrc:/a\ +ssets/busy_dark.\ +png\x22\x0a fil\ +lMode: Image.Pre\ +serveAspectFit\x0a \ + NumberAni\ +mation on rotati\ +on { duration: 3\ +000; from:0; to:\ + 360; loops: Ani\ +mation.Infinite}\ +\x0a }\x0a}\x0a\ +\x00\x00\x05<\ +/\ +/ Copyright (C) \ +2013 BlackBerry \ +Limited. All rig\ +hts reserved.\x0a//\ + Copyright (C) 2\ +023 The Qt Compa\ +ny Ltd.\x0a// SPDX-\ +License-Identifi\ +er: LicenseRef-Q\ +t-Commercial OR \ +BSD-3-Clause\x0a\x0aim\ +port QtQuick 2.0\ +\x0a\x0aRectangle {\x0a\x0a \ + property real\ + menuWidth: 100\x0a\ + property rea\ +l menuHeight: 50\ +\x0a property st\ +ring menuText: \x22\ +Search\x22\x0a sign\ +al buttonClick()\ +\x0a\x0a height: me\ +nuHeight\x0a wid\ +th: menuWidth\x0a\x0a \ + Rectangle {\x0a \ + id: searc\ +h\x0a width:\ + parent.width\x0a \ + height: pa\ +rent.height\x0a \ + anchors.cent\ +erIn: parent\x0a \ + color: \x22#36\ +3636\x22\x0a bo\ +rder.width: 1\x0a \ + border.col\ +or: \x22#E3E3E3\x22\x0a \ + radius: 5\x0a\ + Text {\x0a \ + id: s\ +earchText\x0a \ + horizontal\ +Alignment: Text.\ +AlignHCenter\x0a \ + vertica\ +lAlignment: Text\ +.AlignVCenter\x0a \ + anchor\ +s.fill: parent\x0a \ + text:\ + menuText\x0a \ + elide: Tex\ +t.ElideMiddle\x0a \ + color:\ + \x22#E3E3E3\x22\x0a \ + wrapMode:\ + Text.WordWrap\x0a \ + }\x0a\x0a \ + MouseArea {\x0a \ + anchor\ +s.fill: parent\x0a \ + onPre\ +ssed: {\x0a \ + search.w\ +idth = search.wi\ +dth - 7\x0a \ + search.h\ +eight = search.h\ +eight - 5\x0a \ + }\x0a\x0a \ + onReleased:\ + {\x0a \ + search.width \ += search.width +\ + 7\x0a \ + search.height\ + = search.height\ + + 5\x0a \ + }\x0a\x0a \ +onClicked: {\x0a \ + but\ +tonClick()\x0a \ + }\x0a \ + }\x0a }\x0a}\x0a\ +\x00\x00\x03l\ +\x00\ +\x00\x0c\xf8x\xda\xc5WKs\xda0\x10\xbe\xfbWh\ +|\x22\xed h2Mg\xdc\xc9!\xc0!\x99!m\ +\x132io\x19a/\xa0\x89,9\x92L\x1e\x9d\xfc\ +\xf7\xael\xf3\x90m\xd2&\x07\xea\x03\xe0\xd5>?\xed\ +\x8b^\x8f\x0cU\xf6\xa4\xf9|aIgx@\x0e\xfb\ +\x9f\x8e\xc8@\xb0\xf8n\x00Z?\x911O\xb9\x85\x84\ +\x92S!H\xc1f\x88\x06\x03z\x89\xc4\xa0\xd7\x14?\ +<\x22\xd7\x0b \x97\x16O\xd2\x8cITaK\xce\xc9\ +\x8f\xd1\xaf\xee\x98\xc7 \x0dt\xcf\x13\x90\x96\xcf8\xe8\ +\x88T\xb4+\x98u/m\x17\xc5R\xd01g\x82|\ +\xbf\x22\x83\xc9\xa8{\xd4\x1d\x0a\x96\x1b\x08\x02\x9efJ\ +[T~\x99\xf3\xf8\x8e\x1c\xd2~\x10\x5cAl\x99\x9c\ +\x0b \xbf\x03\x82\xcf\x03O\xec\x22\x22G\xfd~\xf1\xba\ +\x00\xe7\x5cD\x8e\xf1\xbd \x9c\x01K@W\xcc\xee\xe1\ +I\x84\x5c\x8e\xb8&1\x19/\x946\xd4\xaa,\x22\x19\ +\xd3\xe8\xab\xfb\xbd>/\xd9\xaf\xe1\x11\x15\x87\xc3\x05\xd3\ +,\xb6\xa0\xb9\xb1<6D\xe0wX\xb0\xbe\x94\x16G\ +\x18\x8b\x9a\xd7,r9S\x0d{\x88\x03\xea9\x97+\ +\xa3k\x86%7|* \x22V\xe7\xb0\xa6&\x85\xe2\ +\xca\x8dI\xcc\xa4\xe4rNfJ\x93\xd8\xf7\x89R\x1a\ +~\xddvi\xa8\xa4D\xdc\xb8\x92f\xcb/\xcb\xf4\x1c\ +PW\x02K\xbc\x925y\x96\xcb\x82\x95(\xe9\xeb\xbd\ +\xcd\xb3\x84avt\x0e\xb6\x94\xb8'\x05\x99S\xf7\xe1\ +|#'$\x1c`B\x85\x1e\x0b\x9f\x91\x8e\xafm\xc9\ +\xe1\x81\xc6*\x97(qrB\xfau\xa5\x85\x14\xa2F\ +7a;\xd5\xdfT-Z\x04 \x97I\xd8.;\xcd\ +\xcd\xd3y\xca\xe6\x80\xa23&\x0cxl/\x04\x90\xb4\ +\xcbnu\x09\xad\x92\xad\x06\xbc\xcb*\xd1\xdf\xfcj\x83\ +w\xc4M\x5c\xdeL\x0b\xa8\x19*\x1d+\x97x\xd4\xa8\ +\x5c\xc7\xceBx\xaf\xe3\xa8\xc7\x8c\x01kz)\xe3\x92\ +\xde\xa7\x22\x0c|{\x95\xad1\xa2s\x83\x18\xd7\x12\xb1\ +y\x09\xeb\xe3\xaa\x94\xaa\x02(\xde\xd6g\xb1\xe0Y\x95\ +\x8e\xedeS\xd6\x08\x9d*kU\xda`)\xc9Q\x99\ +)\xdb\xa5\x95\xaa\x04\xc4*\x05i-\xdf\x5cim\xcc\ +!#\xcc1\xff\x22R\xef\x01\xbb\xe3\x9b\xaaG\x8f\xa3\ +j\x0f\xabnQ\x8b\xbc%A}\x14\x0a$\x94P\xd8\ +\xc5BQtG\x0b \xa6\x22\x07?\xff\xa6J;0\ +*\xb5\x87mg+5S\xd1\xa8\x15\xcd\x12\x9e\x9b\x88\ +|\x0e<\xf2\x98MA\xb4%k#\xea[\xc9\xd2f\ +\xbeZ\xac l\x04\x16/7*q\x1f1\xcb\xe8\xbf\ +\x88\xfe\xadA\xb6\xf0]`s\xe1\xd2\x05\xe1W\xc4{\ +C\xcas\x9e4\xf8f\x18\x0e\xcd\x14\x97v\xc2\x9f\xa1\ +\x15\x06\xea\xf3|\xe8\xd3/\xefD\xa6\xd5\x03\x0f\x996\ +\xf3\xb5\x82\xd8\x03RK&r\xd8\x0bT\x9d\xf0\xc6\xd9\ +\xc2,&\x1fw\xe3V\xf8s\xb0\x13\x81Uk\xf0\xa5\ +\xce\x98L\x04\xb4&\x19J\xf1gt\x81\x89S\xacA\ +\x99\x16\xae\xb8\xf1@\x8b\xf7\xb3a1U\xf7\x88x\xe9\ +\xeb\x9e\x10/\x8di\xf3:\xe8%\xd7[A\xbf\xcd@\ +\xa7\xdc\x18\x9cN\xfb.\xef\x8d\xe5\xff[\xe4\xaf\xf8Q\ +\xc7\xae\xea\x83\xef-\xf0\xa6\xca]\x98\xb6\x8e\xf7\x0b\x9c\ +\xa4\xb5\xd1\xee\x86k\xf06o\x9d\xc8\xcfW\xc6\xfej\ +\xa9[O\xe8r\x05\xf4\xce\xcf\xaa\x95\xbbSi(g\ +l\xefx\x93}J\x0er4+\x87\x02\xd7\xf8\xe8\xad\ +{\xce\x04\xff}\xa0i\xe3\xef:\xe5F\xb0\xe5Ss\ +\xef\x5c\xe1\xf5\x12\xfc\x01\xe3\xbd\xadu\ +\x00\x00\x02?\ +/\ +/ Copyright (C) \ +2013 BlackBerry \ +Limited. All rig\ +hts reserved.\x0a//\ + SPDX-License-Id\ +entifier: Licens\ +eRef-Qt-Commerci\ +al OR BSD-3-Clau\ +se\x0a\x0aimport QtQui\ +ck 2.0\x0a\x0aRectangl\ +e {\x0a width: p\ +arent.width\x0a \ +height: 70\x0a b\ +order.width: 1\x0a \ + border.color:\ + \x22#363636\x22\x0a r\ +adius: 5\x0a pro\ +perty string hea\ +derText: \x22\x22\x0a\x0a \ + Text {\x0a \ +horizontalAlignm\ +ent: Text.AlignH\ +Center\x0a v\ +erticalAlignment\ +: Text.AlignVCen\ +ter\x0a anch\ +ors.fill: parent\ +\x0a text: h\ +eaderText\x0a \ + font.bold: tru\ +e\x0a font.p\ +ointSize: 20\x0a \ + elide: Text\ +.ElideMiddle\x0a \ + color: \x22#36\ +3636\x22\x0a }\x0a}\x0a\ +\x00\x00\x0b\x96\ +/\ +/ Copyright (C) \ +2013 BlackBerry \ +Limited. All rig\ +hts reserved.\x0a//\ + Copyright (C) 2\ +023 The Qt Compa\ +ny Ltd.\x0a// SPDX-\ +License-Identifi\ +er: LicenseRef-Q\ +t-Commercial OR \ +BSD-3-Clause\x0a\x0aim\ +port QtQuick\x0a\x0aRe\ +ctangle {\x0a id\ +: back\x0a width\ +: 300\x0a height\ +: 600\x0a proper\ +ty bool deviceSt\ +ate: device.stat\ +e\x0a onDevicest\ +ate_changed: {\x0a \ + if (!devi\ +ce.state)\x0a \ + info.visib\ +le = false;\x0a \ +}\x0a\x0a Header {\x0a\ + id: head\ +er\x0a ancho\ +rs.top: parent.t\ +op\x0a heade\ +rText: \x22Start Di\ +scovery\x22\x0a }\x0a\x0a\ + Dialog {\x0a \ + id: info\x0a \ + anchors.ce\ +nterIn: parent\x0a \ + visible: \ +false\x0a }\x0a\x0a \ + ListView {\x0a \ + id: theListV\ +iew\x0a widt\ +h: parent.width\x0a\ + clip: tr\ +ue\x0a\x0a anch\ +ors.top: header.\ +bottom\x0a a\ +nchors.bottom: c\ +onnectToggle.top\ +\x0a model: \ +device.devices_l\ +ist\x0a\x0a del\ +egate: Rectangle\ + {\x0a i\ +d: box\x0a \ + height:100\x0a \ + width:\ + theListView.wid\ +th\x0a c\ +olor: \x22lightstee\ +lblue\x22\x0a \ + border.width:\ + 2\x0a b\ +order.color: \x22bl\ +ack\x22\x0a \ + radius: 5\x0a\x0a \ + Componen\ +t.onCompleted: {\ +\x0a \ + info.visible = \ +false;\x0a \ + header.he\ +aderText = \x22Sele\ +ct a device\x22;\x0a \ + }\x0a\x0a \ + MouseAr\ +ea {\x0a \ + anchors.fil\ +l: parent\x0a \ + onClic\ +ked: {\x0a \ + devic\ +e.scan_services(\ +modelData.device\ +_address);\x0a \ + p\ +ageLoader.source\ + = \x22qrc:/assets/\ +Services.qml\x22\x0a \ + }\x0a\ + }\x0a\x0a \ + Label\ + {\x0a \ + id: device_na\ +me\x0a \ + textContent: \ +modelData.device\ +_name\x0a \ + anchors.to\ +p: parent.top\x0a \ + an\ +chors.topMargin:\ + 5\x0a }\ +\x0a\x0a La\ +bel {\x0a \ + id: device\ +_address\x0a \ + textCon\ +tent: modelData.\ +device_address\x0a \ + f\ +ont.pointSize: d\ +evice_name.font.\ +pointSize*0.7\x0a \ + an\ +chors.bottom: bo\ +x.bottom\x0a \ + anchors\ +.bottomMargin: 5\ +\x0a }\x0a \ + }\x0a }\x0a\x0a\ + Menu {\x0a \ + id: connectTo\ +ggle\x0a\x0a me\ +nuWidth: parent.\ +width\x0a an\ +chors.bottom: me\ +nu.top\x0a m\ +enuText: { if (d\ +evice.devices_li\ +st.length)\x0a \ + \ + visible = tru\ +e\x0a \ + else\x0a \ + \ + visible = fal\ +se\x0a \ + if (devic\ +e.use_random_add\ +ress)\x0a \ + \x22A\ +ddress type: Ran\ +dom\x22\x0a \ + else\x0a \ + \ + \x22Address t\ +ype: Public\x22\x0a \ + }\x0a\x0a \ +onButtonClick: d\ +evice.use_random\ +_address = !devi\ +ce.use_random_ad\ +dress;\x0a }\x0a\x0a \ + Menu {\x0a \ + id: menu\x0a \ + anchors.bottom\ +: parent.bottom\x0a\ + menuWidt\ +h: parent.width\x0a\ + menuHeig\ +ht: (parent.heig\ +ht/6)\x0a me\ +nuText: device.u\ +pdate\x0a on\ +ButtonClick: {\x0a \ + devic\ +e.start_device_d\ +iscovery();\x0a \ + // if st\ +art_device_disco\ +very() failed de\ +vice.state is no\ +t set\x0a \ + if (device.sta\ +te) {\x0a \ + info.dialo\ +gText = \x22Searchi\ +ng...\x22;\x0a \ + info.vis\ +ible = true;\x0a \ + }\x0a \ + }\x0a }\x0a\x0a \ +Loader {\x0a \ + id: pageLoader\x0a\ + anchors.\ +fill: parent\x0a \ + }\x0a}\x0a\ +" + +qt_resource_name = b"\ +\x00\x06\ +\x06\x8a\x9c\xb3\ +\x00a\ +\x00s\x00s\x00e\x00t\x00s\ +\x00\x0d\ +\x0b\x1b\xbd\xa7\ +\x00b\ +\x00u\x00s\x00y\x00_\x00d\x00a\x00r\x00k\x00.\x00p\x00n\x00g\ +\x00\x0c\ +\x02\xffq\xdc\ +\x00S\ +\x00e\x00r\x00v\x00i\x00c\x00e\x00s\x00.\x00q\x00m\x00l\ +\x00\x09\ +\x08\xbf\xf4\xdc\ +\x00L\ +\x00a\x00b\x00e\x00l\x00.\x00q\x00m\x00l\ +\x00\x0a\ +\x03S\x0b<\ +\x00D\ +\x00i\x00a\x00l\x00o\x00g\x00.\x00q\x00m\x00l\ +\x00\x08\ +\x0cX^\x5c\ +\x00M\ +\x00e\x00n\x00u\x00.\x00q\x00m\x00l\ +\x00\x13\ +\x0d\x96a\x9c\ +\x00C\ +\x00h\x00a\x00r\x00a\x00c\x00t\x00e\x00r\x00i\x00s\x00t\x00i\x00c\x00s\x00.\x00q\ +\x00m\x00l\ +\x00\x0a\ +\x0a\xcc\x82\xdc\ +\x00H\ +\x00e\x00a\x00d\x00e\x00r\x00.\x00q\x00m\x00l\ +\x00\x08\ +\x08\x01Z\x5c\ +\x00m\ +\x00a\x00i\x00n\x00.\x00q\x00m\x00l\ +" + +qt_resource_struct = b"\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\ +\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x08\x00\x00\x00\x02\ +\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x002\x00\x00\x00\x00\x00\x01\x00\x00\x04n\ +\x00\x00\x01\x86,\x18\xf8\xdc\ +\x00\x00\x00h\x00\x00\x00\x00\x00\x01\x00\x00\x11\xdb\ +\x00\x00\x01\x86+6\xb9\xe8\ +\x00\x00\x00\xde\x00\x00\x00\x00\x00\x01\x00\x00!\x5c\ +\x00\x00\x01\x86,\x17\xd4$\ +\x00\x00\x00P\x00\x00\x00\x00\x00\x01\x00\x00\x10\x22\ +\x00\x00\x01\x86+6\xb9\xe8\ +\x00\x00\x00\xc4\x00\x00\x00\x00\x00\x01\x00\x00\x1f\x19\ +\x00\x00\x01\x86+6\xb9\xe8\ +\x00\x00\x00\x12\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ +\x00\x00\x01\x86+6\xb9\xe8\ +\x00\x00\x00\x82\x00\x00\x00\x00\x00\x01\x00\x00\x16i\ +\x00\x00\x01\x86+6\xb9\xe8\ +\x00\x00\x00\x98\x00\x01\x00\x00\x00\x01\x00\x00\x1b\xa9\ +\x00\x00\x01\x86+\xfa\xf9\x12\ +" + +def qInitResources(): + QtCore.qRegisterResourceData(0x03, qt_resource_struct, qt_resource_name, qt_resource_data) + +def qCleanupResources(): + QtCore.qUnregisterResourceData(0x03, qt_resource_struct, qt_resource_name, qt_resource_data) + +qInitResources() diff --git a/examples/bluetooth/lowenergyscanner/resources.qrc b/examples/bluetooth/lowenergyscanner/resources.qrc new file mode 100644 index 000000000..c24866534 --- /dev/null +++ b/examples/bluetooth/lowenergyscanner/resources.qrc @@ -0,0 +1,12 @@ +<RCC> + <qresource> + <file>assets/Characteristics.qml</file> + <file>assets/main.qml</file> + <file>assets/Menu.qml</file> + <file>assets/Services.qml</file> + <file>assets/Header.qml</file> + <file>assets/Dialog.qml</file> + <file>assets/Label.qml</file> + <file>assets/busy_dark.png</file> + </qresource> +</RCC> diff --git a/examples/bluetooth/lowenergyscanner/serviceinfo.py b/examples/bluetooth/lowenergyscanner/serviceinfo.py new file mode 100644 index 000000000..092e9898f --- /dev/null +++ b/examples/bluetooth/lowenergyscanner/serviceinfo.py @@ -0,0 +1,66 @@ +# 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 + + |