diff options
author | Friedemann Kleint <Friedemann.Kleint@qt.io> | 2022-08-11 15:20:25 +0200 |
---|---|---|
committer | Friedemann Kleint <Friedemann.Kleint@qt.io> | 2022-08-26 14:24:17 +0200 |
commit | ba5fdf167b79444f210884aa01f484b5283ec67a (patch) | |
tree | 1b0196aaad9ff3eded3f5ed71cc5baee14cc9657 /examples | |
parent | 6c8034b8c195818a8dbd31214b158a82a87c34ae (diff) |
Add the Bluetooth heart rate examples
Task-number: PYSIDE-841
Pick-to: 6.3
Change-Id: If24127be2ab78b3542777fdf691227d51e628373
Reviewed-by: Cristian Maureira-Fredes <cristian.maureira-fredes@qt.io>
Diffstat (limited to 'examples')
29 files changed, 1645 insertions, 0 deletions
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..5bd7bfbb2 --- /dev/null +++ b/examples/bluetooth/heartrate_game/connectionhandler.py @@ -0,0 +1,51 @@ +# 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 + +from heartrate_global import simulator + +# To be used on the @QmlElement decorator +# (QML_IMPORT_MINOR_VERSION is optional) +QML_IMPORT_NAME = "Shared" +QML_IMPORT_MAJOR_VERSION = 1 + + +@QmlElement +class ConnectionHandler(QObject): + + deviceChanged = Signal() + + def __init__(self, parent=None): + super().__init__(parent) + self.m_localDevice = QBluetoothLocalDevice() + self.m_localDevice.hostModeStateChanged.connect(self.hostModeChanged) + + @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() + + @Slot(QBluetoothLocalDevice.HostMode) + def hostModeChanged(self, mode): + 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..c69f8ab89 --- /dev/null +++ b/examples/bluetooth/heartrate_game/devicefinder.py @@ -0,0 +1,122 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +from PySide6.QtBluetooth import (QBluetoothDeviceDiscoveryAgent, + QBluetoothDeviceInfo) +from PySide6.QtQml import QmlElement +from PySide6.QtCore import QTimer, Property, Signal, Slot + +from bluetoothbaseclass import BluetoothBaseClass +from deviceinfo import DeviceInfo +from heartrate_global import simulator + +# To be used on the @QmlElement decorator +# (QML_IMPORT_MINOR_VERSION is optional) +QML_IMPORT_NAME = "Shared" +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): + 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..421102b28 --- /dev/null +++ b/examples/bluetooth/heartrate_game/devicehandler.py @@ -0,0 +1,307 @@ +# 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 QmlNamedElement, QmlUncreatable +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 = "Shared" +QML_IMPORT_MAJOR_VERSION = 1 + + +@QmlNamedElement("AddressType") +@QmlUncreatable("Enum is not a type") +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() + 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) +#! [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.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.setInfo(tr("Discovering services...")) + elif switch == QLowEnergyService.RemoteServiceDiscovered: + self.setInfo(tr("Service discovered.")) + hrChar = 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(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]) + else: + hrvalue = struct.unpack("B", data[1:2]) + + 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..4ea08628f --- /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..0a0938cad --- /dev/null +++ b/examples/bluetooth/heartrate_game/doc/heartrate_game.rst @@ -0,0 +1,9 @@ +Bluetooth Low Energy Heart Rate Game +==================================== + +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..e4c40874a --- /dev/null +++ b/examples/bluetooth/heartrate_game/heartrate_game.pyproject @@ -0,0 +1,22 @@ +{ + "files": ["main.py", + "bluetoothbaseclass.py", + "connectionhandler.py", + "devicefinder.py", + "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"] +} diff --git a/examples/bluetooth/heartrate_game/heartrate_global.py b/examples/bluetooth/heartrate_game/heartrate_global.py new file mode 100644 index 000000000..7d95f1299 --- /dev/null +++ b/examples/bluetooth/heartrate_game/heartrate_global.py @@ -0,0 +1,6 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import sys + +simulator = sys.platform == "win32" diff --git a/examples/bluetooth/heartrate_game/main.py b/examples/bluetooth/heartrate_game/main.py new file mode 100644 index 000000000..a101a05bf --- /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""" + +import os +from pathlib import Path +import sys +from argparse import ArgumentParser, Namespace, RawDescriptionHelpFormatter + +from PySide6.QtQml import QQmlApplicationEngine, QQmlContext +from PySide6.QtGui import QGuiApplication +from PySide6.QtCore import QCoreApplication, QLoggingCategory, QUrl + +from connectionhandler import ConnectionHandler +from devicefinder import DeviceFinder +from devicehandler import DeviceHandler +from heartrate_global import 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() + 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}) + + qml_file = os.fspath(Path(__file__).resolve().parent / "qml" / "main.qml") + engine.load(QUrl.fromLocalFile(qml_file)) + if not engine.rootObjects(): + sys.exit(-1) + + ex = QCoreApplication.exec() + del engine + sys.exit(ex) diff --git a/examples/bluetooth/heartrate_game/qml/App.qml b/examples/bluetooth/heartrate_game/qml/App.qml new file mode 100644 index 000000000..1eb532021 --- /dev/null +++ b/examples/bluetooth/heartrate_game/qml/App.qml @@ -0,0 +1,83 @@ +// 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/BluetoothAlarmDialog.qml b/examples/bluetooth/heartrate_game/qml/BluetoothAlarmDialog.qml new file mode 100644 index 000000000..0be61e4f8 --- /dev/null +++ b/examples/bluetooth/heartrate_game/qml/BluetoothAlarmDialog.qml @@ -0,0 +1,74 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick + +Item { + id: root + 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: 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/qml/BottomLine.qml b/examples/bluetooth/heartrate_game/qml/BottomLine.qml new file mode 100644 index 000000000..caebc307e --- /dev/null +++ b/examples/bluetooth/heartrate_game/qml/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/qml/Connect.qml b/examples/bluetooth/heartrate_game/qml/Connect.qml new file mode 100644 index 000000000..d9ebbdc51 --- /dev/null +++ b/examples/bluetooth/heartrate_game/qml/Connect.qml @@ -0,0 +1,141 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import Shared + +GamePage { + + errorMessage: deviceFinder.error + infoMessage: deviceFinder.info + + 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 + 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: deviceFinder.devices + clip: true + + delegate: Rectangle { + id: box + height:GameSettings.fieldHeight * 1.2 + width: devices.width + color: index % 2 === 0 ? GameSettings.delegate1Color : GameSettings.delegate2Color + + MouseArea { + anchors.fill: parent + onClicked: { + deviceFinder.connectToService(modelData.deviceAddress); + app.showPage("Measure.qml") + } + } + + Text { + id: device + font.pixelSize: GameSettings.smallFontSize + text: 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: 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: connectionHandler.requiresAddressType // only required on BlueZ + 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 } + }, + State { + name: "random" + PropertyChanges { target: addressTypeText; text: qsTr("Random Address") } + PropertyChanges { target: deviceHandler; addressType: AddressType.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: !deviceFinder.scanning + onClicked: 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/qml/GameButton.qml b/examples/bluetooth/heartrate_game/qml/GameButton.qml new file mode 100644 index 000000000..3ce9d66fd --- /dev/null +++ b/examples/bluetooth/heartrate_game/qml/GameButton.qml @@ -0,0 +1,41 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import "." + +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: checkColor() + onReleased: checkColor() + onClicked: { + checkColor() + button.clicked() + } + } +} diff --git a/examples/bluetooth/heartrate_game/qml/GamePage.qml b/examples/bluetooth/heartrate_game/qml/GamePage.qml new file mode 100644 index 000000000..25a5bb3d1 --- /dev/null +++ b/examples/bluetooth/heartrate_game/qml/GamePage.qml @@ -0,0 +1,46 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import "." + +Item { + anchors.fill: parent + + property string errorMessage: "" + property string infoMessage: "" + property real messageHeight: msg.height + 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 + + 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: hasError ? errorMessage : infoMessage + } + } +} diff --git a/examples/bluetooth/heartrate_game/qml/GameSettings.qml b/examples/bluetooth/heartrate_game/qml/GameSettings.qml new file mode 100644 index 000000000..f265b73c3 --- /dev/null +++ b/examples/bluetooth/heartrate_game/qml/GameSettings.qml @@ -0,0 +1,54 @@ +// 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/qml/Measure.qml b/examples/bluetooth/heartrate_game/qml/Measure.qml new file mode 100644 index 000000000..c434d5114 --- /dev/null +++ b/examples/bluetooth/heartrate_game/qml/Measure.qml @@ -0,0 +1,197 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick + +GamePage { + id: measurePage + + 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) + + function close() + { + deviceHandler.stopMeasurement(); + deviceHandler.disconnectService(); + app.prevPage(); + } + + function start() + { + if (!deviceHandler.measuring) { + __timeCounter = 0; + deviceHandler.startMeasurement() + } + } + + function stop() + { + if (deviceHandler.measuring) { + deviceHandler.stopMeasurement() + } + + app.showPage("Stats.qml") + } + + Timer { + id: measureTimer + interval: 1000 + running: deviceHandler.measuring + repeat: true + onTriggered: { + __timeCounter++; + if (__timeCounter >= __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: !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: deviceHandler.hr + visible: 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: deviceHandler.measuring + + Text { + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + text: 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: 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: 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,__timeCounter / __maxTimeCount) * parent.width + } + + Text { + anchors.centerIn: parent + color: "gray" + text: (__maxTimeCount - __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: !deviceHandler.measuring + radius: GameSettings.buttonRadius + + onClicked: 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/qml/SplashScreen.qml b/examples/bluetooth/heartrate_game/qml/SplashScreen.qml new file mode 100644 index 000000000..23f71f08f --- /dev/null +++ b/examples/bluetooth/heartrate_game/qml/SplashScreen.qml @@ -0,0 +1,43 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import "." + +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() + } + + 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/qml/Stats.qml b/examples/bluetooth/heartrate_game/qml/Stats.qml new file mode 100644 index 000000000..b818e85e4 --- /dev/null +++ b/examples/bluetooth/heartrate_game/qml/Stats.qml @@ -0,0 +1,52 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick + +GamePage { + + 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: (deviceHandler.maxHR - deviceHandler.minHR).toFixed(0) + } + + Item { + height: GameSettings.fieldHeight + width: 1 + } + + StatsLabel { + title: qsTr("MIN") + value: deviceHandler.minHR.toFixed(0) + } + + StatsLabel { + title: qsTr("MAX") + value: deviceHandler.maxHR.toFixed(0) + } + + StatsLabel { + title: qsTr("AVG") + value: deviceHandler.average.toFixed(1) + } + + + StatsLabel { + title: qsTr("CALORIES") + value: deviceHandler.calories.toFixed(3) + } + } +} diff --git a/examples/bluetooth/heartrate_game/qml/StatsLabel.qml b/examples/bluetooth/heartrate_game/qml/StatsLabel.qml new file mode 100644 index 000000000..cd5cda5be --- /dev/null +++ b/examples/bluetooth/heartrate_game/qml/StatsLabel.qml @@ -0,0 +1,35 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import "." + +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/qml/TitleBar.qml b/examples/bluetooth/heartrate_game/qml/TitleBar.qml new file mode 100644 index 000000000..b7de77c4b --- /dev/null +++ b/examples/bluetooth/heartrate_game/qml/TitleBar.qml @@ -0,0 +1,50 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick + +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) + + Repeater { + model: 3 + Text { + width: titleBar.width / 3 + height: titleBar.height + x: index * width + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + text: __titles[index] + font.pixelSize: GameSettings.tinyFontSize + color: titleBar.currentIndex === index ? GameSettings.textColor : GameSettings.disabledTextColor + + MouseArea { + anchors.fill: parent + onClicked: titleClicked(index) + } + } + } + + + Item { + anchors.bottom: parent.bottom + width: parent.width / 3 + height: parent.height + x: currentIndex * width + + BottomLine{} + + 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/qml/images/bt_off_to_on.png Binary files differnew file mode 100644 index 000000000..5ea1f3f06 --- /dev/null +++ b/examples/bluetooth/heartrate_game/qml/images/bt_off_to_on.png diff --git a/examples/bluetooth/heartrate_game/qml/images/heart.png b/examples/bluetooth/heartrate_game/qml/images/heart.png Binary files differnew file mode 100644 index 000000000..f2b3c0a3e --- /dev/null +++ b/examples/bluetooth/heartrate_game/qml/images/heart.png diff --git a/examples/bluetooth/heartrate_game/qml/images/logo.png b/examples/bluetooth/heartrate_game/qml/images/logo.png Binary files differnew file mode 100644 index 000000000..ea0af7e00 --- /dev/null +++ b/examples/bluetooth/heartrate_game/qml/images/logo.png diff --git a/examples/bluetooth/heartrate_game/qml/main.qml b/examples/bluetooth/heartrate_game/qml/main.qml new file mode 100644 index 000000000..44d824faf --- /dev/null +++ b/examples/bluetooth/heartrate_game/qml/main.qml @@ -0,0 +1,63 @@ +// 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 new file mode 100644 index 000000000..5e0d2b540 --- /dev/null +++ b/examples/bluetooth/heartrate_game/qml/qmldir @@ -0,0 +1 @@ +singleton GameSettings 1.0 GameSettings.qml 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..f98cc6fe8 --- /dev/null +++ b/examples/bluetooth/heartrate_server/heartrate_server.py @@ -0,0 +1,94 @@ +# 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"] +} |