aboutsummaryrefslogtreecommitdiffstats
path: root/examples/bluetooth
diff options
context:
space:
mode:
Diffstat (limited to 'examples/bluetooth')
-rw-r--r--examples/bluetooth/btscanner/btscanner.pyproject3
-rw-r--r--examples/bluetooth/btscanner/device.py130
-rw-r--r--examples/bluetooth/btscanner/device.ui111
-rw-r--r--examples/bluetooth/btscanner/doc/btscanner.rst4
-rw-r--r--examples/bluetooth/btscanner/main.py17
-rw-r--r--examples/bluetooth/btscanner/service.py48
-rw-r--r--examples/bluetooth/btscanner/service.ui71
-rw-r--r--examples/bluetooth/btscanner/ui_device.py90
-rw-r--r--examples/bluetooth/btscanner/ui_service.py57
-rw-r--r--examples/bluetooth/heartrate_game/HeartRateGame/App.qml99
-rw-r--r--examples/bluetooth/heartrate_game/HeartRateGame/BluetoothAlarmDialog.qml79
-rw-r--r--examples/bluetooth/heartrate_game/HeartRateGame/BottomLine.qml12
-rw-r--r--examples/bluetooth/heartrate_game/HeartRateGame/Connect.qml159
-rw-r--r--examples/bluetooth/heartrate_game/HeartRateGame/GameButton.qml39
-rw-r--r--examples/bluetooth/heartrate_game/HeartRateGame/GamePage.qml36
-rw-r--r--examples/bluetooth/heartrate_game/HeartRateGame/GameSettings.qml51
-rw-r--r--examples/bluetooth/heartrate_game/HeartRateGame/Main.qml71
-rw-r--r--examples/bluetooth/heartrate_game/HeartRateGame/Measure.qml212
-rw-r--r--examples/bluetooth/heartrate_game/HeartRateGame/SplashScreen.qml30
-rw-r--r--examples/bluetooth/heartrate_game/HeartRateGame/Stats.qml55
-rw-r--r--examples/bluetooth/heartrate_game/HeartRateGame/StatsLabel.qml34
-rw-r--r--examples/bluetooth/heartrate_game/HeartRateGame/TitleBar.qml54
-rw-r--r--examples/bluetooth/heartrate_game/HeartRateGame/images/bt_off_to_on.pngbin0 -> 6143 bytes
-rw-r--r--examples/bluetooth/heartrate_game/HeartRateGame/images/heart.pngbin0 -> 2664 bytes
-rw-r--r--examples/bluetooth/heartrate_game/HeartRateGame/images/logo.pngbin0 -> 31915 bytes
-rw-r--r--examples/bluetooth/heartrate_game/HeartRateGame/qmldir14
-rw-r--r--examples/bluetooth/heartrate_game/bluetoothbaseclass.py40
-rw-r--r--examples/bluetooth/heartrate_game/connectionhandler.py77
-rw-r--r--examples/bluetooth/heartrate_game/devicefinder.py139
-rw-r--r--examples/bluetooth/heartrate_game/devicehandler.py309
-rw-r--r--examples/bluetooth/heartrate_game/deviceinfo.py38
-rw-r--r--examples/bluetooth/heartrate_game/doc/heartrate_game.rst11
-rw-r--r--examples/bluetooth/heartrate_game/heartrate_game.pyproject23
-rw-r--r--examples/bluetooth/heartrate_game/heartrate_global.py30
-rw-r--r--examples/bluetooth/heartrate_game/main.py53
-rw-r--r--examples/bluetooth/heartrate_server/doc/heartrate_server.rst8
-rw-r--r--examples/bluetooth/heartrate_server/heartrate_server.py95
-rw-r--r--examples/bluetooth/heartrate_server/heartrate_server.pyproject3
-rw-r--r--examples/bluetooth/lowenergyscanner/Scanner/Characteristics.qml121
-rw-r--r--examples/bluetooth/lowenergyscanner/Scanner/Devices.qml121
-rw-r--r--examples/bluetooth/lowenergyscanner/Scanner/Dialog.qml48
-rw-r--r--examples/bluetooth/lowenergyscanner/Scanner/Header.qml25
-rw-r--r--examples/bluetooth/lowenergyscanner/Scanner/Label.qml16
-rw-r--r--examples/bluetooth/lowenergyscanner/Scanner/Main.qml31
-rw-r--r--examples/bluetooth/lowenergyscanner/Scanner/Menu.qml55
-rw-r--r--examples/bluetooth/lowenergyscanner/Scanner/Services.qml115
-rw-r--r--examples/bluetooth/lowenergyscanner/Scanner/assets/busy_dark.pngbin0 -> 1130 bytes
-rw-r--r--examples/bluetooth/lowenergyscanner/Scanner/qmldir10
-rw-r--r--examples/bluetooth/lowenergyscanner/characteristicinfo.py87
-rw-r--r--examples/bluetooth/lowenergyscanner/device.py278
-rw-r--r--examples/bluetooth/lowenergyscanner/deviceinfo.py34
-rw-r--r--examples/bluetooth/lowenergyscanner/doc/lowenergyscanner.pngbin0 -> 72365 bytes
-rw-r--r--examples/bluetooth/lowenergyscanner/doc/lowenergyscanner.rst11
-rw-r--r--examples/bluetooth/lowenergyscanner/lowenergyscanner.pyproject7
-rw-r--r--examples/bluetooth/lowenergyscanner/main.py27
-rw-r--r--examples/bluetooth/lowenergyscanner/serviceinfo.py64
56 files changed, 3352 insertions, 0 deletions
diff --git a/examples/bluetooth/btscanner/btscanner.pyproject b/examples/bluetooth/btscanner/btscanner.pyproject
new file mode 100644
index 000000000..208487fe7
--- /dev/null
+++ b/examples/bluetooth/btscanner/btscanner.pyproject
@@ -0,0 +1,3 @@
+{
+ "files": ["main.py", "device.py", "service.py", "device.ui", "service.ui"]
+}
diff --git a/examples/bluetooth/btscanner/device.py b/examples/bluetooth/btscanner/device.py
new file mode 100644
index 000000000..c75f5b8a1
--- /dev/null
+++ b/examples/bluetooth/btscanner/device.py
@@ -0,0 +1,130 @@
+# Copyright (C) 2022 The Qt Company Ltd.
+# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+from PySide6.QtCore import QPoint, Qt, Slot
+from PySide6.QtGui import QColor
+from PySide6.QtWidgets import QDialog, QListWidgetItem, QMenu
+from PySide6.QtBluetooth import (QBluetoothAddress, QBluetoothDeviceDiscoveryAgent,
+ QBluetoothDeviceInfo, QBluetoothLocalDevice)
+
+from ui_device import Ui_DeviceDiscovery
+from service import ServiceDiscoveryDialog
+
+
+class DeviceDiscoveryDialog(QDialog):
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ self._local_device = QBluetoothLocalDevice()
+ self._ui = Ui_DeviceDiscovery()
+ self._ui.setupUi(self)
+ # In case of multiple Bluetooth adapters it is possible to set adapter
+ # which will be used. Example code:
+ #
+ # address = QBluetoothAddress("XX:XX:XX:XX:XX:XX")
+ # discoveryAgent = QBluetoothDeviceDiscoveryAgent(address)
+
+ self._discovery_agent = QBluetoothDeviceDiscoveryAgent()
+
+ self._ui.scan.clicked.connect(self.start_scan)
+ self._discovery_agent.deviceDiscovered.connect(self.add_device)
+ self._discovery_agent.finished.connect(self.scan_finished)
+ self._ui.list.itemActivated.connect(self.item_activated)
+ self._local_device.hostModeStateChanged.connect(self.host_mode_state_changed)
+
+ self.host_mode_state_changed(self._local_device.hostMode())
+ # add context menu for devices to be able to pair device
+ self._ui.list.setContextMenuPolicy(Qt.CustomContextMenu)
+ self._ui.list.customContextMenuRequested.connect(self.display_pairing_menu)
+ self._local_device.pairingFinished.connect(self.pairing_done)
+
+ @Slot(QBluetoothDeviceInfo)
+ def add_device(self, info):
+ a = info.address().toString()
+ label = f"{a} {info.name()}"
+ items = self._ui.list.findItems(label, Qt.MatchExactly)
+ if not items:
+ item = QListWidgetItem(label)
+ pairing_status = self._local_device.pairingStatus(info.address())
+ if (pairing_status == QBluetoothLocalDevice.Paired
+ or pairing_status == QBluetoothLocalDevice.AuthorizedPaired):
+ item.setForeground(QColor(Qt.green))
+ else:
+ item.setForeground(QColor(Qt.black))
+ self._ui.list.addItem(item)
+
+ @Slot()
+ def start_scan(self):
+ self._discovery_agent.start()
+ self._ui.scan.setEnabled(False)
+
+ @Slot()
+ def scan_finished(self):
+ self._ui.scan.setEnabled(True)
+
+ @Slot(QListWidgetItem)
+ def item_activated(self, item):
+ text = item.text()
+ index = text.find(' ')
+ if index == -1:
+ return
+
+ address = QBluetoothAddress(text[0:index])
+ name = text[index + 1:]
+
+ d = ServiceDiscoveryDialog(name, address)
+ d.exec()
+
+ @Slot(bool)
+ def on_discoverable_clicked(self, clicked):
+ if clicked:
+ self._local_device.setHostMode(QBluetoothLocalDevice.HostDiscoverable)
+ else:
+ self._local_device.setHostMode(QBluetoothLocalDevice.HostConnectable)
+
+ @Slot(bool)
+ def on_power_clicked(self, clicked):
+ if clicked:
+ self._local_device.powerOn()
+ else:
+ self._local_device.setHostMode(QBluetoothLocalDevice.HostPoweredOff)
+
+ @Slot("QBluetoothLocalDevice::HostMode")
+ def host_mode_state_changed(self, mode):
+ self._ui.power.setChecked(mode != QBluetoothLocalDevice.HostPoweredOff)
+ self._ui.discoverable.setChecked(mode == QBluetoothLocalDevice.HostDiscoverable)
+
+ on = mode != QBluetoothLocalDevice.HostPoweredOff
+ self._ui.scan.setEnabled(on)
+ self._ui.discoverable.setEnabled(on)
+
+ @Slot(QPoint)
+ def display_pairing_menu(self, pos):
+ if self._ui.list.count() == 0:
+ return
+ menu = QMenu(self)
+ pair_action = menu.addAction("Pair")
+ remove_pair_action = menu.addAction("Remove Pairing")
+ chosen_action = menu.exec(self._ui.list.viewport().mapToGlobal(pos))
+ current_item = self._ui.list.currentItem()
+
+ text = current_item.text()
+ index = text.find(' ')
+ if index == -1:
+ return
+
+ address = QBluetoothAddress(text[0:index])
+ if chosen_action == pair_action:
+ self._local_device.requestPairing(address, QBluetoothLocalDevice.Paired)
+ elif chosen_action == remove_pair_action:
+ self._local_device.requestPairing(address, QBluetoothLocalDevice.Unpaired)
+
+ @Slot(QBluetoothAddress, "QBluetoothLocalDevice::Pairing")
+ def pairing_done(self, address, pairing):
+ items = self._ui.list.findItems(address.toString(), Qt.MatchContains)
+
+ color = QColor(Qt.red)
+ if (pairing == QBluetoothLocalDevice.Paired
+ or pairing == QBluetoothLocalDevice.AuthorizedPaired):
+ color = QColor(Qt.green)
+ for item in items:
+ item.setForeground(color)
diff --git a/examples/bluetooth/btscanner/device.ui b/examples/bluetooth/btscanner/device.ui
new file mode 100644
index 000000000..fa81c5cb4
--- /dev/null
+++ b/examples/bluetooth/btscanner/device.ui
@@ -0,0 +1,111 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>DeviceDiscovery</class>
+ <widget class="QDialog" name="DeviceDiscovery">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>400</width>
+ <height>411</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Bluetooth Scanner</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <widget class="QListWidget" name="list"/>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="groupBox">
+ <property name="title">
+ <string>Local Device</string>
+ </property>
+ <layout class="QHBoxLayout" name="horizontalLayout_2">
+ <item>
+ <widget class="QCheckBox" name="power">
+ <property name="text">
+ <string>Bluetooth Powered On</string>
+ </property>
+ <property name="checked">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="discoverable">
+ <property name="text">
+ <string>Discoverable</string>
+ </property>
+ <property name="checked">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout">
+ <item>
+ <widget class="QPushButton" name="scan">
+ <property name="text">
+ <string>Scan</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="clear">
+ <property name="text">
+ <string>Clear</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="quit">
+ <property name="text">
+ <string>Quit</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections>
+ <connection>
+ <sender>quit</sender>
+ <signal>clicked()</signal>
+ <receiver>DeviceDiscovery</receiver>
+ <slot>accept()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>323</x>
+ <y>275</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>396</x>
+ <y>268</y>
+ </hint>
+ </hints>
+ </connection>
+ <connection>
+ <sender>clear</sender>
+ <signal>clicked()</signal>
+ <receiver>list</receiver>
+ <slot>clear()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>188</x>
+ <y>276</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>209</x>
+ <y>172</y>
+ </hint>
+ </hints>
+ </connection>
+ </connections>
+</ui>
diff --git a/examples/bluetooth/btscanner/doc/btscanner.rst b/examples/bluetooth/btscanner/doc/btscanner.rst
new file mode 100644
index 000000000..d99af3be5
--- /dev/null
+++ b/examples/bluetooth/btscanner/doc/btscanner.rst
@@ -0,0 +1,4 @@
+Bluetooth Scanner Example
+=========================
+
+An example showing how to locate Bluetooth devices.
diff --git a/examples/bluetooth/btscanner/main.py b/examples/bluetooth/btscanner/main.py
new file mode 100644
index 000000000..a54a862a2
--- /dev/null
+++ b/examples/bluetooth/btscanner/main.py
@@ -0,0 +1,17 @@
+# Copyright (C) 2022 The Qt Company Ltd.
+# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+"""PySide6 port of the bluetooth/btscanner example from Qt v6.x"""
+
+import sys
+
+from PySide6.QtWidgets import QApplication
+
+from device import DeviceDiscoveryDialog
+
+
+if __name__ == '__main__':
+ app = QApplication(sys.argv)
+ d = DeviceDiscoveryDialog()
+ d.exec()
+ sys.exit(0)
diff --git a/examples/bluetooth/btscanner/service.py b/examples/bluetooth/btscanner/service.py
new file mode 100644
index 000000000..31df8a9ea
--- /dev/null
+++ b/examples/bluetooth/btscanner/service.py
@@ -0,0 +1,48 @@
+# Copyright (C) 2022 The Qt Company Ltd.
+# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+from PySide6.QtCore import Slot
+from PySide6.QtWidgets import QDialog
+from PySide6.QtBluetooth import (QBluetoothAddress, QBluetoothServiceInfo,
+ QBluetoothServiceDiscoveryAgent, QBluetoothLocalDevice)
+
+from ui_service import Ui_ServiceDiscovery
+
+
+class ServiceDiscoveryDialog(QDialog):
+ def __init__(self, name, address, parent=None):
+ super().__init__(parent)
+ self._ui = Ui_ServiceDiscovery()
+ self._ui.setupUi(self)
+
+ # Using default Bluetooth adapter
+ local_device = QBluetoothLocalDevice()
+ adapter_address = QBluetoothAddress(local_device.address())
+
+ # In case of multiple Bluetooth adapters it is possible to
+ # set which adapter will be used by providing MAC Address.
+ # Example code:
+ #
+ # adapterAddress = QBluetoothAddress("XX:XX:XX:XX:XX:XX")
+ # discoveryAgent = QBluetoothServiceDiscoveryAgent(adapterAddress)
+
+ self._discovery_agent = QBluetoothServiceDiscoveryAgent(adapter_address)
+ self._discovery_agent.setRemoteAddress(address)
+
+ self.setWindowTitle(name)
+
+ self._discovery_agent.serviceDiscovered.connect(self.add_service)
+ self._discovery_agent.finished.connect(self._ui.status.hide)
+ self._discovery_agent.start()
+
+ @Slot(QBluetoothServiceInfo)
+ def add_service(self, info):
+ line = info.serviceName()
+ if not line:
+ return
+
+ if info.serviceDescription():
+ line += "\n\t" + info.serviceDescription()
+ if info.serviceProvider():
+ line += "\n\t" + info.serviceProvider()
+ self._ui.list.addItem(line)
diff --git a/examples/bluetooth/btscanner/service.ui b/examples/bluetooth/btscanner/service.ui
new file mode 100644
index 000000000..4ca12ee05
--- /dev/null
+++ b/examples/bluetooth/btscanner/service.ui
@@ -0,0 +1,71 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>ServiceDiscovery</class>
+ <widget class="QDialog" name="ServiceDiscovery">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>539</width>
+ <height>486</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Available Services</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <widget class="QListWidget" name="list"/>
+ </item>
+ <item>
+ <widget class="QLabel" name="status">
+ <property name="text">
+ <string>Querying...</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QDialogButtonBox" name="buttonBox">
+ <property name="standardButtons">
+ <set>QDialogButtonBox::Close</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>accepted()</signal>
+ <receiver>ServiceDiscovery</receiver>
+ <slot>accept()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>396</x>
+ <y>457</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>535</x>
+ <y>443</y>
+ </hint>
+ </hints>
+ </connection>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>rejected()</signal>
+ <receiver>ServiceDiscovery</receiver>
+ <slot>reject()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>339</x>
+ <y>464</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>535</x>
+ <y>368</y>
+ </hint>
+ </hints>
+ </connection>
+ </connections>
+</ui>
diff --git a/examples/bluetooth/btscanner/ui_device.py b/examples/bluetooth/btscanner/ui_device.py
new file mode 100644
index 000000000..b443b2bc2
--- /dev/null
+++ b/examples/bluetooth/btscanner/ui_device.py
@@ -0,0 +1,90 @@
+# -*- coding: utf-8 -*-
+
+################################################################################
+## Form generated from reading UI file 'device.ui'
+##
+## Created by: Qt User Interface Compiler version 6.7.0
+##
+## WARNING! All changes made in this file will be lost when recompiling UI file!
+################################################################################
+
+from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale,
+ QMetaObject, QObject, QPoint, QRect,
+ QSize, QTime, QUrl, Qt)
+from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
+ QFont, QFontDatabase, QGradient, QIcon,
+ QImage, QKeySequence, QLinearGradient, QPainter,
+ QPalette, QPixmap, QRadialGradient, QTransform)
+from PySide6.QtWidgets import (QApplication, QCheckBox, QDialog, QGroupBox,
+ QHBoxLayout, QListWidget, QListWidgetItem, QPushButton,
+ QSizePolicy, QVBoxLayout, QWidget)
+
+class Ui_DeviceDiscovery(object):
+ def setupUi(self, DeviceDiscovery):
+ if not DeviceDiscovery.objectName():
+ DeviceDiscovery.setObjectName(u"DeviceDiscovery")
+ DeviceDiscovery.resize(400, 411)
+ self.verticalLayout = QVBoxLayout(DeviceDiscovery)
+ self.verticalLayout.setObjectName(u"verticalLayout")
+ self.list = QListWidget(DeviceDiscovery)
+ self.list.setObjectName(u"list")
+
+ self.verticalLayout.addWidget(self.list)
+
+ self.groupBox = QGroupBox(DeviceDiscovery)
+ self.groupBox.setObjectName(u"groupBox")
+ self.horizontalLayout_2 = QHBoxLayout(self.groupBox)
+ self.horizontalLayout_2.setObjectName(u"horizontalLayout_2")
+ self.power = QCheckBox(self.groupBox)
+ self.power.setObjectName(u"power")
+ self.power.setChecked(True)
+
+ self.horizontalLayout_2.addWidget(self.power)
+
+ self.discoverable = QCheckBox(self.groupBox)
+ self.discoverable.setObjectName(u"discoverable")
+ self.discoverable.setChecked(True)
+
+ self.horizontalLayout_2.addWidget(self.discoverable)
+
+
+ self.verticalLayout.addWidget(self.groupBox)
+
+ self.horizontalLayout = QHBoxLayout()
+ self.horizontalLayout.setObjectName(u"horizontalLayout")
+ self.scan = QPushButton(DeviceDiscovery)
+ self.scan.setObjectName(u"scan")
+
+ self.horizontalLayout.addWidget(self.scan)
+
+ self.clear = QPushButton(DeviceDiscovery)
+ self.clear.setObjectName(u"clear")
+
+ self.horizontalLayout.addWidget(self.clear)
+
+ self.quit = QPushButton(DeviceDiscovery)
+ self.quit.setObjectName(u"quit")
+
+ self.horizontalLayout.addWidget(self.quit)
+
+
+ self.verticalLayout.addLayout(self.horizontalLayout)
+
+
+ self.retranslateUi(DeviceDiscovery)
+ self.quit.clicked.connect(DeviceDiscovery.accept)
+ self.clear.clicked.connect(self.list.clear)
+
+ QMetaObject.connectSlotsByName(DeviceDiscovery)
+ # setupUi
+
+ def retranslateUi(self, DeviceDiscovery):
+ DeviceDiscovery.setWindowTitle(QCoreApplication.translate("DeviceDiscovery", u"Bluetooth Scanner", None))
+ self.groupBox.setTitle(QCoreApplication.translate("DeviceDiscovery", u"Local Device", None))
+ self.power.setText(QCoreApplication.translate("DeviceDiscovery", u"Bluetooth Powered On", None))
+ self.discoverable.setText(QCoreApplication.translate("DeviceDiscovery", u"Discoverable", None))
+ self.scan.setText(QCoreApplication.translate("DeviceDiscovery", u"Scan", None))
+ self.clear.setText(QCoreApplication.translate("DeviceDiscovery", u"Clear", None))
+ self.quit.setText(QCoreApplication.translate("DeviceDiscovery", u"Quit", None))
+ # retranslateUi
+
diff --git a/examples/bluetooth/btscanner/ui_service.py b/examples/bluetooth/btscanner/ui_service.py
new file mode 100644
index 000000000..ccc36677a
--- /dev/null
+++ b/examples/bluetooth/btscanner/ui_service.py
@@ -0,0 +1,57 @@
+# -*- coding: utf-8 -*-
+
+################################################################################
+## Form generated from reading UI file 'service.ui'
+##
+## Created by: Qt User Interface Compiler version 6.7.0
+##
+## WARNING! All changes made in this file will be lost when recompiling UI file!
+################################################################################
+
+from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale,
+ QMetaObject, QObject, QPoint, QRect,
+ QSize, QTime, QUrl, Qt)
+from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
+ QFont, QFontDatabase, QGradient, QIcon,
+ QImage, QKeySequence, QLinearGradient, QPainter,
+ QPalette, QPixmap, QRadialGradient, QTransform)
+from PySide6.QtWidgets import (QAbstractButton, QApplication, QDialog, QDialogButtonBox,
+ QLabel, QListWidget, QListWidgetItem, QSizePolicy,
+ QVBoxLayout, QWidget)
+
+class Ui_ServiceDiscovery(object):
+ def setupUi(self, ServiceDiscovery):
+ if not ServiceDiscovery.objectName():
+ ServiceDiscovery.setObjectName(u"ServiceDiscovery")
+ ServiceDiscovery.resize(539, 486)
+ self.verticalLayout = QVBoxLayout(ServiceDiscovery)
+ self.verticalLayout.setObjectName(u"verticalLayout")
+ self.list = QListWidget(ServiceDiscovery)
+ self.list.setObjectName(u"list")
+
+ self.verticalLayout.addWidget(self.list)
+
+ self.status = QLabel(ServiceDiscovery)
+ self.status.setObjectName(u"status")
+
+ self.verticalLayout.addWidget(self.status)
+
+ self.buttonBox = QDialogButtonBox(ServiceDiscovery)
+ self.buttonBox.setObjectName(u"buttonBox")
+ self.buttonBox.setStandardButtons(QDialogButtonBox.Close)
+
+ self.verticalLayout.addWidget(self.buttonBox)
+
+
+ self.retranslateUi(ServiceDiscovery)
+ self.buttonBox.accepted.connect(ServiceDiscovery.accept)
+ self.buttonBox.rejected.connect(ServiceDiscovery.reject)
+
+ QMetaObject.connectSlotsByName(ServiceDiscovery)
+ # setupUi
+
+ def retranslateUi(self, ServiceDiscovery):
+ ServiceDiscovery.setWindowTitle(QCoreApplication.translate("ServiceDiscovery", u"Available Services", None))
+ self.status.setText(QCoreApplication.translate("ServiceDiscovery", u"Querying...", None))
+ # retranslateUi
+
diff --git a/examples/bluetooth/heartrate_game/HeartRateGame/App.qml b/examples/bluetooth/heartrate_game/HeartRateGame/App.qml
new file mode 100644
index 000000000..db6aa7145
--- /dev/null
+++ b/examples/bluetooth/heartrate_game/HeartRateGame/App.qml
@@ -0,0 +1,99 @@
+// Copyright (C) 2022 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+import QtQuick
+import QtQuick.Layouts
+import HeartRateGame
+
+Item {
+ id: app
+
+ required property ConnectionHandler connectionHandler
+ required property DeviceFinder deviceFinder
+ required property DeviceHandler deviceHandler
+
+ anchors.fill: parent
+ opacity: 0.0
+
+ Behavior on opacity {
+ NumberAnimation {
+ duration: 500
+ }
+ }
+
+ property int __currentIndex: 0
+
+ TitleBar {
+ id: titleBar
+ anchors.top: parent.top
+ anchors.left: parent.left
+ anchors.right: parent.right
+ currentIndex: app.__currentIndex
+
+ onTitleClicked: (index) => {
+ if (index < app.__currentIndex)
+ app.__currentIndex = index
+ }
+ }
+
+ StackLayout {
+ id: pageStack
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.top: titleBar.bottom
+ anchors.bottom: parent.bottom
+ currentIndex: app.__currentIndex
+
+ Connect {
+ connectionHandler: app.connectionHandler
+ deviceFinder: app.deviceFinder
+ deviceHandler: app.deviceHandler
+
+ onShowMeasurePage: app.__currentIndex = 1
+ }
+ Measure {
+ id: measurePage
+ deviceHandler: app.deviceHandler
+
+ onShowStatsPage: app.__currentIndex = 2
+ }
+ Stats {
+ deviceHandler: app.deviceHandler
+ }
+
+ onCurrentIndexChanged: {
+ if (currentIndex === 0)
+ measurePage.close()
+ }
+ }
+
+ BluetoothAlarmDialog {
+ id: btAlarmDialog
+ anchors.fill: parent
+ visible: !app.connectionHandler.alive || permissionError
+ permissionError: !app.connectionHandler.hasPermission
+ }
+
+ Keys.onReleased: (event) => {
+ switch (event.key) {
+ case Qt.Key_Escape:
+ case Qt.Key_Back:
+ {
+ if (app.__currentIndex > 0) {
+ app.__currentIndex = app.__currentIndex - 1
+ event.accepted = true
+ } else {
+ Qt.quit()
+ }
+ break
+ }
+ default:
+ break
+ }
+ }
+
+ Component.onCompleted: {
+ forceActiveFocus()
+ app.opacity = 1.0
+ }
+}
diff --git a/examples/bluetooth/heartrate_game/HeartRateGame/BluetoothAlarmDialog.qml b/examples/bluetooth/heartrate_game/HeartRateGame/BluetoothAlarmDialog.qml
new file mode 100644
index 000000000..3687b1331
--- /dev/null
+++ b/examples/bluetooth/heartrate_game/HeartRateGame/BluetoothAlarmDialog.qml
@@ -0,0 +1,79 @@
+// Copyright (C) 2022 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+import QtQuick
+
+Item {
+ id: root
+
+ property bool permissionError: false
+
+ anchors.fill: parent
+
+ Rectangle {
+ anchors.fill: parent
+ color: "black"
+ opacity: 0.9
+ }
+
+ MouseArea {
+ id: eventEater
+ }
+
+ Rectangle {
+ id: dialogFrame
+
+ anchors.centerIn: parent
+ width: parent.width * 0.8
+ height: parent.height * 0.6
+ border.color: "#454545"
+ color: GameSettings.backgroundColor
+ radius: width * 0.05
+
+ Item {
+ id: dialogContainer
+ anchors.fill: parent
+ anchors.margins: parent.width*0.05
+
+ Image {
+ id: offOnImage
+ anchors.left: quitButton.left
+ anchors.right: quitButton.right
+ anchors.top: parent.top
+ height: GameSettings.heightForWidth(width, sourceSize)
+ source: "images/bt_off_to_on.png"
+ }
+
+ Text {
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.top: offOnImage.bottom
+ anchors.bottom: quitButton.top
+ horizontalAlignment: Text.AlignHCenter
+ verticalAlignment: Text.AlignVCenter
+ wrapMode: Text.WordWrap
+ font.pixelSize: GameSettings.mediumFontSize
+ color: GameSettings.textColor
+ text: root.permissionError
+ ? qsTr("Bluetooth permissions are not granted. Please grant the permissions in the system settings.")
+ : qsTr("This application cannot be used without Bluetooth. Please switch Bluetooth ON to continue.")
+ }
+
+ GameButton {
+ id: quitButton
+ anchors.bottom: parent.bottom
+ anchors.horizontalCenter: parent.horizontalCenter
+ width: dialogContainer.width * 0.6
+ height: GameSettings.buttonHeight
+ onClicked: Qt.quit()
+
+ Text {
+ anchors.centerIn: parent
+ color: GameSettings.textColor
+ font.pixelSize: GameSettings.bigFontSize
+ text: qsTr("Quit")
+ }
+ }
+ }
+ }
+}
diff --git a/examples/bluetooth/heartrate_game/HeartRateGame/BottomLine.qml b/examples/bluetooth/heartrate_game/HeartRateGame/BottomLine.qml
new file mode 100644
index 000000000..caebc307e
--- /dev/null
+++ b/examples/bluetooth/heartrate_game/HeartRateGame/BottomLine.qml
@@ -0,0 +1,12 @@
+// Copyright (C) 2022 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+import QtQuick
+
+Rectangle {
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.bottom: parent.bottom
+ width: parent.width * 0.85
+ height: parent.height * 0.05
+ radius: height*0.5
+}
diff --git a/examples/bluetooth/heartrate_game/HeartRateGame/Connect.qml b/examples/bluetooth/heartrate_game/HeartRateGame/Connect.qml
new file mode 100644
index 000000000..ca8ef2923
--- /dev/null
+++ b/examples/bluetooth/heartrate_game/HeartRateGame/Connect.qml
@@ -0,0 +1,159 @@
+// Copyright (C) 2022 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+pragma ComponentBehavior: Bound
+import QtQuick
+import HeartRateGame
+
+GamePage {
+ id: connectPage
+
+ required property ConnectionHandler connectionHandler
+ required property DeviceFinder deviceFinder
+ required property DeviceHandler deviceHandler
+
+ signal showMeasurePage
+
+ errorMessage: deviceFinder.error
+ infoMessage: deviceFinder.info
+
+ Rectangle {
+ id: viewContainer
+ anchors.top: parent.top
+ // only BlueZ platform has address type selection
+ anchors.bottom: connectPage.connectionHandler.requiresAddressType ? addressTypeButton.top
+ : searchButton.top
+ anchors.topMargin: GameSettings.fieldMargin + connectPage.messageHeight
+ anchors.bottomMargin: GameSettings.fieldMargin
+ anchors.horizontalCenter: parent.horizontalCenter
+ width: parent.width - GameSettings.fieldMargin * 2
+ color: GameSettings.viewColor
+ radius: GameSettings.buttonRadius
+
+ Text {
+ id: title
+ width: parent.width
+ height: GameSettings.fieldHeight
+ horizontalAlignment: Text.AlignHCenter
+ verticalAlignment: Text.AlignVCenter
+ color: GameSettings.textColor
+ font.pixelSize: GameSettings.mediumFontSize
+ text: qsTr("FOUND DEVICES")
+
+ BottomLine {
+ height: 1
+ width: parent.width
+ color: "#898989"
+ }
+ }
+
+ ListView {
+ id: devices
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.bottom: parent.bottom
+ anchors.top: title.bottom
+ model: connectPage.deviceFinder.devices
+ clip: true
+
+ delegate: Rectangle {
+ id: box
+
+ required property int index
+ required property var modelData
+
+ height: GameSettings.fieldHeight * 1.2
+ width: devices.width
+ color: index % 2 === 0 ? GameSettings.delegate1Color : GameSettings.delegate2Color
+
+ MouseArea {
+ anchors.fill: parent
+ onClicked: {
+ connectPage.deviceFinder.connectToService(box.modelData.deviceAddress)
+ connectPage.showMeasurePage()
+ }
+ }
+
+ Text {
+ id: device
+ font.pixelSize: GameSettings.smallFontSize
+ text: box.modelData.deviceName
+ anchors.top: parent.top
+ anchors.topMargin: parent.height * 0.1
+ anchors.leftMargin: parent.height * 0.1
+ anchors.left: parent.left
+ color: GameSettings.textColor
+ }
+
+ Text {
+ id: deviceAddress
+ font.pixelSize: GameSettings.smallFontSize
+ text: box.modelData.deviceAddress
+ anchors.bottom: parent.bottom
+ anchors.bottomMargin: parent.height * 0.1
+ anchors.rightMargin: parent.height * 0.1
+ anchors.right: parent.right
+ color: Qt.darker(GameSettings.textColor)
+ }
+ }
+ }
+ }
+
+ GameButton {
+ id: addressTypeButton
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.bottom: searchButton.top
+ anchors.bottomMargin: GameSettings.fieldMargin * 0.5
+ width: viewContainer.width
+ height: GameSettings.fieldHeight
+ visible: connectPage.connectionHandler.requiresAddressType // only required on BlueZ
+ state: "public"
+ onClicked: state === "public" ? state = "random" : state = "public"
+
+ states: [
+ State {
+ name: "public"
+ PropertyChanges {
+ addressTypeText.text: qsTr("Public Address")
+ }
+ PropertyChanges {
+ connectPage.deviceHandler.addressType: DeviceHandler.PUBLIC_ADDRESS
+ }
+ },
+ State {
+ name: "random"
+ PropertyChanges {
+ addressTypeText.text: qsTr("Random Address")
+ }
+ PropertyChanges {
+ connectPage.deviceHandler.addressType: DeviceHandler.RANDOM_ADDRESS
+ }
+ }
+ ]
+
+ Text {
+ id: addressTypeText
+ anchors.centerIn: parent
+ font.pixelSize: GameSettings.tinyFontSize
+ color: GameSettings.textColor
+ }
+ }
+
+ GameButton {
+ id: searchButton
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.bottom: parent.bottom
+ anchors.bottomMargin: GameSettings.fieldMargin
+ width: viewContainer.width
+ height: GameSettings.fieldHeight
+ enabled: !connectPage.deviceFinder.scanning
+ onClicked: connectPage.deviceFinder.startSearch()
+
+ Text {
+ anchors.centerIn: parent
+ font.pixelSize: GameSettings.tinyFontSize
+ text: qsTr("START SEARCH")
+ color: searchButton.enabled ? GameSettings.textColor : GameSettings.disabledTextColor
+ }
+ }
+}
diff --git a/examples/bluetooth/heartrate_game/HeartRateGame/GameButton.qml b/examples/bluetooth/heartrate_game/HeartRateGame/GameButton.qml
new file mode 100644
index 000000000..8e8760102
--- /dev/null
+++ b/examples/bluetooth/heartrate_game/HeartRateGame/GameButton.qml
@@ -0,0 +1,39 @@
+// Copyright (C) 2022 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+import QtQuick
+
+Rectangle {
+ id: button
+ color: baseColor
+ onEnabledChanged: checkColor()
+ radius: GameSettings.buttonRadius
+
+ property color baseColor: GameSettings.buttonColor
+ property color pressedColor: GameSettings.buttonPressedColor
+ property color disabledColor: GameSettings.disabledButtonColor
+
+ signal clicked
+
+ function checkColor() {
+ if (!button.enabled) {
+ button.color = disabledColor
+ } else {
+ if (mouseArea.containsPress)
+ button.color = pressedColor
+ else
+ button.color = baseColor
+ }
+ }
+
+ MouseArea {
+ id: mouseArea
+ anchors.fill: parent
+ onPressed: button.checkColor()
+ onReleased: button.checkColor()
+ onClicked: {
+ button.checkColor()
+ button.clicked()
+ }
+ }
+}
diff --git a/examples/bluetooth/heartrate_game/HeartRateGame/GamePage.qml b/examples/bluetooth/heartrate_game/HeartRateGame/GamePage.qml
new file mode 100644
index 000000000..249f94186
--- /dev/null
+++ b/examples/bluetooth/heartrate_game/HeartRateGame/GamePage.qml
@@ -0,0 +1,36 @@
+// Copyright (C) 2022 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+import QtQuick
+
+Item {
+ id: page
+
+ property string errorMessage: ""
+ property string infoMessage: ""
+ property real messageHeight: msg.height
+ property bool hasError: errorMessage != ""
+ property bool hasInfo: infoMessage != ""
+
+ Rectangle {
+ id: msg
+ anchors.top: parent.top
+ anchors.left: parent.left
+ anchors.right: parent.right
+ height: GameSettings.fieldHeight
+ color: page.hasError ? GameSettings.errorColor : GameSettings.infoColor
+ visible: page.hasError || page.hasInfo
+
+ Text {
+ id: error
+ anchors.fill: parent
+ horizontalAlignment: Text.AlignHCenter
+ verticalAlignment: Text.AlignVCenter
+ minimumPixelSize: 5
+ font.pixelSize: GameSettings.smallFontSize
+ fontSizeMode: Text.Fit
+ color: GameSettings.textColor
+ text: page.hasError ? page.errorMessage : page.infoMessage
+ }
+ }
+}
diff --git a/examples/bluetooth/heartrate_game/HeartRateGame/GameSettings.qml b/examples/bluetooth/heartrate_game/HeartRateGame/GameSettings.qml
new file mode 100644
index 000000000..0fe854609
--- /dev/null
+++ b/examples/bluetooth/heartrate_game/HeartRateGame/GameSettings.qml
@@ -0,0 +1,51 @@
+// Copyright (C) 2022 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+pragma Singleton
+import QtQuick
+
+Item {
+ property int wHeight
+ property int wWidth
+
+ // Colors
+ readonly property color backgroundColor: "#2d3037"
+ readonly property color buttonColor: "#202227"
+ readonly property color buttonPressedColor: "#6ccaf2"
+ readonly property color disabledButtonColor: "#555555"
+ readonly property color viewColor: "#202227"
+ readonly property color delegate1Color: Qt.darker(viewColor, 1.2)
+ readonly property color delegate2Color: Qt.lighter(viewColor, 1.2)
+ readonly property color textColor: "#ffffff"
+ readonly property color textDarkColor: "#232323"
+ readonly property color disabledTextColor: "#777777"
+ readonly property color sliderColor: "#6ccaf2"
+ readonly property color errorColor: "#ba3f62"
+ readonly property color infoColor: "#3fba62"
+
+ // Font sizes
+ property real microFontSize: hugeFontSize * 0.2
+ property real tinyFontSize: hugeFontSize * 0.4
+ property real smallTinyFontSize: hugeFontSize * 0.5
+ property real smallFontSize: hugeFontSize * 0.6
+ property real mediumFontSize: hugeFontSize * 0.7
+ property real bigFontSize: hugeFontSize * 0.8
+ property real largeFontSize: hugeFontSize * 0.9
+ property real hugeFontSize: (wWidth + wHeight) * 0.03
+ property real giganticFontSize: (wWidth + wHeight) * 0.04
+
+ // Some other values
+ property real fieldHeight: wHeight * 0.08
+ property real fieldMargin: fieldHeight * 0.5
+ property real buttonHeight: wHeight * 0.08
+ property real buttonRadius: buttonHeight * 0.1
+
+ // Some help functions
+ function widthForHeight(h, ss) {
+ return h / ss.height * ss.width
+ }
+
+ function heightForWidth(w, ss) {
+ return w / ss.width * ss.height
+ }
+}
diff --git a/examples/bluetooth/heartrate_game/HeartRateGame/Main.qml b/examples/bluetooth/heartrate_game/HeartRateGame/Main.qml
new file mode 100644
index 000000000..e26f9b004
--- /dev/null
+++ b/examples/bluetooth/heartrate_game/HeartRateGame/Main.qml
@@ -0,0 +1,71 @@
+// Copyright (C) 2022 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+pragma ComponentBehavior: Bound
+import QtQuick
+import QtQuick.Window
+import HeartRateGame
+
+Window {
+ id: wroot
+ visible: true
+ width: 720 * .7
+ height: 1240 * .7
+ title: qsTr("HeartRateGame")
+ color: GameSettings.backgroundColor
+
+ required property ConnectionHandler connectionHandler
+ required property DeviceFinder deviceFinder
+ required property DeviceHandler deviceHandler
+
+ Component.onCompleted: {
+ GameSettings.wWidth = Qt.binding(function () {
+ return width
+ })
+ GameSettings.wHeight = Qt.binding(function () {
+ return height
+ })
+ }
+
+ Loader {
+ id: splashLoader
+ anchors.fill: parent
+ asynchronous: false
+ visible: true
+
+ sourceComponent: SplashScreen {
+ appIsReady: appLoader.status === Loader.Ready
+ onReadyChanged: {
+ if (ready) {
+ appLoader.visible = true
+ splashLoader.visible = false
+ splashLoader.active = false
+ }
+ }
+ }
+
+ onStatusChanged: {
+ if (status === Loader.Ready)
+ appLoader.active = true
+ }
+ }
+
+ Loader {
+ id: appLoader
+ anchors.fill: parent
+ active: false
+ asynchronous: true
+ visible: false
+
+ sourceComponent: App {
+ connectionHandler: wroot.connectionHandler
+ deviceFinder: wroot.deviceFinder
+ deviceHandler: wroot.deviceHandler
+ }
+
+ onStatusChanged: {
+ if (status === Loader.Error)
+ Qt.quit()
+ }
+ }
+}
diff --git a/examples/bluetooth/heartrate_game/HeartRateGame/Measure.qml b/examples/bluetooth/heartrate_game/HeartRateGame/Measure.qml
new file mode 100644
index 000000000..48e84e762
--- /dev/null
+++ b/examples/bluetooth/heartrate_game/HeartRateGame/Measure.qml
@@ -0,0 +1,212 @@
+// Copyright (C) 2022 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+import QtQuick
+import HeartRateGame
+
+GamePage {
+ id: measurePage
+
+ required property DeviceHandler deviceHandler
+
+ errorMessage: deviceHandler.error
+ infoMessage: deviceHandler.info
+
+ property real __timeCounter: 0
+ property real __maxTimeCount: 60
+ property string relaxText: qsTr("Relax!\nWhen you are ready, press Start. You have %1s time to increase heartrate so much as possible.\nGood luck!").arg(__maxTimeCount)
+
+ signal showStatsPage
+
+ function close() {
+ deviceHandler.stopMeasurement()
+ deviceHandler.disconnectService()
+ }
+
+ function start() {
+ if (!deviceHandler.measuring) {
+ __timeCounter = 0
+ deviceHandler.startMeasurement()
+ }
+ }
+
+ function stop() {
+ if (deviceHandler.measuring)
+ deviceHandler.stopMeasurement()
+
+ measurePage.showStatsPage()
+ }
+
+ Timer {
+ id: measureTimer
+ interval: 1000
+ running: measurePage.deviceHandler.measuring
+ repeat: true
+ onTriggered: {
+ measurePage.__timeCounter++
+ if (measurePage.__timeCounter >= measurePage.__maxTimeCount)
+ measurePage.stop()
+ }
+ }
+
+ Column {
+ anchors.centerIn: parent
+ spacing: GameSettings.fieldHeight * 0.5
+
+ Rectangle {
+ id: circle
+ anchors.horizontalCenter: parent.horizontalCenter
+ width: Math.min(measurePage.width, measurePage.height - GameSettings.fieldHeight * 4)
+ - 2 * GameSettings.fieldMargin
+ height: width
+ radius: width * 0.5
+ color: GameSettings.viewColor
+
+ Text {
+ id: hintText
+ anchors.centerIn: parent
+ anchors.verticalCenterOffset: -parent.height * 0.1
+ horizontalAlignment: Text.AlignHCenter
+ verticalAlignment: Text.AlignVCenter
+ width: parent.width * 0.8
+ height: parent.height * 0.6
+ wrapMode: Text.WordWrap
+ text: measurePage.relaxText
+ visible: !measurePage.deviceHandler.measuring
+ color: GameSettings.textColor
+ fontSizeMode: Text.Fit
+ minimumPixelSize: 10
+ font.pixelSize: GameSettings.mediumFontSize
+ }
+
+ Text {
+ id: text
+ anchors.centerIn: parent
+ anchors.verticalCenterOffset: -parent.height * 0.15
+ font.pixelSize: parent.width * 0.45
+ text: measurePage.deviceHandler.hr
+ visible: measurePage.deviceHandler.measuring
+ color: GameSettings.textColor
+ }
+
+ Item {
+ id: minMaxContainer
+ anchors.horizontalCenter: parent.horizontalCenter
+ width: parent.width * 0.7
+ height: parent.height * 0.15
+ anchors.bottom: parent.bottom
+ anchors.bottomMargin: parent.height * 0.16
+ visible: measurePage.deviceHandler.measuring
+
+ Text {
+ anchors.left: parent.left
+ anchors.verticalCenter: parent.verticalCenter
+ text: measurePage.deviceHandler.minHR
+ color: GameSettings.textColor
+ font.pixelSize: GameSettings.hugeFontSize
+
+ Text {
+ anchors.left: parent.left
+ anchors.bottom: parent.top
+ font.pixelSize: parent.font.pixelSize * 0.8
+ color: parent.color
+ text: "MIN"
+ }
+ }
+
+ Text {
+ anchors.right: parent.right
+ anchors.verticalCenter: parent.verticalCenter
+ text: measurePage.deviceHandler.maxHR
+ color: GameSettings.textColor
+ font.pixelSize: GameSettings.hugeFontSize
+
+ Text {
+ anchors.right: parent.right
+ anchors.bottom: parent.top
+ font.pixelSize: parent.font.pixelSize * 0.8
+ color: parent.color
+ text: "MAX"
+ }
+ }
+ }
+
+ Image {
+ id: heart
+ anchors.horizontalCenter: minMaxContainer.horizontalCenter
+ anchors.verticalCenter: minMaxContainer.bottom
+ width: parent.width * 0.2
+ height: width
+ source: "images/heart.png"
+ smooth: true
+ antialiasing: true
+
+ SequentialAnimation {
+ id: heartAnim
+ running: measurePage.deviceHandler.alive
+ loops: Animation.Infinite
+ alwaysRunToEnd: true
+ PropertyAnimation {
+ target: heart
+ property: "scale"
+ to: 1.2
+ duration: 500
+ easing.type: Easing.InQuad
+ }
+ PropertyAnimation {
+ target: heart
+ property: "scale"
+ to: 1.0
+ duration: 500
+ easing.type: Easing.OutQuad
+ }
+ }
+ }
+ }
+
+ Rectangle {
+ id: timeSlider
+ color: GameSettings.viewColor
+ anchors.horizontalCenter: parent.horizontalCenter
+ width: circle.width
+ height: GameSettings.fieldHeight
+ radius: GameSettings.buttonRadius
+
+ Rectangle {
+ height: parent.height
+ radius: parent.radius
+ color: GameSettings.sliderColor
+ width: Math.min(
+ 1.0,
+ measurePage.__timeCounter / measurePage.__maxTimeCount) * parent.width
+ }
+
+ Text {
+ anchors.centerIn: parent
+ color: "gray"
+ text: (measurePage.__maxTimeCount - measurePage.__timeCounter).toFixed(0) + " s"
+ font.pixelSize: GameSettings.bigFontSize
+ }
+ }
+ }
+
+ GameButton {
+ id: startButton
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.bottom: parent.bottom
+ anchors.bottomMargin: GameSettings.fieldMargin
+ width: circle.width
+ height: GameSettings.fieldHeight
+ enabled: !measurePage.deviceHandler.measuring
+ radius: GameSettings.buttonRadius
+
+ onClicked: measurePage.start()
+
+ Text {
+ anchors.centerIn: parent
+ font.pixelSize: GameSettings.tinyFontSize
+ text: qsTr("START")
+ color: startButton.enabled ? GameSettings.textColor : GameSettings.disabledTextColor
+ }
+ }
+}
diff --git a/examples/bluetooth/heartrate_game/HeartRateGame/SplashScreen.qml b/examples/bluetooth/heartrate_game/HeartRateGame/SplashScreen.qml
new file mode 100644
index 000000000..2f9ac1b3f
--- /dev/null
+++ b/examples/bluetooth/heartrate_game/HeartRateGame/SplashScreen.qml
@@ -0,0 +1,30 @@
+// Copyright (C) 2022 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+import QtQuick
+import HeartRateGame
+
+Item {
+ id: root
+
+ property bool appIsReady: false
+ property bool splashIsReady: false
+ property bool ready: appIsReady && splashIsReady
+
+ anchors.fill: parent
+
+ Image {
+ anchors.centerIn: parent
+ width: Math.min(parent.height, parent.width) * 0.6
+ height: GameSettings.heightForWidth(width, sourceSize)
+ source: "images/logo.png"
+ }
+
+ Timer {
+ id: splashTimer
+ interval: 1000
+ onTriggered: splashIsReady = true
+ }
+
+ Component.onCompleted: splashTimer.start()
+}
diff --git a/examples/bluetooth/heartrate_game/HeartRateGame/Stats.qml b/examples/bluetooth/heartrate_game/HeartRateGame/Stats.qml
new file mode 100644
index 000000000..22cdd5365
--- /dev/null
+++ b/examples/bluetooth/heartrate_game/HeartRateGame/Stats.qml
@@ -0,0 +1,55 @@
+// Copyright (C) 2022 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+import QtQuick
+import HeartRateGame
+
+GamePage {
+ id: statsPage
+
+ required property DeviceHandler deviceHandler
+
+ Column {
+ anchors.centerIn: parent
+ width: parent.width
+
+ Text {
+ anchors.horizontalCenter: parent.horizontalCenter
+ font.pixelSize: GameSettings.hugeFontSize
+ color: GameSettings.textColor
+ text: qsTr("RESULT")
+ }
+
+ Text {
+ anchors.horizontalCenter: parent.horizontalCenter
+ font.pixelSize: GameSettings.giganticFontSize * 3
+ color: GameSettings.textColor
+ text: (statsPage.deviceHandler.maxHR - statsPage.deviceHandler.minHR).toFixed(0)
+ }
+
+ Item {
+ height: GameSettings.fieldHeight
+ width: 1
+ }
+
+ StatsLabel {
+ title: qsTr("MIN")
+ value: statsPage.deviceHandler.minHR.toFixed(0)
+ }
+
+ StatsLabel {
+ title: qsTr("MAX")
+ value: statsPage.deviceHandler.maxHR.toFixed(0)
+ }
+
+ StatsLabel {
+ title: qsTr("AVG")
+ value: statsPage.deviceHandler.average.toFixed(1)
+ }
+
+ StatsLabel {
+ title: qsTr("CALORIES")
+ value: statsPage.deviceHandler.calories.toFixed(3)
+ }
+ }
+}
diff --git a/examples/bluetooth/heartrate_game/HeartRateGame/StatsLabel.qml b/examples/bluetooth/heartrate_game/HeartRateGame/StatsLabel.qml
new file mode 100644
index 000000000..0ea4249a7
--- /dev/null
+++ b/examples/bluetooth/heartrate_game/HeartRateGame/StatsLabel.qml
@@ -0,0 +1,34 @@
+// Copyright (C) 2022 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+import QtQuick
+
+Item {
+ height: GameSettings.fieldHeight
+ width: parent.width
+
+ property alias title: leftText.text
+ property alias value: rightText.text
+
+ Text {
+ id: leftText
+ anchors.left: parent.left
+ height: parent.height
+ width: parent.width * 0.45
+ horizontalAlignment: Text.AlignRight
+ verticalAlignment: Text.AlignVCenter
+ font.pixelSize: GameSettings.mediumFontSize
+ color: GameSettings.textColor
+ }
+
+ Text {
+ id: rightText
+ anchors.right: parent.right
+ height: parent.height
+ width: parent.width * 0.45
+ horizontalAlignment: Text.AlignLeft
+ verticalAlignment: Text.AlignVCenter
+ font.pixelSize: GameSettings.mediumFontSize
+ color: GameSettings.textColor
+ }
+}
diff --git a/examples/bluetooth/heartrate_game/HeartRateGame/TitleBar.qml b/examples/bluetooth/heartrate_game/HeartRateGame/TitleBar.qml
new file mode 100644
index 000000000..016a44358
--- /dev/null
+++ b/examples/bluetooth/heartrate_game/HeartRateGame/TitleBar.qml
@@ -0,0 +1,54 @@
+// Copyright (C) 2022 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+pragma ComponentBehavior: Bound
+import QtQuick
+
+Rectangle {
+ id: titleBar
+
+ property var __titles: ["CONNECT", "MEASURE", "STATS"]
+ property int currentIndex: 0
+
+ signal titleClicked(int index)
+
+ height: GameSettings.fieldHeight
+ color: GameSettings.viewColor
+
+ Repeater {
+ model: 3
+ Text {
+ id: caption
+ required property int index
+ width: titleBar.width / 3
+ height: titleBar.height
+ x: index * width
+ horizontalAlignment: Text.AlignHCenter
+ verticalAlignment: Text.AlignVCenter
+ text: titleBar.__titles[index]
+ font.pixelSize: GameSettings.tinyFontSize
+ color: titleBar.currentIndex === index ? GameSettings.textColor
+ : GameSettings.disabledTextColor
+
+ MouseArea {
+ anchors.fill: parent
+ onClicked: titleBar.titleClicked(caption.index)
+ }
+ }
+ }
+
+ Item {
+ anchors.bottom: parent.bottom
+ width: parent.width / 3
+ height: parent.height
+ x: titleBar.currentIndex * width
+
+ BottomLine {}
+
+ Behavior on x {
+ NumberAnimation {
+ duration: 200
+ }
+ }
+ }
+}
diff --git a/examples/bluetooth/heartrate_game/HeartRateGame/images/bt_off_to_on.png b/examples/bluetooth/heartrate_game/HeartRateGame/images/bt_off_to_on.png
new file mode 100644
index 000000000..5ea1f3f06
--- /dev/null
+++ b/examples/bluetooth/heartrate_game/HeartRateGame/images/bt_off_to_on.png
Binary files differ
diff --git a/examples/bluetooth/heartrate_game/HeartRateGame/images/heart.png b/examples/bluetooth/heartrate_game/HeartRateGame/images/heart.png
new file mode 100644
index 000000000..f2b3c0a3e
--- /dev/null
+++ b/examples/bluetooth/heartrate_game/HeartRateGame/images/heart.png
Binary files differ
diff --git a/examples/bluetooth/heartrate_game/HeartRateGame/images/logo.png b/examples/bluetooth/heartrate_game/HeartRateGame/images/logo.png
new file mode 100644
index 000000000..ea0af7e00
--- /dev/null
+++ b/examples/bluetooth/heartrate_game/HeartRateGame/images/logo.png
Binary files differ
diff --git a/examples/bluetooth/heartrate_game/HeartRateGame/qmldir b/examples/bluetooth/heartrate_game/HeartRateGame/qmldir
new file mode 100644
index 000000000..2baa74a92
--- /dev/null
+++ b/examples/bluetooth/heartrate_game/HeartRateGame/qmldir
@@ -0,0 +1,14 @@
+module HeartRateGame
+App 1.0 App.qml
+BluetoothAlarmDialog 1.0 BluetoothAlarmDialog.qml
+BottomLine 1.0 BottomLine.qml
+Connect 1.0 Connect.qml
+GameButton 1.0 GameButton.qml
+GamePage 1.0 GamePage.qml
+singleton GameSettings 1.0 GameSettings.qml
+Measure 1.0 Measure.qml
+SplashScreen 1.0 SplashScreen.qml
+Stats 1.0 Stats.qml
+StatsLabel 1.0 StatsLabel.qml
+TitleBar 1.0 TitleBar.qml
+Main 1.0 Main.qml
diff --git a/examples/bluetooth/heartrate_game/bluetoothbaseclass.py b/examples/bluetooth/heartrate_game/bluetoothbaseclass.py
new file mode 100644
index 000000000..cc5c9dbd0
--- /dev/null
+++ b/examples/bluetooth/heartrate_game/bluetoothbaseclass.py
@@ -0,0 +1,40 @@
+# Copyright (C) 2022 The Qt Company Ltd.
+# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+from PySide6.QtCore import QObject, Property, Signal, Slot
+
+
+class BluetoothBaseClass(QObject):
+
+ errorChanged = Signal()
+ infoChanged = Signal()
+
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ self.m_error = ""
+ self.m_info = ""
+
+ @Property(str, notify=errorChanged)
+ def error(self):
+ return self.m_error
+
+ @error.setter
+ def error(self, e):
+ if self.m_error != e:
+ self.m_error = e
+ self.errorChanged.emit()
+
+ @Property(str, notify=infoChanged)
+ def info(self):
+ return self.m_info
+
+ @info.setter
+ def info(self, i):
+ if self.m_info != i:
+ self.m_info = i
+ self.infoChanged.emit()
+
+ @Slot()
+ def clearMessages(self):
+ self.info = ""
+ self.error = ""
diff --git a/examples/bluetooth/heartrate_game/connectionhandler.py b/examples/bluetooth/heartrate_game/connectionhandler.py
new file mode 100644
index 000000000..7bf60bbc5
--- /dev/null
+++ b/examples/bluetooth/heartrate_game/connectionhandler.py
@@ -0,0 +1,77 @@
+# Copyright (C) 2022 The Qt Company Ltd.
+# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+import sys
+
+from PySide6.QtBluetooth import QBluetoothLocalDevice
+from PySide6.QtQml import QmlElement
+from PySide6.QtCore import QObject, Property, Signal, Slot, Qt
+
+from heartrate_global import simulator, is_android, error_not_nuitka
+
+if is_android or sys.platform == "darwin":
+ from PySide6.QtCore import QBluetoothPermission
+
+# To be used on the @QmlElement decorator
+# (QML_IMPORT_MINOR_VERSION is optional)
+QML_IMPORT_NAME = "HeartRateGame"
+QML_IMPORT_MAJOR_VERSION = 1
+
+
+@QmlElement
+class ConnectionHandler(QObject):
+
+ deviceChanged = Signal()
+
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ self.m_hasPermission = False
+ self.initLocalDevice()
+
+ @Property(bool, notify=deviceChanged)
+ def alive(self):
+ if sys.platform == "darwin":
+ return True
+ if simulator():
+ return True
+ return (self.m_localDevice.isValid()
+ and self.m_localDevice.hostMode() != QBluetoothLocalDevice.HostPoweredOff)
+
+ @Property(bool, constant=True)
+ def requiresAddressType(self):
+ return sys.platform == "linux" # QT_CONFIG(bluez)?
+
+ @Property(str, notify=deviceChanged)
+ def name(self):
+ return self.m_localDevice.name()
+
+ @Property(str, notify=deviceChanged)
+ def address(self):
+ return self.m_localDevice.address().toString()
+
+ @Property(bool, notify=deviceChanged)
+ def hasPermission(self):
+ return self.m_hasPermission
+
+ @Slot(QBluetoothLocalDevice.HostMode)
+ def hostModeChanged(self, mode):
+ self.deviceChanged.emit()
+
+ def initLocalDevice(self):
+ if is_android or sys.platform == "darwin":
+ error_not_nuitka()
+ permission = QBluetoothPermission()
+ permission.setCommunicationModes(QBluetoothPermission.Access)
+ permission_status = qApp.checkPermission(permission) # noqa: F821
+ if permission_status == Qt.PermissionStatus.Undetermined:
+ qApp.requestPermission(permission, self, self.initLocalDevice) # noqa: F821
+ return
+ if permission_status == Qt.PermissionStatus.Denied:
+ return
+ elif permission_status == Qt.PermissionStatus.Granted:
+ print("[HeartRateGame] Bluetooth Permission Granted")
+
+ self.m_localDevice = QBluetoothLocalDevice()
+ self.m_localDevice.hostModeStateChanged.connect(self.hostModeChanged)
+ self.m_hasPermission = True
+ self.deviceChanged.emit()
diff --git a/examples/bluetooth/heartrate_game/devicefinder.py b/examples/bluetooth/heartrate_game/devicefinder.py
new file mode 100644
index 000000000..e581d12ec
--- /dev/null
+++ b/examples/bluetooth/heartrate_game/devicefinder.py
@@ -0,0 +1,139 @@
+# Copyright (C) 2022 The Qt Company Ltd.
+# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+import sys
+
+from PySide6.QtBluetooth import (QBluetoothDeviceDiscoveryAgent,
+ QBluetoothDeviceInfo)
+from PySide6.QtQml import QmlElement
+from PySide6.QtCore import QTimer, Property, Signal, Slot, Qt
+
+from bluetoothbaseclass import BluetoothBaseClass
+from deviceinfo import DeviceInfo
+from heartrate_global import simulator, is_android, error_not_nuitka
+
+if is_android or sys.platform == "darwin":
+ from PySide6.QtCore import QBluetoothPermission
+
+# To be used on the @QmlElement decorator
+# (QML_IMPORT_MINOR_VERSION is optional)
+QML_IMPORT_NAME = "HeartRateGame"
+QML_IMPORT_MAJOR_VERSION = 1
+
+
+@QmlElement
+class DeviceFinder(BluetoothBaseClass):
+
+ scanningChanged = Signal()
+ devicesChanged = Signal()
+
+ def __init__(self, handler, parent=None):
+ super().__init__(parent)
+ self.m_deviceHandler = handler
+ self.m_devices = []
+ self.m_demoTimer = QTimer()
+#! [devicediscovery-1]
+ self.m_deviceDiscoveryAgent = QBluetoothDeviceDiscoveryAgent(self)
+ self.m_deviceDiscoveryAgent.setLowEnergyDiscoveryTimeout(15000)
+ self.m_deviceDiscoveryAgent.deviceDiscovered.connect(self.addDevice)
+ self.m_deviceDiscoveryAgent.errorOccurred.connect(self.scanError)
+
+ self.m_deviceDiscoveryAgent.finished.connect(self.scanFinished)
+ self.m_deviceDiscoveryAgent.canceled.connect(self.scanFinished)
+#! [devicediscovery-1]
+ if simulator():
+ self.m_demoTimer.setSingleShot(True)
+ self.m_demoTimer.setInterval(2000)
+ self.m_demoTimer.timeout.connect(self.scanFinished)
+
+ @Slot()
+ def startSearch(self):
+ if is_android or sys.platform == "darwin":
+ error_not_nuitka()
+ permission = QBluetoothPermission()
+ permission.setCommunicationModes(QBluetoothPermission.Access)
+ permission_status = qApp.checkPermission(permission) # noqa: F821
+ if permission_status == Qt.PermissionStatus.Undetermined:
+ qApp.requestPermission(permission, self, self.startSearch) # noqa: F82 1
+ return
+ elif permission_status == Qt.PermissionStatus.Denied:
+ return
+ elif permission_status == Qt.PermissionStatus.Granted:
+ print("[HeartRateGame] Bluetooth Permission Granted")
+
+ self.clearMessages()
+ self.m_deviceHandler.setDevice(None)
+ self.m_devices.clear()
+
+ self.devicesChanged.emit()
+
+ if simulator():
+ self.m_demoTimer.start()
+ else:
+#! [devicediscovery-2]
+ self.m_deviceDiscoveryAgent.start(QBluetoothDeviceDiscoveryAgent.LowEnergyMethod)
+#! [devicediscovery-2]
+ self.scanningChanged.emit()
+ self.info = "Scanning for devices..."
+
+#! [devicediscovery-3]
+ @Slot(QBluetoothDeviceInfo)
+ def addDevice(self, device):
+ # If device is LowEnergy-device, add it to the list
+ if device.coreConfigurations() & QBluetoothDeviceInfo.LowEnergyCoreConfiguration:
+ self.m_devices.append(DeviceInfo(device))
+ self.info = "Low Energy device found. Scanning more..."
+#! [devicediscovery-3]
+ self.devicesChanged.emit()
+#! [devicediscovery-4]
+ #...
+#! [devicediscovery-4]
+
+ @Slot(QBluetoothDeviceDiscoveryAgent.Error)
+ def scanError(self, error):
+ if error == QBluetoothDeviceDiscoveryAgent.PoweredOffError:
+ self.error = "The Bluetooth adaptor is powered off."
+ elif error == QBluetoothDeviceDiscoveryAgent.InputOutputError:
+ self.error = "Writing or reading from the device resulted in an error."
+ else:
+ self.error = "An unknown error has occurred."
+
+ @Slot()
+ def scanFinished(self):
+ if simulator():
+ # Only for testing
+ for i in range(5):
+ self.m_devices.append(DeviceInfo(QBluetoothDeviceInfo()))
+
+ if self.m_devices:
+ self.info = "Scanning done."
+ else:
+ self.error = "No Low Energy devices found."
+
+ self.scanningChanged.emit()
+ self.devicesChanged.emit()
+
+ @Slot(str)
+ def connectToService(self, address):
+ self.m_deviceDiscoveryAgent.stop()
+
+ currentDevice = None
+ for entry in self.m_devices:
+ device = entry
+ if device and device.deviceAddress == address:
+ currentDevice = device
+ break
+
+ if currentDevice:
+ self.m_deviceHandler.setDevice(currentDevice)
+
+ self.clearMessages()
+
+ @Property(bool, notify=scanningChanged)
+ def scanning(self):
+ if simulator():
+ return self.m_demoTimer.isActive()
+ return self.m_deviceDiscoveryAgent.isActive()
+
+ @Property("QVariant", notify=devicesChanged)
+ def devices(self):
+ return self.m_devices
diff --git a/examples/bluetooth/heartrate_game/devicehandler.py b/examples/bluetooth/heartrate_game/devicehandler.py
new file mode 100644
index 000000000..df34052b8
--- /dev/null
+++ b/examples/bluetooth/heartrate_game/devicehandler.py
@@ -0,0 +1,309 @@
+# Copyright (C) 2022 The Qt Company Ltd.
+# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+import struct
+
+from enum import IntEnum
+
+from PySide6.QtBluetooth import (QLowEnergyCharacteristic,
+ QLowEnergyController,
+ QLowEnergyDescriptor,
+ QLowEnergyService,
+ QBluetoothUuid)
+from PySide6.QtQml import QmlElement
+from PySide6.QtCore import (QByteArray, QDateTime, QRandomGenerator, QTimer,
+ Property, Signal, Slot, QEnum)
+
+from bluetoothbaseclass import BluetoothBaseClass
+from heartrate_global import simulator
+
+
+# To be used on the @QmlElement decorator
+# (QML_IMPORT_MINOR_VERSION is optional)
+QML_IMPORT_NAME = "HeartRateGame"
+QML_IMPORT_MAJOR_VERSION = 1
+
+
+@QmlElement
+class DeviceHandler(BluetoothBaseClass):
+
+ @QEnum
+ class AddressType(IntEnum):
+ PUBLIC_ADDRESS = 1
+ RANDOM_ADDRESS = 2
+
+ measuringChanged = Signal()
+ aliveChanged = Signal()
+ statsChanged = Signal()
+
+ def __init__(self, parent=None):
+ super().__init__(parent)
+
+ self.m_control = None
+ self.m_service = None
+ self.m_notificationDesc = QLowEnergyDescriptor()
+ self.m_currentDevice = None
+
+ self.m_foundHeartRateService = False
+ self.m_measuring = False
+ self.m_currentValue = 0
+ self.m_min = 0
+ self.m_max = 0
+ self.m_sum = 0
+ self.m_avg = 0.0
+ self.m_calories = 0.0
+
+ self.m_start = QDateTime()
+ self.m_stop = QDateTime()
+
+ self.m_measurements = []
+ self.m_addressType = QLowEnergyController.PublicAddress
+
+ self.m_demoTimer = QTimer()
+
+ if simulator():
+ self.m_demoTimer.setSingleShot(False)
+ self.m_demoTimer.setInterval(2000)
+ self.m_demoTimer.timeout.connect(self.updateDemoHR)
+ self.m_demoTimer.start()
+ self.updateDemoHR()
+
+ @Property(int)
+ def addressType(self):
+ if self.m_addressType == QLowEnergyController.RandomAddress:
+ return DeviceHandler.AddressType.RANDOM_ADDRESS
+ return DeviceHandler.AddressType.PUBLIC_ADDRESS
+
+ @addressType.setter
+ def addressType(self, type):
+ if type == DeviceHandler.AddressType.PUBLIC_ADDRESS:
+ self.m_addressType = QLowEnergyController.PublicAddress
+ elif type == DeviceHandler.AddressType.RANDOM_ADDRESS:
+ self.m_addressType = QLowEnergyController.RandomAddress
+
+ @Slot(QLowEnergyController.Error)
+ def controllerErrorOccurred(self, device):
+ self.error = "Cannot connect to remote device."
+
+ @Slot()
+ def controllerConnected(self):
+ self.info = "Controller connected. Search services..."
+ self.m_control.discoverServices()
+
+ @Slot()
+ def controllerDisconnected(self):
+ self.error = "LowEnergy controller disconnected"
+
+ def setDevice(self, device):
+ self.clearMessages()
+ self.m_currentDevice = device
+
+ if simulator():
+ self.info = "Demo device connected."
+ return
+
+ # Disconnect and delete old connection
+ if self.m_control:
+ self.m_control.disconnectFromDevice()
+ self.m_control = None
+
+ # Create new controller and connect it if device available
+ if self.m_currentDevice:
+
+ # Make connections
+#! [Connect-Signals-1]
+ self.m_control = QLowEnergyController.createCentral(self.m_currentDevice.device(), self)
+#! [Connect-Signals-1]
+ self.m_control.setRemoteAddressType(self.m_addressType)
+#! [Connect-Signals-2]
+
+ self.m_control.serviceDiscovered.connect(self.serviceDiscovered)
+ self.m_control.discoveryFinished.connect(self.serviceScanDone)
+
+ self.m_control.errorOccurred.connect(self.controllerErrorOccurred)
+ self.m_control.connected.connect(self.controllerConnected)
+ self.m_control.disconnected.connect(self.controllerDisconnected)
+
+ # Connect
+ self.m_control.connectToDevice()
+#! [Connect-Signals-2]
+
+ @Slot()
+ def startMeasurement(self):
+ if self.alive:
+ self.m_start = QDateTime.currentDateTime()
+ self.m_min = 0
+ self.m_max = 0
+ self.m_avg = 0
+ self.m_sum = 0
+ self.m_calories = 0.0
+ self.m_measuring = True
+ self.m_measurements.clear()
+ self.measuringChanged.emit()
+
+ @Slot()
+ def stopMeasurement(self):
+ self.m_measuring = False
+ self.measuringChanged.emit()
+
+#! [Filter HeartRate service 1]
+ @Slot(QBluetoothUuid)
+ def serviceDiscovered(self, gatt):
+ if gatt == QBluetoothUuid(QBluetoothUuid.ServiceClassUuid.HeartRate):
+ self.info = "Heart Rate service discovered. Waiting for service scan to be done..."
+ self.m_foundHeartRateService = True
+
+#! [Filter HeartRate service 1]
+
+ @Slot()
+ def serviceScanDone(self):
+ self.info = "Service scan done."
+
+ # Delete old service if available
+ if self.m_service:
+ self.m_service = None
+
+#! [Filter HeartRate service 2]
+ # If heartRateService found, create new service
+ if self.m_foundHeartRateService:
+ self.m_service = self.m_control.createServiceObject(
+ QBluetoothUuid(QBluetoothUuid.ServiceClassUuid.HeartRate), self)
+
+ if self.m_service:
+ self.m_service.stateChanged.connect(self.serviceStateChanged)
+ self.m_service.characteristicChanged.connect(self.updateHeartRateValue)
+ self.m_service.descriptorWritten.connect(self.confirmedDescriptorWrite)
+ self.m_service.discoverDetails()
+ else:
+ self.error = "Heart Rate Service not found."
+#! [Filter HeartRate service 2]
+
+# Service functions
+#! [Find HRM characteristic]
+ @Slot(QLowEnergyService.ServiceState)
+ def serviceStateChanged(self, switch):
+ if switch == QLowEnergyService.RemoteServiceDiscovering:
+ self.info = "Discovering services..."
+ elif switch == QLowEnergyService.RemoteServiceDiscovered:
+ self.info = "Service discovered."
+ hrChar = self.m_service.characteristic(
+ QBluetoothUuid(QBluetoothUuid.CharacteristicType.HeartRateMeasurement))
+ if hrChar.isValid():
+ self.m_notificationDesc = hrChar.descriptor(
+ QBluetoothUuid.DescriptorType.ClientCharacteristicConfiguration)
+ if self.m_notificationDesc.isValid():
+ self.m_service.writeDescriptor(self.m_notificationDesc,
+ QByteArray.fromHex(b"0100"))
+ else:
+ self.error = "HR Data not found."
+ self.aliveChanged.emit()
+#! [Find HRM characteristic]
+
+#! [Reading value]
+ @Slot(QLowEnergyCharacteristic, QByteArray)
+ def updateHeartRateValue(self, c, value):
+ # ignore any other characteristic change. Shouldn't really happen though
+ if c.uuid() != QBluetoothUuid(QBluetoothUuid.CharacteristicType.HeartRateMeasurement):
+ return
+
+ data = value.data()
+ flags = int(data[0])
+ # Heart Rate
+ hrvalue = 0
+ if flags & 0x1: # HR 16 bit little endian? otherwise 8 bit
+ hrvalue = struct.unpack("<H", data[1:3])[0]
+ else:
+ hrvalue = struct.unpack("B", data[1:2])[0]
+
+ self.addMeasurement(hrvalue)
+
+#! [Reading value]
+ @Slot()
+ def updateDemoHR(self):
+ randomValue = 0
+ if self.m_currentValue < 30: # Initial value
+ randomValue = 55 + QRandomGenerator.global_().bounded(30)
+ elif not self.m_measuring: # Value when relax
+ random = QRandomGenerator.global_().bounded(5)
+ randomValue = self.m_currentValue - 2 + random
+ randomValue = max(min(randomValue, 55), 75)
+ else: # Measuring
+ random = QRandomGenerator.global_().bounded(10)
+ randomValue = self.m_currentValue + random - 2
+
+ self.addMeasurement(randomValue)
+
+ @Slot(QLowEnergyCharacteristic, QByteArray)
+ def confirmedDescriptorWrite(self, d, value):
+ if (d.isValid() and d == self.m_notificationDesc
+ and value == QByteArray.fromHex(b"0000")):
+ # disabled notifications . assume disconnect intent
+ self.m_control.disconnectFromDevice()
+ self.m_service = None
+
+ @Slot()
+ def disconnectService(self):
+ self.m_foundHeartRateService = False
+
+ # disable notifications
+ if (self.m_notificationDesc.isValid() and self.m_service
+ and self.m_notificationDesc.value() == QByteArray.fromHex(b"0100")):
+ self.m_service.writeDescriptor(self.m_notificationDesc,
+ QByteArray.fromHex(b"0000"))
+ else:
+ if self.m_control:
+ self.m_control.disconnectFromDevice()
+ self.m_service = None
+
+ @Property(bool, notify=measuringChanged)
+ def measuring(self):
+ return self.m_measuring
+
+ @Property(bool, notify=aliveChanged)
+ def alive(self):
+ if simulator():
+ return True
+ if self.m_service:
+ return self.m_service.state() == QLowEnergyService.RemoteServiceDiscovered
+ return False
+
+ @Property(int, notify=statsChanged)
+ def hr(self):
+ return self.m_currentValue
+
+ @Property(int, notify=statsChanged)
+ def time(self):
+ return self.m_start.secsTo(self.m_stop)
+
+ @Property(int, notify=statsChanged)
+ def maxHR(self):
+ return self.m_max
+
+ @Property(int, notify=statsChanged)
+ def minHR(self):
+ return self.m_min
+
+ @Property(float, notify=statsChanged)
+ def average(self):
+ return self.m_avg
+
+ @Property(float, notify=statsChanged)
+ def calories(self):
+ return self.m_calories
+
+ def addMeasurement(self, value):
+ self.m_currentValue = value
+
+ # If measuring and value is appropriate
+ if self.m_measuring and value > 30 and value < 250:
+ self.m_stop = QDateTime.currentDateTime()
+ self.m_measurements.append(value)
+
+ self.m_min = value if self.m_min == 0 else min(value, self.m_min)
+ self.m_max = max(value, self.m_max)
+ self.m_sum += value
+ self.m_avg = float(self.m_sum) / len(self.m_measurements)
+ self.m_calories = ((-55.0969 + (0.6309 * self.m_avg) + (0.1988 * 94)
+ + (0.2017 * 24)) / 4.184) * 60 * self.time / 3600
+
+ self.statsChanged.emit()
diff --git a/examples/bluetooth/heartrate_game/deviceinfo.py b/examples/bluetooth/heartrate_game/deviceinfo.py
new file mode 100644
index 000000000..5fd5c3270
--- /dev/null
+++ b/examples/bluetooth/heartrate_game/deviceinfo.py
@@ -0,0 +1,38 @@
+# 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 heartrate_global import simulator
+
+
+class DeviceInfo(QObject):
+
+ deviceChanged = Signal()
+
+ def __init__(self, device):
+ super().__init__()
+ self.m_device = device
+
+ def device(self):
+ return self.m_device
+
+ def setDevice(self, device):
+ self.m_device = device
+ self.deviceChanged.emit()
+
+ @Property(str, notify=deviceChanged)
+ def deviceName(self):
+ if simulator():
+ return "Demo device"
+ return self.m_device.name()
+
+ @Property(str, notify=deviceChanged)
+ def deviceAddress(self):
+ if simulator():
+ return "00:11:22:33:44:55"
+ if sys.platform == "Darwin": # workaround for Core Bluetooth:
+ return self.m_device.deviceUuid().toString()
+ return self.m_device.address().toString()
diff --git a/examples/bluetooth/heartrate_game/doc/heartrate_game.rst b/examples/bluetooth/heartrate_game/doc/heartrate_game.rst
new file mode 100644
index 000000000..9d190d991
--- /dev/null
+++ b/examples/bluetooth/heartrate_game/doc/heartrate_game.rst
@@ -0,0 +1,11 @@
+Bluetooth Low Energy Heart Rate Game
+====================================
+
+.. tags:: Android
+
+The Bluetooth Low Energy Heart Rate Game shows how to develop a
+Bluetooth Low Energy application using the Qt Bluetooth API. The
+application covers the scanning for Bluetooth Low Energy devices,
+connecting to a Heart Rate service on the device, writing
+characteristics and descriptors, and receiving updates from the device
+once the heart rate has changed.
diff --git a/examples/bluetooth/heartrate_game/heartrate_game.pyproject b/examples/bluetooth/heartrate_game/heartrate_game.pyproject
new file mode 100644
index 000000000..94b7e3978
--- /dev/null
+++ b/examples/bluetooth/heartrate_game/heartrate_game.pyproject
@@ -0,0 +1,23 @@
+{
+ "files": ["main.py",
+ "bluetoothbaseclass.py",
+ "connectionhandler.py",
+ "devicefinder.py",
+ "devicehandler.py",
+ "deviceinfo.py",
+ "heartrate_global.py",
+ "HeartRateGame/qmldir",
+ "HeartRateGame/Main.qml",
+ "HeartRateGame/App.qml",
+ "HeartRateGame/BluetoothAlarmDialog.qml",
+ "HeartRateGame/BottomLine.qml",
+ "HeartRateGame/Connect.qml",
+ "HeartRateGame/GameButton.qml",
+ "HeartRateGame/GamePage.qml",
+ "HeartRateGame/GameSettings.qml",
+ "HeartRateGame/Measure.qml",
+ "HeartRateGame/SplashScreen.qml",
+ "HeartRateGame/Stats.qml",
+ "HeartRateGame/StatsLabel.qml",
+ "HeartRateGame/TitleBar.qml"]
+}
diff --git a/examples/bluetooth/heartrate_game/heartrate_global.py b/examples/bluetooth/heartrate_game/heartrate_global.py
new file mode 100644
index 000000000..de5c37ac3
--- /dev/null
+++ b/examples/bluetooth/heartrate_game/heartrate_global.py
@@ -0,0 +1,30 @@
+# Copyright (C) 2022 The Qt Company Ltd.
+# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+import os
+import sys
+
+_simulator = False
+
+
+def simulator():
+ global _simulator
+ return _simulator
+
+
+def set_simulator(s):
+ global _simulator
+ _simulator = s
+
+
+is_android = os.environ.get('ANDROID_ARGUMENT')
+
+
+def error_not_nuitka():
+ """Errors and exits for macOS if run in interpreted mode.
+ """
+ is_nuitka = "__compiled__" in globals()
+ if not is_nuitka and sys.platform == "darwin":
+ print("This example does not work on macOS when Python is run in interpreted mode."
+ "For this example to work on macOS, package the example using pyside6-deploy"
+ "For more information, read `Notes for Developer` in the documentation")
+ sys.exit(0)
diff --git a/examples/bluetooth/heartrate_game/main.py b/examples/bluetooth/heartrate_game/main.py
new file mode 100644
index 000000000..3cb4f0672
--- /dev/null
+++ b/examples/bluetooth/heartrate_game/main.py
@@ -0,0 +1,53 @@
+# Copyright (C) 2022 The Qt Company Ltd.
+# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+"""PySide6 port of the bluetooth/heartrate-game example from Qt v6.x"""
+
+from pathlib import Path
+import sys
+from argparse import ArgumentParser, RawDescriptionHelpFormatter
+
+from PySide6.QtQml import QQmlApplicationEngine
+from PySide6.QtGui import QGuiApplication
+from PySide6.QtCore import QCoreApplication, QLoggingCategory
+
+from connectionhandler import ConnectionHandler
+from devicefinder import DeviceFinder
+from devicehandler import DeviceHandler
+from heartrate_global import set_simulator
+
+
+if __name__ == '__main__':
+ parser = ArgumentParser(prog="heartrate-game",
+ formatter_class=RawDescriptionHelpFormatter)
+
+ parser.add_argument("-v", "--verbose", action="store_true",
+ help="Generate more output")
+ parser.add_argument("-s", "--simulator", action="store_true",
+ help="Use Simulator")
+ options = parser.parse_args()
+ set_simulator(options.simulator)
+ if options.verbose:
+ QLoggingCategory.setFilterRules("qt.bluetooth* = true")
+
+ app = QGuiApplication(sys.argv)
+
+ connectionHandler = ConnectionHandler()
+ deviceHandler = DeviceHandler()
+ deviceFinder = DeviceFinder(deviceHandler)
+
+ engine = QQmlApplicationEngine()
+ engine.setInitialProperties({
+ "connectionHandler": connectionHandler,
+ "deviceFinder": deviceFinder,
+ "deviceHandler": deviceHandler})
+
+ engine.addImportPath(Path(__file__).parent)
+ engine.loadFromModule("HeartRateGame", "Main")
+
+ if not engine.rootObjects():
+ sys.exit(-1)
+
+ ex = QCoreApplication.exec()
+ del engine
+ sys.exit(ex)
diff --git a/examples/bluetooth/heartrate_server/doc/heartrate_server.rst b/examples/bluetooth/heartrate_server/doc/heartrate_server.rst
new file mode 100644
index 000000000..aaa1a0988
--- /dev/null
+++ b/examples/bluetooth/heartrate_server/doc/heartrate_server.rst
@@ -0,0 +1,8 @@
+Bluetooth Low Energy Heart Rate Server
+======================================
+
+The Bluetooth Low Energy Heart Rate Server is a command-line
+application that shows how to develop a Bluetooth GATT server using
+the Qt Bluetooth API. The application covers setting up a GATT
+service, advertising it and notifying clients about changes to
+characteristic values.
diff --git a/examples/bluetooth/heartrate_server/heartrate_server.py b/examples/bluetooth/heartrate_server/heartrate_server.py
new file mode 100644
index 000000000..abbf4eb7f
--- /dev/null
+++ b/examples/bluetooth/heartrate_server/heartrate_server.py
@@ -0,0 +1,95 @@
+# Copyright (C) 2022 The Qt Company Ltd.
+# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+"""PySide6 port of the bluetooth/heartrate-server example from Qt v6.x"""
+
+import sys
+from enum import Enum
+
+from PySide6.QtBluetooth import (QBluetoothUuid, QLowEnergyAdvertisingData,
+ QLowEnergyAdvertisingParameters,
+ QLowEnergyCharacteristic,
+ QLowEnergyCharacteristicData,
+ QLowEnergyController,
+ QLowEnergyDescriptorData,
+ QLowEnergyServiceData)
+from PySide6.QtGui import QGuiApplication
+from PySide6.QtCore import QByteArray, QTimer, QLoggingCategory
+
+
+class ValueChange(Enum):
+ VALUE_UP = 1
+ VALUE_DOWN = 2
+
+
+if __name__ == '__main__':
+ app = QGuiApplication(sys.argv)
+ QLoggingCategory.setFilterRules("qt.bluetooth* = true")
+
+#! [Advertising Data]
+ advertising_data = QLowEnergyAdvertisingData()
+ advertising_data.setDiscoverability(QLowEnergyAdvertisingData.DiscoverabilityGeneral)
+ advertising_data.setIncludePowerLevel(True)
+ advertising_data.setLocalName("HeartRateServer")
+ advertising_data.setServices([QBluetoothUuid.ServiceClassUuid.HeartRate])
+#! [Advertising Data]
+
+#! [Service Data]
+ char_data = QLowEnergyCharacteristicData()
+ char_data.setUuid(QBluetoothUuid.CharacteristicType.HeartRateMeasurement)
+ char_data.setValue(QByteArray(2, 0))
+ char_data.setProperties(QLowEnergyCharacteristic.Notify)
+ client_config = QLowEnergyDescriptorData(
+ QBluetoothUuid.DescriptorType.ClientCharacteristicConfiguration, QByteArray(2, 0))
+ char_data.addDescriptor(client_config)
+
+ service_data = QLowEnergyServiceData()
+ service_data.setType(QLowEnergyServiceData.ServiceTypePrimary)
+ service_data.setUuid(QBluetoothUuid.ServiceClassUuid.HeartRate)
+ service_data.addCharacteristic(char_data)
+#! [Service Data]
+
+#! [Start Advertising]
+ le_controller = QLowEnergyController.createPeripheral()
+ service = le_controller.addService(service_data)
+ le_controller.startAdvertising(QLowEnergyAdvertisingParameters(),
+ advertising_data, advertising_data)
+#! [Start Advertising]
+
+#! [Provide Heartbeat]
+ value_change = ValueChange.VALUE_UP
+ heartbeat_timer = QTimer()
+ current_heart_rate = 60
+
+ def heartbeat_provider():
+ global current_heart_rate, value_change, current_heart_rate
+ value = QByteArray()
+ value.append(chr(0)) # Flags that specify the format of the value.
+ value.append(chr(current_heart_rate)) # Actual value.
+ characteristic = service.characteristic(
+ QBluetoothUuid.CharacteristicType.HeartRateMeasurement)
+ assert characteristic.isValid()
+ # Potentially causes notification.
+ service.writeCharacteristic(characteristic, value)
+ if current_heart_rate == 60:
+ value_change = ValueChange.VALUE_UP
+ elif current_heart_rate == 100:
+ value_change = ValueChange.VALUE_DOWN
+ if value_change == ValueChange.VALUE_UP:
+ current_heart_rate += 1
+ else:
+ current_heart_rate -= 1
+
+ heartbeat_timer.timeout.connect(heartbeat_provider)
+ heartbeat_timer.start(1000)
+#! [Provide Heartbeat]
+
+ def reconnect():
+ service = le_controller.addService(service_data)
+ if not service.isNull():
+ le_controller.startAdvertising(QLowEnergyAdvertisingParameters(),
+ advertising_data, advertising_data)
+
+ le_controller.disconnected.connect(reconnect)
+
+ sys.exit(app.exec())
diff --git a/examples/bluetooth/heartrate_server/heartrate_server.pyproject b/examples/bluetooth/heartrate_server/heartrate_server.pyproject
new file mode 100644
index 000000000..de1fd14a0
--- /dev/null
+++ b/examples/bluetooth/heartrate_server/heartrate_server.pyproject
@@ -0,0 +1,3 @@
+{
+ "files": ["heartrate_server.py"]
+}
diff --git a/examples/bluetooth/lowenergyscanner/Scanner/Characteristics.qml b/examples/bluetooth/lowenergyscanner/Scanner/Characteristics.qml
new file mode 100644
index 000000000..bd3ccbfcb
--- /dev/null
+++ b/examples/bluetooth/lowenergyscanner/Scanner/Characteristics.qml
@@ -0,0 +1,121 @@
+// Copyright (C) 2013 BlackBerry Limited. All rights reserved.
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+pragma ComponentBehavior: Bound
+import QtQuick
+
+Rectangle {
+ id: characteristicsPage
+
+ signal showServices
+ signal showDevices
+
+ width: 300
+ height: 600
+
+ Header {
+ id: header
+ anchors.top: parent.top
+ headerText: "Characteristics list"
+ }
+
+ Dialog {
+ id: info
+ anchors.centerIn: parent
+ visible: true
+ dialogText: "Scanning for characteristics..."
+ }
+
+ Connections {
+ target: Device
+ function oncharacteristics_pdated() {
+ menu.menuText = "Back"
+ if (characteristicview.count === 0) {
+ info.dialogText = "No characteristic found"
+ info.busyImage = false
+ } else {
+ info.visible = false
+ info.busyImage = true
+ }
+ }
+
+ function onDisconnected() {
+ characteristicsPage.showDevices()
+ }
+ }
+
+ ListView {
+ id: characteristicview
+ width: parent.width
+ clip: true
+
+ anchors.top: header.bottom
+ anchors.bottom: menu.top
+ model: Device.characteristicList
+
+ delegate: Rectangle {
+ required property var modelData
+ id: box
+ height: 300
+ width: characteristicview.width
+ color: "lightsteelblue"
+ border.width: 2
+ border.color: "black"
+ radius: 5
+
+ Label {
+ id: characteristicName
+ textContent: box.modelData.characteristic_name
+ anchors.top: parent.top
+ anchors.topMargin: 5
+ }
+
+ Label {
+ id: characteristicUuid
+ font.pointSize: characteristicName.font.pointSize * 0.7
+ textContent: box.modelData.characteristic_uuid
+ anchors.top: characteristicName.bottom
+ anchors.topMargin: 5
+ }
+
+ Label {
+ id: characteristicValue
+ font.pointSize: characteristicName.font.pointSize * 0.7
+ textContent: ("Value: " + box.modelData.characteristic_value)
+ anchors.bottom: characteristicHandle.top
+ horizontalAlignment: Text.AlignHCenter
+ anchors.topMargin: 5
+ }
+
+ Label {
+ id: characteristicHandle
+ font.pointSize: characteristicName.font.pointSize * 0.7
+ textContent: ("Handlers: " + box.modelData.characteristic_handle)
+ anchors.bottom: characteristicPermission.top
+ anchors.topMargin: 5
+ }
+
+ Label {
+ id: characteristicPermission
+ font.pointSize: characteristicName.font.pointSize * 0.7
+ textContent: box.modelData.characteristic_permission
+ anchors.bottom: parent.bottom
+ anchors.topMargin: 5
+ anchors.bottomMargin: 5
+ }
+ }
+ }
+
+ Menu {
+ id: menu
+ anchors.bottom: parent.bottom
+ menuWidth: parent.width
+ menuText: Device.update
+ menuHeight: (parent.height / 6)
+ onButtonClick: {
+ characteristicsPage.showServices()
+ Device.update = "Back"
+ }
+ }
+}
diff --git a/examples/bluetooth/lowenergyscanner/Scanner/Devices.qml b/examples/bluetooth/lowenergyscanner/Scanner/Devices.qml
new file mode 100644
index 000000000..6e5e85a52
--- /dev/null
+++ b/examples/bluetooth/lowenergyscanner/Scanner/Devices.qml
@@ -0,0 +1,121 @@
+// Copyright (C) 2013 BlackBerry Limited. All rights reserved.
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+pragma ComponentBehavior: Bound
+import QtQuick
+
+Rectangle {
+ id: devicesPage
+
+ property bool deviceState: Device.state
+ signal showServices
+
+ width: 300
+ height: 600
+
+ onDeviceStateChanged: {
+ if (!Device.state)
+ info.visible = false
+ }
+
+ Header {
+ id: header
+ anchors.top: parent.top
+ headerText: {
+ if (Device.state)
+ return "Discovering"
+
+ if (Device.devices_list.length > 0)
+ return "Select a device"
+
+ return "Start Discovery"
+ }
+ }
+
+ Dialog {
+ id: info
+ anchors.centerIn: parent
+ visible: false
+ }
+
+ ListView {
+ id: theListView
+ width: parent.width
+ clip: true
+
+ anchors.top: header.bottom
+ anchors.bottom: connectToggle.top
+ model: Device.devices_list
+
+ delegate: Rectangle {
+ required property var modelData
+ id: box
+ height: 100
+ width: theListView.width
+ color: "lightsteelblue"
+ border.width: 2
+ border.color: "black"
+ radius: 5
+
+ MouseArea {
+ anchors.fill: parent
+ onClicked: {
+ Device.scan_services(box.modelData.device_address)
+ showServices()
+ }
+ }
+
+ Label {
+ id: deviceName
+ textContent: box.modelData.device_name
+ anchors.top: parent.top
+ anchors.topMargin: 5
+ }
+
+ Label {
+ id: deviceAddress
+ textContent: box.modelData.device_address
+ font.pointSize: deviceName.font.pointSize * 0.7
+ anchors.bottom: box.bottom
+ anchors.bottomMargin: 5
+ }
+ }
+ }
+
+ Menu {
+ id: connectToggle
+
+ menuWidth: parent.width
+ anchors.bottom: menu.top
+ menuText: {
+ visible = Device.devices_list.length > 0
+ if (Device.use_random_address)
+ return "Address type: Random"
+ else
+ return "Address type: Public"
+ }
+
+ onButtonClick: Device.use_random_address = !Device.use_random_address
+ }
+
+ Menu {
+ id: menu
+ anchors.bottom: parent.bottom
+ menuWidth: parent.width
+ menuHeight: (parent.height / 6)
+ menuText: Device.update
+ onButtonClick: {
+ if (!Device.state) {
+ Device.start_device_discovery()
+ // if start_device_discovery() failed Device.state is not set
+ if (Device.state) {
+ info.dialogText = "Searching..."
+ info.visible = true
+ }
+ } else {
+ Device.stop_device_discovery()
+ }
+ }
+ }
+}
diff --git a/examples/bluetooth/lowenergyscanner/Scanner/Dialog.qml b/examples/bluetooth/lowenergyscanner/Scanner/Dialog.qml
new file mode 100644
index 000000000..75e82642a
--- /dev/null
+++ b/examples/bluetooth/lowenergyscanner/Scanner/Dialog.qml
@@ -0,0 +1,48 @@
+// Copyright (C) 2013 BlackBerry Limited. All rights reserved.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+import QtQuick
+
+Rectangle {
+ id: dialog
+ width: parent.width / 3 * 2
+ height: dialogTextId.height + background.height + 20
+ z: 50
+ property string dialogText: ""
+ property bool busyImage: true
+ border.width: 1
+ border.color: "#363636"
+ radius: 10
+
+ Text {
+ id: dialogTextId
+ horizontalAlignment: Text.AlignHCenter
+ verticalAlignment: Text.AlignVCenter
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.top: parent.top
+ anchors.topMargin: 10
+
+ elide: Text.ElideMiddle
+ text: dialog.dialogText
+ color: "#363636"
+ wrapMode: Text.Wrap
+ }
+
+ Image {
+ id: background
+
+ width: 20
+ height: 20
+ anchors.top: dialogTextId.bottom
+ anchors.horizontalCenter: dialogTextId.horizontalCenter
+ visible: parent.busyImage
+ source: "assets/busy_dark.png"
+ fillMode: Image.PreserveAspectFit
+ NumberAnimation on rotation {
+ duration: 3000
+ from: 0
+ to: 360
+ loops: Animation.Infinite
+ }
+ }
+}
diff --git a/examples/bluetooth/lowenergyscanner/Scanner/Header.qml b/examples/bluetooth/lowenergyscanner/Scanner/Header.qml
new file mode 100644
index 000000000..c95385dd3
--- /dev/null
+++ b/examples/bluetooth/lowenergyscanner/Scanner/Header.qml
@@ -0,0 +1,25 @@
+// Copyright (C) 2013 BlackBerry Limited. All rights reserved.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+import QtQuick
+
+Rectangle {
+ id: header
+ width: parent.width
+ height: 70
+ border.width: 1
+ border.color: "#363636"
+ radius: 5
+ property string headerText: ""
+
+ Text {
+ horizontalAlignment: Text.AlignHCenter
+ verticalAlignment: Text.AlignVCenter
+ anchors.fill: parent
+ text: header.headerText
+ font.bold: true
+ font.pointSize: 20
+ elide: Text.ElideMiddle
+ color: "#363636"
+ }
+}
diff --git a/examples/bluetooth/lowenergyscanner/Scanner/Label.qml b/examples/bluetooth/lowenergyscanner/Scanner/Label.qml
new file mode 100644
index 000000000..e31156740
--- /dev/null
+++ b/examples/bluetooth/lowenergyscanner/Scanner/Label.qml
@@ -0,0 +1,16 @@
+// Copyright (C) 2013 BlackBerry Limited. All rights reserved.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+import QtQuick
+
+Text {
+ property string textContent: ""
+ font.pointSize: 20
+ anchors.horizontalCenter: parent.horizontalCenter
+ color: "#363636"
+ horizontalAlignment: Text.AlignHCenter
+ elide: Text.ElideMiddle
+ width: parent.width
+ wrapMode: Text.Wrap
+ text: textContent
+}
diff --git a/examples/bluetooth/lowenergyscanner/Scanner/Main.qml b/examples/bluetooth/lowenergyscanner/Scanner/Main.qml
new file mode 100644
index 000000000..88600bace
--- /dev/null
+++ b/examples/bluetooth/lowenergyscanner/Scanner/Main.qml
@@ -0,0 +1,31 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+import QtQuick
+import QtQuick.Layouts
+
+Window {
+ id: main
+
+ width: 300
+ height: 600
+ visible: true
+
+ StackLayout {
+ id: pagesLayout
+ anchors.fill: parent
+ currentIndex: 0
+
+ Devices {
+ onShowServices: pagesLayout.currentIndex = 1
+ }
+ Services {
+ onShowDevices: pagesLayout.currentIndex = 0
+ onShowCharacteristics: pagesLayout.currentIndex = 2
+ }
+ Characteristics {
+ onShowDevices: pagesLayout.currentIndex = 0
+ onShowServices: pagesLayout.currentIndex = 1
+ }
+ }
+}
diff --git a/examples/bluetooth/lowenergyscanner/Scanner/Menu.qml b/examples/bluetooth/lowenergyscanner/Scanner/Menu.qml
new file mode 100644
index 000000000..ef69c895e
--- /dev/null
+++ b/examples/bluetooth/lowenergyscanner/Scanner/Menu.qml
@@ -0,0 +1,55 @@
+// Copyright (C) 2013 BlackBerry Limited. All rights reserved.
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+import QtQuick
+
+Rectangle {
+ id: menu
+
+ property real menuWidth: 100
+ property real menuHeight: 50
+ property string menuText: "Search"
+ signal buttonClick
+
+ height: menuHeight
+ width: menuWidth
+
+ Rectangle {
+ id: search
+ width: parent.width
+ height: parent.height
+ anchors.centerIn: parent
+ color: "#363636"
+ border.width: 1
+ border.color: "#E3E3E3"
+ radius: 5
+ Text {
+ id: searchText
+ horizontalAlignment: Text.AlignHCenter
+ verticalAlignment: Text.AlignVCenter
+ anchors.fill: parent
+ text: menu.menuText
+ elide: Text.ElideMiddle
+ color: "#E3E3E3"
+ wrapMode: Text.WordWrap
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ onPressed: {
+ search.width = search.width - 7
+ search.height = search.height - 5
+ }
+
+ onReleased: {
+ search.width = search.width + 7
+ search.height = search.height + 5
+ }
+
+ onClicked: {
+ menu.buttonClick()
+ }
+ }
+ }
+}
diff --git a/examples/bluetooth/lowenergyscanner/Scanner/Services.qml b/examples/bluetooth/lowenergyscanner/Scanner/Services.qml
new file mode 100644
index 000000000..70326242e
--- /dev/null
+++ b/examples/bluetooth/lowenergyscanner/Scanner/Services.qml
@@ -0,0 +1,115 @@
+// Copyright (C) 2013 BlackBerry Limited. All rights reserved.
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+pragma ComponentBehavior: Bound
+import QtQuick
+
+Rectangle {
+ id: servicesPage
+
+ signal showCharacteristics
+ signal showDevices
+
+ width: 300
+ height: 600
+
+ Component.onCompleted: {
+ // Loading this page may take longer than QLEController
+ // stopping with an error, go back and readjust this view
+ // based on controller errors
+ if (Device.controller_error) {
+ info.visible = false
+ menu.menuText = Device.update
+ }
+ }
+
+ Header {
+ id: header
+ anchors.top: parent.top
+ headerText: "Services list"
+ }
+
+ Dialog {
+ id: info
+ anchors.centerIn: parent
+ visible: true
+ dialogText: "Scanning for services..."
+ }
+
+ Connections {
+ target: Device
+ function onservices_updated() {
+ if (servicesview.count === 0)
+ info.dialogText = "No services found"
+ else
+ info.visible = false
+ }
+
+ function ondisconnected() {
+ servicesPage.showDevices()
+ }
+ }
+
+ ListView {
+ id: servicesview
+ width: parent.width
+ anchors.top: header.bottom
+ anchors.bottom: menu.top
+ model: Device.servicesList
+ clip: true
+
+ delegate: Rectangle {
+ required property var modelData
+ id: box
+ height: 100
+ color: "lightsteelblue"
+ border.width: 2
+ border.color: "black"
+ radius: 5
+ width: servicesview.width
+
+ MouseArea {
+ anchors.fill: parent
+ onClicked: {
+ Device.connectToService(box.modelData.service_uuid)
+ servicesPage.showCharacteristics()
+ }
+ }
+
+ Label {
+ id: serviceName
+ textContent: box.modelData.service_name
+ anchors.top: parent.top
+ anchors.topMargin: 5
+ }
+
+ Label {
+ textContent: box.modelData.service_type
+ font.pointSize: serviceName.font.pointSize * 0.5
+ anchors.top: serviceName.bottom
+ }
+
+ Label {
+ id: serviceUuid
+ font.pointSize: serviceName.font.pointSize * 0.5
+ textContent: box.modelData.service_uuid
+ anchors.bottom: box.bottom
+ anchors.bottomMargin: 5
+ }
+ }
+ }
+
+ Menu {
+ id: menu
+ anchors.bottom: parent.bottom
+ menuWidth: parent.width
+ menuText: Device.update
+ menuHeight: (parent.height / 6)
+ onButtonClick: {
+ Device.disconnect_from_device()
+ servicesPage.showDevices()
+ Device.update = "Search"
+ }
+ }
+}
diff --git a/examples/bluetooth/lowenergyscanner/Scanner/assets/busy_dark.png b/examples/bluetooth/lowenergyscanner/Scanner/assets/busy_dark.png
new file mode 100644
index 000000000..3a1059531
--- /dev/null
+++ b/examples/bluetooth/lowenergyscanner/Scanner/assets/busy_dark.png
Binary files differ
diff --git a/examples/bluetooth/lowenergyscanner/Scanner/qmldir b/examples/bluetooth/lowenergyscanner/Scanner/qmldir
new file mode 100644
index 000000000..0adf6fb19
--- /dev/null
+++ b/examples/bluetooth/lowenergyscanner/Scanner/qmldir
@@ -0,0 +1,10 @@
+module Scanner
+typeinfo scanner.qmltypes
+Characteristics 1.0 Characteristics.qml
+Devices 1.0 Devices.qml
+Dialog 1.0 Dialog.qml
+Header 1.0 Header.qml
+Label 1.0 Label.qml
+Main 1.0 Main.qml
+Menu 1.0 Menu.qml
+Services 1.0 Services.qml
diff --git a/examples/bluetooth/lowenergyscanner/characteristicinfo.py b/examples/bluetooth/lowenergyscanner/characteristicinfo.py
new file mode 100644
index 000000000..42bde8753
--- /dev/null
+++ b/examples/bluetooth/lowenergyscanner/characteristicinfo.py
@@ -0,0 +1,87 @@
+# Copyright (C) 2022 The Qt Company Ltd.
+# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+from PySide6.QtCore import QObject, Property, Signal
+from PySide6.QtBluetooth import QLowEnergyCharacteristic, QBluetoothUuid
+
+
+class CharacteristicInfo(QObject):
+
+ characteristic_changed = Signal()
+
+ def __init__(self, characteristic=None) -> None:
+ super().__init__()
+ self._characteristic = characteristic
+
+ @Property(str, notify=characteristic_changed)
+ def characteristic_name(self):
+ if not self.characteristic:
+ raise Exception("characteristic unset")
+ name = self.characteristic.name()
+ if name:
+ return name
+
+ for descriptor in self.characteristic.descriptors():
+ if descriptor.type() == QBluetoothUuid.DescriptorType.CharacteristicUserDescription:
+ name = descriptor.value()
+ break
+
+ if not name:
+ name = "Unknown"
+
+ return name
+
+ @Property(str, notify=characteristic_changed)
+ def characteristic_uuid(self):
+ uuid = self.characteristic.uuid()
+ result16, success16 = uuid.toUInt16()
+ if success16:
+ return f"0x{result16:x}"
+
+ result32, sucess32 = uuid.toUInt32()
+ if sucess32:
+ return f"0x{result32:x}"
+
+ return uuid.toString().replace('{', '').replace('}', '')
+
+ @Property(str, notify=characteristic_changed)
+ def characteristic_value(self):
+ # Show raw string first and hex value below
+ a = self.characteristic.value()
+ if not a:
+ return "<none>"
+
+ result = f"{str(a)}\n{str(a.toHex())}"
+ return result
+
+ @Property(str, notify=characteristic_changed)
+ def characteristic_permission(self):
+ properties = "( "
+ permission = self.characteristic.properties()
+ if (permission & QLowEnergyCharacteristic.Read):
+ properties += " Read"
+ if (permission & QLowEnergyCharacteristic.Write):
+ properties += " Write"
+ if (permission & QLowEnergyCharacteristic.Notify):
+ properties += " Notify"
+ if (permission & QLowEnergyCharacteristic.Indicate):
+ properties += " Indicate"
+ if (permission & QLowEnergyCharacteristic.ExtendedProperty):
+ properties += " ExtendedProperty"
+ if (permission & QLowEnergyCharacteristic.Broadcasting):
+ properties += " Broadcast"
+ if (permission & QLowEnergyCharacteristic.WriteNoResponse):
+ properties += " WriteNoResp"
+ if (permission & QLowEnergyCharacteristic.WriteSigned):
+ properties += " WriteSigned"
+ properties += " )"
+ return properties
+
+ @property
+ def characteristic(self):
+ return self._characteristic
+
+ @characteristic.setter
+ def characteristic(self, characteristic):
+ self._characteristic = characteristic
+ self.characteristic_changed.emit()
diff --git a/examples/bluetooth/lowenergyscanner/device.py b/examples/bluetooth/lowenergyscanner/device.py
new file mode 100644
index 000000000..09108cf69
--- /dev/null
+++ b/examples/bluetooth/lowenergyscanner/device.py
@@ -0,0 +1,278 @@
+# Copyright (C) 2022 The Qt Company Ltd.
+# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+import warnings
+from PySide6.QtBluetooth import (QBluetoothDeviceDiscoveryAgent, QLowEnergyController,
+ QBluetoothDeviceInfo, QBluetoothUuid, QLowEnergyService)
+from PySide6.QtCore import QObject, Property, Signal, Slot, QTimer, QMetaObject, Qt
+from PySide6.QtQml import QmlElement, QmlSingleton
+
+from deviceinfo import DeviceInfo
+from serviceinfo import ServiceInfo
+from characteristicinfo import CharacteristicInfo
+
+QML_IMPORT_NAME = "Scanner"
+QML_IMPORT_MAJOR_VERSION = 1
+
+
+@QmlElement
+@QmlSingleton
+class Device(QObject):
+
+ devices_updated = Signal()
+ services_updated = Signal()
+ characteristic_updated = Signal()
+ update_changed = Signal()
+ state_changed = Signal()
+ disconnected = Signal()
+ random_address_changed = Signal()
+
+ def __init__(self, parent=None) -> None:
+ super().__init__(parent)
+ self.devices = []
+ self._services = []
+ self._characteristics = []
+ self._previousAddress = ""
+ self._message = ""
+ self.currentDevice = DeviceInfo()
+ self.connected = False
+ self.controller: QLowEnergyController = None
+ self._deviceScanState = False
+ self.random_address = False
+ self.discovery_agent = QBluetoothDeviceDiscoveryAgent()
+ self.discovery_agent.setLowEnergyDiscoveryTimeout(25000)
+ self.discovery_agent.deviceDiscovered.connect(self.add_device)
+ self.discovery_agent.errorOccurred.connect(self.device_scan_error)
+ self.discovery_agent.finished.connect(self.device_scan_finished)
+ self.update = "Search"
+
+ @Property("QVariant", notify=devices_updated)
+ def devices_list(self):
+ return self.devices
+
+ @Property("QVariant", notify=services_updated)
+ def services_list(self):
+ return self._services
+
+ @Property("QVariant", notify=characteristic_updated)
+ def characteristic_list(self):
+ return self._characteristics
+
+ @Property(str, notify=update_changed)
+ def update(self):
+ return self._message
+
+ @update.setter
+ def update(self, message):
+ self._message = message
+ self.update_changed.emit()
+
+ @Property(bool, notify=random_address_changed)
+ def use_random_address(self):
+ return self.random_address
+
+ @use_random_address.setter
+ def use_random_address(self, newValue):
+ self.random_address = newValue
+ self.random_address_changed.emit()
+
+ @Property(bool, notify=state_changed)
+ def state(self):
+ return self._deviceScanState
+
+ @Property(bool)
+ def controller_error(self):
+ return self.controller and (self.controller.error() != QLowEnergyController.NoError)
+
+ @Slot()
+ def start_device_discovery(self):
+ self.devices.clear()
+ self.devices_updated.emit()
+ self.update = "Scanning for devices ..."
+ self.discovery_agent.start(QBluetoothDeviceDiscoveryAgent.LowEnergyMethod)
+
+ if self.discovery_agent.isActive():
+ self._deviceScanState = True
+ self.state_changed.emit()
+
+ @Slot(str)
+ def scan_services(self, address):
+ # We need the current device for service discovery.
+ for device in self.devices:
+ if device.device_address == address:
+ self.currentDevice.set_device(device.get_device())
+ break
+
+ if not self.currentDevice.get_device().isValid():
+ warnings.warn("Not a valid device")
+ return
+
+ self._characteristics.clear()
+ self.characteristic_updated.emit()
+ self._services.clear()
+ self.services_updated.emit()
+
+ self.update = "Back\n(Connecting to device...)"
+
+ if self.controller and (self._previousAddress != self.currentDevice.device_address):
+ self.controller.disconnectFromDevice()
+ del self.controller
+ self.controller = None
+
+ if not self.controller:
+ self.controller = QLowEnergyController.createCentral(self.currentDevice.get_device())
+ self.controller.connected.connect(self.device_connected)
+ self.controller.errorOccurred.connect(self.error_received)
+ self.controller.disconnected.connect(self.device_disconnected)
+ self.controller.serviceDiscovered.connect(self.add_low_energy_service)
+ self.controller.discoveryFinished.connect(self.services_scan_done)
+
+ if self.random_address:
+ self.controller.setRemoteAddressType(QLowEnergyController.RandomAddress)
+ else:
+ self.controller.setRemoteAddressType(QLowEnergyController.PublicAddress)
+ self.controller.connectToDevice()
+
+ self._previousAddress = self.currentDevice.device_address
+
+ @Slot(str)
+ def connect_to_service(self, uuid):
+ service: QLowEnergyService = None
+ for serviceInfo in self._services:
+ if not serviceInfo:
+ continue
+
+ if serviceInfo.service_uuid == uuid:
+ service = serviceInfo.service
+ break
+
+ if not service:
+ return
+
+ self._characteristics.clear()
+ self.characteristic_updated.emit()
+
+ if service.state() == QLowEnergyService.RemoteService:
+ service.state_changed.connect(self.service_details_discovered)
+ service.discoverDetails()
+ self.update = "Back\n(Discovering details...)"
+ return
+
+ # discovery already done
+ chars = service.characteristics()
+ for ch in chars:
+ cInfo = CharacteristicInfo(ch)
+ self._characteristics.append(cInfo)
+
+ QTimer.singleShot(0, self.characteristic_updated)
+
+ @Slot()
+ def disconnect_from_device(self):
+ # UI always expects disconnect() signal when calling this signal
+ # TODO what is really needed is to extend state() to a multi value
+ # and thus allowing UI to keep track of controller progress in addition to
+ # device scan progress
+
+ if self.controller.state() != QLowEnergyController.UnconnectedState:
+ self.controller.disconnectFromDevice()
+ else:
+ self.device_disconnected()
+
+ @Slot(QBluetoothDeviceInfo)
+ def add_device(self, info):
+ if info.coreConfigurations() & QBluetoothDeviceInfo.LowEnergyCoreConfiguration:
+ self.update = "Last device added: " + info.name()
+
+ @Slot()
+ def device_scan_finished(self):
+ foundDevices = self.discovery_agent.discoveredDevices()
+ for nextDevice in foundDevices:
+ if nextDevice.coreConfigurations() & QBluetoothDeviceInfo.LowEnergyCoreConfiguration:
+ device = DeviceInfo(nextDevice)
+ self.devices.append(device)
+
+ self.devices_updated.emit()
+ self._deviceScanState = False
+ self.state_changed.emit()
+ if not self.devices:
+ self.update = "No Low Energy devices found..."
+ else:
+ self.update = "Done! Scan Again!"
+
+ @Slot("QBluetoothDeviceDiscovertAgent::Error")
+ def device_scan_error(self, error):
+ if error == QBluetoothDeviceDiscoveryAgent.PoweredOffError:
+ self.update = (
+ "The Bluetooth adaptor is powered off, power it on before doing discovery."
+ )
+ elif error == QBluetoothDeviceDiscoveryAgent.InputOutputError:
+ self.update = "Writing or reading from the device resulted in an error."
+ else:
+ qme = self.discovery_agent.metaObject().enumerator(
+ self.discovery_agent.metaObject().indexOfEnumerator("Error")
+ )
+ self.update = f"Error: {qme.valueToKey(error)}"
+
+ self._deviceScanState = False
+ self.devices_updated.emit()
+ self.state_changed.emit()
+
+ @Slot(QBluetoothUuid)
+ def add_low_energy_service(self, service_uuid):
+ service = self.controller.createServiceObject(service_uuid)
+ if not service:
+ warnings.warn("Cannot create service from uuid")
+ return
+
+ serv = ServiceInfo(service)
+ self._services.append(serv)
+ self.services_updated.emit()
+
+ @Slot()
+ def device_connected(self):
+ self.update = "Back\n(Discovering services...)"
+ self.connected = True
+ self.controller.discoverServices()
+
+ @Slot("QLowEnergyController::Error")
+ def error_received(self, error):
+ warnings.warn(f"Error: {self.controller.errorString()}")
+ self.update = f"Back\n({self.controller.errorString()})"
+
+ @Slot()
+ def services_scan_done(self):
+ self.update = "Back\n(Service scan done!)"
+ # force UI in case we didn't find anything
+ if not self._services:
+ self.services_updated.emit()
+
+ @Slot()
+ def device_disconnected(self):
+ warnings.warn("Disconnect from Device")
+ self.disconnected.emit()
+
+ @Slot("QLowEnergyService::ServiceState")
+ def service_details_discovered(self, newState):
+ if newState != QLowEnergyService.RemoteServiceDiscovered:
+ # do not hang in "Scanning for characteristics" mode forever
+ # in case the service discovery failed
+ # We have to queue the signal up to give UI time to even enter
+ # the above mode
+ if newState != QLowEnergyService.RemoteServiceDiscovering:
+ QMetaObject.invokeMethod(self.characteristic_updated, Qt.QueuedConnection)
+ return
+
+ service = self.sender()
+ if not service:
+ return
+
+ chars = service.characteristics()
+ for ch in chars:
+ cInfo = CharacteristicInfo(ch)
+ self._characteristics.append(cInfo)
+
+ self.characteristic_updated.emit()
+
+ @Slot()
+ def stop_device_discovery(self):
+ if self.discovery_agent.isActive():
+ self.discovery_agent.stop()
diff --git a/examples/bluetooth/lowenergyscanner/deviceinfo.py b/examples/bluetooth/lowenergyscanner/deviceinfo.py
new file mode 100644
index 000000000..35a568821
--- /dev/null
+++ b/examples/bluetooth/lowenergyscanner/deviceinfo.py
@@ -0,0 +1,34 @@
+# Copyright (C) 2022 The Qt Company Ltd.
+# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+import sys
+
+from PySide6.QtCore import QObject, Property, Signal
+from PySide6.QtBluetooth import QBluetoothDeviceInfo
+
+
+class DeviceInfo(QObject):
+
+ device_changed = Signal()
+
+ def __init__(self, d: QBluetoothDeviceInfo = None) -> None:
+ super().__init__()
+ self._device = d
+
+ @Property(str, notify=device_changed)
+ def device_name(self):
+ return self._device.name()
+
+ @Property(str, notify=device_changed)
+ def device_address(self):
+ if sys.platform == "darwin":
+ return self._device.deviceUuid().toString()
+
+ return self._device.address().toString()
+
+ def get_device(self):
+ return self._device
+
+ def set_device(self, device):
+ self._device = device
+ self.device_changed.emit()
diff --git a/examples/bluetooth/lowenergyscanner/doc/lowenergyscanner.png b/examples/bluetooth/lowenergyscanner/doc/lowenergyscanner.png
new file mode 100644
index 000000000..29f41deb4
--- /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..a0c574350
--- /dev/null
+++ b/examples/bluetooth/lowenergyscanner/doc/lowenergyscanner.rst
@@ -0,0 +1,11 @@
+Bluetooth Low Energy Scanner Example
+====================================
+
+.. tags:: Android
+
+A Python application that demonstrates the analogous example in Qt
+`Bluetooth Low Energy Scanner <https://doc.qt.io/qt-6/qtbluetooth-lowenergyscanner-example.html>`_
+
+.. image:: lowenergyscanner.png
+ :width: 400
+ :alt: lowenergyscanner screenshot
diff --git a/examples/bluetooth/lowenergyscanner/lowenergyscanner.pyproject b/examples/bluetooth/lowenergyscanner/lowenergyscanner.pyproject
new file mode 100644
index 000000000..7e0cfa3d9
--- /dev/null
+++ b/examples/bluetooth/lowenergyscanner/lowenergyscanner.pyproject
@@ -0,0 +1,7 @@
+{
+ "files": ["main.py", "device.py", "deviceinfo.py", "serviceinfo.py", "characteristicinfo.py",
+ "Scanner/Main.qml", "Scanner/Menu.qml","Scanner/Header.qml",
+ "Scanner/Characteristics.qml", "Scanner/Dialog.qml", "Scanner/Services.qml",
+ "Scanner/Label.qml", "Scanner/Devices.qml", "Scanner/assets/busy_dark.png",
+ "Scanner/qmldir"]
+}
diff --git a/examples/bluetooth/lowenergyscanner/main.py b/examples/bluetooth/lowenergyscanner/main.py
new file mode 100644
index 000000000..ec12f99e7
--- /dev/null
+++ b/examples/bluetooth/lowenergyscanner/main.py
@@ -0,0 +1,27 @@
+# Copyright (C) 2022 The Qt Company Ltd.
+# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+"""PySide6 port of the bluetooth/lowenergyscanner example from Qt v6.x"""
+
+
+import sys
+
+from PySide6.QtCore import QCoreApplication
+from PySide6.QtGui import QGuiApplication
+from PySide6.QtQml import QQmlApplicationEngine
+
+from device import Device # noqa: F401
+from pathlib import Path
+
+if __name__ == '__main__':
+ app = QGuiApplication(sys.argv)
+ engine = QQmlApplicationEngine()
+ engine.addImportPath(Path(__file__).parent)
+ engine.loadFromModule("Scanner", "Main")
+
+ if not engine.rootObjects():
+ sys.exit(-1)
+
+ ex = QCoreApplication.exec()
+ del engine
+ sys.exit(ex)
diff --git a/examples/bluetooth/lowenergyscanner/serviceinfo.py b/examples/bluetooth/lowenergyscanner/serviceinfo.py
new file mode 100644
index 000000000..cddffe663
--- /dev/null
+++ b/examples/bluetooth/lowenergyscanner/serviceinfo.py
@@ -0,0 +1,64 @@
+# Copyright (C) 2022 The Qt Company Ltd.
+# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+from PySide6.QtCore import QObject, Property, Signal
+from PySide6.QtBluetooth import QLowEnergyService
+
+
+class ServiceInfo(QObject):
+
+ service_changed = Signal()
+
+ def __init__(self, service: QLowEnergyService) -> None:
+ super().__init__()
+ self._service = service
+ self.service.setParent(self)
+
+ @Property(str, notify=service_changed)
+ def service_name(self):
+ if not self.service:
+ return ""
+
+ return self.service.service_name()
+
+ @Property(str, notify=service_changed)
+ def service_type(self):
+ if not self.service:
+ return ""
+
+ result = ""
+ if (self.service.type() & QLowEnergyService.PrimaryService):
+ result += "primary"
+ else:
+ result += "secondary"
+
+ if (self.service.type() & QLowEnergyService.IncludedService):
+ result += " included"
+
+ result = '<' + result + '>'
+
+ return result
+
+ @Property(str, notify=service_changed)
+ def service_uuid(self):
+ if not self.service:
+ return ""
+
+ uuid = self.service.service_uuid()
+ result16, success16 = uuid.toUInt16()
+ if success16:
+ return f"0x{result16:x}"
+
+ result32, sucesss32 = uuid.toUInt32()
+ if sucesss32:
+ return f"0x{result32:x}"
+
+ return uuid.toString().replace('{', '').replace('}', '')
+
+ @property
+ def service(self):
+ return self._service
+
+ @service.setter
+ def service(self, service):
+ self._service = service