aboutsummaryrefslogtreecommitdiffstats
path: root/examples
diff options
context:
space:
mode:
authorShyamnath Premnadh <Shyamnath.Premnadh@qt.io>2023-01-12 17:02:04 +0100
committerShyamnath Premnadh <Shyamnath.Premnadh@qt.io>2023-02-22 10:58:48 +0100
commit9a5a0310d73fddf88848ed73949237af255d828f (patch)
tree12e674a1f3707c752ba6e2a683f961cb663ab562 /examples
parent4a3f337d0154105f924e2e076d8f2cb63b3aca8e (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')
-rw-r--r--examples/bluetooth/lowenergyscanner/assets/Characteristics.qml114
-rw-r--r--examples/bluetooth/lowenergyscanner/assets/Dialog.qml42
-rw-r--r--examples/bluetooth/lowenergyscanner/assets/Header.qml24
-rw-r--r--examples/bluetooth/lowenergyscanner/assets/Label.qml16
-rw-r--r--examples/bluetooth/lowenergyscanner/assets/Menu.qml54
-rw-r--r--examples/bluetooth/lowenergyscanner/assets/Services.qml111
-rw-r--r--examples/bluetooth/lowenergyscanner/assets/busy_dark.pngbin0 -> 1130 bytes
-rw-r--r--examples/bluetooth/lowenergyscanner/assets/main.qml115
-rw-r--r--examples/bluetooth/lowenergyscanner/characteristicinfo.py88
-rw-r--r--examples/bluetooth/lowenergyscanner/device.py268
-rw-r--r--examples/bluetooth/lowenergyscanner/deviceinfo.py35
-rw-r--r--examples/bluetooth/lowenergyscanner/doc/lowenergyscanner.pngbin0 -> 15150 bytes
-rw-r--r--examples/bluetooth/lowenergyscanner/doc/lowenergyscanner.rst9
-rw-r--r--examples/bluetooth/lowenergyscanner/lowenergyscanner.pyproject6
-rw-r--r--examples/bluetooth/lowenergyscanner/main.py33
-rw-r--r--examples/bluetooth/lowenergyscanner/rc_resources.py816
-rw-r--r--examples/bluetooth/lowenergyscanner/resources.qrc12
-rw-r--r--examples/bluetooth/lowenergyscanner/serviceinfo.py66
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
new file mode 100644
index 000000000..3a1059531
--- /dev/null
+++ b/examples/bluetooth/lowenergyscanner/assets/busy_dark.png
Binary files differ
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
new file mode 100644
index 000000000..842b0f713
--- /dev/null
+++ b/examples/bluetooth/lowenergyscanner/doc/lowenergyscanner.png
Binary files differ
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
+
+