diff options
author | Øystein Heskestad <oystein.heskestad@qt.io> | 2023-09-27 15:14:38 +0200 |
---|---|---|
committer | Øystein Heskestad <oystein.heskestad@qt.io> | 2023-10-11 18:14:32 +0200 |
commit | fdc8f50466f0c8d04e954123a405047435e83517 (patch) | |
tree | 68e30097e3061cc656cbdf45f1f47b90aa4d27b1 /tests | |
parent | c2ba4a68bb50d274ec984e4473709e05722e72bd (diff) |
Move bluetooth examples to tests/manual/examples
These examples are more related to bluetooth than remote objects,
and are out or date because they don't use the permission API.
Task-number: QTBUG-112850
Pick-to: 6.6 6.5
Change-Id: Ic3059a8cf7a03583cc525db7c8021dd5b607115a
Reviewed-by: Brett Stottlemyer <bstottle@ford.com>
Diffstat (limited to 'tests')
25 files changed, 1248 insertions, 0 deletions
diff --git a/tests/manual/examples/CMakeLists.txt b/tests/manual/examples/CMakeLists.txt index 011d163..1418055 100644 --- a/tests/manual/examples/CMakeLists.txt +++ b/tests/manual/examples/CMakeLists.txt @@ -5,3 +5,6 @@ if(TARGET Qt::Quick AND UNIX AND NOT ANDROID) add_subdirectory(qmlmodelviewclient) endif() add_subdirectory(simpleswitch) +if(TARGET Qt::Bluetooth AND TARGET Qt::Widgets) + add_subdirectory(ble) +endif() diff --git a/tests/manual/examples/ble/CMakeLists.txt b/tests/manual/examples/ble/CMakeLists.txt new file mode 100644 index 0000000..52adc96 --- /dev/null +++ b/tests/manual/examples/ble/CMakeLists.txt @@ -0,0 +1,5 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +qt_internal_add_example(bleclient) +qt_internal_add_example(bleserver) diff --git a/tests/manual/examples/ble/ble.pro b/tests/manual/examples/ble/ble.pro new file mode 100644 index 0000000..341681f --- /dev/null +++ b/tests/manual/examples/ble/ble.pro @@ -0,0 +1,5 @@ +TEMPLATE = subdirs + +SUBDIRS += \ + bleclient \ + bleserver diff --git a/tests/manual/examples/ble/bleclient/CMakeLists.txt b/tests/manual/examples/ble/bleclient/CMakeLists.txt new file mode 100644 index 0000000..adef48b --- /dev/null +++ b/tests/manual/examples/ble/bleclient/CMakeLists.txt @@ -0,0 +1,65 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +cmake_minimum_required(VERSION 3.16) +project(bleclient LANGUAGES CXX) + +set(CMAKE_INCLUDE_CURRENT_DIR ON) +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) + +if(NOT DEFINED INSTALL_EXAMPLESDIR) + set(INSTALL_EXAMPLESDIR "examples") +endif() + +set(INSTALL_EXAMPLEDIR "${INSTALL_EXAMPLESDIR}/remoteobjects/ble/bleclient") + +find_package(Qt6 REQUIRED COMPONENTS Core Widgets Bluetooth RemoteObjects) + +qt_add_executable(bleclient + ../common/bleiodevice.cpp ../common/bleiodevice.h + connectpage.cpp connectpage.h connectpage.ui + mainwindow.cpp mainwindow.h + heaterview.cpp heaterview.h heaterview.ui + main.cpp +) + +set_target_properties(bleclient PROPERTIES + WIN32_EXECUTABLE TRUE + MACOSX_BUNDLE TRUE +) + +target_include_directories(bleclient PUBLIC + ../common +) + +target_link_libraries(bleclient PUBLIC + Qt::Core + Qt::Widgets + Qt::Bluetooth + Qt::RemoteObjects +) + +qt6_add_repc_replicas(bleclient + ../common/heater.rep +) + +if (APPLE) + # Using absolute path for shared plist files is a Ninja bug workaround + get_filename_component(SHARED_PLIST_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../common ABSOLUTE) + if (IOS) + set_target_properties(bleclient PROPERTIES + MACOSX_BUNDLE_INFO_PLIST "${SHARED_PLIST_DIR}/Info.ios.plist" + ) + else() + set_target_properties(bleclient PROPERTIES + MACOSX_BUNDLE_INFO_PLIST "${SHARED_PLIST_DIR}/Info.cmake.macos.plist" + ) + endif() +endif() + +install(TARGETS bleclient + RUNTIME DESTINATION "${INSTALL_EXAMPLEDIR}" + BUNDLE DESTINATION "${INSTALL_EXAMPLEDIR}" + LIBRARY DESTINATION "${INSTALL_EXAMPLEDIR}" +) diff --git a/tests/manual/examples/ble/bleclient/bleclient.pro b/tests/manual/examples/ble/bleclient/bleclient.pro new file mode 100644 index 0000000..327ec20 --- /dev/null +++ b/tests/manual/examples/ble/bleclient/bleclient.pro @@ -0,0 +1,22 @@ +QT += widgets + +SOURCES += main.cpp \ + connectpage.cpp \ + mainwindow.cpp \ + heaterview.cpp + +HEADERS += \ + connectpage.h \ + mainwindow.h \ + heaterview.h + +FORMS += \ + connectpage.ui \ + heaterview.ui + +include(../common/common.pri) + +REPC_REPLICA = ../common/heater.rep + +target.path = $$[QT_INSTALL_EXAMPLES]/remoteobjects/ble/bleclient +INSTALLS += target diff --git a/tests/manual/examples/ble/bleclient/connectpage.cpp b/tests/manual/examples/ble/bleclient/connectpage.cpp new file mode 100644 index 0000000..78ea1b7 --- /dev/null +++ b/tests/manual/examples/ble/bleclient/connectpage.cpp @@ -0,0 +1,215 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// Copyright (C) 2019 Ford Motor Company +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#include "connectpage.h" +#include "ui_connectpage.h" +#include "bleiodevice.h" + +#include <QtBluetooth/QBluetoothDeviceDiscoveryAgent> +#include <QtBluetooth/QLowEnergyController> +#include <QtBluetooth/QLowEnergyService> + +using namespace Qt::Literals::StringLiterals; + +ConnectPage::ConnectPage(QWidget *parent) + : QWidget(parent) + , ui(new Ui::ConnectPage) + , m_discoveryAgent(new QBluetoothDeviceDiscoveryAgent(this)) +{ + ui->setupUi(this); + m_discoveryAgent->setLowEnergyDiscoveryTimeout(20000); + connect(m_discoveryAgent, &QBluetoothDeviceDiscoveryAgent::errorOccurred, this, [this]() { + emit showMessage(m_discoveryAgent->errorString()); + ui->scanButton->setEnabled(true); + ui->connectButton->setEnabled(false); + }); + connect(m_discoveryAgent, &QBluetoothDeviceDiscoveryAgent::deviceDiscovered, + this, &ConnectPage::refreshDevices); + connect(m_discoveryAgent, &QBluetoothDeviceDiscoveryAgent::finished, this, [this]() { + ui->scanButton->setEnabled(true); + emit showMessage("Scan finished"_L1); + refreshDevices(); + }); + connect(ui->devicesListWidget, &QListWidget::clicked, this, [this](const QModelIndex &index) { + ui->connectButton->setEnabled(index.isValid() && index.row() >= 0 + && index.row() < m_leDevices.size()); + showMessage("Scan stopped"); + ui->scanButton->setEnabled(true); + m_discoveryAgent->stop(); + }); + connect(ui->connectButton, &QPushButton::clicked, this, [this]() { + if (ui->devicesListWidget->currentRow() < 0 + || ui->devicesListWidget->currentRow() >= m_leDevices.size()) { + return; // Should never happen, but better safe than sorry + } + auto device = m_leDevices[ui->devicesListWidget->currentRow()]; + qDebug() << "Connecting to:" << device.name() << device.address() << device.deviceUuid(); + connectToDevice(device); + }); + + connect(ui->scanButton, &QPushButton::clicked, this, &ConnectPage::startScanning); +} + +ConnectPage::~ConnectPage() +{ + if (m_controller) + m_controller->disconnect(); // Avoid possible stray signals + if (m_discoveryAgent && m_discoveryAgent->isActive()) + m_discoveryAgent->stop(); + delete ui; +} + +void ConnectPage::startScanning() +{ + ui->scanButton->setEnabled(false); + ui->connectButton->setEnabled(false); + emit showMessage("Scanning for BLE devices, please wait"_L1); + m_discoveryAgent->start(QBluetoothDeviceDiscoveryAgent::LowEnergyMethod); + ui->devicesListWidget->setEnabled(true); + ui->devicesListWidget->clear(); + m_leDevices.clear(); +} + +void ConnectPage::disconnectFromDevice() +{ + if (m_controller) + m_controller->disconnectFromDevice(); +} + +void ConnectPage::refreshDevices() +{ + ui->devicesListWidget->clear(); + m_leDevices.clear(); + // The LE device list is used to filter out classic bluetooth devices which the device + // discovery method may find (even if using low energy device discovery method). This list + // helps in matching the right device listwidget index the user has selected + for (const auto &device : m_discoveryAgent->discoveredDevices()) { + if (device.coreConfigurations() & QBluetoothDeviceInfo::LowEnergyCoreConfiguration) { + m_leDevices.append(device); +#ifdef Q_OS_DARWIN + // On macOS/iOS we don't have address available + ui->devicesListWidget->addItem(device.name() + " " + device.deviceUuid().toString()); +#else + ui->devicesListWidget->addItem(device.name() + " " + device.address().toString()); +#endif + } + } +} + +void ConnectPage::connectToDevice(const QBluetoothDeviceInfo &device) +{ + ui->scanButton->setEnabled(false); + ui->connectButton->setEnabled(false); + ui->devicesListWidget->setEnabled(false); + + QString deviceIdentifier; + if (!device.name().isEmpty()) + deviceIdentifier = device.name(); + else if (!device.address().isNull()) + deviceIdentifier = device.address().toString(); + else + deviceIdentifier = device.deviceUuid().toString(QUuid::WithoutBraces); + emit showMessage("Connecting to %1"_L1.arg(deviceIdentifier)); + + // If this is not the first time, release the previous low energy controller + if (m_controller) { + m_controller->disconnectFromDevice(); + delete m_controller; + m_controller = nullptr; + } + + m_controller = QLowEnergyController::createCentral(device, this); + connect(m_controller, &QLowEnergyController::connected, this, [this]() { + m_controller->discoverServices(); + }); + connect(m_controller, &QLowEnergyController::errorOccurred, this, [this]() { + ui->devicesListWidget->setEnabled(true); + ui->scanButton->setEnabled(!m_discoveryAgent->isActive()); + m_service = nullptr; + emit showMessage("Error occurred: "_L1 + m_controller->errorString()); + }); + + connect(m_controller, &QLowEnergyController::disconnected, this, [this]() { + m_service = nullptr; + ui->devicesListWidget->setEnabled(true); + emit showMessage("Diconnected from remote"_L1); + emit disconnected(); + }); + + connect(m_controller, &QLowEnergyController::serviceDiscovered, + this, [this](const QBluetoothUuid &newService) { + if (newService != BLEIoDevice::SERVICE_UUID) + return; + emit showMessage("Service found"_L1); + m_service = m_controller->createServiceObject(newService, m_controller); + if (!m_service) { + qWarning("BT LE Service couldn't be created"); + return; + } + if (m_service->state() == QLowEnergyService::RemoteService) { + emit showMessage("Service found, scanning for details"_L1); + connect(m_service, &QLowEnergyService::stateChanged, + this, [this](QLowEnergyService::ServiceState state) { + + if (state == QLowEnergyService::ServiceState::RemoteServiceDiscovered) + connectNode(); + }); + m_service->discoverDetails(); + } else { + connectNode(); + } + }); + + connect(m_controller, &QLowEnergyController::discoveryFinished, this, [this]() { + if (m_service) + return; + emit showMessage("Service was not found"); + ui->scanButton->setEnabled(true); + ui->devicesListWidget->setEnabled(true); + }); + + m_controller->setRemoteAddressType(QLowEnergyController::PublicAddress); + m_controller->connectToDevice(); +} + +void ConnectPage::connectNode() +{ + if (!m_service) + return; + + // Below we subscribe to characteristic value change notifications. + // If subscription fails, disconnect as there's no point in continuing + QObject::connect(m_service.data(), &QLowEnergyService::descriptorWritten, this, + [this](const QLowEnergyDescriptor &info, + const QByteArray &value) { + + if (info.uuid() != QBluetoothUuid::DescriptorType::ClientCharacteristicConfiguration) + return; + + if (value != QLowEnergyCharacteristic::CCCDEnableNotification) { + // We failed to subscribe to characteristic change notifications + m_controller->disconnectFromDevice(); + } else { + emit showMessage("Connected to QtRO node"_L1); + auto ioDevice = new BLEIoDevice(m_service, + BLEIoDevice::CLIENT_RX_SERVER_TX_CHAR_UUID, + BLEIoDevice::CLIENT_TX_SERVER_RX_CHAR_UUID, + this); + connect(this, &ConnectPage::disconnected, ioDevice, &BLEIoDevice::disconnected); + connect(ioDevice, &BLEIoDevice::disconnected, ioDevice, &BLEIoDevice::deleteLater); + emit connected(ioDevice); + } + }); + + QObject::connect(m_service, &QLowEnergyService::errorOccurred, [this] + (QLowEnergyService::ServiceError error) { + if (error == QLowEnergyService::ServiceError::DescriptorWriteError) + m_controller->disconnectFromDevice(); + }); + + m_service->writeDescriptor( + m_service->characteristic(BLEIoDevice::CLIENT_RX_SERVER_TX_CHAR_UUID).descriptor( + QBluetoothUuid::DescriptorType::ClientCharacteristicConfiguration), + QLowEnergyCharacteristic::CCCDEnableNotification); +} diff --git a/tests/manual/examples/ble/bleclient/connectpage.h b/tests/manual/examples/ble/bleclient/connectpage.h new file mode 100644 index 0000000..c189dd8 --- /dev/null +++ b/tests/manual/examples/ble/bleclient/connectpage.h @@ -0,0 +1,55 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// Copyright (C) 2019 Ford Motor Company +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#ifndef CONNECTPAGE_H +#define CONNECTPAGE_H + +#include <QtBluetooth/QBluetoothDeviceInfo> +#include <QtWidgets/QWidget> +#include <QtWidgets/QListWidget> +#include <QtCore/QStringListModel> +#include <QtCore/QPointer> + +class BLEIoDevice; + +QT_BEGIN_NAMESPACE +class QBluetoothDeviceDiscoveryAgent; +class QLowEnergyController; +class QLowEnergyService; + +namespace Ui { +class ConnectPage; +} +QT_END_NAMESPACE + +class ConnectPage : public QWidget +{ + Q_OBJECT +public: + explicit ConnectPage(QWidget *parent = nullptr); + ~ConnectPage() override; + void startScanning(); + void disconnectFromDevice(); + +signals: + void showMessage(const QString &message); + void connected(BLEIoDevice *ioDevice); + void disconnected(); + +private: + void connectToDevice(const QBluetoothDeviceInfo &device); + void connectNode(); + +private slots: + void refreshDevices(); + +private: + Ui::ConnectPage *ui; + QBluetoothDeviceDiscoveryAgent *m_discoveryAgent = nullptr; + QLowEnergyController *m_controller = nullptr; + QPointer<QLowEnergyService> m_service; + QList<QBluetoothDeviceInfo> m_leDevices; +}; + +#endif // CONNECTPAGE_H diff --git a/tests/manual/examples/ble/bleclient/connectpage.ui b/tests/manual/examples/ble/bleclient/connectpage.ui new file mode 100644 index 0000000..1b4de58 --- /dev/null +++ b/tests/manual/examples/ble/bleclient/connectpage.ui @@ -0,0 +1,59 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>ConnectPage</class> + <widget class="QWidget" name="ConnectPage"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>400</width> + <height>300</height> + </rect> + </property> + <property name="windowTitle"> + <string>Form</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <widget class="QPushButton" name="scanButton"> + <property name="text"> + <string>Scan</string> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QPushButton" name="connectButton"> + <property name="text"> + <string>Connect</string> + </property> + </widget> + </item> + </layout> + </item> + <item> + <widget class="QListWidget" name="devicesListWidget"> + <property name="selectionBehavior"> + <enum>QAbstractItemView::SelectRows</enum> + </property> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections/> +</ui> diff --git a/tests/manual/examples/ble/bleclient/heaterview.cpp b/tests/manual/examples/ble/bleclient/heaterview.cpp new file mode 100644 index 0000000..c6991fe --- /dev/null +++ b/tests/manual/examples/ble/bleclient/heaterview.cpp @@ -0,0 +1,53 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// Copyright (C) 2019 Ford Motor Company +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#include "rep_heater_replica.h" + +#include "heaterview.h" +#include "ui_heaterview.h" +#include "bleiodevice.h" + +using namespace Qt::Literals::StringLiterals; + +HeaterView::HeaterView(BLEIoDevice *ioDevice, QWidget *parent) : + QWidget(parent), + ui(new Ui::HeaterView) +{ + ui->setupUi(this); + connect(ui->closeButton, &QPushButton::clicked, this, &HeaterView::closeMe); + + // Signal the server that we are ready to start + ioDevice->write(BLEIoDevice::CLIENT_READY_SIGNAL); + + m_node.addClientSideConnection(ioDevice); + m_heater.reset(m_node.acquire<HeaterReplica>()); + + const auto updateHeaterPowerUi = [this](){ + ui->toggleHeater->setText(m_heater->heaterPoweredOn() + ? "ON (switch OFF)"_L1 + : "OFF (switch ON)"_L1); + }; + + const auto updateTemperatureUi = [this](){ + ui->temperatureDigits->display(m_heater->currentTemperature()); + }; + + // Initial updates and connect UI updates to changes in values + updateHeaterPowerUi(); + updateTemperatureUi(); + QObject::connect(m_heater.get(), &HeaterReplica::heaterPoweredOnChanged, + this, updateHeaterPowerUi); + QObject::connect(m_heater.get(), &HeaterReplica::currentTemperatureChanged, + this, updateTemperatureUi); + + // When user toggles heater ON/OFF, push the value to server/source + QObject::connect(ui->toggleHeater, &QPushButton::clicked, this, [this]() { + m_heater->pushHeaterPoweredOn(!m_heater->heaterPoweredOn()); + }); +} + +HeaterView::~HeaterView() +{ + delete ui; +} diff --git a/tests/manual/examples/ble/bleclient/heaterview.h b/tests/manual/examples/ble/bleclient/heaterview.h new file mode 100644 index 0000000..b67c249 --- /dev/null +++ b/tests/manual/examples/ble/bleclient/heaterview.h @@ -0,0 +1,40 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// Copyright (C) 2019 Ford Motor Company +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#ifndef HEATERVIEW_H +#define HEATERVIEW_H + +#include <QtRemoteObjects/QRemoteObjectNode> +#include <QtWidgets/QWidget> + +#include <memory> + +class BLEIoDevice; +class HeaterReplica; + +QT_BEGIN_NAMESPACE +namespace Ui { +class HeaterView; +} +QT_END_NAMESPACE + +class HeaterView : public QWidget +{ + Q_OBJECT + +public: + explicit HeaterView(BLEIoDevice *ioDevice, QWidget *parent = nullptr); + ~HeaterView() override; + +signals: + void closeMe(); + void showMessage(const QString &message); + +private: + Ui::HeaterView *ui; + QRemoteObjectNode m_node; + std::unique_ptr<HeaterReplica> m_heater; +}; + +#endif diff --git a/tests/manual/examples/ble/bleclient/heaterview.ui b/tests/manual/examples/ble/bleclient/heaterview.ui new file mode 100644 index 0000000..875043a --- /dev/null +++ b/tests/manual/examples/ble/bleclient/heaterview.ui @@ -0,0 +1,56 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>HeaterView</class> + <widget class="QWidget" name="HeaterView"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>400</width> + <height>300</height> + </rect> + </property> + <property name="windowTitle"> + <string>Form</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <layout class="QGridLayout" name="gridLayout"> + <item row="1" column="1"> + <widget class="QPushButton" name="toggleHeater"> + <property name="text"> + <string>Toggle heater</string> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="heaterStatusLabel"> + <property name="text"> + <string>Heater status</string> + </property> + </widget> + </item> + <item row="2" column="0"> + <widget class="QLabel" name="temperatureLabel"> + <property name="text"> + <string>Temperature</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QPushButton" name="closeButton"> + <property name="text"> + <string>Close connection</string> + </property> + </widget> + </item> + <item row="2" column="1"> + <widget class="QLCDNumber" name="temperatureDigits"/> + </item> + </layout> + </item> + </layout> + </widget> + <resources/> + <connections/> +</ui> diff --git a/tests/manual/examples/ble/bleclient/main.cpp b/tests/manual/examples/ble/bleclient/main.cpp new file mode 100644 index 0000000..bbc2d56 --- /dev/null +++ b/tests/manual/examples/ble/bleclient/main.cpp @@ -0,0 +1,31 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// Copyright (C) 2019 Ford Motor Company +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#include "mainwindow.h" + +#include <QtRemoteObjects/QRemoteObjectNode> +#include <QtWidgets/QApplication> +#include <QtCore/QCommandLineOption> +#include <QtCore/QCommandLineParser> + +using namespace Qt::Literals::StringLiterals; + +int main(int argc, char **argv) +{ + QApplication app(argc, argv); + + QCommandLineParser parser; + parser.addHelpOption(); + QCommandLineOption verboseOption("verbose", "Verbose mode"); + parser.addOption(verboseOption); + parser.process(app); + if (parser.isSet(verboseOption)) { + QLoggingCategory::setFilterRules("qt.remoteobjects* = true"_L1); + QLoggingCategory::setFilterRules("qt.bluetooth* = true"_L1); + } + + MainWindow w; + w.show(); + return app.exec(); +} diff --git a/tests/manual/examples/ble/bleclient/mainwindow.cpp b/tests/manual/examples/ble/bleclient/mainwindow.cpp new file mode 100644 index 0000000..f0af85e --- /dev/null +++ b/tests/manual/examples/ble/bleclient/mainwindow.cpp @@ -0,0 +1,52 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// Copyright (C) 2019 Ford Motor Company +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#include "mainwindow.h" +#include "bleiodevice.h" + +#include <QtWidgets/QStatusBar> + +MainWindow::MainWindow(QWidget *parent) + : QMainWindow(parent) + , m_stack(new QStackedWidget(this)) + , m_connectPage(new ConnectPage()) +{ + setStatusBar(new QStatusBar); + setCentralWidget(m_stack); + m_stack->addWidget(m_connectPage); + + QObject::connect(m_connectPage, &ConnectPage::showMessage, this, &MainWindow::showMessage); + QObject::connect(m_connectPage, &ConnectPage::connected, this, [this](BLEIoDevice *ioDevice) { + if (m_heaterView) { + m_stack->removeWidget(m_heaterView); + delete m_heaterView; + } + m_heaterView = new HeaterView(ioDevice, this); + QObject::connect(m_heaterView.data(), &HeaterView::showMessage, + this, &MainWindow::showMessage); + QObject::connect(m_heaterView.data(), &HeaterView::closeMe, + m_connectPage, &ConnectPage::disconnectFromDevice); + QObject::connect(m_heaterView.data(), &HeaterView::closeMe, + this, &MainWindow::showConnectPage); + m_stack->addWidget(m_heaterView); + m_stack->setCurrentWidget(m_heaterView); + }); + QObject::connect(m_connectPage, &ConnectPage::disconnected, + this, &MainWindow::showConnectPage); + m_connectPage->startScanning(); +} + +void MainWindow::showMessage(const QString &message) +{ + statusBar()->showMessage(message); +} + +void MainWindow::showConnectPage() +{ + if (m_heaterView) { + m_stack->removeWidget(m_heaterView); + delete m_heaterView; + } + m_connectPage->startScanning(); +} diff --git a/tests/manual/examples/ble/bleclient/mainwindow.h b/tests/manual/examples/ble/bleclient/mainwindow.h new file mode 100644 index 0000000..39e22c2 --- /dev/null +++ b/tests/manual/examples/ble/bleclient/mainwindow.h @@ -0,0 +1,34 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// Copyright (C) 2019 Ford Motor Company +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#ifndef MAINWINDOW_H +#define MAINWINDOW_H + +#include "connectpage.h" +#include "heaterview.h" + +#include <QtWidgets/QMainWindow> +#include <QtWidgets/QStackedWidget> + +class MainWindow : public QMainWindow +{ + Q_OBJECT +public: + explicit MainWindow(QWidget *parent = nullptr); + +signals: + +public slots: + void showMessage(const QString &message); + +private: + void showConnectPage(); + +private: + QStackedWidget *m_stack; + ConnectPage *m_connectPage; + QPointer<HeaterView> m_heaterView; +}; + +#endif // MAINWINDOW_H diff --git a/tests/manual/examples/ble/bleserver/CMakeLists.txt b/tests/manual/examples/ble/bleserver/CMakeLists.txt new file mode 100644 index 0000000..c5c676a --- /dev/null +++ b/tests/manual/examples/ble/bleserver/CMakeLists.txt @@ -0,0 +1,61 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +cmake_minimum_required(VERSION 3.16) +project(bleserver LANGUAGES CXX) + +set(CMAKE_INCLUDE_CURRENT_DIR ON) +set(CMAKE_AUTOMOC ON) + +if(NOT DEFINED INSTALL_EXAMPLESDIR) + set(INSTALL_EXAMPLESDIR "examples") +endif() + +set(INSTALL_EXAMPLEDIR "${INSTALL_EXAMPLESDIR}/remoteobjects/ble/bleserver") + +find_package(Qt6 REQUIRED COMPONENTS Core Gui Bluetooth RemoteObjects) + +qt_add_executable(bleserver + ../common/bleiodevice.cpp ../common/bleiodevice.h + main.cpp +) + +set_target_properties(bleserver PROPERTIES + WIN32_EXECUTABLE TRUE + MACOSX_BUNDLE TRUE +) + +target_include_directories(bleserver PUBLIC + ../common +) + +target_link_libraries(bleserver PUBLIC + Qt::Core + Qt::Gui + Qt::Bluetooth + Qt::RemoteObjects +) + +qt6_add_repc_sources(bleserver + ../common/heater.rep +) + +if (APPLE) + # Using absolute path for shared plist files is a Ninja bug workaround + get_filename_component(SHARED_PLIST_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../common ABSOLUTE) + if (IOS) + set_target_properties(bleserver PROPERTIES + MACOSX_BUNDLE_INFO_PLIST "${SHARED_PLIST_DIR}/Info.ios.plist" + ) + else() + set_target_properties(bleserver PROPERTIES + MACOSX_BUNDLE_INFO_PLIST "${SHARED_PLIST_DIR}/Info.cmake.macos.plist" + ) + endif() +endif() + +install(TARGETS bleserver + RUNTIME DESTINATION "${INSTALL_EXAMPLEDIR}" + BUNDLE DESTINATION "${INSTALL_EXAMPLEDIR}" + LIBRARY DESTINATION "${INSTALL_EXAMPLEDIR}" +) diff --git a/tests/manual/examples/ble/bleserver/bleserver.pro b/tests/manual/examples/ble/bleserver/bleserver.pro new file mode 100644 index 0000000..9b35365 --- /dev/null +++ b/tests/manual/examples/ble/bleserver/bleserver.pro @@ -0,0 +1,8 @@ +SOURCES += main.cpp + +include(../common/common.pri) + +REPC_SOURCE = ../common/heater.rep + +target.path = $$[QT_INSTALL_EXAMPLES]/remoteobjects/ble/bleserver +INSTALLS += target diff --git a/tests/manual/examples/ble/bleserver/main.cpp b/tests/manual/examples/ble/bleserver/main.cpp new file mode 100644 index 0000000..0e0904b --- /dev/null +++ b/tests/manual/examples/ble/bleserver/main.cpp @@ -0,0 +1,180 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// Copyright (C) 2019 Ford Motor Company +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#include "rep_heater_source.h" +#include "bleiodevice.h" + +#include <QtRemoteObjects/QRemoteObjectNode> +#include <QtBluetooth/QLowEnergyCharacteristicData> +#include <QtBluetooth/QLowEnergyDescriptorData> +#include <QtBluetooth/QLowEnergyServiceData> +#include <QtBluetooth/QLowEnergyAdvertisingData> +#include <QtBluetooth/QLowEnergyController> +#include <QtBluetooth/QLowEnergyAdvertisingParameters> +#include <QtCore/QCoreApplication> +#include <QtCore/QTimer> +#include <QtCore/QLoggingCategory> +#include <QtCore/QCommandLineOption> +#include <QtCore/QCommandLineParser> + +using namespace Qt::Literals::StringLiterals; + +class Heater : public HeaterSimpleSource +{ + Q_OBJECT +public: + explicit Heater(QObject* parent = nullptr) : HeaterSimpleSource(parent) + { + m_changeTimer.setInterval(std::chrono::seconds{2}); + m_changeTimer.setSingleShot(false); + + QObject::connect(&m_changeTimer, &QTimer::timeout, this, [this]() { + if (heaterPoweredOn()) + setCurrentTemperature(currentTemperature() + 1); + else + setCurrentTemperature(currentTemperature() - 1); + }); + + m_changeTimer.start(); + } + +private: + QTimer m_changeTimer; +}; + +int main(int argc, char *argv[]) +{ + QCoreApplication app(argc, argv); + + QCommandLineParser parser; + parser.addHelpOption(); + QCommandLineOption verboseOption("verbose", "Verbose mode"); + parser.addOption(verboseOption); + parser.process(app); + if (parser.isSet(verboseOption)) { + QLoggingCategory::setFilterRules("qt.remoteobjects* = true"_L1); + QLoggingCategory::setFilterRules("qt.bluetooth* = true"_L1); + } + + // The model that will be used as the source object + std::unique_ptr<Heater> heater = std::make_unique<Heater>(); + std::unique_ptr<QLowEnergyController> bleController; + std::unique_ptr<QRemoteObjectHost> hostNode; + std::unique_ptr<BLEIoDevice> ioDevice; + + // Setup BLE server. The two-way communication consists of a GATT service + // which has two characteristics. The characteristics form the two-way communication + // between client/server. The characteristic notifications are enabled so that each + // side (client/server) gets notified if the other has written data. + QLowEnergyCharacteristicData rxCharData; + rxCharData.setUuid(BLEIoDevice::CLIENT_TX_SERVER_RX_CHAR_UUID); + // Allow the remote end (client) to write to this characteristic + rxCharData.setProperties(QLowEnergyCharacteristic::Write); + + QLowEnergyCharacteristicData txCharData; + txCharData.setUuid(BLEIoDevice::CLIENT_RX_SERVER_TX_CHAR_UUID); + // Allow the remote end (client) to read characteristic's data and subscribe to value changes + const QLowEnergyDescriptorData clientConfig( + QBluetoothUuid::DescriptorType::ClientCharacteristicConfiguration, + QLowEnergyCharacteristic::CCCDDisable); + txCharData.addDescriptor(clientConfig); + txCharData.setProperties(QLowEnergyCharacteristic::Read | QLowEnergyCharacteristic::Notify); + + // Set up the service, its characteristics, and advertisement data + QLowEnergyServiceData sppServiceData; + sppServiceData.setType(QLowEnergyServiceData::ServiceTypePrimary); + sppServiceData.setUuid(BLEIoDevice::SERVICE_UUID); + sppServiceData.addCharacteristic(txCharData); + sppServiceData.addCharacteristic(rxCharData); + + QLowEnergyAdvertisingData sppAdvertisingData; + sppAdvertisingData.setDiscoverability(QLowEnergyAdvertisingData::DiscoverabilityGeneral); + sppAdvertisingData.setIncludePowerLevel(true); + // The device name that is broadcasted in the advertisement. Note that this is not supported + // on Android where the device's name is used instead + sppAdvertisingData.setLocalName("QtRO peripheral"_L1); + + bleController.reset(QLowEnergyController::createPeripheral()); + + // Add the service to the BT LE controller and start advertising so that clients + // can discover this server. When the client disconnects the advertisement is + // resumed so that next clients can find the server + std::unique_ptr<QLowEnergyService> sppService; + auto startAdvertising = [&bleController, sppAdvertisingData, sppServiceData, &sppService] + { + sppService.reset(bleController->addService(sppServiceData)); + if (sppService) { + bleController->startAdvertising(QLowEnergyAdvertisingParameters(), + sppAdvertisingData, sppAdvertisingData); + } + }; + + QObject::connect(bleController.get(), &QLowEnergyController::disconnected, [&]() { + // Upon disconnection the underlying BLE service used by the ioDevice becomes + // obsolete. Free up the resources, resume advertising and wait for a new client + hostNode.reset(nullptr); + ioDevice.reset(nullptr); + startAdvertising(); + }); + + auto errorHandler = [&](QLowEnergyController::Error errorCode) + { + // Exit server in cases we won't try to recover + qWarning().noquote().nospace() << errorCode << " error: " << bleController->errorString(); + if (errorCode != QLowEnergyController::RemoteHostClosedError) { + qWarning("BLE server quitting due to the error."); + QCoreApplication::quit(); + } + }; + // Use queued connection in case the advertising fails before we even enter + // the event loop / exec(), in which case the quit() does nothing + QObject::connect(bleController.get(), &QLowEnergyController::errorOccurred, + &app, errorHandler, Qt::QueuedConnection); + + // Start initial advertising + startAdvertising(); + + // Now we are advertising, next: + // 1) Wait for a client to discover the service and get connected + // 2) Wait for client to indicate it is fully ready + // 3) Create RO sourcenode and BLE IO device + QMetaObject::Connection readConnection; + QObject::connect(bleController.get(), &QLowEnergyController::connected, [&]() { + Q_ASSERT(!hostNode); + Q_ASSERT(!ioDevice); + readConnection = QObject::connect(sppService.get(), + &QLowEnergyService::characteristicChanged, + bleController.get(), + [&](const QLowEnergyCharacteristic &info, + const QByteArray &value) { + // Wait for the client to signal it is ready + if (info.uuid() != BLEIoDevice::CLIENT_TX_SERVER_RX_CHAR_UUID + || value != BLEIoDevice::CLIENT_READY_SIGNAL) { + return; + } + + QObject::disconnect(readConnection); + readConnection = {}; + + // We have a client that is connected and has indicated it is ready; + // Create the hostnode and the associated BLE IO device and enable + // remote access + hostNode = std::make_unique<QRemoteObjectHost>(); + hostNode->setHostUrl(u"ble://qt_ro_ble"_s, + QRemoteObjectHost::AllowExternalRegistration); + hostNode->enableRemoting(heater.get()); + + ioDevice = std::make_unique<BLEIoDevice>(sppService.get(), + BLEIoDevice::CLIENT_TX_SERVER_RX_CHAR_UUID, + BLEIoDevice::CLIENT_RX_SERVER_TX_CHAR_UUID); + QObject::connect(ioDevice.get(), &BLEIoDevice::closing, bleController.get(), + &QLowEnergyController::disconnectFromDevice); + hostNode->addHostSideConnection(ioDevice.get()); + }); + }); + + return QCoreApplication::exec(); +} + +#include "main.moc" diff --git a/tests/manual/examples/ble/common/Info.cmake.macos.plist b/tests/manual/examples/ble/common/Info.cmake.macos.plist new file mode 100644 index 0000000..114f926 --- /dev/null +++ b/tests/manual/examples/ble/common/Info.cmake.macos.plist @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>CFBundleExecutable</key> + <string>${MACOSX_BUNDLE_EXECUTABLE_NAME}</string> + <key>CFBundleIconFile</key> + <string>${MACOSX_BUNDLE_ICON_FILE}</string> + <key>CFBundleIdentifier</key> + <string>${MACOSX_BUNDLE_GUI_IDENTIFIER}</string> + <key>CFBundlePackageType</key> + <string>APPL</string> + <key>CFBundleSignature</key> + <string>????</string> + <key>LSMinimumSystemVersion</key> + <string>${CMAKE_OSX_DEPLOYMENT_TARGET}</string> + <key>NSPrincipalClass</key> + <string>NSApplication</string> + <key>NSBluetoothAlwaysUsageDescription</key> + <string>Qt RO Example wants to access your Bluetooth adapter</string> + <key>NSSupportsAutomaticGraphicsSwitching</key> + <true/> +</dict> +</plist> diff --git a/tests/manual/examples/ble/common/Info.ios.plist b/tests/manual/examples/ble/common/Info.ios.plist new file mode 100644 index 0000000..113e112 --- /dev/null +++ b/tests/manual/examples/ble/common/Info.ios.plist @@ -0,0 +1,39 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>CFBundleDisplayName</key> + <string>${PRODUCT_NAME}</string> + <key>CFBundleExecutable</key> + <string>${EXECUTABLE_NAME}</string> + <key>CFBundleIconFile</key> + <string>${ASSETCATALOG_COMPILER_APPICON_NAME}</string> + <key>CFBundleIdentifier</key> + <string>${PRODUCT_BUNDLE_IDENTIFIER}</string> + <key>CFBundleName</key> + <string>${PRODUCT_NAME}</string> + <key>CFBundlePackageType</key> + <string>APPL</string> + <key>CFBundleShortVersionString</key> + <string>${QMAKE_SHORT_VERSION}</string> + <key>CFBundleSignature</key> + <string>${QMAKE_PKGINFO_TYPEINFO}</string> + <key>CFBundleVersion</key> + <string>${QMAKE_FULL_VERSION}</string> + <key>LSRequiresIPhoneOS</key> + <true/> + <key>MinimumOSVersion</key> + <string>${IPHONEOS_DEPLOYMENT_TARGET}</string> + <key>NSBluetoothAlwaysUsageDescription</key> + <string>Qt RO example wants to access your Bluetooth adapter</string> + <key>UILaunchStoryboardName</key> + <string>LaunchScreen</string> + <key>UISupportedInterfaceOrientations</key> + <array> + <string>UIInterfaceOrientationPortrait</string> + <string>UIInterfaceOrientationPortraitUpsideDown</string> + <string>UIInterfaceOrientationLandscapeLeft</string> + <string>UIInterfaceOrientationLandscapeRight</string> + </array> +</dict> +</plist> diff --git a/tests/manual/examples/ble/common/Info.qmake.macos.plist b/tests/manual/examples/ble/common/Info.qmake.macos.plist new file mode 100644 index 0000000..efd3b9c --- /dev/null +++ b/tests/manual/examples/ble/common/Info.qmake.macos.plist @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>CFBundleExecutable</key> + <string>${EXECUTABLE_NAME}</string> + <key>CFBundleIconFile</key> + <string></string> + <key>CFBundleIdentifier</key> + <string>${PRODUCT_BUNDLE_IDENTIFIER}</string> + <key>CFBundlePackageType</key> + <string>APPL</string> + <key>CFBundleSignature</key> + <string>????</string> + <key>LSMinimumSystemVersion</key> + <string>${MACOSX_DEPLOYMENT_TARGET}</string> + <key>NSPrincipalClass</key> + <string>NSApplication</string> + <key>NSBluetoothAlwaysUsageDescription</key> + <string>Qt RO Example wants to access your Bluetooth adapter</string> + <key>NSSupportsAutomaticGraphicsSwitching</key> + <true/> +</dict> +</plist> diff --git a/tests/manual/examples/ble/common/bleiodevice.cpp b/tests/manual/examples/ble/common/bleiodevice.cpp new file mode 100644 index 0000000..f957495 --- /dev/null +++ b/tests/manual/examples/ble/common/bleiodevice.cpp @@ -0,0 +1,102 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// Copyright (C) 2019 Ford Motor Company +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#include "bleiodevice.h" + +#include <QtBluetooth/QLowEnergyController> + +using namespace Qt::Literals::StringLiterals; + +// The BT LE service consists of two characteristics which are used to establish +// a two-way ommunication channel. The Service which contains the characteristics: +const QBluetoothUuid BLEIoDevice::SERVICE_UUID = + QBluetoothUuid{"11223344-ac16-11eb-ae5c-93d3a763feed"_L1}; +// Characteristic for data Client => Server +const QBluetoothUuid BLEIoDevice::CLIENT_TX_SERVER_RX_CHAR_UUID = + QBluetoothUuid{"889930f0-b9cc-4c27-8c1b-ebc2bcae5c95"_L1}; +// Characteristic for data Server => Client +const QBluetoothUuid BLEIoDevice::CLIENT_RX_SERVER_TX_CHAR_UUID = + QBluetoothUuid{"667730f0-b9cc-4c27-8c1b-ebc2bcae5c95"_L1}; + +const QByteArray BLEIoDevice::CLIENT_READY_SIGNAL = "clientready"_ba; + +BLEIoDevice::BLEIoDevice(QLowEnergyService *service, const QBluetoothUuid &rx, + const QBluetoothUuid &tx, QObject *parent) + : QIODevice(parent) + , m_service(service) + , m_txCharacteristic(service->characteristic(tx)) + , m_rxCharacteristicUuid(rx) +{ + Q_ASSERT(service); + + open(QIODevice::ReadWrite); + + QObject::connect(service, &QLowEnergyService::characteristicChanged, this, [this]( + const QLowEnergyCharacteristic &info, const QByteArray &value) { + // If the characteristic where we are the receiving end has new value (ie. the remote end + // has written), store the data and indicate that we have it + if (info.uuid() != m_rxCharacteristicUuid) + return; + m_rxBuffer.append(value); + emit readyRead(); + }); + + QObject::connect(service, &QLowEnergyService::errorOccurred, this, + [this](QLowEnergyService::ServiceError error) { + qWarning() << "A BT LE Service error occurred:" << error; + emit disconnected(); + }); +} + +qint64 BLEIoDevice::bytesAvailable() const +{ + return QIODevice::bytesAvailable() + m_rxBuffer.size(); +} + +bool BLEIoDevice::isSequential() const +{ + return true; +} + +void BLEIoDevice::close() +{ + emit closing(); +} + +qint64 BLEIoDevice::readData(char *data, qint64 maxlen) +{ + auto sz = (std::min)(qsizetype(maxlen), m_rxBuffer.size()); + if (sz <= 0) + return sz; + memcpy(data, m_rxBuffer.constData(), size_t(sz)); + m_rxBuffer.remove(0, sz); + return sz; +} + +qint64 BLEIoDevice::writeData(const char *data, qint64 len) +{ + // Write data by writing to the characteristic where we are the transmitting end. + // As there may be potentially a lot of data, we need to decide in how big chunks + // we send. Maximum is 512 - 3 = 509 bytes but it depends on the bluetooth stacks. + // We take a very conservative choice which should work with all bluetooth stacks, as this + // doesn't require support for long / prepared writes (ie. writes larger than MTU, + // whose default is 23) and also there is no risk of exceeding the notification mechanism + // payload limit (MTU - 3). + static const int MAX_PAYLOAD = 20; + if (m_service) { + do { + auto sz = (std::min)(qint64(MAX_PAYLOAD) , len); + m_service->writeCharacteristic(m_txCharacteristic, QByteArray{data, qsizetype(sz)}); + len -= sz; + data += sz; + // Here we consider bytes now written to the underlying 'channel'. BT LE below + // does any necessary queueing. In principle we could also wait for + // characteristicWritten() signal but it would only be available on BT LE + // Client/Central side, whereas this class is used on both client/server side. + emit bytesWritten(sz); + } while (len > 0); + return len; + } + return -1; +} diff --git a/tests/manual/examples/ble/common/bleiodevice.h b/tests/manual/examples/ble/common/bleiodevice.h new file mode 100644 index 0000000..359913a --- /dev/null +++ b/tests/manual/examples/ble/common/bleiodevice.h @@ -0,0 +1,45 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// Copyright (C) 2019 Ford Motor Company +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#pragma once + +#include <QtBluetooth/QBluetoothUuid> +#include <QtBluetooth/QLowEnergyCharacteristic> +#include <QtBluetooth/QLowEnergyService> + +#include <QtCore/QIODevice> +#include <QtCore/QPointer> + +class BLEIoDevice : public QIODevice +{ + Q_OBJECT +public: + BLEIoDevice(QLowEnergyService *service, const QBluetoothUuid &rx, + const QBluetoothUuid &tx, QObject *parent = nullptr); + + static const QBluetoothUuid SERVICE_UUID; + static const QBluetoothUuid CLIENT_TX_SERVER_RX_CHAR_UUID; + static const QBluetoothUuid CLIENT_RX_SERVER_TX_CHAR_UUID; + static const QByteArray CLIENT_READY_SIGNAL; + +signals: + void disconnected(); + void closing(); + + // QIODevice interface +public: + qint64 bytesAvailable() const override; + bool isSequential() const override; + void close() override; + +protected: + qint64 readData(char *data, qint64 maxlen) override; + qint64 writeData(const char *data, qint64 len) override; + +private: + QPointer<QLowEnergyService> m_service; + QLowEnergyCharacteristic m_txCharacteristic; + QBluetoothUuid m_rxCharacteristicUuid; + QByteArray m_rxBuffer; +}; diff --git a/tests/manual/examples/ble/common/common.pri b/tests/manual/examples/ble/common/common.pri new file mode 100644 index 0000000..ff90ae2 --- /dev/null +++ b/tests/manual/examples/ble/common/common.pri @@ -0,0 +1,14 @@ +QT += remoteobjects bluetooth + +CONFIG -= app_bundle + +INCLUDEPATH += $$PWD + +HEADERS += \ + $$PWD/bleiodevice.h + +SOURCES += \ + $$PWD/bleiodevice.cpp + +ios: QMAKE_INFO_PLIST = $$PWD/Info.ios.plist +macos: QMAKE_INFO_PLIST = $$PWD/Info.qmake.macos.plist diff --git a/tests/manual/examples/ble/common/heater.rep b/tests/manual/examples/ble/common/heater.rep new file mode 100644 index 0000000..6c5022b --- /dev/null +++ b/tests/manual/examples/ble/common/heater.rep @@ -0,0 +1,7 @@ +#include <QtCore> + +class Heater +{ + PROP(bool heaterPoweredOn=true); + PROP(int currentTemperature=20 SOURCEONLYSETTER); +}; diff --git a/tests/manual/examples/ble/doc/src/ble.qdoc b/tests/manual/examples/ble/doc/src/ble.qdoc new file mode 100644 index 0000000..44e82bc --- /dev/null +++ b/tests/manual/examples/ble/doc/src/ble.qdoc @@ -0,0 +1,49 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GFDL-1.3-no-invariants-only + +/*! + \example ble + \title QtRemoteObjects Bluetooth LE Example + \examplecategory {Connectivity} + \brief Using QtBluetooth Low Energy (BT LE or BLE for short) as a transport medium with + Qt Remote Objects. + \ingroup qtremoteobjects-examples + + This is achieved by wrapping the bluetooth low energy transport logic in a \l QIODevice. + + The example consists of a client and server application which communicate over the bluetooth + low energy radio. + + The server-side is a simple heater object which is either turned ON (heats) or OFF (cools). + The temperature changes periodically and this temperature is shown by the client-side GUI. + This changing temperature value flows from the server to client as remote object property. + The client-side can control the ON/OFF status of the server-side heater object. This data + flows similarly as a remote object property. + + \section1 Overview + + Bluetooth Low Energy is inherently a client/server architecture, where server + advertises its services and client discovers them, after which the data can be + transferred. + + The following Figure illustrates the main steps when running the example: + + \image ble_example_flow.png "Main phases when starting the example" + + \section1 Data flow + + The data flows over the Bluetooth Low Energy medium. This is implemented with + two GATT characteristics, one for each direction. This is illustrated by the following Figure: + + \image ble_example_iodevice.png "BT LE IO Device" + + \section1 Known limitations + + The example relies on \l QLowEnergyController::connected() and + \l QLowEnergyController::disconnected() signals to detect client connection and + disconnection. These signals are not always reliable on the server (peripheral) side + on all platforms. This is typically not a problem when connecting for + the first time, but may be an issue on subsequent reconnections if the server is not + restarted. + +*/ |