diff options
Diffstat (limited to 'examples/bluetooth')
52 files changed, 1526 insertions, 350 deletions
diff --git a/examples/bluetooth/btscanner/device.py b/examples/bluetooth/btscanner/device.py index 02fc53f0b..c75f5b8a1 100644 --- a/examples/bluetooth/btscanner/device.py +++ b/examples/bluetooth/btscanner/device.py @@ -3,7 +3,7 @@ from PySide6.QtCore import QPoint, Qt, Slot from PySide6.QtGui import QColor -from PySide6.QtWidgets import QDialog, QListWidgetItem, QListWidget, QMenu +from PySide6.QtWidgets import QDialog, QListWidgetItem, QMenu from PySide6.QtBluetooth import (QBluetoothAddress, QBluetoothDeviceDiscoveryAgent, QBluetoothDeviceInfo, QBluetoothLocalDevice) @@ -46,7 +46,7 @@ class DeviceDiscoveryDialog(QDialog): item = QListWidgetItem(label) pairing_status = self._local_device.pairingStatus(info.address()) if (pairing_status == QBluetoothLocalDevice.Paired - or pairing_status == QBluetoothLocalDevice.AuthorizedPaired): + or pairing_status == QBluetoothLocalDevice.AuthorizedPaired): item.setForeground(QColor(Qt.green)) else: item.setForeground(QColor(Qt.black)) @@ -123,7 +123,8 @@ class DeviceDiscoveryDialog(QDialog): items = self._ui.list.findItems(address.toString(), Qt.MatchContains) color = QColor(Qt.red) - if pairing == QBluetoothLocalDevice.Paired or pairing == QBluetoothLocalDevice.AuthorizedPaired: + 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/main.py b/examples/bluetooth/btscanner/main.py index bf8f596a3..a54a862a2 100644 --- a/examples/bluetooth/btscanner/main.py +++ b/examples/bluetooth/btscanner/main.py @@ -5,8 +5,7 @@ import sys -from PySide6.QtCore import Qt -from PySide6.QtWidgets import QApplication, QWidget +from PySide6.QtWidgets import QApplication from device import DeviceDiscoveryDialog diff --git a/examples/bluetooth/btscanner/service.py b/examples/bluetooth/btscanner/service.py index 73e174b82..31df8a9ea 100644 --- a/examples/bluetooth/btscanner/service.py +++ b/examples/bluetooth/btscanner/service.py @@ -1,7 +1,7 @@ # Copyright (C) 2022 The Qt Company Ltd. # SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause -from PySide6.QtCore import Qt, Slot +from PySide6.QtCore import Slot from PySide6.QtWidgets import QDialog from PySide6.QtBluetooth import (QBluetoothAddress, QBluetoothServiceInfo, QBluetoothServiceDiscoveryAgent, QBluetoothLocalDevice) diff --git a/examples/bluetooth/btscanner/ui_device.py b/examples/bluetooth/btscanner/ui_device.py index 0740afa7a..b443b2bc2 100644 --- a/examples/bluetooth/btscanner/ui_device.py +++ b/examples/bluetooth/btscanner/ui_device.py @@ -3,7 +3,7 @@ ################################################################################ ## Form generated from reading UI file 'device.ui' ## -## Created by: Qt User Interface Compiler version 6.2.3 +## Created by: Qt User Interface Compiler version 6.7.0 ## ## WARNING! All changes made in this file will be lost when recompiling UI file! ################################################################################ diff --git a/examples/bluetooth/btscanner/ui_service.py b/examples/bluetooth/btscanner/ui_service.py index fc1f45fef..ccc36677a 100644 --- a/examples/bluetooth/btscanner/ui_service.py +++ b/examples/bluetooth/btscanner/ui_service.py @@ -3,7 +3,7 @@ ################################################################################ ## Form generated from reading UI file 'service.ui' ## -## Created by: Qt User Interface Compiler version 6.2.3 +## Created by: Qt User Interface Compiler version 6.7.0 ## ## WARNING! All changes made in this file will be lost when recompiling UI file! ################################################################################ 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/qml/BluetoothAlarmDialog.qml b/examples/bluetooth/heartrate_game/HeartRateGame/BluetoothAlarmDialog.qml index 0be61e4f8..3687b1331 100644 --- a/examples/bluetooth/heartrate_game/qml/BluetoothAlarmDialog.qml +++ b/examples/bluetooth/heartrate_game/HeartRateGame/BluetoothAlarmDialog.qml @@ -5,6 +5,9 @@ import QtQuick Item { id: root + + property bool permissionError: false + anchors.fill: parent Rectangle { @@ -51,7 +54,9 @@ Item { wrapMode: Text.WordWrap font.pixelSize: GameSettings.mediumFontSize color: GameSettings.textColor - text: qsTr("This application cannot be used without Bluetooth. Please switch Bluetooth ON to continue.") + 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 { diff --git a/examples/bluetooth/heartrate_game/qml/BottomLine.qml b/examples/bluetooth/heartrate_game/HeartRateGame/BottomLine.qml index caebc307e..caebc307e 100644 --- a/examples/bluetooth/heartrate_game/qml/BottomLine.qml +++ b/examples/bluetooth/heartrate_game/HeartRateGame/BottomLine.qml diff --git a/examples/bluetooth/heartrate_game/qml/Connect.qml b/examples/bluetooth/heartrate_game/HeartRateGame/Connect.qml index d9ebbdc51..ca8ef2923 100644 --- a/examples/bluetooth/heartrate_game/qml/Connect.qml +++ b/examples/bluetooth/heartrate_game/HeartRateGame/Connect.qml @@ -1,10 +1,18 @@ // Copyright (C) 2022 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause +pragma ComponentBehavior: Bound import QtQuick -import Shared +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 @@ -12,17 +20,16 @@ GamePage { Rectangle { id: viewContainer anchors.top: parent.top - anchors.bottom: - // only BlueZ platform has address type selection - connectionHandler.requiresAddressType ? addressTypeButton.top : searchButton.top - anchors.topMargin: GameSettings.fieldMargin + messageHeight + // 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 + width: parent.width - GameSettings.fieldMargin * 2 color: GameSettings.viewColor radius: GameSettings.buttonRadius - Text { id: title width: parent.width @@ -34,40 +41,43 @@ GamePage { text: qsTr("FOUND DEVICES") BottomLine { - height: 1; + 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: deviceFinder.devices + model: connectPage.deviceFinder.devices clip: true delegate: Rectangle { id: box - height:GameSettings.fieldHeight * 1.2 + + 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 + anchors.fill: parent onClicked: { - deviceFinder.connectToService(modelData.deviceAddress); - app.showPage("Measure.qml") + connectPage.deviceFinder.connectToService(box.modelData.deviceAddress) + connectPage.showMeasurePage() } } Text { id: device font.pixelSize: GameSettings.smallFontSize - text: modelData.deviceName + text: box.modelData.deviceName anchors.top: parent.top anchors.topMargin: parent.height * 0.1 anchors.leftMargin: parent.height * 0.1 @@ -78,7 +88,7 @@ GamePage { Text { id: deviceAddress font.pixelSize: GameSettings.smallFontSize - text: modelData.deviceAddress + text: box.modelData.deviceAddress anchors.bottom: parent.bottom anchors.bottomMargin: parent.height * 0.1 anchors.rightMargin: parent.height * 0.1 @@ -93,23 +103,31 @@ GamePage { id: addressTypeButton anchors.horizontalCenter: parent.horizontalCenter anchors.bottom: searchButton.top - anchors.bottomMargin: GameSettings.fieldMargin*0.5 + anchors.bottomMargin: GameSettings.fieldMargin * 0.5 width: viewContainer.width height: GameSettings.fieldHeight - visible: connectionHandler.requiresAddressType // only required on BlueZ + visible: connectPage.connectionHandler.requiresAddressType // only required on BlueZ state: "public" - onClicked: state == "public" ? state = "random" : state = "public" + onClicked: state === "public" ? state = "random" : state = "public" states: [ State { name: "public" - PropertyChanges { target: addressTypeText; text: qsTr("Public Address") } - PropertyChanges { target: deviceHandler; addressType: AddressType.PUBLIC_ADDRESS } + PropertyChanges { + addressTypeText.text: qsTr("Public Address") + } + PropertyChanges { + connectPage.deviceHandler.addressType: DeviceHandler.PUBLIC_ADDRESS + } }, State { name: "random" - PropertyChanges { target: addressTypeText; text: qsTr("Random Address") } - PropertyChanges { target: deviceHandler; addressType: AddressType.RANDOM_ADDRESS } + PropertyChanges { + addressTypeText.text: qsTr("Random Address") + } + PropertyChanges { + connectPage.deviceHandler.addressType: DeviceHandler.RANDOM_ADDRESS + } } ] @@ -128,8 +146,8 @@ GamePage { anchors.bottomMargin: GameSettings.fieldMargin width: viewContainer.width height: GameSettings.fieldHeight - enabled: !deviceFinder.scanning - onClicked: deviceFinder.startSearch() + enabled: !connectPage.deviceFinder.scanning + onClicked: connectPage.deviceFinder.startSearch() Text { anchors.centerIn: parent diff --git a/examples/bluetooth/heartrate_game/qml/GameButton.qml b/examples/bluetooth/heartrate_game/HeartRateGame/GameButton.qml index 3ce9d66fd..8e8760102 100644 --- a/examples/bluetooth/heartrate_game/qml/GameButton.qml +++ b/examples/bluetooth/heartrate_game/HeartRateGame/GameButton.qml @@ -2,7 +2,6 @@ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause import QtQuick -import "." Rectangle { id: button @@ -14,10 +13,9 @@ Rectangle { property color pressedColor: GameSettings.buttonPressedColor property color disabledColor: GameSettings.disabledButtonColor - signal clicked() + signal clicked - function checkColor() - { + function checkColor() { if (!button.enabled) { button.color = disabledColor } else { @@ -31,10 +29,10 @@ Rectangle { MouseArea { id: mouseArea anchors.fill: parent - onPressed: checkColor() - onReleased: checkColor() + onPressed: button.checkColor() + onReleased: button.checkColor() onClicked: { - checkColor() + button.checkColor() button.clicked() } } diff --git a/examples/bluetooth/heartrate_game/qml/GamePage.qml b/examples/bluetooth/heartrate_game/HeartRateGame/GamePage.qml index 25a5bb3d1..249f94186 100644 --- a/examples/bluetooth/heartrate_game/qml/GamePage.qml +++ b/examples/bluetooth/heartrate_game/HeartRateGame/GamePage.qml @@ -2,10 +2,9 @@ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause import QtQuick -import "." Item { - anchors.fill: parent + id: page property string errorMessage: "" property string infoMessage: "" @@ -13,23 +12,14 @@ Item { property bool hasError: errorMessage != "" property bool hasInfo: infoMessage != "" - function init() - { - } - - function close() - { - app.prevPage() - } - Rectangle { id: msg anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right height: GameSettings.fieldHeight - color: hasError ? GameSettings.errorColor : GameSettings.infoColor - visible: hasError || hasInfo + color: page.hasError ? GameSettings.errorColor : GameSettings.infoColor + visible: page.hasError || page.hasInfo Text { id: error @@ -40,7 +30,7 @@ Item { font.pixelSize: GameSettings.smallFontSize fontSizeMode: Text.Fit color: GameSettings.textColor - text: hasError ? errorMessage : infoMessage + text: page.hasError ? page.errorMessage : page.infoMessage } } } diff --git a/examples/bluetooth/heartrate_game/qml/GameSettings.qml b/examples/bluetooth/heartrate_game/HeartRateGame/GameSettings.qml index f265b73c3..0fe854609 100644 --- a/examples/bluetooth/heartrate_game/qml/GameSettings.qml +++ b/examples/bluetooth/heartrate_game/HeartRateGame/GameSettings.qml @@ -41,14 +41,11 @@ Item { property real buttonRadius: buttonHeight * 0.1 // Some help functions - function widthForHeight(h, ss) - { - return h/ss.height * ss.width; + function widthForHeight(h, ss) { + return h / ss.height * ss.width } - function heightForWidth(w, ss) - { - return w/ss.width * ss.height; + 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/qml/Measure.qml b/examples/bluetooth/heartrate_game/HeartRateGame/Measure.qml index c434d5114..48e84e762 100644 --- a/examples/bluetooth/heartrate_game/qml/Measure.qml +++ b/examples/bluetooth/heartrate_game/HeartRateGame/Measure.qml @@ -2,49 +2,49 @@ // 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 __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) - function close() - { - deviceHandler.stopMeasurement(); - deviceHandler.disconnectService(); - app.prevPage(); + signal showStatsPage + + function close() { + deviceHandler.stopMeasurement() + deviceHandler.disconnectService() } - function start() - { + function start() { if (!deviceHandler.measuring) { - __timeCounter = 0; + __timeCounter = 0 deviceHandler.startMeasurement() } } - function stop() - { - if (deviceHandler.measuring) { + function stop() { + if (deviceHandler.measuring) deviceHandler.stopMeasurement() - } - app.showPage("Stats.qml") + measurePage.showStatsPage() } Timer { id: measureTimer interval: 1000 - running: deviceHandler.measuring + running: measurePage.deviceHandler.measuring repeat: true onTriggered: { - __timeCounter++; - if (__timeCounter >= __maxTimeCount) + measurePage.__timeCounter++ + if (measurePage.__timeCounter >= measurePage.__maxTimeCount) measurePage.stop() } } @@ -56,22 +56,23 @@ GamePage { Rectangle { id: circle anchors.horizontalCenter: parent.horizontalCenter - width: Math.min(measurePage.width, measurePage.height-GameSettings.fieldHeight*4) - 2*GameSettings.fieldMargin + width: Math.min(measurePage.width, measurePage.height - GameSettings.fieldHeight * 4) + - 2 * GameSettings.fieldMargin height: width - radius: width*0.5 + radius: width * 0.5 color: GameSettings.viewColor Text { id: hintText anchors.centerIn: parent - anchors.verticalCenterOffset: -parent.height*0.1 + 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: !deviceHandler.measuring + visible: !measurePage.deviceHandler.measuring color: GameSettings.textColor fontSizeMode: Text.Fit minimumPixelSize: 10 @@ -81,33 +82,33 @@ GamePage { Text { id: text anchors.centerIn: parent - anchors.verticalCenterOffset: -parent.height*0.15 + anchors.verticalCenterOffset: -parent.height * 0.15 font.pixelSize: parent.width * 0.45 - text: deviceHandler.hr - visible: deviceHandler.measuring + text: measurePage.deviceHandler.hr + visible: measurePage.deviceHandler.measuring color: GameSettings.textColor } Item { id: minMaxContainer anchors.horizontalCenter: parent.horizontalCenter - width: parent.width*0.7 + width: parent.width * 0.7 height: parent.height * 0.15 anchors.bottom: parent.bottom - anchors.bottomMargin: parent.height*0.16 - visible: deviceHandler.measuring + anchors.bottomMargin: parent.height * 0.16 + visible: measurePage.deviceHandler.measuring Text { anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter - text: deviceHandler.minHR + 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 + font.pixelSize: parent.font.pixelSize * 0.8 color: parent.color text: "MIN" } @@ -116,14 +117,14 @@ GamePage { Text { anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - text: deviceHandler.maxHR + 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 + font.pixelSize: parent.font.pixelSize * 0.8 color: parent.color text: "MAX" } @@ -140,13 +141,25 @@ GamePage { smooth: true antialiasing: true - SequentialAnimation{ + SequentialAnimation { id: heartAnim - running: deviceHandler.alive + 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 } + 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 + } } } } @@ -163,13 +176,15 @@ GamePage { height: parent.height radius: parent.radius color: GameSettings.sliderColor - width: Math.min(1.0,__timeCounter / __maxTimeCount) * parent.width + width: Math.min( + 1.0, + measurePage.__timeCounter / measurePage.__maxTimeCount) * parent.width } Text { anchors.centerIn: parent color: "gray" - text: (__maxTimeCount - __timeCounter).toFixed(0) + " s" + text: (measurePage.__maxTimeCount - measurePage.__timeCounter).toFixed(0) + " s" font.pixelSize: GameSettings.bigFontSize } } @@ -182,10 +197,10 @@ GamePage { anchors.bottomMargin: GameSettings.fieldMargin width: circle.width height: GameSettings.fieldHeight - enabled: !deviceHandler.measuring + enabled: !measurePage.deviceHandler.measuring radius: GameSettings.buttonRadius - onClicked: start() + onClicked: measurePage.start() Text { anchors.centerIn: parent diff --git a/examples/bluetooth/heartrate_game/qml/SplashScreen.qml b/examples/bluetooth/heartrate_game/HeartRateGame/SplashScreen.qml index 23f71f08f..2f9ac1b3f 100644 --- a/examples/bluetooth/heartrate_game/qml/SplashScreen.qml +++ b/examples/bluetooth/heartrate_game/HeartRateGame/SplashScreen.qml @@ -2,33 +2,20 @@ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause import QtQuick -import "." +import HeartRateGame Item { id: root - anchors.fill: parent property bool appIsReady: false property bool splashIsReady: false - property bool ready: appIsReady && splashIsReady - onReadyChanged: if (ready) readyToGo(); - signal readyToGo() - - function appReady() - { - appIsReady = true - } - - function errorInLoadingApp() - { - Qt.quit() - } + anchors.fill: parent Image { anchors.centerIn: parent - width: Math.min(parent.height, parent.width)*0.6 + width: Math.min(parent.height, parent.width) * 0.6 height: GameSettings.heightForWidth(width, sourceSize) source: "images/logo.png" } diff --git a/examples/bluetooth/heartrate_game/qml/Stats.qml b/examples/bluetooth/heartrate_game/HeartRateGame/Stats.qml index b818e85e4..22cdd5365 100644 --- a/examples/bluetooth/heartrate_game/qml/Stats.qml +++ b/examples/bluetooth/heartrate_game/HeartRateGame/Stats.qml @@ -2,8 +2,12 @@ // 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 @@ -18,9 +22,9 @@ GamePage { Text { anchors.horizontalCenter: parent.horizontalCenter - font.pixelSize: GameSettings.giganticFontSize*3 + font.pixelSize: GameSettings.giganticFontSize * 3 color: GameSettings.textColor - text: (deviceHandler.maxHR - deviceHandler.minHR).toFixed(0) + text: (statsPage.deviceHandler.maxHR - statsPage.deviceHandler.minHR).toFixed(0) } Item { @@ -30,23 +34,22 @@ GamePage { StatsLabel { title: qsTr("MIN") - value: deviceHandler.minHR.toFixed(0) + value: statsPage.deviceHandler.minHR.toFixed(0) } StatsLabel { title: qsTr("MAX") - value: deviceHandler.maxHR.toFixed(0) + value: statsPage.deviceHandler.maxHR.toFixed(0) } StatsLabel { title: qsTr("AVG") - value: deviceHandler.average.toFixed(1) + value: statsPage.deviceHandler.average.toFixed(1) } - StatsLabel { title: qsTr("CALORIES") - value: deviceHandler.calories.toFixed(3) + value: statsPage.deviceHandler.calories.toFixed(3) } } } diff --git a/examples/bluetooth/heartrate_game/qml/StatsLabel.qml b/examples/bluetooth/heartrate_game/HeartRateGame/StatsLabel.qml index cd5cda5be..0ea4249a7 100644 --- a/examples/bluetooth/heartrate_game/qml/StatsLabel.qml +++ b/examples/bluetooth/heartrate_game/HeartRateGame/StatsLabel.qml @@ -2,7 +2,6 @@ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause import QtQuick -import "." Item { height: GameSettings.fieldHeight diff --git a/examples/bluetooth/heartrate_game/qml/TitleBar.qml b/examples/bluetooth/heartrate_game/HeartRateGame/TitleBar.qml index b7de77c4b..016a44358 100644 --- a/examples/bluetooth/heartrate_game/qml/TitleBar.qml +++ b/examples/bluetooth/heartrate_game/HeartRateGame/TitleBar.qml @@ -1,50 +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 { +Rectangle { id: titleBar - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - height: GameSettings.fieldHeight - color: GameSettings.viewColor 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: __titles[index] + text: titleBar.__titles[index] font.pixelSize: GameSettings.tinyFontSize - color: titleBar.currentIndex === index ? GameSettings.textColor : GameSettings.disabledTextColor + color: titleBar.currentIndex === index ? GameSettings.textColor + : GameSettings.disabledTextColor MouseArea { anchors.fill: parent - onClicked: titleClicked(index) + onClicked: titleBar.titleClicked(caption.index) } } } - Item { anchors.bottom: parent.bottom width: parent.width / 3 height: parent.height - x: currentIndex * width + x: titleBar.currentIndex * width - BottomLine{} + BottomLine {} - Behavior on x { NumberAnimation { duration: 200 } } + Behavior on x { + NumberAnimation { + duration: 200 + } + } } - } diff --git a/examples/bluetooth/heartrate_game/qml/images/bt_off_to_on.png b/examples/bluetooth/heartrate_game/HeartRateGame/images/bt_off_to_on.png Binary files differindex 5ea1f3f06..5ea1f3f06 100644 --- a/examples/bluetooth/heartrate_game/qml/images/bt_off_to_on.png +++ b/examples/bluetooth/heartrate_game/HeartRateGame/images/bt_off_to_on.png diff --git a/examples/bluetooth/heartrate_game/qml/images/heart.png b/examples/bluetooth/heartrate_game/HeartRateGame/images/heart.png Binary files differindex f2b3c0a3e..f2b3c0a3e 100644 --- a/examples/bluetooth/heartrate_game/qml/images/heart.png +++ b/examples/bluetooth/heartrate_game/HeartRateGame/images/heart.png diff --git a/examples/bluetooth/heartrate_game/qml/images/logo.png b/examples/bluetooth/heartrate_game/HeartRateGame/images/logo.png Binary files differindex ea0af7e00..ea0af7e00 100644 --- a/examples/bluetooth/heartrate_game/qml/images/logo.png +++ b/examples/bluetooth/heartrate_game/HeartRateGame/images/logo.png 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/connectionhandler.py b/examples/bluetooth/heartrate_game/connectionhandler.py index 5bd7bfbb2..7bf60bbc5 100644 --- a/examples/bluetooth/heartrate_game/connectionhandler.py +++ b/examples/bluetooth/heartrate_game/connectionhandler.py @@ -5,13 +5,16 @@ import sys from PySide6.QtBluetooth import QBluetoothLocalDevice from PySide6.QtQml import QmlElement -from PySide6.QtCore import QObject, Property, Signal, Slot +from PySide6.QtCore import QObject, Property, Signal, Slot, Qt -from heartrate_global import simulator +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 = "Shared" +QML_IMPORT_NAME = "HeartRateGame" QML_IMPORT_MAJOR_VERSION = 1 @@ -22,14 +25,14 @@ class ConnectionHandler(QObject): def __init__(self, parent=None): super().__init__(parent) - self.m_localDevice = QBluetoothLocalDevice() - self.m_localDevice.hostModeStateChanged.connect(self.hostModeChanged) + self.m_hasPermission = False + self.initLocalDevice() @Property(bool, notify=deviceChanged) def alive(self): if sys.platform == "darwin": return True - if simulator: + if simulator(): return True return (self.m_localDevice.isValid() and self.m_localDevice.hostMode() != QBluetoothLocalDevice.HostPoweredOff) @@ -46,6 +49,29 @@ class ConnectionHandler(QObject): 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 index c69f8ab89..e581d12ec 100644 --- a/examples/bluetooth/heartrate_game/devicefinder.py +++ b/examples/bluetooth/heartrate_game/devicefinder.py @@ -1,18 +1,22 @@ # 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 +from PySide6.QtCore import QTimer, Property, Signal, Slot, Qt from bluetoothbaseclass import BluetoothBaseClass from deviceinfo import DeviceInfo -from heartrate_global import simulator +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 = "Shared" +QML_IMPORT_NAME = "HeartRateGame" QML_IMPORT_MAJOR_VERSION = 1 @@ -36,20 +40,33 @@ class DeviceFinder(BluetoothBaseClass): self.m_deviceDiscoveryAgent.finished.connect(self.scanFinished) self.m_deviceDiscoveryAgent.canceled.connect(self.scanFinished) #! [devicediscovery-1] - if simulator: + 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: + if simulator(): self.m_demoTimer.start() else: #! [devicediscovery-2] @@ -82,7 +99,7 @@ class DeviceFinder(BluetoothBaseClass): @Slot() def scanFinished(self): - if simulator: + if simulator(): # Only for testing for i in range(5): self.m_devices.append(DeviceInfo(QBluetoothDeviceInfo())) @@ -113,7 +130,7 @@ class DeviceFinder(BluetoothBaseClass): @Property(bool, notify=scanningChanged) def scanning(self): - if simulator: + if simulator(): return self.m_demoTimer.isActive() return self.m_deviceDiscoveryAgent.isActive() diff --git a/examples/bluetooth/heartrate_game/devicehandler.py b/examples/bluetooth/heartrate_game/devicehandler.py index 421102b28..df34052b8 100644 --- a/examples/bluetooth/heartrate_game/devicehandler.py +++ b/examples/bluetooth/heartrate_game/devicehandler.py @@ -10,7 +10,7 @@ from PySide6.QtBluetooth import (QLowEnergyCharacteristic, QLowEnergyDescriptor, QLowEnergyService, QBluetoothUuid) -from PySide6.QtQml import QmlNamedElement, QmlUncreatable +from PySide6.QtQml import QmlElement from PySide6.QtCore import (QByteArray, QDateTime, QRandomGenerator, QTimer, Property, Signal, Slot, QEnum) @@ -20,12 +20,11 @@ from heartrate_global import simulator # To be used on the @QmlElement decorator # (QML_IMPORT_MINOR_VERSION is optional) -QML_IMPORT_NAME = "Shared" +QML_IMPORT_NAME = "HeartRateGame" QML_IMPORT_MAJOR_VERSION = 1 -@QmlNamedElement("AddressType") -@QmlUncreatable("Enum is not a type") +@QmlElement class DeviceHandler(BluetoothBaseClass): @QEnum @@ -62,7 +61,7 @@ class DeviceHandler(BluetoothBaseClass): self.m_demoTimer = QTimer() - if simulator: + if simulator(): self.m_demoTimer.setSingleShot(False) self.m_demoTimer.setInterval(2000) self.m_demoTimer.timeout.connect(self.updateDemoHR) @@ -99,27 +98,27 @@ class DeviceHandler(BluetoothBaseClass): self.clearMessages() self.m_currentDevice = device - if simulator: + if simulator(): self.info = "Demo device connected." return # Disconnect and delete old connection if self.m_control: self.m_control.disconnectFromDevice() - m_control = None + 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.getDevice(), self) + self.m_control = QLowEnergyController.createCentral(self.m_currentDevice.device(), self) #! [Connect-Signals-1] self.m_control.setRemoteAddressType(self.m_addressType) #! [Connect-Signals-2] - m_control.serviceDiscovered.connect(self.serviceDiscovered) - m_control.discoveryFinished.connect(self.serviceScanDone) + 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) @@ -167,7 +166,8 @@ class DeviceHandler(BluetoothBaseClass): #! [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) + self.m_service = self.m_control.createServiceObject( + QBluetoothUuid(QBluetoothUuid.ServiceClassUuid.HeartRate), self) if self.m_service: self.m_service.stateChanged.connect(self.serviceStateChanged) @@ -183,14 +183,16 @@ class DeviceHandler(BluetoothBaseClass): @Slot(QLowEnergyService.ServiceState) def serviceStateChanged(self, switch): if switch == QLowEnergyService.RemoteServiceDiscovering: - self.setInfo(tr("Discovering services...")) + self.info = "Discovering services..." elif switch == QLowEnergyService.RemoteServiceDiscovered: - self.setInfo(tr("Service discovered.")) - hrChar = m_service.characteristic(QBluetoothUuid(QBluetoothUuid.CharacteristicType.HeartRateMeasurement)) + self.info = "Service discovered." + hrChar = self.m_service.characteristic( + QBluetoothUuid(QBluetoothUuid.CharacteristicType.HeartRateMeasurement)) if hrChar.isValid(): - self.m_notificationDesc = hrChar.descriptor(QBluetoothUuid.DescriptorType.ClientCharacteristicConfiguration) + self.m_notificationDesc = hrChar.descriptor( + QBluetoothUuid.DescriptorType.ClientCharacteristicConfiguration) if self.m_notificationDesc.isValid(): - self.m_service.writeDescriptor(m_notificationDesc, + self.m_service.writeDescriptor(self.m_notificationDesc, QByteArray.fromHex(b"0100")) else: self.error = "HR Data not found." @@ -209,9 +211,9 @@ class DeviceHandler(BluetoothBaseClass): # Heart Rate hrvalue = 0 if flags & 0x1: # HR 16 bit little endian? otherwise 8 bit - hrvalue = struct.unpack("<H", data[1:3]) + hrvalue = struct.unpack("<H", data[1:3])[0] else: - hrvalue = struct.unpack("B", data[1:2]) + hrvalue = struct.unpack("B", data[1:2])[0] self.addMeasurement(hrvalue) @@ -234,7 +236,7 @@ class DeviceHandler(BluetoothBaseClass): @Slot(QLowEnergyCharacteristic, QByteArray) def confirmedDescriptorWrite(self, d, value): if (d.isValid() and d == self.m_notificationDesc - and value == QByteArray.fromHex(b"0000")): + and value == QByteArray.fromHex(b"0000")): # disabled notifications . assume disconnect intent self.m_control.disconnectFromDevice() self.m_service = None @@ -245,7 +247,7 @@ class DeviceHandler(BluetoothBaseClass): # disable notifications if (self.m_notificationDesc.isValid() and self.m_service - and self.m_notificationDesc.value() == QByteArray.fromHex(b"0100")): + and self.m_notificationDesc.value() == QByteArray.fromHex(b"0100")): self.m_service.writeDescriptor(self.m_notificationDesc, QByteArray.fromHex(b"0000")) else: @@ -259,7 +261,7 @@ class DeviceHandler(BluetoothBaseClass): @Property(bool, notify=aliveChanged) def alive(self): - if simulator: + if simulator(): return True if self.m_service: return self.m_service.state() == QLowEnergyService.RemoteServiceDiscovered @@ -302,6 +304,6 @@ class DeviceHandler(BluetoothBaseClass): 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 + + (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 index 4ea08628f..5fd5c3270 100644 --- a/examples/bluetooth/heartrate_game/deviceinfo.py +++ b/examples/bluetooth/heartrate_game/deviceinfo.py @@ -25,13 +25,13 @@ class DeviceInfo(QObject): @Property(str, notify=deviceChanged) def deviceName(self): - if simulator: + if simulator(): return "Demo device" return self.m_device.name() @Property(str, notify=deviceChanged) def deviceAddress(self): - if simulator: + if simulator(): return "00:11:22:33:44:55" if sys.platform == "Darwin": # workaround for Core Bluetooth: return self.m_device.deviceUuid().toString() diff --git a/examples/bluetooth/heartrate_game/doc/heartrate_game.rst b/examples/bluetooth/heartrate_game/doc/heartrate_game.rst index 0a0938cad..9d190d991 100644 --- a/examples/bluetooth/heartrate_game/doc/heartrate_game.rst +++ b/examples/bluetooth/heartrate_game/doc/heartrate_game.rst @@ -1,6 +1,8 @@ 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, diff --git a/examples/bluetooth/heartrate_game/heartrate_game.pyproject b/examples/bluetooth/heartrate_game/heartrate_game.pyproject index e4c40874a..94b7e3978 100644 --- a/examples/bluetooth/heartrate_game/heartrate_game.pyproject +++ b/examples/bluetooth/heartrate_game/heartrate_game.pyproject @@ -6,17 +6,18 @@ "devicehandler.py", "deviceinfo.py", "heartrate_global.py", - "qml/main.qml", - "qml/App.qml", - "qml/BluetoothAlarmDialog.qml", - "qml/BottomLine.qml", - "qml/Connect.qml", - "qml/GameButton.qml", - "qml/GamePage.qml", - "qml/GameSettings.qml", - "qml/Measure.qml", - "qml/SplashScreen.qml", - "qml/Stats.qml", - "qml/StatsLabel.qml", - "qml/TitleBar.qml"] + "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 index 7d95f1299..de5c37ac3 100644 --- a/examples/bluetooth/heartrate_game/heartrate_global.py +++ b/examples/bluetooth/heartrate_game/heartrate_global.py @@ -1,6 +1,30 @@ # Copyright (C) 2022 The Qt Company Ltd. # SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause - +import os import sys -simulator = sys.platform == "win32" +_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 index a101a05bf..3cb4f0672 100644 --- a/examples/bluetooth/heartrate_game/main.py +++ b/examples/bluetooth/heartrate_game/main.py @@ -3,19 +3,18 @@ """PySide6 port of the bluetooth/heartrate-game example from Qt v6.x""" -import os from pathlib import Path import sys -from argparse import ArgumentParser, Namespace, RawDescriptionHelpFormatter +from argparse import ArgumentParser, RawDescriptionHelpFormatter -from PySide6.QtQml import QQmlApplicationEngine, QQmlContext +from PySide6.QtQml import QQmlApplicationEngine from PySide6.QtGui import QGuiApplication -from PySide6.QtCore import QCoreApplication, QLoggingCategory, QUrl +from PySide6.QtCore import QCoreApplication, QLoggingCategory from connectionhandler import ConnectionHandler from devicefinder import DeviceFinder from devicehandler import DeviceHandler -from heartrate_global import simulator +from heartrate_global import set_simulator if __name__ == '__main__': @@ -27,7 +26,7 @@ if __name__ == '__main__': parser.add_argument("-s", "--simulator", action="store_true", help="Use Simulator") options = parser.parse_args() - simulator = options.simulator + set_simulator(options.simulator) if options.verbose: QLoggingCategory.setFilterRules("qt.bluetooth* = true") @@ -43,8 +42,9 @@ if __name__ == '__main__': "deviceFinder": deviceFinder, "deviceHandler": deviceHandler}) - qml_file = os.fspath(Path(__file__).resolve().parent / "qml" / "main.qml") - engine.load(QUrl.fromLocalFile(qml_file)) + engine.addImportPath(Path(__file__).parent) + engine.loadFromModule("HeartRateGame", "Main") + if not engine.rootObjects(): sys.exit(-1) diff --git a/examples/bluetooth/heartrate_game/qml/App.qml b/examples/bluetooth/heartrate_game/qml/App.qml deleted file mode 100644 index 1eb532021..000000000 --- a/examples/bluetooth/heartrate_game/qml/App.qml +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright (C) 2022 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause - -import QtQuick - -Item { - id: app - anchors.fill: parent - opacity: 0.0 - - Behavior on opacity { NumberAnimation { duration: 500 } } - - property var lastPages: [] - property int __currentIndex: 0 - - function init() - { - opacity = 1.0 - showPage("Connect.qml") - } - - function prevPage() - { - lastPages.pop() - pageLoader.setSource(lastPages[lastPages.length-1]) - __currentIndex = lastPages.length-1; - } - - function showPage(name) - { - lastPages.push(name) - pageLoader.setSource(name) - __currentIndex = lastPages.length-1; - } - - TitleBar { - id: titleBar - currentIndex: __currentIndex - - onTitleClicked: (index) => { - if (index < __currentIndex) - pageLoader.item.close() - } - } - - Loader { - id: pageLoader - anchors.left: parent.left - anchors.right: parent.right - anchors.top: titleBar.bottom - anchors.bottom: parent.bottom - - onStatusChanged: { - if (status === Loader.Ready) - { - pageLoader.item.init(); - pageLoader.item.forceActiveFocus() - } - } - } - - Keys.onReleased: (event) => { - switch (event.key) { - case Qt.Key_Escape: - case Qt.Key_Back: { - if (__currentIndex > 0) { - pageLoader.item.close() - event.accepted = true - } else { - Qt.quit() - } - break; - } - default: break; - } - } - - BluetoothAlarmDialog { - id: btAlarmDialog - anchors.fill: parent - visible: !connectionHandler.alive - } -} diff --git a/examples/bluetooth/heartrate_game/qml/main.qml b/examples/bluetooth/heartrate_game/qml/main.qml deleted file mode 100644 index 44d824faf..000000000 --- a/examples/bluetooth/heartrate_game/qml/main.qml +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright (C) 2022 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause - -import QtQuick -import QtQuick.Window -import "." -import Shared - -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 AddressType deviceHandler - - Component.onCompleted: { - GameSettings.wWidth = Qt.binding(function() {return width}) - GameSettings.wHeight = Qt.binding(function() {return height}) - } - - Loader { - id: splashLoader - anchors.fill: parent - source: "SplashScreen.qml" - asynchronous: false - visible: true - - onStatusChanged: { - if (status === Loader.Ready) { - appLoader.setSource("App.qml"); - } - } - } - - Connections { - target: splashLoader.item - function onReadyToGo() { - appLoader.visible = true - appLoader.item.init() - splashLoader.visible = false - splashLoader.setSource("") - appLoader.item.forceActiveFocus(); - } - } - - Loader { - id: appLoader - anchors.fill: parent - visible: false - asynchronous: true - onStatusChanged: { - if (status === Loader.Ready) - splashLoader.item.appReady() - if (status === Loader.Error) - splashLoader.item.errorInLoadingApp(); - } - } -} diff --git a/examples/bluetooth/heartrate_game/qml/qmldir b/examples/bluetooth/heartrate_game/qml/qmldir deleted file mode 100644 index 5e0d2b540..000000000 --- a/examples/bluetooth/heartrate_game/qml/qmldir +++ /dev/null @@ -1 +0,0 @@ -singleton GameSettings 1.0 GameSettings.qml diff --git a/examples/bluetooth/heartrate_server/heartrate_server.py b/examples/bluetooth/heartrate_server/heartrate_server.py index f98cc6fe8..abbf4eb7f 100644 --- a/examples/bluetooth/heartrate_server/heartrate_server.py +++ b/examples/bluetooth/heartrate_server/heartrate_server.py @@ -39,8 +39,8 @@ if __name__ == '__main__': 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)) + client_config = QLowEnergyDescriptorData( + QBluetoothUuid.DescriptorType.ClientCharacteristicConfiguration, QByteArray(2, 0)) char_data.addDescriptor(client_config) service_data = QLowEnergyServiceData() @@ -66,8 +66,9 @@ if __name__ == '__main__': 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()) + characteristic = service.characteristic( + QBluetoothUuid.CharacteristicType.HeartRateMeasurement) + assert characteristic.isValid() # Potentially causes notification. service.writeCharacteristic(characteristic, value) if current_heart_rate == 60: diff --git a/examples/bluetooth/lowenergyscanner/Scanner/Characteristics.qml b/examples/bluetooth/lowenergyscanner/Scanner/Characteristics.qml new file mode 100644 index 000000000..bd3ccbfcb --- /dev/null +++ b/examples/bluetooth/lowenergyscanner/Scanner/Characteristics.qml @@ -0,0 +1,121 @@ +// Copyright (C) 2013 BlackBerry Limited. All rights reserved. +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +pragma ComponentBehavior: Bound +import QtQuick + +Rectangle { + id: characteristicsPage + + signal showServices + signal showDevices + + width: 300 + height: 600 + + Header { + id: header + anchors.top: parent.top + headerText: "Characteristics list" + } + + Dialog { + id: info + anchors.centerIn: parent + visible: true + dialogText: "Scanning for characteristics..." + } + + Connections { + target: Device + function oncharacteristics_pdated() { + menu.menuText = "Back" + if (characteristicview.count === 0) { + info.dialogText = "No characteristic found" + info.busyImage = false + } else { + info.visible = false + info.busyImage = true + } + } + + function onDisconnected() { + characteristicsPage.showDevices() + } + } + + ListView { + id: characteristicview + width: parent.width + clip: true + + anchors.top: header.bottom + anchors.bottom: menu.top + model: Device.characteristicList + + delegate: Rectangle { + required property var modelData + id: box + height: 300 + width: characteristicview.width + color: "lightsteelblue" + border.width: 2 + border.color: "black" + radius: 5 + + Label { + id: characteristicName + textContent: box.modelData.characteristic_name + anchors.top: parent.top + anchors.topMargin: 5 + } + + Label { + id: characteristicUuid + font.pointSize: characteristicName.font.pointSize * 0.7 + textContent: box.modelData.characteristic_uuid + anchors.top: characteristicName.bottom + anchors.topMargin: 5 + } + + Label { + id: characteristicValue + font.pointSize: characteristicName.font.pointSize * 0.7 + textContent: ("Value: " + box.modelData.characteristic_value) + anchors.bottom: characteristicHandle.top + horizontalAlignment: Text.AlignHCenter + anchors.topMargin: 5 + } + + Label { + id: characteristicHandle + font.pointSize: characteristicName.font.pointSize * 0.7 + textContent: ("Handlers: " + box.modelData.characteristic_handle) + anchors.bottom: characteristicPermission.top + anchors.topMargin: 5 + } + + Label { + id: characteristicPermission + font.pointSize: characteristicName.font.pointSize * 0.7 + textContent: box.modelData.characteristic_permission + anchors.bottom: parent.bottom + anchors.topMargin: 5 + anchors.bottomMargin: 5 + } + } + } + + Menu { + id: menu + anchors.bottom: parent.bottom + menuWidth: parent.width + menuText: Device.update + menuHeight: (parent.height / 6) + onButtonClick: { + characteristicsPage.showServices() + Device.update = "Back" + } + } +} diff --git a/examples/bluetooth/lowenergyscanner/Scanner/Devices.qml b/examples/bluetooth/lowenergyscanner/Scanner/Devices.qml new file mode 100644 index 000000000..6e5e85a52 --- /dev/null +++ b/examples/bluetooth/lowenergyscanner/Scanner/Devices.qml @@ -0,0 +1,121 @@ +// Copyright (C) 2013 BlackBerry Limited. All rights reserved. +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +pragma ComponentBehavior: Bound +import QtQuick + +Rectangle { + id: devicesPage + + property bool deviceState: Device.state + signal showServices + + width: 300 + height: 600 + + onDeviceStateChanged: { + if (!Device.state) + info.visible = false + } + + Header { + id: header + anchors.top: parent.top + headerText: { + if (Device.state) + return "Discovering" + + if (Device.devices_list.length > 0) + return "Select a device" + + return "Start Discovery" + } + } + + Dialog { + id: info + anchors.centerIn: parent + visible: false + } + + ListView { + id: theListView + width: parent.width + clip: true + + anchors.top: header.bottom + anchors.bottom: connectToggle.top + model: Device.devices_list + + delegate: Rectangle { + required property var modelData + id: box + height: 100 + width: theListView.width + color: "lightsteelblue" + border.width: 2 + border.color: "black" + radius: 5 + + MouseArea { + anchors.fill: parent + onClicked: { + Device.scan_services(box.modelData.device_address) + showServices() + } + } + + Label { + id: deviceName + textContent: box.modelData.device_name + anchors.top: parent.top + anchors.topMargin: 5 + } + + Label { + id: deviceAddress + textContent: box.modelData.device_address + font.pointSize: deviceName.font.pointSize * 0.7 + anchors.bottom: box.bottom + anchors.bottomMargin: 5 + } + } + } + + Menu { + id: connectToggle + + menuWidth: parent.width + anchors.bottom: menu.top + menuText: { + visible = Device.devices_list.length > 0 + if (Device.use_random_address) + return "Address type: Random" + else + return "Address type: Public" + } + + onButtonClick: Device.use_random_address = !Device.use_random_address + } + + Menu { + id: menu + anchors.bottom: parent.bottom + menuWidth: parent.width + menuHeight: (parent.height / 6) + menuText: Device.update + onButtonClick: { + if (!Device.state) { + Device.start_device_discovery() + // if start_device_discovery() failed Device.state is not set + if (Device.state) { + info.dialogText = "Searching..." + info.visible = true + } + } else { + Device.stop_device_discovery() + } + } + } +} diff --git a/examples/bluetooth/lowenergyscanner/Scanner/Dialog.qml b/examples/bluetooth/lowenergyscanner/Scanner/Dialog.qml new file mode 100644 index 000000000..75e82642a --- /dev/null +++ b/examples/bluetooth/lowenergyscanner/Scanner/Dialog.qml @@ -0,0 +1,48 @@ +// Copyright (C) 2013 BlackBerry Limited. All rights reserved. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick + +Rectangle { + id: dialog + width: parent.width / 3 * 2 + height: dialogTextId.height + background.height + 20 + z: 50 + property string dialogText: "" + property bool busyImage: true + border.width: 1 + border.color: "#363636" + radius: 10 + + Text { + id: dialogTextId + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + anchors.topMargin: 10 + + elide: Text.ElideMiddle + text: dialog.dialogText + color: "#363636" + wrapMode: Text.Wrap + } + + Image { + id: background + + width: 20 + height: 20 + anchors.top: dialogTextId.bottom + anchors.horizontalCenter: dialogTextId.horizontalCenter + visible: parent.busyImage + source: "assets/busy_dark.png" + fillMode: Image.PreserveAspectFit + NumberAnimation on rotation { + duration: 3000 + from: 0 + to: 360 + loops: Animation.Infinite + } + } +} diff --git a/examples/bluetooth/lowenergyscanner/Scanner/Header.qml b/examples/bluetooth/lowenergyscanner/Scanner/Header.qml new file mode 100644 index 000000000..c95385dd3 --- /dev/null +++ b/examples/bluetooth/lowenergyscanner/Scanner/Header.qml @@ -0,0 +1,25 @@ +// Copyright (C) 2013 BlackBerry Limited. All rights reserved. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick + +Rectangle { + id: header + width: parent.width + height: 70 + border.width: 1 + border.color: "#363636" + radius: 5 + property string headerText: "" + + Text { + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + anchors.fill: parent + text: header.headerText + font.bold: true + font.pointSize: 20 + elide: Text.ElideMiddle + color: "#363636" + } +} diff --git a/examples/bluetooth/lowenergyscanner/Scanner/Label.qml b/examples/bluetooth/lowenergyscanner/Scanner/Label.qml new file mode 100644 index 000000000..e31156740 --- /dev/null +++ b/examples/bluetooth/lowenergyscanner/Scanner/Label.qml @@ -0,0 +1,16 @@ +// Copyright (C) 2013 BlackBerry Limited. All rights reserved. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick + +Text { + property string textContent: "" + font.pointSize: 20 + anchors.horizontalCenter: parent.horizontalCenter + color: "#363636" + horizontalAlignment: Text.AlignHCenter + elide: Text.ElideMiddle + width: parent.width + wrapMode: Text.Wrap + text: textContent +} diff --git a/examples/bluetooth/lowenergyscanner/Scanner/Main.qml b/examples/bluetooth/lowenergyscanner/Scanner/Main.qml new file mode 100644 index 000000000..88600bace --- /dev/null +++ b/examples/bluetooth/lowenergyscanner/Scanner/Main.qml @@ -0,0 +1,31 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import QtQuick.Layouts + +Window { + id: main + + width: 300 + height: 600 + visible: true + + StackLayout { + id: pagesLayout + anchors.fill: parent + currentIndex: 0 + + Devices { + onShowServices: pagesLayout.currentIndex = 1 + } + Services { + onShowDevices: pagesLayout.currentIndex = 0 + onShowCharacteristics: pagesLayout.currentIndex = 2 + } + Characteristics { + onShowDevices: pagesLayout.currentIndex = 0 + onShowServices: pagesLayout.currentIndex = 1 + } + } +} diff --git a/examples/bluetooth/lowenergyscanner/Scanner/Menu.qml b/examples/bluetooth/lowenergyscanner/Scanner/Menu.qml new file mode 100644 index 000000000..ef69c895e --- /dev/null +++ b/examples/bluetooth/lowenergyscanner/Scanner/Menu.qml @@ -0,0 +1,55 @@ +// Copyright (C) 2013 BlackBerry Limited. All rights reserved. +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick + +Rectangle { + id: menu + + property real menuWidth: 100 + property real menuHeight: 50 + property string menuText: "Search" + signal buttonClick + + height: menuHeight + width: menuWidth + + Rectangle { + id: search + width: parent.width + height: parent.height + anchors.centerIn: parent + color: "#363636" + border.width: 1 + border.color: "#E3E3E3" + radius: 5 + Text { + id: searchText + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + anchors.fill: parent + text: menu.menuText + elide: Text.ElideMiddle + color: "#E3E3E3" + wrapMode: Text.WordWrap + } + + MouseArea { + anchors.fill: parent + onPressed: { + search.width = search.width - 7 + search.height = search.height - 5 + } + + onReleased: { + search.width = search.width + 7 + search.height = search.height + 5 + } + + onClicked: { + menu.buttonClick() + } + } + } +} diff --git a/examples/bluetooth/lowenergyscanner/Scanner/Services.qml b/examples/bluetooth/lowenergyscanner/Scanner/Services.qml new file mode 100644 index 000000000..70326242e --- /dev/null +++ b/examples/bluetooth/lowenergyscanner/Scanner/Services.qml @@ -0,0 +1,115 @@ +// Copyright (C) 2013 BlackBerry Limited. All rights reserved. +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +pragma ComponentBehavior: Bound +import QtQuick + +Rectangle { + id: servicesPage + + signal showCharacteristics + signal showDevices + + width: 300 + height: 600 + + Component.onCompleted: { + // Loading this page may take longer than QLEController + // stopping with an error, go back and readjust this view + // based on controller errors + if (Device.controller_error) { + info.visible = false + menu.menuText = Device.update + } + } + + Header { + id: header + anchors.top: parent.top + headerText: "Services list" + } + + Dialog { + id: info + anchors.centerIn: parent + visible: true + dialogText: "Scanning for services..." + } + + Connections { + target: Device + function onservices_updated() { + if (servicesview.count === 0) + info.dialogText = "No services found" + else + info.visible = false + } + + function ondisconnected() { + servicesPage.showDevices() + } + } + + ListView { + id: servicesview + width: parent.width + anchors.top: header.bottom + anchors.bottom: menu.top + model: Device.servicesList + clip: true + + delegate: Rectangle { + required property var modelData + id: box + height: 100 + color: "lightsteelblue" + border.width: 2 + border.color: "black" + radius: 5 + width: servicesview.width + + MouseArea { + anchors.fill: parent + onClicked: { + Device.connectToService(box.modelData.service_uuid) + servicesPage.showCharacteristics() + } + } + + Label { + id: serviceName + textContent: box.modelData.service_name + anchors.top: parent.top + anchors.topMargin: 5 + } + + Label { + textContent: box.modelData.service_type + font.pointSize: serviceName.font.pointSize * 0.5 + anchors.top: serviceName.bottom + } + + Label { + id: serviceUuid + font.pointSize: serviceName.font.pointSize * 0.5 + textContent: box.modelData.service_uuid + anchors.bottom: box.bottom + anchors.bottomMargin: 5 + } + } + } + + Menu { + id: menu + anchors.bottom: parent.bottom + menuWidth: parent.width + menuText: Device.update + menuHeight: (parent.height / 6) + onButtonClick: { + Device.disconnect_from_device() + servicesPage.showDevices() + Device.update = "Search" + } + } +} diff --git a/examples/bluetooth/lowenergyscanner/Scanner/assets/busy_dark.png b/examples/bluetooth/lowenergyscanner/Scanner/assets/busy_dark.png Binary files differnew file mode 100644 index 000000000..3a1059531 --- /dev/null +++ b/examples/bluetooth/lowenergyscanner/Scanner/assets/busy_dark.png diff --git a/examples/bluetooth/lowenergyscanner/Scanner/qmldir b/examples/bluetooth/lowenergyscanner/Scanner/qmldir new file mode 100644 index 000000000..0adf6fb19 --- /dev/null +++ b/examples/bluetooth/lowenergyscanner/Scanner/qmldir @@ -0,0 +1,10 @@ +module Scanner +typeinfo scanner.qmltypes +Characteristics 1.0 Characteristics.qml +Devices 1.0 Devices.qml +Dialog 1.0 Dialog.qml +Header 1.0 Header.qml +Label 1.0 Label.qml +Main 1.0 Main.qml +Menu 1.0 Menu.qml +Services 1.0 Services.qml diff --git a/examples/bluetooth/lowenergyscanner/characteristicinfo.py b/examples/bluetooth/lowenergyscanner/characteristicinfo.py new file mode 100644 index 000000000..42bde8753 --- /dev/null +++ b/examples/bluetooth/lowenergyscanner/characteristicinfo.py @@ -0,0 +1,87 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +from PySide6.QtCore import QObject, Property, Signal +from PySide6.QtBluetooth import QLowEnergyCharacteristic, QBluetoothUuid + + +class CharacteristicInfo(QObject): + + characteristic_changed = Signal() + + def __init__(self, characteristic=None) -> None: + super().__init__() + self._characteristic = characteristic + + @Property(str, notify=characteristic_changed) + def characteristic_name(self): + if not self.characteristic: + raise Exception("characteristic unset") + name = self.characteristic.name() + if name: + return name + + for descriptor in self.characteristic.descriptors(): + if descriptor.type() == QBluetoothUuid.DescriptorType.CharacteristicUserDescription: + name = descriptor.value() + break + + if not name: + name = "Unknown" + + return name + + @Property(str, notify=characteristic_changed) + def characteristic_uuid(self): + uuid = self.characteristic.uuid() + result16, success16 = uuid.toUInt16() + if success16: + return f"0x{result16:x}" + + result32, sucess32 = uuid.toUInt32() + if sucess32: + return f"0x{result32:x}" + + return uuid.toString().replace('{', '').replace('}', '') + + @Property(str, notify=characteristic_changed) + def characteristic_value(self): + # Show raw string first and hex value below + a = self.characteristic.value() + if not a: + return "<none>" + + result = f"{str(a)}\n{str(a.toHex())}" + return result + + @Property(str, notify=characteristic_changed) + def characteristic_permission(self): + properties = "( " + permission = self.characteristic.properties() + if (permission & QLowEnergyCharacteristic.Read): + properties += " Read" + if (permission & QLowEnergyCharacteristic.Write): + properties += " Write" + if (permission & QLowEnergyCharacteristic.Notify): + properties += " Notify" + if (permission & QLowEnergyCharacteristic.Indicate): + properties += " Indicate" + if (permission & QLowEnergyCharacteristic.ExtendedProperty): + properties += " ExtendedProperty" + if (permission & QLowEnergyCharacteristic.Broadcasting): + properties += " Broadcast" + if (permission & QLowEnergyCharacteristic.WriteNoResponse): + properties += " WriteNoResp" + if (permission & QLowEnergyCharacteristic.WriteSigned): + properties += " WriteSigned" + properties += " )" + return properties + + @property + def characteristic(self): + return self._characteristic + + @characteristic.setter + def characteristic(self, characteristic): + self._characteristic = characteristic + self.characteristic_changed.emit() diff --git a/examples/bluetooth/lowenergyscanner/device.py b/examples/bluetooth/lowenergyscanner/device.py new file mode 100644 index 000000000..09108cf69 --- /dev/null +++ b/examples/bluetooth/lowenergyscanner/device.py @@ -0,0 +1,278 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause +import warnings +from PySide6.QtBluetooth import (QBluetoothDeviceDiscoveryAgent, QLowEnergyController, + QBluetoothDeviceInfo, QBluetoothUuid, QLowEnergyService) +from PySide6.QtCore import QObject, Property, Signal, Slot, QTimer, QMetaObject, Qt +from PySide6.QtQml import QmlElement, QmlSingleton + +from deviceinfo import DeviceInfo +from serviceinfo import ServiceInfo +from characteristicinfo import CharacteristicInfo + +QML_IMPORT_NAME = "Scanner" +QML_IMPORT_MAJOR_VERSION = 1 + + +@QmlElement +@QmlSingleton +class Device(QObject): + + devices_updated = Signal() + services_updated = Signal() + characteristic_updated = Signal() + update_changed = Signal() + state_changed = Signal() + disconnected = Signal() + random_address_changed = Signal() + + def __init__(self, parent=None) -> None: + super().__init__(parent) + self.devices = [] + self._services = [] + self._characteristics = [] + self._previousAddress = "" + self._message = "" + self.currentDevice = DeviceInfo() + self.connected = False + self.controller: QLowEnergyController = None + self._deviceScanState = False + self.random_address = False + self.discovery_agent = QBluetoothDeviceDiscoveryAgent() + self.discovery_agent.setLowEnergyDiscoveryTimeout(25000) + self.discovery_agent.deviceDiscovered.connect(self.add_device) + self.discovery_agent.errorOccurred.connect(self.device_scan_error) + self.discovery_agent.finished.connect(self.device_scan_finished) + self.update = "Search" + + @Property("QVariant", notify=devices_updated) + def devices_list(self): + return self.devices + + @Property("QVariant", notify=services_updated) + def services_list(self): + return self._services + + @Property("QVariant", notify=characteristic_updated) + def characteristic_list(self): + return self._characteristics + + @Property(str, notify=update_changed) + def update(self): + return self._message + + @update.setter + def update(self, message): + self._message = message + self.update_changed.emit() + + @Property(bool, notify=random_address_changed) + def use_random_address(self): + return self.random_address + + @use_random_address.setter + def use_random_address(self, newValue): + self.random_address = newValue + self.random_address_changed.emit() + + @Property(bool, notify=state_changed) + def state(self): + return self._deviceScanState + + @Property(bool) + def controller_error(self): + return self.controller and (self.controller.error() != QLowEnergyController.NoError) + + @Slot() + def start_device_discovery(self): + self.devices.clear() + self.devices_updated.emit() + self.update = "Scanning for devices ..." + self.discovery_agent.start(QBluetoothDeviceDiscoveryAgent.LowEnergyMethod) + + if self.discovery_agent.isActive(): + self._deviceScanState = True + self.state_changed.emit() + + @Slot(str) + def scan_services(self, address): + # We need the current device for service discovery. + for device in self.devices: + if device.device_address == address: + self.currentDevice.set_device(device.get_device()) + break + + if not self.currentDevice.get_device().isValid(): + warnings.warn("Not a valid device") + return + + self._characteristics.clear() + self.characteristic_updated.emit() + self._services.clear() + self.services_updated.emit() + + self.update = "Back\n(Connecting to device...)" + + if self.controller and (self._previousAddress != self.currentDevice.device_address): + self.controller.disconnectFromDevice() + del self.controller + self.controller = None + + if not self.controller: + self.controller = QLowEnergyController.createCentral(self.currentDevice.get_device()) + self.controller.connected.connect(self.device_connected) + self.controller.errorOccurred.connect(self.error_received) + self.controller.disconnected.connect(self.device_disconnected) + self.controller.serviceDiscovered.connect(self.add_low_energy_service) + self.controller.discoveryFinished.connect(self.services_scan_done) + + if self.random_address: + self.controller.setRemoteAddressType(QLowEnergyController.RandomAddress) + else: + self.controller.setRemoteAddressType(QLowEnergyController.PublicAddress) + self.controller.connectToDevice() + + self._previousAddress = self.currentDevice.device_address + + @Slot(str) + def connect_to_service(self, uuid): + service: QLowEnergyService = None + for serviceInfo in self._services: + if not serviceInfo: + continue + + if serviceInfo.service_uuid == uuid: + service = serviceInfo.service + break + + if not service: + return + + self._characteristics.clear() + self.characteristic_updated.emit() + + if service.state() == QLowEnergyService.RemoteService: + service.state_changed.connect(self.service_details_discovered) + service.discoverDetails() + self.update = "Back\n(Discovering details...)" + return + + # discovery already done + chars = service.characteristics() + for ch in chars: + cInfo = CharacteristicInfo(ch) + self._characteristics.append(cInfo) + + QTimer.singleShot(0, self.characteristic_updated) + + @Slot() + def disconnect_from_device(self): + # UI always expects disconnect() signal when calling this signal + # TODO what is really needed is to extend state() to a multi value + # and thus allowing UI to keep track of controller progress in addition to + # device scan progress + + if self.controller.state() != QLowEnergyController.UnconnectedState: + self.controller.disconnectFromDevice() + else: + self.device_disconnected() + + @Slot(QBluetoothDeviceInfo) + def add_device(self, info): + if info.coreConfigurations() & QBluetoothDeviceInfo.LowEnergyCoreConfiguration: + self.update = "Last device added: " + info.name() + + @Slot() + def device_scan_finished(self): + foundDevices = self.discovery_agent.discoveredDevices() + for nextDevice in foundDevices: + if nextDevice.coreConfigurations() & QBluetoothDeviceInfo.LowEnergyCoreConfiguration: + device = DeviceInfo(nextDevice) + self.devices.append(device) + + self.devices_updated.emit() + self._deviceScanState = False + self.state_changed.emit() + if not self.devices: + self.update = "No Low Energy devices found..." + else: + self.update = "Done! Scan Again!" + + @Slot("QBluetoothDeviceDiscovertAgent::Error") + def device_scan_error(self, error): + if error == QBluetoothDeviceDiscoveryAgent.PoweredOffError: + self.update = ( + "The Bluetooth adaptor is powered off, power it on before doing discovery." + ) + elif error == QBluetoothDeviceDiscoveryAgent.InputOutputError: + self.update = "Writing or reading from the device resulted in an error." + else: + qme = self.discovery_agent.metaObject().enumerator( + self.discovery_agent.metaObject().indexOfEnumerator("Error") + ) + self.update = f"Error: {qme.valueToKey(error)}" + + self._deviceScanState = False + self.devices_updated.emit() + self.state_changed.emit() + + @Slot(QBluetoothUuid) + def add_low_energy_service(self, service_uuid): + service = self.controller.createServiceObject(service_uuid) + if not service: + warnings.warn("Cannot create service from uuid") + return + + serv = ServiceInfo(service) + self._services.append(serv) + self.services_updated.emit() + + @Slot() + def device_connected(self): + self.update = "Back\n(Discovering services...)" + self.connected = True + self.controller.discoverServices() + + @Slot("QLowEnergyController::Error") + def error_received(self, error): + warnings.warn(f"Error: {self.controller.errorString()}") + self.update = f"Back\n({self.controller.errorString()})" + + @Slot() + def services_scan_done(self): + self.update = "Back\n(Service scan done!)" + # force UI in case we didn't find anything + if not self._services: + self.services_updated.emit() + + @Slot() + def device_disconnected(self): + warnings.warn("Disconnect from Device") + self.disconnected.emit() + + @Slot("QLowEnergyService::ServiceState") + def service_details_discovered(self, newState): + if newState != QLowEnergyService.RemoteServiceDiscovered: + # do not hang in "Scanning for characteristics" mode forever + # in case the service discovery failed + # We have to queue the signal up to give UI time to even enter + # the above mode + if newState != QLowEnergyService.RemoteServiceDiscovering: + QMetaObject.invokeMethod(self.characteristic_updated, Qt.QueuedConnection) + return + + service = self.sender() + if not service: + return + + chars = service.characteristics() + for ch in chars: + cInfo = CharacteristicInfo(ch) + self._characteristics.append(cInfo) + + self.characteristic_updated.emit() + + @Slot() + def stop_device_discovery(self): + if self.discovery_agent.isActive(): + self.discovery_agent.stop() diff --git a/examples/bluetooth/lowenergyscanner/deviceinfo.py b/examples/bluetooth/lowenergyscanner/deviceinfo.py new file mode 100644 index 000000000..35a568821 --- /dev/null +++ b/examples/bluetooth/lowenergyscanner/deviceinfo.py @@ -0,0 +1,34 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import sys + +from PySide6.QtCore import QObject, Property, Signal +from PySide6.QtBluetooth import QBluetoothDeviceInfo + + +class DeviceInfo(QObject): + + device_changed = Signal() + + def __init__(self, d: QBluetoothDeviceInfo = None) -> None: + super().__init__() + self._device = d + + @Property(str, notify=device_changed) + def device_name(self): + return self._device.name() + + @Property(str, notify=device_changed) + def device_address(self): + if sys.platform == "darwin": + return self._device.deviceUuid().toString() + + return self._device.address().toString() + + def get_device(self): + return self._device + + def set_device(self, device): + self._device = device + self.device_changed.emit() diff --git a/examples/bluetooth/lowenergyscanner/doc/lowenergyscanner.png b/examples/bluetooth/lowenergyscanner/doc/lowenergyscanner.png Binary files differnew file mode 100644 index 000000000..29f41deb4 --- /dev/null +++ b/examples/bluetooth/lowenergyscanner/doc/lowenergyscanner.png diff --git a/examples/bluetooth/lowenergyscanner/doc/lowenergyscanner.rst b/examples/bluetooth/lowenergyscanner/doc/lowenergyscanner.rst new file mode 100644 index 000000000..a0c574350 --- /dev/null +++ b/examples/bluetooth/lowenergyscanner/doc/lowenergyscanner.rst @@ -0,0 +1,11 @@ +Bluetooth Low Energy Scanner Example +==================================== + +.. tags:: Android + +A Python application that demonstrates the analogous example in Qt +`Bluetooth Low Energy Scanner <https://doc.qt.io/qt-6/qtbluetooth-lowenergyscanner-example.html>`_ + +.. image:: lowenergyscanner.png + :width: 400 + :alt: lowenergyscanner screenshot diff --git a/examples/bluetooth/lowenergyscanner/lowenergyscanner.pyproject b/examples/bluetooth/lowenergyscanner/lowenergyscanner.pyproject new file mode 100644 index 000000000..7e0cfa3d9 --- /dev/null +++ b/examples/bluetooth/lowenergyscanner/lowenergyscanner.pyproject @@ -0,0 +1,7 @@ +{ + "files": ["main.py", "device.py", "deviceinfo.py", "serviceinfo.py", "characteristicinfo.py", + "Scanner/Main.qml", "Scanner/Menu.qml","Scanner/Header.qml", + "Scanner/Characteristics.qml", "Scanner/Dialog.qml", "Scanner/Services.qml", + "Scanner/Label.qml", "Scanner/Devices.qml", "Scanner/assets/busy_dark.png", + "Scanner/qmldir"] +} diff --git a/examples/bluetooth/lowenergyscanner/main.py b/examples/bluetooth/lowenergyscanner/main.py new file mode 100644 index 000000000..ec12f99e7 --- /dev/null +++ b/examples/bluetooth/lowenergyscanner/main.py @@ -0,0 +1,27 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +"""PySide6 port of the bluetooth/lowenergyscanner example from Qt v6.x""" + + +import sys + +from PySide6.QtCore import QCoreApplication +from PySide6.QtGui import QGuiApplication +from PySide6.QtQml import QQmlApplicationEngine + +from device import Device # noqa: F401 +from pathlib import Path + +if __name__ == '__main__': + app = QGuiApplication(sys.argv) + engine = QQmlApplicationEngine() + engine.addImportPath(Path(__file__).parent) + engine.loadFromModule("Scanner", "Main") + + if not engine.rootObjects(): + sys.exit(-1) + + ex = QCoreApplication.exec() + del engine + sys.exit(ex) diff --git a/examples/bluetooth/lowenergyscanner/serviceinfo.py b/examples/bluetooth/lowenergyscanner/serviceinfo.py new file mode 100644 index 000000000..cddffe663 --- /dev/null +++ b/examples/bluetooth/lowenergyscanner/serviceinfo.py @@ -0,0 +1,64 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +from PySide6.QtCore import QObject, Property, Signal +from PySide6.QtBluetooth import QLowEnergyService + + +class ServiceInfo(QObject): + + service_changed = Signal() + + def __init__(self, service: QLowEnergyService) -> None: + super().__init__() + self._service = service + self.service.setParent(self) + + @Property(str, notify=service_changed) + def service_name(self): + if not self.service: + return "" + + return self.service.service_name() + + @Property(str, notify=service_changed) + def service_type(self): + if not self.service: + return "" + + result = "" + if (self.service.type() & QLowEnergyService.PrimaryService): + result += "primary" + else: + result += "secondary" + + if (self.service.type() & QLowEnergyService.IncludedService): + result += " included" + + result = '<' + result + '>' + + return result + + @Property(str, notify=service_changed) + def service_uuid(self): + if not self.service: + return "" + + uuid = self.service.service_uuid() + result16, success16 = uuid.toUInt16() + if success16: + return f"0x{result16:x}" + + result32, sucesss32 = uuid.toUInt32() + if sucesss32: + return f"0x{result32:x}" + + return uuid.toString().replace('{', '').replace('}', '') + + @property + def service(self): + return self._service + + @service.setter + def service(self, service): + self._service = service |