summaryrefslogtreecommitdiffstats
path: root/tests
diff options
context:
space:
mode:
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
commitfdc8f50466f0c8d04e954123a405047435e83517 (patch)
tree68e30097e3061cc656cbdf45f1f47b90aa4d27b1 /tests
parentc2ba4a68bb50d274ec984e4473709e05722e72bd (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')
-rw-r--r--tests/manual/examples/CMakeLists.txt3
-rw-r--r--tests/manual/examples/ble/CMakeLists.txt5
-rw-r--r--tests/manual/examples/ble/ble.pro5
-rw-r--r--tests/manual/examples/ble/bleclient/CMakeLists.txt65
-rw-r--r--tests/manual/examples/ble/bleclient/bleclient.pro22
-rw-r--r--tests/manual/examples/ble/bleclient/connectpage.cpp215
-rw-r--r--tests/manual/examples/ble/bleclient/connectpage.h55
-rw-r--r--tests/manual/examples/ble/bleclient/connectpage.ui59
-rw-r--r--tests/manual/examples/ble/bleclient/heaterview.cpp53
-rw-r--r--tests/manual/examples/ble/bleclient/heaterview.h40
-rw-r--r--tests/manual/examples/ble/bleclient/heaterview.ui56
-rw-r--r--tests/manual/examples/ble/bleclient/main.cpp31
-rw-r--r--tests/manual/examples/ble/bleclient/mainwindow.cpp52
-rw-r--r--tests/manual/examples/ble/bleclient/mainwindow.h34
-rw-r--r--tests/manual/examples/ble/bleserver/CMakeLists.txt61
-rw-r--r--tests/manual/examples/ble/bleserver/bleserver.pro8
-rw-r--r--tests/manual/examples/ble/bleserver/main.cpp180
-rw-r--r--tests/manual/examples/ble/common/Info.cmake.macos.plist24
-rw-r--r--tests/manual/examples/ble/common/Info.ios.plist39
-rw-r--r--tests/manual/examples/ble/common/Info.qmake.macos.plist24
-rw-r--r--tests/manual/examples/ble/common/bleiodevice.cpp102
-rw-r--r--tests/manual/examples/ble/common/bleiodevice.h45
-rw-r--r--tests/manual/examples/ble/common/common.pri14
-rw-r--r--tests/manual/examples/ble/common/heater.rep7
-rw-r--r--tests/manual/examples/ble/doc/src/ble.qdoc49
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.
+
+*/