From 524ba1e4d06f20f442058116cd3ba77daddbd866 Mon Sep 17 00:00:00 2001 From: Jaime Resano Date: Mon, 21 Oct 2024 13:16:50 +0200 Subject: [PATCH] Rework vehicle example Improve QML UI, simplify server logic, add documentation Task-number: QTBUG-129571 Task-number: QTBUG-128468 Pick-to: 6.8 Change-Id: I46d04a033a7301b9c98a3e03861b9212e7a2378d Reviewed-by: Alexey Edelev Reviewed-by: Jaime Resano --- examples/grpc/vehicle/CMakeLists.txt | 48 +- examples/grpc/vehicle/ClusterProgressBar.qml | 36 -- examples/grpc/vehicle/Main.qml | 471 +++++++++--------- examples/grpc/vehicle/StyledProgressBar.qml | 69 +++ .../{ClusterText.qml => StyledText.qml} | 2 - examples/grpc/vehicle/clusterdatamanager.cpp | 81 --- examples/grpc/vehicle/clusterdatamanager.h | 63 --- examples/grpc/vehicle/doc/images/vehicle.webp | Bin 0 -> 26510 bytes examples/grpc/vehicle/doc/src/vehicle.qdoc | 60 +++ examples/grpc/vehicle/forward.png | Bin 1227 -> 0 bytes examples/grpc/vehicle/fuel_lvl.png | Bin 939 -> 0 bytes .../grpc/vehicle/grpc_vehicle_server/main.cpp | 13 - .../grpc_vehicle_server/serverrunner.cpp | 154 ------ .../grpc_vehicle_server/serverrunner.h | 13 - examples/grpc/vehicle/left.png | Bin 1211 -> 0 bytes examples/grpc/vehicle/navigationthread.cpp | 68 +++ examples/grpc/vehicle/navigationthread.h | 39 ++ examples/grpc/vehicle/navithread.cpp | 51 -- examples/grpc/vehicle/navithread.h | 34 -- ...iservice.proto => navigationservice.proto} | 19 +- .../grpc/vehicle/proto/vehicleservice.proto | 13 +- .../grpc/vehicle/resources/direction_left.svg | 3 + .../vehicle/resources/direction_right.svg | 3 + .../vehicle/resources/direction_straight.svg | 3 + examples/grpc/vehicle/resources/fuel_icon.svg | 4 + examples/grpc/vehicle/right.png | Bin 1197 -> 0 bytes .../CMakeLists.txt | 25 +- examples/grpc/vehicle/server/main.cpp | 157 ++++++ examples/grpc/vehicle/vehiclemanager.cpp | 99 ++++ examples/grpc/vehicle/vehiclemanager.h | 68 +++ examples/grpc/vehicle/vehiclethread.cpp | 103 ++-- examples/grpc/vehicle/vehiclethread.h | 28 +- 32 files changed, 939 insertions(+), 788 deletions(-) delete mode 100644 examples/grpc/vehicle/ClusterProgressBar.qml create mode 100644 examples/grpc/vehicle/StyledProgressBar.qml rename examples/grpc/vehicle/{ClusterText.qml => StyledText.qml} (80%) delete mode 100644 examples/grpc/vehicle/clusterdatamanager.cpp delete mode 100644 examples/grpc/vehicle/clusterdatamanager.h create mode 100644 examples/grpc/vehicle/doc/images/vehicle.webp create mode 100644 examples/grpc/vehicle/doc/src/vehicle.qdoc delete mode 100644 examples/grpc/vehicle/forward.png delete mode 100644 examples/grpc/vehicle/fuel_lvl.png delete mode 100644 examples/grpc/vehicle/grpc_vehicle_server/main.cpp delete mode 100644 examples/grpc/vehicle/grpc_vehicle_server/serverrunner.cpp delete mode 100644 examples/grpc/vehicle/grpc_vehicle_server/serverrunner.h delete mode 100644 examples/grpc/vehicle/left.png create mode 100644 examples/grpc/vehicle/navigationthread.cpp create mode 100644 examples/grpc/vehicle/navigationthread.h delete mode 100644 examples/grpc/vehicle/navithread.cpp delete mode 100644 examples/grpc/vehicle/navithread.h rename examples/grpc/vehicle/proto/{naviservice.proto => navigationservice.proto} (60%) create mode 100644 examples/grpc/vehicle/resources/direction_left.svg create mode 100644 examples/grpc/vehicle/resources/direction_right.svg create mode 100644 examples/grpc/vehicle/resources/direction_straight.svg create mode 100644 examples/grpc/vehicle/resources/fuel_icon.svg delete mode 100644 examples/grpc/vehicle/right.png rename examples/grpc/vehicle/{grpc_vehicle_server => server}/CMakeLists.txt (70%) create mode 100644 examples/grpc/vehicle/server/main.cpp create mode 100644 examples/grpc/vehicle/vehiclemanager.cpp create mode 100644 examples/grpc/vehicle/vehiclemanager.h diff --git a/examples/grpc/vehicle/CMakeLists.txt b/examples/grpc/vehicle/CMakeLists.txt index 399eaf6a..78917ae0 100644 --- a/examples/grpc/vehicle/CMakeLists.txt +++ b/examples/grpc/vehicle/CMakeLists.txt @@ -2,7 +2,7 @@ # SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause cmake_minimum_required(VERSION 3.16) -project(vehicle_cluster LANGUAGES CXX) +project(vehicle LANGUAGES CXX) if(NOT DEFINED INSTALL_EXAMPLESDIR) set(INSTALL_EXAMPLESDIR "examples") @@ -20,58 +20,57 @@ find_package(Qt6 REQUIRED COMPONENTS qt_standard_project_setup() -add_subdirectory(grpc_vehicle_server) - -qt_add_executable(vehicle_cluster +qt_add_executable(vehicle_client main.cpp - clusterdatamanager.h - clusterdatamanager.cpp + vehiclemanager.h + vehiclemanager.cpp vehiclethread.h vehiclethread.cpp - navithread.h - navithread.cpp + navigationthread.h + navigationthread.cpp ) qt_add_protobuf(vehiclelib PROTO_FILES proto/vehicleservice.proto - proto/naviservice.proto + proto/navigationservice.proto ) qt_add_grpc(vehiclelib CLIENT PROTO_FILES proto/vehicleservice.proto - proto/naviservice.proto + proto/navigationservice.proto ) target_link_libraries(vehiclelib PRIVATE Qt6::ProtobufWellKnownTypes) -set_target_properties(vehicle_cluster PROPERTIES +set_target_properties(vehicle_client PROPERTIES WIN32_EXECUTABLE TRUE MACOSX_BUNDLE TRUE ) -qt_add_qml_module(vehicle_cluster +qt_add_qml_module(vehicle_client URI qtgrpc.examples.vehicle VERSION 1.0 RESOURCE_PREFIX "/qt/qml" QML_FILES - "ClusterText.qml" - "ClusterProgressBar.qml" + "StyledText.qml" + "StyledProgressBar.qml" "Main.qml" ) -qt_add_resources(vehicle_cluster - "vehicle_cluster" +qt_add_resources(vehicle_client + "resources" PREFIX "/" + BASE "resources" FILES - "left.png" - "right.png" - "forward.png" - "fuel_lvl.png" + "resources/direction_left.svg" + "resources/direction_right.svg" + "resources/direction_straight.svg" + "resources/fuel_icon.svg" ) -target_link_libraries(vehicle_cluster PRIVATE +target_link_libraries(vehicle_client PRIVATE Qt6::Core Qt6::Quick Qt6::Protobuf @@ -80,8 +79,13 @@ target_link_libraries(vehicle_cluster PRIVATE vehiclelib ) -install(TARGETS vehicle_cluster vehiclelib +install(TARGETS vehicle_client vehiclelib RUNTIME DESTINATION "${INSTALL_EXAMPLEDIR}" BUNDLE DESTINATION "${INSTALL_EXAMPLEDIR}" LIBRARY DESTINATION "${INSTALL_EXAMPLEDIR}" ) + +add_subdirectory(server) +if(TARGET vehicle_server) + add_dependencies(vehicle_client vehicle_server) +endif() diff --git a/examples/grpc/vehicle/ClusterProgressBar.qml b/examples/grpc/vehicle/ClusterProgressBar.qml deleted file mode 100644 index d54b2a28..00000000 --- a/examples/grpc/vehicle/ClusterProgressBar.qml +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright (C) 2024 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause - -import QtQuick - -Item { - id: root - property int currentBarValue: 0 - property int totalBarValue: 0 - - property string activeColor: "" - property string bgColor: "" - - width: 300 - height: 10 - - Rectangle { - id: totalValue - anchors.centerIn: parent - width: 300 - height: 10 - radius: 80 - - color: root.bgColor - } - - Rectangle { - anchors.left: parent.left - width: (root.currentBarValue && root.totalBarValue > 0) - ? totalValue.width * (root.currentBarValue/root.totalBarValue) - : 0 - height: 10 - radius: 80 - color: root.activeColor - } -} diff --git a/examples/grpc/vehicle/Main.qml b/examples/grpc/vehicle/Main.qml index 24118685..b44d0b60 100644 --- a/examples/grpc/vehicle/Main.qml +++ b/examples/grpc/vehicle/Main.qml @@ -3,299 +3,286 @@ import QtQuick import QtQuick.Controls -import QtQuick.Controls.Material +import QtQuick.Layouts +import QtQuick.VectorImage +import QtQuick.Controls.Basic import qtgrpc.examples.vehicle ApplicationWindow { id: root - width: 1280 - height: 518 - minimumWidth: width - maximumWidth: width - minimumHeight: height - maximumHeight: height - - property int speed: 0 - property int rpm: 0 - property int totalDistance: 0 - property int remainingDistance: 0 - property int direction: ClusterDataManager.BACKWARD - property int fuelLevel: 0 - property bool availableBtn: false + property int speed: -1 + property int rpm: -1 + property int totalDistance: -1 + property int traveledDistance: -1 + property int estimatedAutonomy: -1 + property string directionImageSource: "" + property string street: "" + property string vehicleErrors: "" + property string navigationErrors: "" + width: 1200 + height: 500 visible: true - title: qsTr("Cluster Qt GRPC Example") - Material.theme: Material.Light + title: qsTr("Vehicle Qt GRPC example") - Rectangle { - anchors.fill: parent - color: "#040a16" +//! [Connections] + Connections { + target: VehicleManager + + // This slot will be executed when the VehicleManager::totalDistanceChanged + // signal is emitted + function onTotalDistanceChanged(distance: int): void { + root.totalDistance = distance; + } + + function onSpeedChanged(speed: int): void { + root.speed = speed; + } + + function onRpmChanged(rpm: int): void { + root.rpm = rpm; + } + + function onTraveledDistanceChanged(distance: int): void { + root.traveledDistance = distance; + } + + function onDirectionChanged(direction: int): void { + if (direction == VehicleManager.RIGHT) { + root.directionImageSource = "qrc:/direction_right.svg"; + } else if (direction == VehicleManager.LEFT) { + root.directionImageSource = "qrc:/direction_left.svg"; + } else if (direction == VehicleManager.STRAIGHT) { + root.directionImageSource = "qrc:/direction_straight.svg"; + } else { + root.directionImageSource = ""; + } + } +//! [Connections] + function onStreetChanged(street: string): void { + root.street = street; + } + + function onAutonomyChanged(autonomy: int): void { + root.estimatedAutonomy = autonomy; + } + + function onVehicleErrorsChanged(vehicleErrors: string): void { + root.vehicleErrors = vehicleErrors; + } + + function onNavigationErrorsChanged(navigationErrors: string): void { + root.navigationErrors = navigationErrors; + } } - Item { - id: background + // Background + Rectangle { anchors.fill: parent - visible: !root.availableBtn + color: "#160404" + } - Row { - id: textsBar - anchors.horizontalCenter: background.horizontalCenter - anchors.bottom: progressBars.top - spacing: 130 + // Information screen + GridLayout { + anchors.fill: parent + anchors.margins: 40 + visible: !(root.vehicleErrors && root.navigationErrors) + columnSpacing: 20 + rowSpacing: 10 + columns: 2 + uniformCellWidths: true - width: root.width - 110 - height: 200 + Item { + Layout.fillHeight: true + Layout.fillWidth: true - Item { - width: 300 - height: 200 - - ClusterText { - width: 200 - height: 200 - anchors.bottom: parent.bottom - anchors.left: parent.left - - verticalAlignment: Text.AlignBottom - font.pointSize: 90 - text: root.speed - } - - ClusterText { - width: 100 - height: 200 - anchors.bottom: parent.bottom - anchors.bottomMargin: 27 - anchors.right: parent.right - - verticalAlignment: Text.AlignBottom - horizontalAlignment: Text.AlignRight - text: "Kmph" - } + StyledText { + id: speedText + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + text: root.speed == -1 ? "-" : root.speed + font.pointSize: 90 } - Item { - width: 300 - height: 200 + StyledText { + id: speedUnitText + anchors.bottom: parent.bottom + anchors.right: parent.right + text: "Km/h" + } + } - Image { - id: arrow - anchors.left: parent.left - anchors.bottom: parent.bottom - anchors.bottomMargin: 33 - source: root.getImage() - width: implicitWidth - height: implicitHeight + Item { + Layout.fillHeight: true + Layout.fillWidth: true - Timer { - interval: 2000 - running: arrow.source !== "" - repeat: true - onTriggered: arrow.visible = !arrow.visible + VectorImage { + id: directionImage + source: root.directionImageSource + + width: 100 + height: 100 + + anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left + } + + Rectangle { + visible: root.directionImageSource + anchors.fill: directionImage + color: "#363636" + radius: 10 + z: -1 + } + + StyledText { + anchors.right: parent.right + anchors.bottom: streetText.top + font.pointSize: 24 + horizontalAlignment: Text.AlignRight + text: { + if (root.totalDistance == -1 || root.traveledDistance == -1) { + return "- km"; } - } - ClusterText { - width: 200 - height: 40 - anchors.bottom: street.top - anchors.right: parent.right + let remainingDistance = root.totalDistance - root.traveledDistance; - verticalAlignment: Text.AlignBottom - horizontalAlignment: Text.AlignRight - text: Number(root.remainingDistance * 0.001).toFixed(2) + " km" - } - - ClusterText { - id: street - width: 200 - height: 40 - anchors.bottom: parent.bottom - anchors.bottomMargin: 27 - anchors.right: parent.right - - verticalAlignment: Text.AlignBottom - horizontalAlignment: Text.AlignRight - color: "#828284" - text: (root.getImage() !== "") ? "Erich-Thilo St" : "None" + if (remainingDistance > 1000) { + return `${Number(remainingDistance * 0.001).toFixed(2)} km`; + } + return `${remainingDistance} m` } } - Item { - width: 300 - height: 200 + StyledText { + id: streetText - ClusterText { - id: rightSide - width: 200 - height: 200 - anchors.bottom: parent.bottom - anchors.bottomMargin: 27 - anchors.right: parent.right - - verticalAlignment: Text.AlignBottom - horizontalAlignment: Text.AlignRight - text: root.rpm + " rpm" - } + anchors.right: parent.right + anchors.bottom: parent.bottom + horizontalAlignment: Text.AlignRight + color: "#828284" + text: root.street } } - Row { - id: progressBars + StyledProgressBar { + Layout.fillWidth: true + value: root.speed + to: 200 + activeColor: "#04e2ed" + bgColor: "#023061" + } - anchors.horizontalCenter: background.horizontalCenter - anchors.verticalCenter: background.verticalCenter - anchors.verticalCenterOffset: 50 - spacing: 130 + StyledProgressBar { + Layout.fillWidth: true + value: root.traveledDistance + to: root.totalDistance != -1 ? root.totalDistance : 0 + activeColor: "#dde90000" + bgColor: "#860000" + } - width: root.width - 110 - height: 20 + Item { + Layout.fillHeight: true + Layout.fillWidth: true - ClusterProgressBar { - currentBarValue: root.speed; - totalBarValue: 200 - activeColor: "#04e2ed" - bgColor: "#023061" + StyledText { + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + text: root.rpm == -1 ? "-" : root.rpm + font.pointSize: 60 } - ClusterProgressBar { - currentBarValue: root.remainingDistance; - totalBarValue: root.totalDistance - activeColor: "#04e2ed" - bgColor: "#023061" - } - ClusterProgressBar { - currentBarValue: root.rpm; - totalBarValue: 9000 - activeColor: "#f8c607" - bgColor: "#5f3f04" + + StyledText { + anchors.bottom: parent.bottom + anchors.right: parent.right + text: "rpm" } } - ClusterProgressBar { - id: fuelLevel - anchors.leftMargin: 55 - anchors.left: parent.left - anchors.topMargin: 100 - anchors.top: progressBars.bottom - currentBarValue: root.fuelLevel - totalBarValue: 250 + Item { + Layout.fillHeight: true + Layout.fillWidth: true + + VectorImage { + id: fuelIcon + width: 28 + height: 28 + source: "qrc:/fuel_icon.svg" + anchors.bottom: parent.bottom + anchors.right: autonomyText.left + anchors.rightMargin: 12 + } + + StyledText { + id: autonomyText + text: `${root.estimatedAutonomy == -1 ? "-" : root.estimatedAutonomy} km` + anchors.bottom: parent.bottom + anchors.right: parent.right + } + } + + StyledProgressBar { + Layout.fillWidth: true + value: root.rpm + to: 9000 + activeColor: "#f8c607" + bgColor: "#5f3f04" + } + + StyledProgressBar { + Layout.fillWidth: true + value: root.estimatedAutonomy + to: 250 activeColor: "#05c848" bgColor: "#03511f" } - - ClusterText { - id: miles - anchors.bottom: fuelLevel.bottom - anchors.bottomMargin: 27 - anchors.right: fuelLevel.right - - verticalAlignment: Text.AlignBottom - horizontalAlignment: Text.AlignRight - text: root.fuelLevel + " Km" - } - - Image { - anchors.bottom: fuelLevel.bottom - anchors.bottomMargin: 35 - anchors.left: fuelLevel.left - - source:"qrc:/fuel_lvl.png" - } } - Item { - anchors.fill: parent - visible: root.availableBtn - ClusterText { - anchors.top: parent.top - anchors.topMargin: 60 - anchors.right: restartBtn.left - anchors.rightMargin: 10 + // No connection error screen + ColumnLayout { + anchors.centerIn: parent + visible: root.vehicleErrors && root.navigationErrors - width: 200 - height: 80 + StyledText { + Layout.alignment: Qt.AlignHCenter font.pointSize: 14 - visible: root.availableBtn - text: "Please, start server and press run." + text: qsTr("Please, start vehicle server. Then, press try again.") } - Rectangle { - id: restartBtn - anchors.right: parent.right - anchors.top: parent.top - anchors.margins: 60 + Button { + id: runButton + property string baseColor: "white" + property string clickedColor: "#828284" + property string hoverColor: "#a9a9a9" - width: 100 - height: 50 - radius: 80 - color: "#040a16" - border.color: btn.btnPressed ? "#828284" : "white" - border.width: 2 - visible: root.availableBtn + Layout.alignment: Qt.AlignHCenter - ClusterText { - anchors.centerIn: parent - color: btn.btnPressed ? "#828284" : "white" - text: "RUN" + background: Rectangle { + border.color: runButton.down ? + runButton.clickedColor + : (runButton.hovered ? runButton.hoverColor : runButton.baseColor) + border.width: 2 + radius: 2 + color: "transparent" } - MouseArea { - id: btn - property bool btnPressed: false - anchors.fill: parent - - enabled: root.availableBtn - onClicked: ClusterDataManager.restart() - onPressed: btnPressed = true - onReleased: btnPressed = false + contentItem: Text { + text: qsTr("Try again") + font.pointSize: 16 + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + color: runButton.down ? + runButton.clickedColor + : (runButton.hovered ? runButton.hoverColor : runButton.baseColor) } - } - } - function getImage() { - switch (root.direction) { - case ClusterDataManager.RIGHT: - return "qrc:/right.png" - case ClusterDataManager.LEFT: - return "qrc:/left.png" - case ClusterDataManager.STRAIGHT: - return "qrc:/forward.png" - default: - return "" - } - } - - Connections { - target: ClusterDataManager - - function onTotalDistanceChanged(distance) { - root.totalDistance = distance - } - - function onSpeedChanged(speed) { - root.speed = speed - } - - function onRpmChanged(rpm) { - root.rpm = rpm - } - - function onRemainingDistanceChanged(distance) { - root.remainingDistance = distance - } - - function onDirectionChanged(direction) { - root.direction = direction - } - - function onFuelLevelChanged(level) { - root.fuelLevel = level - } - - function onShowRestartClient(value) { - root.availableBtn = value + onClicked: { + root.vehicleErrors = ""; + root.navigationErrors = ""; + VehicleManager.restart(); + } } } } diff --git a/examples/grpc/vehicle/StyledProgressBar.qml b/examples/grpc/vehicle/StyledProgressBar.qml new file mode 100644 index 00000000..10b89a8e --- /dev/null +++ b/examples/grpc/vehicle/StyledProgressBar.qml @@ -0,0 +1,69 @@ +pragma ComponentBehavior: Bound + +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import QtQuick.Controls +import QtQuick.Controls.Basic + +ProgressBar { + id: root + + property string activeColor: "" + property string bgColor: "" + + height: 10 + + background: Rectangle { + anchors.fill: root + radius: 2 + color: root.bgColor + } + + contentItem: Item { + // Progress indicator for determinate state. + Rectangle { + visible: !root.indeterminate + width: root.visualPosition * root.width + height: root.height + radius: 2 + color: root.activeColor + + Behavior on width { + NumberAnimation { + duration: 500 + easing.type: Easing.InOutQuad + } + } + } + + // Scrolling animation for indeterminate state. + Item { + visible: root.indeterminate + anchors.fill: parent + clip: true + + Row { + spacing: 20 + + Repeater { + model: root.width / 40 + 1 + + Rectangle { + color: root.activeColor + width: 20 + height: parent.height + } + } + + XAnimator on x { + from: 0 + to: -40 + loops: Animation.Infinite + running: root.indeterminate + } + } + } + } +} diff --git a/examples/grpc/vehicle/ClusterText.qml b/examples/grpc/vehicle/StyledText.qml similarity index 80% rename from examples/grpc/vehicle/ClusterText.qml rename to examples/grpc/vehicle/StyledText.qml index 97cd73e3..c31f78cf 100644 --- a/examples/grpc/vehicle/ClusterText.qml +++ b/examples/grpc/vehicle/StyledText.qml @@ -4,9 +4,7 @@ import QtQuick Text { - wrapMode: Text.WordWrap font.family: "Helvetica" color: "white" - style: Text.Sunken font.pointSize: 18 } diff --git a/examples/grpc/vehicle/clusterdatamanager.cpp b/examples/grpc/vehicle/clusterdatamanager.cpp deleted file mode 100644 index b7c19bac..00000000 --- a/examples/grpc/vehicle/clusterdatamanager.cpp +++ /dev/null @@ -1,81 +0,0 @@ -// Copyright (C) 2024 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause - -#include "clusterdatamanager.h" -#include "navithread.h" -#include "vehiclethread.h" - -using namespace qtgrpc::examples; -ClusterDataManager::ClusterDataManager(QObject *parent) : QObject(parent) -{ - startVehicleClient(); - startNaviClient(); -} - -ClusterDataManager::~ClusterDataManager() -{ - if (m_vehicleThread->isRunning()) { - m_vehicleThread->quit(); - m_vehicleThread->wait(); - } - if (m_naviThread && m_naviThread->isRunning()) { - m_naviThread->quit(); - m_naviThread->wait(); - } -} - -void ClusterDataManager::startNaviClient() -{ - m_naviThread = std::make_unique(); - connect(m_naviThread.get(), &NaviThread::totalDistanceChanged, this, - &ClusterDataManager::totalDistanceChanged, Qt::QueuedConnection); - connect(m_naviThread.get(), &NaviThread::remainingDistanceChanged, this, - &ClusterDataManager::remainingDistanceChanged, Qt::QueuedConnection); - connect( - m_naviThread.get(), &NaviThread::directionChanged, this, - [this](qtgrpc::examples::DirectionEnumGadget::DirectionEnum direction) { - ClusterDataManager::directionChanged(qToUnderlying(direction)); - }, - Qt::QueuedConnection); - - m_naviThread->start(); -} - -void ClusterDataManager::startVehicleClient() -{ - m_vehicleThread = std::make_unique(); - connect(m_vehicleThread.get(), &VehicleThread::speedChanged, this, - &ClusterDataManager::speedChanged, Qt::QueuedConnection); - connect(m_vehicleThread.get(), &VehicleThread::fuelLevelChanged, this, - &ClusterDataManager::fuelLevelChanged, Qt::QueuedConnection); - connect(m_vehicleThread.get(), &VehicleThread::rpmChanged, this, - &ClusterDataManager::rpmChanged, Qt::QueuedConnection); - connect(m_vehicleThread.get(), &VehicleThread::connectionError, this, - &ClusterDataManager::setThreadsAvailable, Qt::QueuedConnection); - - m_vehicleThread->start(); -} - -void ClusterDataManager::setThreadsAvailable(bool value) -{ - if (m_threadsAvailable != value) { - m_threadsAvailable = value; - emit showRestartClient(m_threadsAvailable); - } -} - -void ClusterDataManager::restart() -{ - setThreadsAvailable(false); - if (m_vehicleThread->isRunning()) { - m_vehicleThread->quit(); - m_vehicleThread->wait(); - m_vehicleThread->start(); - } - - if (m_naviThread->isRunning()) { - m_naviThread->quit(); - m_naviThread->wait(); - m_naviThread->start(); - } -} diff --git a/examples/grpc/vehicle/clusterdatamanager.h b/examples/grpc/vehicle/clusterdatamanager.h deleted file mode 100644 index c38deaa3..00000000 --- a/examples/grpc/vehicle/clusterdatamanager.h +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright (C) 2024 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause - -#ifndef CLUSTERDATAMANAGER_H -#define CLUSTERDATAMANAGER_H - -#include -#include -#include -#include - -#include - -class VehicleThread; -class NaviThread; -class ClusterDataManager : public QObject -{ - Q_OBJECT - QML_ELEMENT - QML_SINGLETON - - Q_PROPERTY(bool threadsAvailable READ threadsAvailable WRITE setThreadsAvailable NOTIFY - showRestartClient FINAL) -public: - enum NaviDirection { - RIGHT = qToUnderlying(qtgrpc::examples::DirectionEnumGadget::DirectionEnum::RIGHT), - LEFT = qToUnderlying(qtgrpc::examples::DirectionEnumGadget::DirectionEnum::LEFT), - STRAIGHT = qToUnderlying(qtgrpc::examples::DirectionEnumGadget::DirectionEnum::STRAIGHT), - BACKWARD = qToUnderlying(qtgrpc::examples::DirectionEnumGadget::DirectionEnum::BACKWARD) - }; - - Q_ENUM(NaviDirection); - - explicit ClusterDataManager(QObject *parent = nullptr); - ~ClusterDataManager() override; - - Q_INVOKABLE void restart(); - - bool threadsAvailable() const { return m_threadsAvailable; } - void setThreadsAvailable(bool value); - -signals: - void speedChanged(int speed); - void rpmChanged(int rpm); - void fuelLevelChanged(int level); - - void totalDistanceChanged(int distance); - void remainingDistanceChanged(int distance); - void directionChanged(int direction); - - void showRestartClient(bool value); - -private: - void startVehicleClient(); - void startNaviClient(); - - std::unique_ptr m_naviThread; - std::unique_ptr m_vehicleThread; - - bool m_threadsAvailable = false; -}; - -#endif // CLUSTERDATAMANAGER_H diff --git a/examples/grpc/vehicle/doc/images/vehicle.webp b/examples/grpc/vehicle/doc/images/vehicle.webp new file mode 100644 index 0000000000000000000000000000000000000000..ba83a9e296016556d3b237c88beffeb8be3242d2 GIT binary patch literal 26510 zcmbrkV~{05qb=IDZQGo-ZQHhu>D{(_+UB%vPaD&=ZQH%yeCOQr-urQX-CMCDqavy* zDl=oPl~}1NBPkhf1p=ZaA*!sY%uV9^?|v2qG#8YP1pFHm1O$XKx9b-DL_VS~TFtZ;`|GxN;`{n{7fA*!GXTJzt?Q8?r18)5hfG#f@v-v{6 zR3PYU;mw~TqfDUJ_u9AEH}o?QDD)(F39$dZdrQ4{dpP+tx&?q3Rp#FUIsm_bH8)Bh zBG+;sb)PdUGq*sHzI=cqaQqvvmHqMW(@P-$&3fj5s5AB4b=&zTP;V4nzZTBr^<`&9wl z0D=R7tI!3|A4(qq$431^zC8=?UY`$lV$a2kPDeZAKqR4BUoS!BN4IRXs&S9~+y_T5cxsjxs_-L?i&Zp4p%_{M|(OVy=Py`V7JN;|p^W+6^N&F0${2F|}`|5kf z`(k_h1bX^XojaM*a1@>!S&mV3m;QR+`O@mGB(U{{so4X#WmbR;R3p*GjGVeYn`}9f#@E37byd zP&{ZF?whuQuRns1$rOlI4Tn6YN?uvvV*$U+Yk#_2W!s@r?K22_@q;Tvp&6ggycgGw zQxXk^P{;j<`VV-=gxTSYsv8GaBOB_#L~0O4r7JJFZ&)_N8U+8%}_sAAvYxn zl2!>4u6P0C@dJe-Gljx!2L3x#9xkTf_#wC#H!@j~ak~oK0ZzL;q!9;n4Cxq=6xm`} z*0Q<(fH>Wr;4Q=?CIY>dQHq3mKH`?ANO$gjR&&{NUXopLoelfLhx{>I5w^T<+WCIv zUpqLlP(s<%WZW;k!s`SRgqeGV3mb4p1)$AN#r+XJ^Vxh38fn+ekZ91XBaZVp^gC$m(1swW6}?yoKHhS7u}z+*m*Y}A(Z4_Fa+v@*Jbq2&>NNw zGYO5$`+42EPaRi|V;`8J8pW9bo?03h_m-T$Otjvf7AWzBAV23_0U*B36y`U>$@%7; zy4dZSDoIG-$!*|aczjj6mUi4jROom-V0Nz;3;)es`&UZByMOhtM(4q!Sn%ZbzB)z- zWzyCtgTU={zBVdO)+rzBw;YBuDePG1b8t8yBN#F)>0DQ-s5a;@#`0{RW+5A7&6eI& zlf&+=&)0$FhL3>E?fto+beT znyZ1q4!8q`N}X(X>05*%RCVf=F2kv1(I(KC_#5~zub{E!Fx9MMQ}tik{BMQ+pOtpr zZ7Nf6JtUQ#b1IL-DDIs?eKumu%3UlAXTE9~qeUZbpvD`?L)*FJ%A3)=JI-fpe>hP8 zOB4TO@!!Z^NYf<2MaMo`W5RlBQ*mb}rj?#0wI3y3m6E85o`wJ2Gv_T`Svp)Qf zU;MKS^m6|(y@<|Q%39fFYgL0M1*8}L6OsFDC3487AEJ(#7ak$UXVSAsH4}9|^}2T} zP|S+dtTPt=f0+N?@kCuL9HUCJy%A-yH2btXL~vRuE)@+_+v@DyH|si)uUOYTW@^I6 zts)jQzx!`x|8GkwG`^J^F{U34+1|8>Ke?_0#sB9|=29lS+b7@}6Yeh4%$Mk?(HMJ2 z6-711|1>|rSR{_yefDAxF$fqDd2wuN$>Jn?`W^Gmw&J}mP$ze=xY&kPhcm{km~3bx z6f^+f_ObgfLZex?rg};r>(LV$FzcNCr&CadA75~Z>wWL^9h%ZtlVY~5ESjav>;fF7 z&D}GG(p=6cp82|V$xTjEJ@wrFIV)9)b~y|2_1D~gNHmilik)|2v2EOxAVXbWEX1Wr zG)K$BNUM?lk85o|K&eQfDE>X-#D-36r49KnLW@K(%#Sas{l|t`q)}_(|F^ueuR3>t z`k%4m|9b-o1mqitKYw1R#aWbV-gHS_2j*?7LX{cm_lVo2F4uJK??~N74xDkiU^!Q- z=VS{WE!aJ1FG}elCtT+;MFzBZ6Gb&0A<}938-Y#dmM?7 z2c(=9kc&-QHC5KD2t3AoN4XC*1LpUb_O6l<&ppz&%tP5GQlHv{@{^nIBw}-yT8@cP zYN@Q@Y;)1ZE9n&E=DJdD(pHuWmRyxHD`}lU?S#t=W*8x08}RKK&NG$}B#n`LrC25> z9qftj!Pbb9-+Xf9sPwN%g-?48>I6rV`z=h2zH@uqX7Ud5<;BHvBy3UD3&%6MjBPh_Qe$Z2H)cbcxlqhKq5n)gH`O}0+c*zAV)*48~F1C z_PokcZm(`1Wrkp9S|}iM2vG@_Bj4Ao(ys>&FUgR<_s8fxDZ2+W3%s%C^nOIqCcOs| z>2}|j{d#s|iIbAWM+NUt3S@zCdW0G8xr;De5{dfc!J*8ACy$7C@y8}m>@Z|cR)=tT zIZ%W44;l{T%c|z)t093bxHZ9M31YKl#2`zXPfP=ZDFu?`*If>*^xxI&c<#Mn3{Aia z6%B7^aukM;(k1Xre#?Fmo;WqhF4KOXv|@*nKPh)i6w``U>_dWDb1OL;4F^P10#TM7 z7Lhs1_?#X0U@+Zal&skg42`mcCWxXl86`+`1lw?~_+{d>h<)erCC#I#^j@sI>_*=PwGD*%2uTbR;Fn0llU7auNaxxQCKJ6}NIZwV;~42ft7!)*xUFxs`D> ziJd1wo@PF=<{a~@{$Hv-%7s=0htul|RybD`vTw4ie6nkOzI4m?)yOkfyP=hhikGq} zH0V;7N@Y%eI^`#QQa%eFHe0Tn4_jrOO@Dpxs`~OG9Sn8Z1tn^aG(l8OkjezbZ=Tw} zlhtbRj`UO5d(E!?ICL{xG2VE!=`VKHgyv`*4Szl?x*Jo1OP4K>wE3$OSPP54Ot9{#1DM;=!*o;<_Jz!D1G)?Nj>+ru>H}DSf|M zRJ%1x6+bof_rC=m%DR?eA0x@`$$oL}WagY!I_`432;io|cy^IlD;?Sz;?~B8!GmFO z83eGPW7%HgWi z7u@{22+CEc^n@h?$Nh-63w3gvb4biv2&~K&8mfPE zC5=c6sfmGJYh_qL{!U?viPc{;4{YE};NEsJ1Kd~;1VSct#9K00i#+OTbzq#d^YoZF zC5-yy-m0cJ=fYnob4=YZxM6+u9lCMjGf?WXXL$8pklj@HDcUf>gkmN`%>-S9 zF6X33Nv4RoTO@D)Ll8DGOM)E5Rw&}y5WaF*4sw(hhAApRP76BasxN&-q%>-9u-TZo zQ@I20vr*@cV`kOLEHWbcPS-D|KkLslbk*ZZ#o3=v5o1@wwOY3qXlFKlxG;PIHyT38 zgWiZHk?ohf{ALE%>uh1h?m>9}pMLud20yorF6s6UyMFj%2hmU4MAuZ>&!bc7zYju| zecy$I$i*RH<^rl}D!FYPNmOoLNLCx!?+vsX8OY#i=U->v7b{jVqxweJ;~Gt{22E*m zGKMnGvQG(_Kl{(G2-c$8Uc~2Il>dHq-)V7-iHiJ;lu`7m9JyR=c8$5o)npWz8`8pf z0;&5^w4>$fSo$!C6j2@Z9@RjCm6MF|rb$WB#rS63uh*7k!;KL(0|BDGFQR#XVwmpg&Xz2EtD>@!PkS8VY#NqV<=@g{qz6tJD|vLfk!W^Ra7DiQRu& zTL0@B&!X>gnM13^9hiYA!bYeD=fZ;xnm6YZ^Y5YB{Qb9Ui9&9IIf``pfP=`nF`aje zHax)Sa0^=r;ngMEtd!=1|Ui0F-YjL_qQ>!^?pXr!z>&L{mtRTKD< zU|PoWR)DYp3;lcRa57n6I_;};cbtU2p=kz>=01;nHcHf>{!_1Up4bonEh^EFO5aVn_N}HzquQb5{G>7<>V=&HFgrbiE_Wpz8;lprz z2Zy%UzpQMJ4goo*k)qP}8dE0Z>JA%h4^ZF!5EWa%V7 z$ob*Jf>L_$#`Dv&(|Fsseb~P#Wg+JRN-3P)`(WKsQB(cM0u@V%Lc&o zze zokDYH^lDO@AEx{qvTediAb4bw53d{{Cxfgpmyl1t-F2eypKEbL@XP^;YtZA^X7^%`;Be?q}3< z=bnh`;dkGp%ZA2u8%IaUmEcTQ^zV`C{1l5CM`Mi>&erH9|DUl%-q{UI!~5g$J?^oJdRmMfoT z$Qg>bhn@ex_qXtLUkrQRBU97}l8Ce?TuAnb_{}2gMYua*oZXW~J~JO|T`wz&9SS!k z=*GUG(%0WZ`0(2I#^82J&SX+iw8c%o-M?s1k3ro9b|iSp;c;_GBa5W^i+Oni3Zzm; z%?wyc;nol2ZO2wA5nTCXzW#t!3^YzepSLe_{2EseS&d19U-cu=IIT+z7E-@mtuKMj zV<>ap6|6EDP2zQcVBFri)}>n$2!s-@^mTr$)^8-`hY8Nds;pj;%$ylm327kqje%k&q$v;9T$@AATxA8XnUDraNqemqid+RtP-*-4vK(@s5- zkwbql2c;}+xjfSdbNLJ@SRFtT%`nseUd{L6B>hFDn2jc{DO!NWSTFqeU6rC2=%bd9 z0G>{75^ghrX{4Z-xRYor8^z>Ts}=3M8;pByxra!NR3y`Dx8H(FVL!P|E16W|V`!-K z%5Xpo@_r{!Qr~+l9X?7>_IYC(qmXj54X0V#pfFE8xPXb|hjKYsqD3w$by_&SE7c0| z2X8XguwfNPWi)MCDrn>HEonZVZn3*2aKqHXj<`=N+bOrTe6c#iB|{SWb9O{aU^VCD zK}?UA?DEsK)ktuZw{ni@dTC1idRrdF;4I7+w58!7V1^yO9bFqob4?K3O`S)}%u)G; zZaq|jUfRZcpPSR`n?U@+Y?Kv?dtojeQ<*Cqq&bWeW9F!)D#T_2t#-IQa5MvDex(!_ zn)WgSA6`Heeq}-Ly`w~Oy={-PYa%P+&X$HR!6BK}a^JhnGqUagj@?yCr&119&(kLl zx7+%5Z-RaWDtzBFVs2=dk2|QCBqKO=fiPUaQ9g#p5ZHnDPCI50z+3kd zJv6PjRZ9SC3S60OUvu|bNjrK=UeH06_1Z(ZJid<#! zEjX@KNs~1Go}UB$VoohActoWr9q`01M!M1>i2&VxgP}$+mBWO-luJTU0KYKn4M&B- zJKxr$+vBQf%F@IzO&g8>O*6}7G!3{ea3|IMHEMip)EKNUv3YHH^yy6JqFMPk3t-Tb z->0^T1t9_qmrvoLR#J)fIJr{AmbGCao6w_Q;Xg%w`B`#`FD&& zW~X`80(2+GM@b;$5jLkmZ%zFc+ueP|(@Pg3?aB~hW9R-=gv$4a+#Hq7l}eT5{mgR4 zI?H-iqL#y*_8I9FGz~XA!f2$1cm+T!)=ic|iUa$55*<`2C>F%c*MXb>MW4=+h|Txn z{FJR6J17c0Cz46s$?un_#=y>wF1@!M8g!q5q6z98yIGe0oflev z!aW+#b;3mGjBt_2&{UW#0y5UFBz$Jb|OGdnI}y9@BChJ0Ue0Xm~5$H;GD`dALzt8>5i@kc%{a zMPdg$N5WlVtMHk}xlb;*gfnp8A-a0(-MI{<&dE&eL`C0$f_Z2(P=L%h+&^0>k0HcI zZhaIb+~^!OC0Z^Nga^*mr?Jv=v9R(9d#jO$$TkcqesQgx_;AWc3uYzoX(`0Z1-3t2 z&T*7%`e_mLuKd+&RXbKBhW4dNg(;nuLAar1|`r9LBgLvok~)l6mG&U#h~*Y)7HP(EfmzHkrBj zZ?E?>p7vG_$@!UHM_!r?dLBRxreW$s5Z&`Bu!hyWQ6?Bxa7MNfy`hT!W7zCOMw)oW zSCeK{@?&a?$~Hu>Vq3JI#qDStxS((A9zvSvq;HUn zp7Sr(-65(AIsy4iOvh{3JAp=x0Y56Cnm-73Qng8JG27uWE0cv+As*~JyDj})^iDy# zD}SnU(l~#&jbZeUeA*%i4m7CnItr3%G^H9bgg z!sb<(KvlRQfxN!hir}D9%$AhtZgFSYu>u+&9X3j}ne z%Sw5O;M`c}5cZN>6;y|`lDRV1+$6^0Am=kEo#9c<%x1eLF$k~1eMyP_g4Mp+FO{aB zT)>2eu3wQS6hMJCl_QGN9(PW=-YWb?BfE?vDO{A{Z=e#*uJ?WxeJOaR8s+tXzz`L- z+7+%+^%Lv^7RTCx2?S~bH_}a_(I`D3mKHt{MO(-XdZQ<>vNt$+&Qcr zM)La>?B>T+R!aG0{p!+`z92b)Nq%QZ+s%q=j;Dcx4aIg%1bYPayDeeI;13`Vz`@B3 z=i)5F;OWJnZjI8*e10O%T_S=3i;h@IST`JDztv~&On5h~J7rZ+Y|0^g`a6xdd*SW* zr&?1_;!sT~tD=c!)2MV#DitS<$HIjDs#%L?q!`r^C|}(fYiPZJeCLX`9eH~Zz(J?Q zwv;2TEwVLG=Y8`X!=@9i_Xvwvb(KwBh;B+E%oNRNj|mM1Ff1SD;VYQ%y%vysue(~8 z5oY3*2hdgxN%Ix*KTs8mRTSr~;j@ayq@G-e$SPl~KGVgZJK5U~XN}QgVNMeaYGWp4 zJe#q$*|T9rzbS(fIpL-itk5#9Ae|>Xi;l{5ibUJPX{yY5At8GACf5srK<_XI+x$k? zE>uogiokqBO7et8EVn)(JgDr(Soy-=hjkk+lbCQ+qDMpNZgm4lqow*^cqtU&SnyQO z<{@r@IsK8nfkp=zvNNW0g%wLS87k7l{Ejh353;Pj9@z#1h$Hb+sG{6 zB8_kq8lM(efm^ zBYh=xrLv$qwpZu0NCer}E(3r@gmv>8y!<-S$WR|0Nk%`q6}QUX1kww$w|lBq(CgHe zpY?RLMM>BQ&7IL~AEV|fqwI(%fzLzsROt`9BDtzGSOLid`>+6R#{b`?s_nUF}&Q% z5lYuzmnnqknPb%3!{(SW5HS~;+}oUQm_esB952QDy6GFV7D1$g$m;!?HucJCj&_a7 zyuSC)Kx2|)*uG6f-E~8Tzt|0ioj%f1t}!s5lV|yq8cLw?z_F9_GES{-CupN?OGTUO zU4u}hVf+S%u;;O`dOJyqu7DmH!ZPi3wBwG>l?|iLKKzU%*8h zTc^8+{E8PHq=f#}q%!0~_-DyJ>B4r%jCrOxo!7{Vy$dwYay0cvwo!Mlvf@b{l#4`6 zC1l!+pm9t45?;1~KNRLN2z0!WQL!TCc2Kbz^5Ac~z3@MBXXP-Oh#Q;0Y;Vrx%rk9g zHB53!1O%o>H1VmKpLJL@W7g9WArMI^^@_k!k{0M_v0&etrKPD!ZKzp^&?)>XEpm^u z*i4iP)h&9#1EQS7F;J;iNQU2I{?SPsw*CVM%acAPK=%eU1cAgqjP^BpsC1bUVTJUk z6Hn3nU2u)H-y9+i;~(F})}^A@7I&M6W_(G|$V7$eOT%X|l4qD*z>PXVPm6ip?(zJr zcs01~b-0y})>n5B1kR%s70HDPth@ z@lAO~6t;;a~cQkpP& ztk{lL@44Q-fTp+l)Z{(B&;57!Nw9Y#XhW%Tb!fgtooTn|W`F~N-9=F#V+kh+A<1&Ki|G6KrBhJv~=Rx~ImZK8Qb#UN;v zbqkKuK2~{xIxHnQy2ROl_gZW~->fQ^!Y#}-ulYiyP7;Ulk5ah5ON0uMXvt=qobDB! z7JH@YTjd|Z1_Qwz^Xn;t*MGYQ(F<07t7aBE=^-8BY#B}~_?~CBUn@-TSy`oP&~vaZ zyW1roCB0B!ccoScIM!|*t@4SCUl_8R5;U{??KxC4McB6v65!fgVacczY#BH3(T3tk zgw|%!h89d2@Oup6mzb_>o)dAdKTqb?v6W0Zg>er8gU0uM%_E*9Rjy?66o+m9O`*N( zb2b`d-;*uH9nX|GbXaO!qa&9BGfPvB^SJKoFaZ0LDM%Z{Ha5nMVQ7XZKWD@CKQz-c#egXTs>Q_r+g}3cyR6~<5xsJ63xcLf_8ywoJ>qF9_I9xN~G^`^%YO6sEfSt z>lEi!Q)V0JT;(eblM9p+dc&`KnSFui@!U!QiP98xc~&K1bP zC%XA}uZ!w8cyFg^`Yg=8kDeK3aNMd(5i8?0%js3Ay3LD2$v2x^#YQ^0< z5euf8ST9-=z3KGEi_raus)^x!pEA2G`1ua`UDjY>!iglI#%`fb#)DEv~ZX|UT6rKzVfKLuQn zEZ*Z2>l+X9P_o)Tm_qu;cz^P>X}j2(I2ZY>Fa1fHH*TL1KX$|PB|8CG7=+iqg;~5& z28#Rz`wcm|V;$S<46}BYauBVLV%UMEs>jia6n!qaj)?Jnqlz4eQ*tdbE=e-tOE=K= z^zc5A(GV>dt-f4H&5$&d~KigYOjY+Ob068Z*Nhl@ty zt$3_91Uq?i{~jT~t{Y<#%p2pONl8$Px1OcK?jZGasp?HQ*MT#qFdd!|Yswph_C&f{ zA^IYp?b%|kyAphWeXuiH@O|3ziKE$H(>k)hy%3{kxIC zhd7vVoyM#o{v1($3o}|d?f7u#)wK~%kmNVtTF^>7jJ)@Q*0m%WIW!rJopkb|^sA|r zLIn#cF3(5LCD(KP@>ojN#M!G2Nr#40!=;hM-?l283j3%c#TH=aw+b`hyP16 zgn7lrKT`g+mvBaB=+L7p%^l$-*%!bU|5ePPlC`pu{mPOp0Dgbx54lI#r^)v57ZDJ& z%Y8X(=me^#b)Zaee{h6=T2*>m-N%Oao5Uc@ertpLVhcTtb&0)Mn*Lnk0+yoEF3}DL zgX!TFjB&-!;L6+_YYq_NpOrLoCz{EAIr25OXGHxS#M?@=qs^(3XO4XkE5gJ!{S0;L zVG0*?jjpHB%P(b`1iLVm(l^+E2Ihg&YG|v z{eXE5b&EAzM7WKXPsZ2VP+kkzi9D_>goj4I&HBS(mDwKq*ojXQIx|Z2E;LTrJ`VwN zyPoO;;{(``o_OU7*Z_2M=RFA3hV*)f^Vy{&Z@QXV$w)cDbBG~Nx$RV2=g~GXd zc1xQ0u#h+P-W127;JT#=_c9V?YhgEUC0x`9f_n^Y_ziiULj{^IRvDA-JQ(_O>8dj) zXQV~=DYAH;$})(}2qXu!101M5<40*S7j`z1yc>UB)E2xK42qMa2rr$#KW%3nJ6v2n zQ+4dQSe?u#f%91j?^72Ha;xWvcjg^Bl}%rFCec(Sm}SVS`D@#p^@bxyw)!4jLzO?) zEXLQbv@*JQs>-!nxvtU^LRulxSPp#ADAn8+R{iOSii(^n5$Z-zeCBnDAih+=%sXSM z9?1A5;B(A@J(xL3MxS?@-bAF9W+vbEfkS3&G4DEAqsQ=9prB2D^N6$KC0cE`$Jn>C zIx+N9xyen;fbZTHpZnPkrsjo&r84Ezkbs*e4W3V_x)CUW{`$h^i=pq)R#-IpFPQXM za%8CQJFvlhsBK@{_6;;t-k_#F^_!HZtP2j~o?`Ge0JHCjoywFX$jQE?S->1k{o+rf zBT7Hjp@r|h(LTLk!H@`_bQC~jH!BXKJknSO-4<3XoKvNXGZ9eLl>YUqEN?jN1l)YX z`UUcx?8|!!fvVY*OtGm48)!_=t_(B;aM&oyTuWsl6Q*hkr5}^~XzMC^)>u|OX zv7=q(i4^3!fh&IVH!|ZNj{eYF_`UtkgjONLo^SgkexPYzkQf3wa&v>m6b{#Gp?bS~ z@gly#nTgPkecg3Xx9TFkR!?y~}Itx%9KB#7nV zG?F}u!!*IqwNOVQCT`UW6>h3vMBIcKDn;4Q>PQkMdl_PmOMZwg3{r(=@(&+PlZSeQ zG^QHWUK_ed$>CyaFw4E(f79KYl+;K z^b?iND|`_>KFWYqEx~F{Z>Cd_90Lu;vzH@=GRNsENht9gEl)K(ZtAMVS!LJE6LXoY z^e6rbPSd4YVQ?r&u`e3ui@Wxb^jDDrj_&x`i_>AI$&7BL$>Ps%<^wGH-|&3MgT;5=b@ zya=UZl3D$gJa=68*NPh=9i+ku!*j?l5;SvIFJ%`k#$(ri6C_a9w<2}$8HRF<-c0#Z zvI!+E@ky`|af9#=xAG_jfTs0!)}f9h|LS6s5Jra(UpDXiS(Beq8VIkTyc4h%On z|CvSUq7&ki9fD}4j8u3>!ea96)u(b3TNp|3^#V>*uu+bzoUIzwLa#;Ns> z>wUZ4S5Jnua7wB#bQJTLU83}hY7wDg8gWR|7QX;%%5>$Kfh+9G z77KJA&^JWSUe>5}#L}x?%7LTS%8%E{$sV<3ZR-jHGhJa%(rRuz>FELEu0@Kt(C^##`!?r7l zTai@^PM$`yL}X%epq_eIbHxs=fWluwNIW8}9KC>)1GStoB)|N}a*i#lz^v`_hFh=} zy_gJMCi$hbP6?FWgupOP;SW-_Ev^Ho@bNI$Ikl%j#6rxM(O>B=9zikQw$F8`f4B=h z@@gWr8G7F4Z*b@cb#JhG*a)>!ajCl07B^nap>b0ewxsX8R{L#bb5o7gc9=ReqvDzi zs{`aW-8HNxx3+ixZi%Cw1w$gfwC#2&TqxhHjTGjJNX1jv+se=-{pgFIs28*J4jE*p z5Tk$K^k$MiXTVku=fI@%CPVd(ps`WMxow^#8@^4L)4GKpUm+!FMr(y8nN5jlc+7f2 zZIrSpRG12#EDB)4L<2OS-TCZq`oG+ovGm4Wap~iARbXq^Q94*IH2!GT)NiPEZWMQe ztI#uJM{0gV&C&cug)(Z-DtD8h6o;o&RexN%zCu*96R0;H_ z5wi$aSy67{U7Y5MJF^#0!N8_dC-0cel#tJPu#NebJ>XYlDBs3|(67A@KaB$%7`9yrj$CL7Y#A zuF%{@P)a>UH!rdjVLyVbhg#+!euMQ~vC0Mi)sxXhB<$_+-cYU#)o>3Xm$-=Wz@sK@ zlNJPF9&wMJ=%!WPoJ54HbNiI8ZM;##vdSb}xb-v4rxcYGp06JjA=O zk$HvHr40jot_zNZHfh#({; znC&wE8j#iCcEsS=leLGIdIVdiV3tgW1BXv3jOR%d|var2&_A9TWH zKdZ%3SmVp3TwAd1{9VYWwW7(e+td-AR)6NZ0)c5sW;D1SnS|{?u^5bgMxvax_-pXk#8zXAx3YlWSU{H zyD!QiMv(9m;M9O*)|zsUHhIp+(&6`NC~^snev%!{vo3Q>%PUmhc-by7@?kPv)uq1$ zPrLrR;P(Z|^4L`p;!IgvKnZp0GrgLlj!$dJz4BEV01vlQIt{Ju@O)zQFcukindjm_ zW806Fi7$uWB;;|RpEk$1zuE=A4_kNU)F2V|T{Kdz<>g|K&@0ACrGFg?KT5$xm%0NS zLt1o&AL6?4GWE!;fdahI-rY%TLanY*={L?#jYJzR8epdb0Shg$#8rvTX{l@N=YWNb z_^*(q>@8ZKL7a(nBDmEItkS46C_8vFEvK6_K4KKF82rno_({n6=+Qe!Nq(M};t#yM zt-W}OmU16Xe9d7n09Pwmy! zPN^I-?)?1G_VO4p}rVSckt1n^fp=X$GXa2>kVtMJ?i(KuP~RjW-~qN zaSW8ax7_u1^ZPZV2E6q&4+9Ab9EyGK;--vaL9Kz}TuZ%18lOKiiFltJ^_JyRC&(?E zZ5hMCdPjf9VYO^9I86SkJ(9EqX89%X4%sgg0v3At&yj@hxM+cgA^fP#=ScWu12$@m z=Ze;1M$WB88y9Ya{V+?%lD@LzUe*JgT4k13y>*W)E-t98QfVrha3jonY&qyD)r%dk zj#UmQubhgAYp2%r0**RTuJ?N{Et&Gt3Fl#DzwvkW#q40SSjyyin+@@HjJg+o4Bb?} zb+`yJmYdS@2$p@i*-~%9&r)ZS5Uw~|5~Iz=zDxY{p*z`>UF!cZ`#$K8v{%F6Ys!p# zxnAN0jTc>m3C|rwvv_+96vW&yQLQ1vE>`71yqQsW#z~pNv@5@2ddM%vhi}o`b2&4mW{ybxz?pPNfEI<-z6v- zc{x$R9;kYd*kuDYhRx(x=GIie5E(Z+>%jJ2G;In`7Na-pZXvLuP6F+%{i6Q1q43n$ z=l{jWs~iHrycuc!4(UKsfAKWB8~1J0v@bVCCo~XhH=-z#`P9Fg7F4%AHCu>y!t!>0 zG5c;xV=(UTt38~ot!y;<^lhe|+ii1?nZnWJl{bwL`_(u1a~0Jh*#a3J>PKg%wFOmNC^Kw6Js0hQC~}6055VF8K(T3es`%!||}`biThs_2z2l9K&=56bex8p~=C{=Dyl@{HXs0k!u2nq#50tn{=w0(2)!6$9B3#2cH>|3q z1uPgu_P@UZ;PEBEtwQ>#*0djigZA;N>RB}`3dgV!!Q7%g{Fy{QO*zIch);FOl5#Rd z7MBFTbS;?}yz|ovbtzR{z!8VZXn`NH91&K zUuPk=4$9*^2;sFN+~CH;(CzNbxrQ%HqMadQe~4j-?OeE}sO2>ny|mxO-4Wq@6E$X1 zbN?};(MalD+^iS?$RQ|utKl_$VwQgcms1`hmt+y!q^}^SlJ6u+{ocf!CzB1cs8M~D zvPricggamy+}G&~De})R6{p3ZQXtB^0^x$r)djrTJ?fM1!K`hHeoZ8b{!JFI(fteO z@`unyHU$!X^?)7+Vq4(WYCc6Gzi+Kf+%}2rt8+xG(%Wv!*KMr}o)q1& zvuODoucqLVpI5DqYho53Xxsz1#IE}aegf7T_C3R|eE)GE`eHeSuqdf#>A_vdO_ozE zUnN)W+`tKwz5Kf`b}sqH_U|Yf()J0U|B6L6&!V`lM9s^|n*J0qrw zSPk7UtZwK+fGN5l?-9s|;&iJ!6x`Q%Slpbg-YAw=6_D`x; zJJrhGGJ#FKoFE~wg0iNYPh3xO`15@}Tv_ywDOG`)942I3x0Ef6Rgr=kQkvPp9$hC) ztH(ItUotkUI`;A|82-!v2o_KTs9^YOY;~QH@|;)0c16Ys-Cn1>W0udJc!Q@uEA1s0 zW^6zc)!pQ|M!`$jRm?LOBKEz4G-D(vo#t+_+mw*Us>+#ojvSX%JbViJ0lJ(Toda{U z#{VZ!Ca>Ay$5{BWbm@@Uq|rhd{#@~VE%<9GLy8K*7&&TzVt2*q-;+U$8lL+oTwu6+ zu#N&<9f*OUpiF6H+x?v{z=mGpD*0Fs=B;~|B4T{^g_iVD;L8R2XKoZ4-x1=BpGD9| zQ7}^h;a>Q+ap*J0PF4W`A=*womF8X*=WgrVWq&FN){m!~OhD7eyaRAMU8y^6dM>!x zti5fzDt6l2=gcxWa@PkU&4@u)i*a3@DSqc5>fQ;9;bJQ80{dvM7+p=)>}wW?W1FSR zrkz0JRJ}4cYybcN00000000001P6|ocZliPpP8V>awoYGC{7neaaWQN2uf*@6Jmr_ zAjsE_;oNx^51Ou8-uWwvk_`|_c1T@RU02zo6pa7_vU(u`A6*X@9?GAPYT=NQi0K_J zD3R2yCdazozUc|PaU5zaVn&!uaM_fBCqyG%VE}Mq^#W< zF>l|$-S%zP2=`9`fz10?9Sz!;>D(AaEv-4%-9*(~U0EMZV8O@ME|}fTr#O>EH_= z0;JidM~!5XM3e6eRG{$h*uhQE)^R~2AAG7K36Ukmjd-=*MM38_6_UqE7~^9xZJ2o* zsf!|0TtO10o;K{CdY7XO*C!Z6-Q`# z!^Z;~$oGo&#(X~o7L`zm>u+&%B&VA>EXpFuNxfrS=Prpe5FX`vd!R2Xp1+WM4FVzP z-c$nU-0`$@n#fy65nB6@#J9*WclcqLl0hB7+L6}S4=m(qFLwOpSvts=@3S39ZdKOE zU81o*K3Ph$Su(k;)78RkomfZd4%3BVXwP`@zV>ry6kT8D^>|&{8dr3jaX%KpRHhwC z!H!qf+PLo;(yOE98J56Bw zm3NH57OoJY?qM+K4geY58DIwwCaC`D=7X+2kdH@DVy=mG!bU03IdGEA5F-xRIeVBD z8e+l?F0QNCn@3|9;$}jU-Ce!6faO8Dx2PgBv)0Q+)CZqy?NqPlT&vPYUpnjEnG&bG zm>>)R^+n|zq-zzPv{*e04hh|)5-34g)zqq5woHx{RGB=Sg=i{rOO=Mt=WVbnq5MC) z>eV9rW|o})q8Bb^`_-jcNwjE7FLWn&0*wN8Kv187pB;h1X#ysOV~lVWTo73YwB4xL zDOVXq)~p5&z5KyofaCo5i1M~|A9c9bH*bx^%{(^=YS?n5eBEPG3=1YeO|EY$dM{Y+N?P4be^Y>otS?6+#L10f~ZaB>igiePl^WfOiD* zHe6gNc7}-ftj>l^o>=h0NaFAZ<{k{t{l+sxU6RP3f;_2)2tECa$iHE?;6Gc%Ms4OV zwZZ2wun6A6KOMo@q2J%XCB&hGyT%t~p9G|J3kDvPp_%U$Q(v+ZeogI8T!AW+{_{UNdTbNAW2!=3Jg znr-zW`zjnhgqjA(ep&S5DZTW>*M-8LZ>JpT)*bu`a$jWb3IU)1iY1C?(W%$UXW~m6 z30-H^BDfY?0y6%vPNTG7Hv*hr%@2~`@h;moT=geZ_jZG2$AdtC>QCpdum>0px&f@8 z!dM)|h?N8EJjJF{f-1$Eiz!0xVnTUSY21Hs^QE^W#Ui_vnV$3BQhM6qJ0MEAVx`qz zv4w!f{P?8;8eUKu9pbY|rDF2UMUqjaYI5}MZbsyK0bRavBFtxbMUTGlEQ$?rQZ!+P5rJz#d?3Ri` zW)t>OcM5~vxn}N@+coL&9f;02^zfDFWZo3Fykk|O=7NeOXE@rKROVQa@qzQ3L!OPc zsyk#I#94RasG`~u@BZMNy+c9*{i4Om6V0>Qco9BDC-AOXaxm*06*}B$*mU3;oeNJ+ ze;I%kAVL_Of`z8}lj5WC9v<{jJr$uOK#FAa+3~Kg>zxP5Uf#CW&&(m=lIQYR?`Sb5 z90b3ryQ!;RYGtMDlgrmFOMIf)JwwEp0k1_*cC-qf8e+YQv(2~qs?Q1F38_$>9>5O- ztFUZfuZp3><$FIMhh5`qz1Pe|t!0qHmiw!s!o&g~z@p@O@gOBo0PMjQZFPk9)+WBI zG1fyD=_mPM;w{bxCt%q{N*~Eji>W2Za}v>N-@fEd)-V>9#6iyxo<75Qq3CyNp4q~x zst<-CIC)04y1`q^^HI5g0!xc$r*0A(sB#chZ`w zSYuOepU%L84(2rAEb|C7XarE^BhtWZ#S#utXUEZ@bHJQ)5Jj2_51aW|_WhxC0dkFY zC-I9#_(aG!nEw7b%imSRuxe6splety-kkWiMnOJf-3^B z9bR5P0MhmW;cgi}2)Cs(da>hfa-6Ve;b-pv7@qK^hq=y=Ve76PtmX7`g-Yg83+d@( z*zZ#C**-PYBqAe+PYUG zpaq1VlBWx!Gz737s7z@ zr~|OUrO?VES>UfWD1??!=$XV~y*`0nNb}NB9kuQ4Oui6M3*6fXrWYmb^=E#vP~Cl6 zLxAZ(LZa?Rz{&~Z1`f6D8Kt8t)DT3eJ#+|+momj;f}kW^>zS+)e~U{Mu69VP#ah!( z6Trgi)rk!nEZhZ4m{1pB`)?>*zz<%Gr-+Y znT^4N-=Do^R7*w~)NF05!r=>xP&nqU; zav$aZ2nm9;*ywj8mk;m5QjF3t;|tnU>m?B?*@vMe|C^pq9wO6;wE6NP5bN1(ka`t& z(sz**0<0X$ueqYZWZEO1yhCnnpF8bGPp405P#_p9d~ymos(*hQbf3NG&ZNmn7|2=m z_b@aQ?StHZ`bzLfHJgt!0QhyOBE(3v48W+ztG;30n)SwQ&kTsSiBSH;_uFXh@7RHu zTdh~M7Rko+kqHoWk&Y(F?bvY~bHFl+j&bV*5rn3W|54Y%3VkUiQ2f$rMF9nwOoQ`- z&WckA(ZWUnRed{>0JU_$hQQ>NB)1mi3f1KaZBm$0u};R2?8kR#0A50Jm{f zyu&tiHochcVB8|jc@(moU9-?ZcXbSs|7QQfVKD}R%H5+D$iTSo_l*y3e<|}-8|?6n z)QXhP5#ta{CewYB)sQXf8x@j`_<$;Yh->|Akd7??hrPw3(!~2(!rYUUu{!9gu?Wyn zC7iAJR^zMC-T&y54RPk`s7rUClL3KzBU%r019EJ^G^Ujv=kh>z-3o*-xT~*e$|GPR zD*yPE>KXx@4$Shg!rMpydEIwa08V>J$6HSXwEW%C!=J!7hMe~?B%KtoJfBIxR1NON8Uh6HKkhBF-9(D+RVy+{vsEt-gQV*aKlu;|elKSKH+$-c(Jg5IAYCwSu~r<%Ny6iAm;I{Ekmc zxC{w1fgd3B$kl^wTVa>i9`6lDvaEj+(^?pqE$6qH$U&}YC`V0->uMts6r%~6l|K|+ zfxe3hmh%jF>QwgExF`!?!Br%p-=y>wJ>K;KQ?(!*3H|wFi!|njHyySi2VH#W;-lJq zkR4F`V*rrU5lN{{JeA4KB}7pZxSxcCX$y-mU5yvdg0YaJ6xcML-LyEGEPe99d@-vjQIuFBH(J zW>WTCno4l+W@26tKbQQ|o{%ZqtmA^_K1KeLYs;Z4vgFF$m1ljcm-x!|K)AG3mt~q} zPq*gS?1lV73a54?*B*NoxA$h%T6uPni&Qq!Ly8*RV{PUGdgY4n+UzUU*SLKsqi{gY zJ=?|QYsT}-W>0sB0T78-?bW4MKb3RUdnkdfd7mIvSUKaA z&?ugAD%GowK|$4=gCT@?S5pb1MU}1bM)Rc}|4Nk4GK?aAz)6l!jRiPq;BsllC5Xl$ z)r`p=9Ed`fbesfC{tvyS_=T25M9^iNwnaBWy`P+? z#auQ`W=j6zbs5zG=|?Ivv^=bHRJ-R6=Qpb8534R;eT@3FJ{gM3(`Ux7NS7)W_1nPc ziA@q56raUlmdV1wVb;7ejy@JBK2yQVX3s>qwL$#qcK9AZFIg1h3=GRsB1fF)5t)HQ zW$~nlxU9fYKBP=4oe9BuAN@c`ay=%#P>2M|aQ#YJ7m|xl^sD*1HI9wRnQwKgj)!!s znDf>Ub43+V)Ztow9K>jp|G)eo?Og*HhoCTn5G2S#Kd#`0I!n&rm{s>FP9Z-moBf>N znpgRm>?zuTT3JscfCZWbAS5lB-6n8wgO5)bUt)+SiZEi)p1(sF89gZRq zy9IOB5>V4N;4wZ(3WLmW5SDMcOi)$`+I7^jSArS1KAlbiyv~`J%(^^XLO^XywJrE1 z%KLENotE*_QMj^-EG-g=MvEIi|2sVs(#5heub&aqL>xZ>o3TNXp(6(|T>p?CKitdF zn2fcqXYtWfUwD?WQYAqO*A&hj={-!~@xDDBXy{v@FpYFM_y61;F z5}&Qq0@o5V4lbNDV)`*`cc!~{imUcbqt^)wD(0gR0vo(Sg~?;E04BNuFjDao;vvSw z1bNzlEvBN~glZK0rbu}PipJ4|7;W+qt_n}IRVQK^3eP+=IUpULAR4aa@{9=6gPuB zwW)*!vF3LlSMSQ$oDu3s^;$-n0ZXbGt$7?K8e3u(iP?f$1ru9eD%Ms@VG_>~14BTa z@iH`+A|vmhn)UtC_#a;vtx(g6|3GMx1C>eNRFq&D_#8HQ_HHBN%aI=0XYbB*$mRY6 zPLOlW!mzi4JTZ6%Nhq*`{auEgE6WNk$uZq;J|M1rqhn*B-phuH)z)1W({l(=pka@b zf-pg9?&fi2nGhDYjb)d37)oLZmB4lV11NJcKwzc0H7bI-HS&1LC4bbo8jB?!7f6X_ zhx7_$tsfH+JnRNtt_?v0LIMsi?4(cN;YgULWxwAd9gXB3z(~1owv^kutUr%{5U-dfRRQT6s}+FePw{idHHsTeoaEIjLGG z<4PN0Y|i^{&91*T%$zZsb=vAqW>DTjp#cf~s2S5F5 zFSw2>U5fu?j@0KCB7H|&!yN*!wx^C1tlX_&akvojv%JS8$# z+|7Czbm~>@mZ;9|esJMqf>oE3uDzbsXNS|h=re~JB~%q=vc?@^`I;Zo5hwW#AA{kz@1WXD7sb_VB)Mz_KJ}=1~co zMw_IRI|0egc6wkmdefcFVa?(wF0Jr`&qQyeRXP^2FtXrf50$o=V|@ic<+U$5##MAQ z$eC1aJZ5%8!K1&Hzlq3Rnvq zPz{Rs>^c`8J|dJrU<}FAx|s(ws8s7GRP2t`mZy5clZ)(da9EJt=`tfa_67cW@n2Bo zJC#Trxe>Wwg1ecAIPUKvzO;?|++o>jMaOblKOJH6w{$cr!I9CxE()(|er*R@;8cGY zhS3p(#GiDIPI>@9stpjKIaJAo7<9*Ix{J;8|F!-ybNY@B|L%sBAeI~e4eI%|zF1Md z2}DAQ^SQwcLQqwf%G{7#{YA$v--|#IzbADOp-446xuOsN1+Bs(NPzIjJaDR)7_Xq& zm%Od9Y$nG;=9|^50V!3ru1TXY&+o&FS*!h&*s+R+T;8e_V)Q2o=p9li6(hi4jI_S9 zr)Wv%*Z;_&G@U1F0t(;RBe~5=RY=S~(5gK#$}d#C6GHLzc4m)U99w-~xOPVW-oh zL(FDTh*Aa~f&iN!Xc*+akEfQXQ`Y^Fw2Q2+t&hK!JbG7~5T21lZdjL0I{xKZhQa0l zuRZdoT-ep4p6g9<#{3Mc10Ca{`cNshfChGf_#;2uXL@&)UG(tmgD}E0xdt~Hs>C|n3C)=%~XNR;m zu%k&bc;pY)C|kvuu-em7mT+4Q*fL&-D71fBt9Ctu5^xh*Ggelf1hosLoC#+te z=`KMQBg`7TH$Q{IqZFpC%3B+*ioyUYzE2|Rxy0H+nHCZ%!_s-F@0TD9vS~C&@Q8Y8 z^55N7NzP)zcG1LP!Nau=l-Li?+aNh_Bf;_+jWYO1muUQiaO4!qbAokfqV7>fl$!7n zebCdQJRL)6jelR>RtQ(#xeFK+rLUV{R4jDZr}Nq}E2?+?IADwdup4^$0x)_@rzT7` zCsw_EU3w^H_FvMx>pFCxRN+$>Q_VRB8lF32x~JM%kX-B@fqPh(rmTI4?X}A~mAg5w zCERd2dRA!aZpq1g9_sP9!(_4Z-+*5J0>Wdac#QfFe~!G|LMvQ^99o~LjW!9|NL!Et zs7Lug|JJ#`jGXyRNIw$ObWds=srW?TMoBrjk!K-y0*GtRhac&g4M?{as$xStIXE8b z*Xf4Pfj%%?AY!=6ZJq#Tr^yXKs}U^d6O;RyoNjyn0(vxl(-ou}O(Vf~ZSA6nX`lMoZwkbS?qL5!>nqd-7L`JYx8vds+vn;updXN%NjJIHWCsyPk!d%@L1o!pqRdc?z}0C*^tWMEVQG}pu4#R+|h zsMoo1@Rx}H5|VSr^L1%JJsK^`6GHMp@b%;RS>dN+V%litk2)zX`}+Y3G~y**c-+B_ zmlTcDp2c571p$hy6~a_M@YMGcl)C3Y;yHK<%~D{$*-zWgc;spS?Dm2t0-q&U3+KAG z)ZG?`RXfZcxaTA(a*(+L|4>Zr*gLWR8$>CkxN58>PGHSjo$~lv_6j~q4ZDyQ>oeiNyKwpAT#G>$oYDEXi+_!K?2}&s z09y6^g}89WqLe@CVQcb{hzHA(|B%?4<>DK&;_iN_WoAs1N>M0D)niy+d6A#(&uV&D zfK-s^&NCIiCm^hKWy{^`6m=$L)4tWmn7_jO98M}C#11$bu`x;A@YZ3_up9)mhn&{g zOY#8enCuKjo44u{w{i@g0z`CL9?6E@u*0vhd{pze@xny`SJJ^70kj5&nvPj3qo?Z^ zUYiS(*&bONSzI-Kv*n%F7?r6E<{q|;PFbAo$|i7ycB|A+>~0u@veHS+uKC8G0JiFv zw3)ro|xM*7aq9&`V$H35yn!3aH!w+ZPs*3TE&o{ za6klGK?(tNskK+3Z<}>Eh3Q|jS-`Iv$9Z7R71SFG-zFuQxNV9#XkQX-;#Iq z60o^Ne7dpvVEM*3nQgtkwrDTQi`+pznY<}uVwI+xYQu-LE|IHb&ZumAz@VbQr`-BT zd4ha=>Zx0Ik6xHI3muNuZOvNa8|Ds7iu58yLcY0w|F(!|&|J>usjkUtVmY3z>z+?a zkuvyvj*s10FOfDIwUwWe%Pc7%nN1911ysOW2d-Gzxi$85==W?p=ZZqBtS9gMZlzuJ zwOs;QqZc2?KQAF}u_HHjgo**K=zA+Hen{QxeEW)tv!6P22P;>y%g+=`CEy~68RfCX zI2zV_%1l9i#m1q~l>g&}@+PqCiOTaqS(|%P1g&7tij(CmF}@8cKr_gBh5T~sp!2dL zG@yTBb5UiUjjtHv5-cHZp(H&;Nuzlujn1vIcZ*MsRY<1{UiTzvwkf;?aFK~}75;Z* zFjWjx%mD0Ot@FZ8&u2;+DLC~k&a1{tg{-DzM!_#Gc_VPr#t~7$j5>28^Z0rq*{0j0 zhoySyd)ZSCwtp_+Ws`l+T1K_Q^@2YL1b_UQnKP7qty%AfI>2thF1j9o z`6$s#T3U@?iw^d7Vr3UHsbG9h<}xlm(F+MKVU4k}hY0sm?HgOjXQVpT3?UWNv|W~D z{kQrZn~P&nX1VXtfbQ^*lf?ee3oV69Q+vUyE>>gORMQc5Y}~)@Y+1n4)arG0OYtC` z?Ir=Go1U$cJN$7qzIvf(Q~!SWc$~4L&(nO~P5v$zqz&#;zW}aG_(%@1Y?v5->>30&|(JNX}YRj?^QHEhN3alHLK{IP-co! zFT%E)Fjk6dWWZD-X&hvhli*4=C`h=RM`PdDzjoW!_V-{_> z6{8M{7n1k7706lnx7JG+)#a$~{h1alD3?sb?XA1?&{^SS*5mV+ahoW==rDaOjCl(7 zArLPoz`)Ps0tE+@rqOA%Gd)FD(d7tE%z1JL8z#|MSW`d=K_&ii z`7t&%lCM0Gk9+0%;#!{AiNrCqM<9Y@Mv3(OMsR zBT%%bDtq-nM(F*c8LCOS)ab@|#usfm(ZWJ(t65s6DWswkMLY~}rTxEQ_1^&y(j6X| zF79adDU|FNY>C{jU72T@)~!L(o(K4kh+y@b5Ho`D^Sw zbKc;K=TWQ?bh5wRQlyj4FSuIjU3kTwvIT3a$euXEYIR1;a1bI#Bha3fDBw-KxfvHA8h9K z!t?5*m4rk9b_RX!D^b5cHV+t;)L;E%!NGbpGgL$hvI3wLetH!$WRu+;sl$4ZM8Kvk{8)^PIs6@cxq$Yy^Pq|g?imU)a8yE*=h?hey zxeUW~ybrgPFZ&V2Tl9!*u0>=1{xqE^N>UivPTpEa`wxR-x`wF^sbB&?jP#~%yAsYa zQD+v>LB_%-fxsA^zAO7D|1B{4k|8{0@&Je68j|DL-n`XU85*kNBUM~vYO9Q6bAh{nJ7oXKmH>eAG+chj4&0I)=ad q6m$S028Mwwm7$_&@k_LqwL!}u{1;0{KmY&$A2G^4ibwze0001NaaYa& literal 0 HcmV?d00001 diff --git a/examples/grpc/vehicle/doc/src/vehicle.qdoc b/examples/grpc/vehicle/doc/src/vehicle.qdoc new file mode 100644 index 00000000..98dadc60 --- /dev/null +++ b/examples/grpc/vehicle/doc/src/vehicle.qdoc @@ -0,0 +1,60 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GFDL-1.3-no-invariants-only + +/*! + \example vehicle + \ingroup qtgrpc-examples + \examplecategory {Networking} + \meta tag {network,protobuf,grpc,threading} + \title Vehicle + + \brief Manage two threaded connections between a Qt gRPC client and a C++ gRPC server. + + The example simulates a vehicle dashboard that displays data sent by a gRPC server. + \image vehicle.webp "Vehicle example dashboard screenshot" + + The example code has the following components: + \list + \li \c vehicle_client Qt gRPC client application that uses the + \l {qt_add_protobuf}{qt_add_protobuf()} and \l {qt_add_grpc}{qt_add_grpc()} CMake functions + for message and service Qt code generation. + \li \c vehicle_server application that calls C++ gRPC plugin for generating server code and + implementing simple server logic. + \endlist + + \note you need the C++ gRPC plugin installed. + Find details here: \l {Module prerequisites} + + Both components use generated messages from the protobuf schemas described in the files + \c {vehicleservice.proto} and \c {navigationservice.proto}. + + Vehicle service: + \snippet vehicle/proto/vehicleservice.proto Proto types + + Navigation service: + \snippet vehicle/proto/navigationservice.proto Proto types + + The \c VehicleManager \l {Singletons in QML#Defining singletons in C++} {C++ singleton} uses + two \l QThread instances to communicate with the server in parallel. The threads have different + gRPC connection types. In this example, there are two types: + \list + \li Server streaming RPCs + For example, the speed stream of the vehicle thread. It uses two callback functions: + QGrpcServerStream::messageReceived and QGrpcOperation::finished + \snippet vehicle/vehiclethread.cpp Speed stream + + \li Unary RPCs + The RPC \c getAutonomy operation is a unary RPC. It returns a single response. It is only + connected to the QGrpcOperation::finished signal. + \snippet vehicle/vehiclethread.cpp Autonomy call + \endlist + + The client main window interface is defined in the Main.qml file. It uses QML \l Connections + type in order to connect to the signals of the \c VehicleManager + \l {Singletons in QML#Defining singletons in C++} {C++ singleton} to custom slots: + \snippet vehicle/Main.qml Connections + + After receiving a response, the client window updates the UI with the received data. This way, + messages can be received in different threads and be sent to the client UI in a thread-safe way + thanks to the signals. +*/ diff --git a/examples/grpc/vehicle/forward.png b/examples/grpc/vehicle/forward.png deleted file mode 100644 index 0a187c44061c53f2a5500b8a531d003a13c76cf0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1227 zcmV;+1T_1JP)EX>4Tx04R}tkv&MmKpe$iQ>7x+4t5Z6$WT893!);9T7@E12(?114kp)6Xws0R zxHt-~1qVMCs}3&Cx;nTDg5U>;vxAeOiU6f~epZjz4WOF71d?N8I(+!JwgLrz= z(mC%Fhgnh15}y-~8FWG7N3JU_zi}=&Ebz>*kxI@Jhl#~P8!K(hiiS!&MI6bh8s!UV zm$RI=IIHCDp!3DHKZbz7F3_mi_V=-EH%)gan}=MIDklMF)cp)v1m_L=bc-ia-j=F5ZJqbtsB1{eb?3uHMX6-8_`A zrmKrl{>a^(ZwJ@P)z+EW*)?K5$7k7RUi*E%^UO0V_{r4NK5H@hu6Q^b`F@nHu1{X4 zGyNFsrN~Q#j!ok~>ffxwVXyNUzAAwn*lN^44*HqRJ7PT5ue$a31PsPg{S`-8j{Ugg z=h%;`upGL(^yf}zMG=t?v$qhWv{5ATe)fi^4)b(ZB+?5i?AmrxWns0awW%VNa038P zgLp-j!%WOPbFT$6G5<91<*>%mD?r3U-Bz;QZMDHwVpv;1BOaSR%COc2hOlGvZ$0)f zAVgww1A!qNIO1nJ&LbCsK}=?j30mXp34?Yrnb}h|!ph~JQ%0<8?tb}|Q1((*cGnsxt^<$b7=_|hw z;>hV8Kp>7>UE4a6$wn(5|H{rpvo#BG!_GC{Vdo-tuIaXzW9eyu_U(>|YwtiP5OsNh zRlwS~St7*;XN76gP@X7SU${s;Fp5Younc?#nt&ys0hkBe9)5=k^C^%6z5(s1wgHQU zaSUWxir&2ce1w~YUtLpZ;PpM_i`(vBztDz~cDFiZ{Zmtbx;k`ayIWn)ZSW5yM1+VC p5h6lFhzJoOB1D9U5b<9legYNy!=;rfRb~JH002ovPDHLkV1gLmIU@i7 diff --git a/examples/grpc/vehicle/fuel_lvl.png b/examples/grpc/vehicle/fuel_lvl.png deleted file mode 100644 index de1abe243c0cec0b923e0f25fc18404c2110dafd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 939 zcmV;c162HpP)X1^@s6D=Y3@0004mX+uL$Nkc;* zaB^>EX>4Tx04R}tkv&MmKpe$iQ>7vmMLUQ%WN4i%h+jBr6^c+H)C#RSm|Xe=O&XFE z7e~Rh;NZt%)xpJCR|i)?5c~jfc5qU3krMxx7Fxu3aNLh~_a1le0HIc5n$s0?!PYspLFyh*-?GvC_t@U~0sZ#9>v_DPKr8 zSmnIMSu0go^Pc>L!K}Wr%ypW>h+`2;kRU=q1x1u#BTB1IiiITY$2|N)u3sXTLarhh zITlcc2HEw4|H1EWt=z;BLW0=fqK<(zKeSad^gZEa<4bO1wgWnpw> zWFU8GbZ8()Nlj2!fese{00DzZL_t(Y$L*CrOC3QJ#eZk!t$zxGX++XQ2nZHwOqEWM z^uBFxwwx40PIV0r85`G8g(i?J`+M%^nm8>cDr*~ zmc=rVq<6N9RF996zSO^i_V}LXd9b~b?7h%Zo+Drr_>t`K7^CVR+U@ZbSOvD~x-5*K z$gZ?#pKlZd+rEa{DGU#1UySiQF;y|kSHv%oXp1lTQqw5)|RDzlf=1rF1?#(#n?RsMJ8%$EQF N002ovPDHLkV1m)%pU40J diff --git a/examples/grpc/vehicle/grpc_vehicle_server/main.cpp b/examples/grpc/vehicle/grpc_vehicle_server/main.cpp deleted file mode 100644 index 4575af15..00000000 --- a/examples/grpc/vehicle/grpc_vehicle_server/main.cpp +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright (C) 2023 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause - -#include "serverrunner.h" - -#include - -int main() -{ - auto server = std::make_unique(); - server->run(); - return 0; -} diff --git a/examples/grpc/vehicle/grpc_vehicle_server/serverrunner.cpp b/examples/grpc/vehicle/grpc_vehicle_server/serverrunner.cpp deleted file mode 100644 index 2c9d8e17..00000000 --- a/examples/grpc/vehicle/grpc_vehicle_server/serverrunner.cpp +++ /dev/null @@ -1,154 +0,0 @@ -// Copyright (C) 2024 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause - -#include "serverrunner.h" -#include "naviservice.grpc.pb.h" -#include "vehicleservice.grpc.pb.h" - -#include - -#include -#include -#include -#include - -namespace { - -using grpc::Server; -using grpc::ServerBuilder; -using grpc::ServerContext; -using grpc::ServerWriter; -using grpc::Status; -using qtgrpc::examples::DistanceMsg; -using qtgrpc::examples::FuelLevelMsg; -using qtgrpc::examples::GearMsg; -using qtgrpc::examples::NaviService; -using qtgrpc::examples::SpeedMsg; -using qtgrpc::examples::VehicleService; - -class VehicleServiceServiceImpl final : public qtgrpc::examples::VehicleService::Service -{ - grpc::Status getSpeedStream(grpc::ServerContext *, const ::google::protobuf::Empty *, - ServerWriter *writer) override; - grpc::Status getGearStream(grpc::ServerContext *, const ::google::protobuf::Empty *, - ServerWriter *writer) override; - grpc::Status getFuelLevel(grpc::ServerContext *, const ::google::protobuf::Empty *, - FuelLevelMsg *response) override; - - constexpr static std::chrono::seconds vehicleTimeout = std::chrono::seconds(1); - int speed = 0; - int rpm = 100; -}; - -class NaviServiceServiceImpl final : public qtgrpc::examples::NaviService::Service -{ - grpc::Status getNaviStream(grpc::ServerContext *, const ::google::protobuf::Empty *, - ServerWriter *writer) override; - constexpr static std::chrono::seconds naviTimeout = std::chrono::seconds(2); - int totaldistance = 2000; - int remainingdistance = 0; -}; -} // namespace - -Status NaviServiceServiceImpl::getNaviStream(grpc::ServerContext *, - const ::google::protobuf::Empty *, - ServerWriter *writer) -{ - volatile bool naviRequired = false; - DistanceMsg msg; - msg.set_totaldistance(totaldistance); - std::this_thread::sleep_for(naviTimeout); - naviRequired = writer->Write(msg); - - while (naviRequired) { - if (remainingdistance < totaldistance) { - msg.set_remainingdistance(remainingdistance += 50); - } else { - msg.set_remainingdistance(0); - remainingdistance = 0; - } - std::this_thread::sleep_for(naviTimeout); - naviRequired = writer->Write(msg); - - switch (remainingdistance) { - case 0: { - msg.set_direction(qtgrpc::examples::DirectionEnum::STRAIGHT); - } break; - case 50: { - msg.set_direction(qtgrpc::examples::DirectionEnum::RIGHT); - } break; - case 900: { - msg.set_direction(qtgrpc::examples::DirectionEnum::LEFT); - } break; - } - std::this_thread::sleep_for(naviTimeout); - naviRequired = writer->Write(msg); - } - - return Status(); -} -grpc::Status VehicleServiceServiceImpl::getFuelLevel(grpc::ServerContext *, - const ::google::protobuf::Empty *, - FuelLevelMsg *response) -{ - response->set_fuellevel(152); - return Status(); -} - -Status VehicleServiceServiceImpl::getSpeedStream(grpc::ServerContext *, - const ::google::protobuf::Empty *, - ServerWriter *writer) -{ - SpeedMsg msg; - volatile bool vehicleRequired = true; - while (vehicleRequired) { - if (speed < 180) { - msg.set_speed(speed += 10); - } else { - msg.set_speed(speed -= 10); - } - std::this_thread::sleep_for(vehicleTimeout); - vehicleRequired = writer->Write(msg); - } - return Status(); -} - -Status VehicleServiceServiceImpl::getGearStream(grpc::ServerContext *, - const ::google::protobuf::Empty *, - ServerWriter *writer) -{ - GearMsg msg; - volatile bool vehicleRequired = true; - while (vehicleRequired) { - if (rpm < 3800) { - msg.set_rpm(rpm += 200); - } else { - msg.set_rpm(rpm -= 200); - } - std::this_thread::sleep_for(vehicleTimeout); - vehicleRequired = writer->Write(msg); - } - - return Status(); -} - -void VehicleServer::run() -{ - std::string serverUri("127.0.0.1:50051"); - VehicleServiceServiceImpl vehicleService; - NaviServiceServiceImpl naviService; - - grpc::ServerBuilder builder; - builder.AddListeningPort(serverUri, grpc::InsecureServerCredentials()); - builder.RegisterService(&vehicleService); - builder.RegisterService(&naviService); - - std::unique_ptr server(builder.BuildAndStart()); - if (!server) { - std::cout << "Creating grpc::Server failed." << std::endl; - return; - } - - std::cout << "Server listening on " << serverUri << std::endl; - server->Wait(); -} diff --git a/examples/grpc/vehicle/grpc_vehicle_server/serverrunner.h b/examples/grpc/vehicle/grpc_vehicle_server/serverrunner.h deleted file mode 100644 index a92ea6dd..00000000 --- a/examples/grpc/vehicle/grpc_vehicle_server/serverrunner.h +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright (C) 2024 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause - -#ifndef SERVER_RUNNER_H -#define SERVER_RUNNER_H - -class VehicleServer -{ -public: - void run(); -}; - -#endif // SERVER_RUNNER_H diff --git a/examples/grpc/vehicle/left.png b/examples/grpc/vehicle/left.png deleted file mode 100644 index 5b11fb0cf2a6243c51d8a175f9b1e75f334c0afb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1211 zcmV;s1VsCZP)EX>4Tx04R}tkv&MmKpe$iQ>7x+4t5Z6$WT893!);9T7@E12(?114kp)6Xws0R zxHt-~1qVMCs}3&Cx;nTDg5U>;vxAeOiU6f~epZjz4WOF71d?N8I(+!JwgLrz= z(mC%Fhgnh15}y-~8FWG7N3JU_zi}=&Ebz>*kxI@Jhl#~P8!K(hiiS!&MI6bh8s!UV zm$RI=IIHCDp!3DHKZbz7F3_mi_V=-EH%I?R8bVizu!0S z&8Xuq^j#&hLL%o)8W9LW5wuEZ6D_Pw(5fJ)MNmYxQCk)5YH1KbyP($n0l9GzWa(&O zE7O_L0yUhu@4eH)788**jlTK$UElleIiGj$Ip2GMQq%l=b9g5Gz?@AjhG$Zbf?C!r ziR48Lb1rq2KCi(;OeiD(fz($8h;Sl(27(buqz3_-VDVxJ9>lxle#_!Gz#$Q7_{(Tk z7Ko`uI~=vfL7W$$J&&2u@&_RjHfL6E(D1JS=#l*Yu7FHhvnfM6$}c+*)?D%eJl_`v$T5;;91x8TUl;Lx1fcl zq#@Fn9`V3S)LwT0MZL7ThB>qHjP!VcSBr=`6DIwk!1Fga2 zU0nSNBC&YdLU;o3yRqeRt474a!hvum`qBrF5(TxpvX*IO8YczVLsYj6)Kmje15pF9 z4G10GJW0x15`Hy<_}xeN;U|FX1;OeCk?Sl!ByS6OM~Idp#kL7x6u?vMsO5s62WC_9 zUo|3gxZ{2{u3ELDDB?c{kU_9so5=4qYxj$#8}GcR9aX2-l_EGPpchbVYd|oyE5EKW zz;xEJkGqa-YAbXC#Aoc@Hn9LSlqQH=9c+7}W2#3x$_RO(%OYIa1tebqc%=;@x2pBE zO}KH})^SCIllIe1oJ!ZBGZ*+u?M>5vX1J{I(A5F{~UlX|2VU+a_<^PI$vo=b!kVD2FFOQ zZ8Lg9JIbw05IM^B+N0T+-K=A_C5F5JkS$hxrE&WtUAMPmSUW1BL*H5`{zCzl2yO%w Z!Vk-p=tfp|gU + +#include +#include +#include +#include + +using namespace qtgrpc::examples; +using namespace google::protobuf; + +NavigationThread::NavigationThread(QObject *parent) : QThread(parent) +{ +} + +NavigationThread::~NavigationThread() = default; + +void NavigationThread::run() +{ + if (!m_client) { + auto channel = std::shared_ptr< + QAbstractGrpcChannel>(new QGrpcHttp2Channel(QUrl("http://localhost:50051", + QUrl::StrictMode))); + m_client = std::make_shared(); + m_client->attachChannel(channel); + } + + Empty request; + m_stream = m_client->getNavigationStream(request); + + connect(m_stream.get(), &QGrpcServerStream::messageReceived, this, [this] { + const auto result = m_stream->read(); + if (!result) + return; + + emit totalDistanceChanged(result->totalDistance()); + emit traveledDistanceChanged(result->traveledDistance()); + emit directionChanged(result->direction()); + emit streetChanged(result->street()); + }); + + connect( + m_stream.get(), &QGrpcServerStream::finished, this, + [this](const QGrpcStatus &status) { + if (!status.isOk()) { + auto error = QString("Stream error fetching navigation %1 (%2)") + .arg(status.message()) + .arg(QVariant::fromValue(status.code()).toString()); + emit connectionError(error); + qWarning() << error; + return; + } + + emit totalDistanceChanged(0); + emit traveledDistanceChanged(0); + emit directionChanged(DirectionEnumGadget::DirectionEnum::BACKWARD); + m_stream.reset(); + }, + Qt::SingleShotConnection); + + QThread::run(); + + // Delete the NavigationService::Client object to shut down the connection + m_client.reset(); +} diff --git a/examples/grpc/vehicle/navigationthread.h b/examples/grpc/vehicle/navigationthread.h new file mode 100644 index 00000000..b540694d --- /dev/null +++ b/examples/grpc/vehicle/navigationthread.h @@ -0,0 +1,39 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#ifndef NAVIGATIONTHREAD_H +#define NAVIGATIONTHREAD_H + +#include +#include +#include +#include + +namespace qtgrpc::examples { + +class NavigationThread : public QThread +{ + Q_OBJECT + +public: + explicit NavigationThread(QObject *parent = nullptr); + ~NavigationThread() override; + + void run() override; + +signals: + void totalDistanceChanged(int distance); + void traveledDistanceChanged(int distance); + void directionChanged(qtgrpc::examples::DirectionEnumGadget::DirectionEnum direction); + void streetChanged(QString street); + + void connectionError(QString error); + +private: + std::unique_ptr m_stream; + std::shared_ptr m_client; +}; + +} + +#endif // NAVIGATIONTHREAD_H diff --git a/examples/grpc/vehicle/navithread.cpp b/examples/grpc/vehicle/navithread.cpp deleted file mode 100644 index 6869efb1..00000000 --- a/examples/grpc/vehicle/navithread.cpp +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright (C) 2024 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause - -#include "navithread.h" -#include - -#include -#include -#include -#include - -using namespace qtgrpc::examples; -using namespace google::protobuf; -NaviThread::NaviThread(QObject *parent) : QThread(parent) -{ -} - -NaviThread::~NaviThread() = default; - -void NaviThread::run() -{ - if (!m_client) { - auto channel = std::shared_ptr< - QAbstractGrpcChannel>(new QGrpcHttp2Channel(QUrl("http://localhost:50051", - QUrl::StrictMode))); - m_client = std::make_shared(); - m_client->attachChannel(channel); - } - - Empty request; - m_stream = m_client->getNaviStream(request); - connect(m_stream.get(), &QGrpcServerStream::messageReceived, this, [this] { - const auto result = m_stream->read(); - if (!result) - return; - emit totalDistanceChanged(result->totalDistance()); - emit remainingDistanceChanged(result->remainingDistance()); - emit directionChanged(result->direction()); - }); - - connect(m_stream.get(), &QGrpcServerStream::finished, this, [this] (const QGrpcStatus &status) { - if (status.code() != QtGrpc::StatusCode::Ok) - qWarning() << "Stream error(" << status.code() << "):" << status.message(); - - emit totalDistanceChanged(0); - emit remainingDistanceChanged(0); - emit directionChanged(DirectionEnumGadget::DirectionEnum::BACKWARD); - }); - - QThread::run(); -} diff --git a/examples/grpc/vehicle/navithread.h b/examples/grpc/vehicle/navithread.h deleted file mode 100644 index 7cc0aa0c..00000000 --- a/examples/grpc/vehicle/navithread.h +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (C) 2024 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause - -#ifndef NAVITHREAD_H -#define NAVITHREAD_H - -#include -#include -#include - -QT_BEGIN_NAMESPACE -class QGrpcServerStream; -QT_END_NAMESPACE - -class NaviThread : public QThread -{ - Q_OBJECT - -public: - explicit NaviThread(QObject *parent = nullptr); - ~NaviThread() override; - - void run() override; -signals: - void totalDistanceChanged(int distance); - void remainingDistanceChanged(int distance); - void directionChanged(qtgrpc::examples::DirectionEnumGadget::DirectionEnum direction); - -private: - std::shared_ptr m_stream; - std::shared_ptr m_client; -}; - -#endif // NAVITHREAD_H diff --git a/examples/grpc/vehicle/proto/naviservice.proto b/examples/grpc/vehicle/proto/navigationservice.proto similarity index 60% rename from examples/grpc/vehicle/proto/naviservice.proto rename to examples/grpc/vehicle/proto/navigationservice.proto index 97d22da4..1e7f82d8 100644 --- a/examples/grpc/vehicle/proto/naviservice.proto +++ b/examples/grpc/vehicle/proto/navigationservice.proto @@ -5,12 +5,7 @@ syntax = "proto3"; package qtgrpc.examples; import "google/protobuf/empty.proto"; -message DistanceMsg { - int32 totalDistance = 1; - int32 remainingDistance = 2; - DirectionEnum direction = 3; -} - +//! [Proto types] enum DirectionEnum { RIGHT = 0; LEFT = 1; @@ -18,6 +13,14 @@ enum DirectionEnum { BACKWARD = 3; } -service NaviService { - rpc getNaviStream(google.protobuf.Empty) returns (stream DistanceMsg) {} +message NavigationMsg { + int32 totalDistance = 1; + int32 traveledDistance = 2; + DirectionEnum direction = 3; + string street = 4; } + +service NavigationService { + rpc getNavigationStream(google.protobuf.Empty) returns (stream NavigationMsg) {} +} +//! [Proto types] diff --git a/examples/grpc/vehicle/proto/vehicleservice.proto b/examples/grpc/vehicle/proto/vehicleservice.proto index 5055cc29..947e6958 100644 --- a/examples/grpc/vehicle/proto/vehicleservice.proto +++ b/examples/grpc/vehicle/proto/vehicleservice.proto @@ -5,20 +5,23 @@ syntax = "proto3"; package qtgrpc.examples; import "google/protobuf/empty.proto"; + +//! [Proto types] message SpeedMsg { int32 speed = 1; } -message GearMsg { +message RpmMsg { int32 rpm = 1; } -message FuelLevelMsg { - int32 fuelLevel = 1; +message AutonomyMsg { + int32 autonomy = 1; } service VehicleService { rpc getSpeedStream(google.protobuf.Empty) returns (stream SpeedMsg) {} - rpc getGearStream(google.protobuf.Empty) returns (stream GearMsg) {} - rpc getFuelLevel(google.protobuf.Empty) returns (FuelLevelMsg) {} + rpc getRpmStream(google.protobuf.Empty) returns (stream RpmMsg) {} + rpc getAutonomy(google.protobuf.Empty) returns (AutonomyMsg) {} } +//! [Proto types] diff --git a/examples/grpc/vehicle/resources/direction_left.svg b/examples/grpc/vehicle/resources/direction_left.svg new file mode 100644 index 00000000..784925ec --- /dev/null +++ b/examples/grpc/vehicle/resources/direction_left.svg @@ -0,0 +1,3 @@ + + + diff --git a/examples/grpc/vehicle/resources/direction_right.svg b/examples/grpc/vehicle/resources/direction_right.svg new file mode 100644 index 00000000..57ec522b --- /dev/null +++ b/examples/grpc/vehicle/resources/direction_right.svg @@ -0,0 +1,3 @@ + + + diff --git a/examples/grpc/vehicle/resources/direction_straight.svg b/examples/grpc/vehicle/resources/direction_straight.svg new file mode 100644 index 00000000..0122e4c8 --- /dev/null +++ b/examples/grpc/vehicle/resources/direction_straight.svg @@ -0,0 +1,3 @@ + + + diff --git a/examples/grpc/vehicle/resources/fuel_icon.svg b/examples/grpc/vehicle/resources/fuel_icon.svg new file mode 100644 index 00000000..ea857793 --- /dev/null +++ b/examples/grpc/vehicle/resources/fuel_icon.svg @@ -0,0 +1,4 @@ + + + diff --git a/examples/grpc/vehicle/right.png b/examples/grpc/vehicle/right.png deleted file mode 100644 index c1433b4a841c7da231b8b5687e75dec3418c1555..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1197 zcmV;e1XBBnP)EX>4Tx04R}tkv&MmKpe$iQ>7x+4t5Z6$WT893!);9T7@E12(?114kp)6Xws0R zxHt-~1qVMCs}3&Cx;nTDg5U>;vxAeOiU6f~epZjz4WOF71d?N8I(+!JwgLrz= z(mC%Fhgnh15}y-~8FWG7N3JU_zi}=&Ebz>*kxI@Jhl#~P8!K(hiiS!&MI6bh8s!UV zm$RI=IIHCDp!3DHKZbz7F3_mi_V=-EH%8-#0;)+wK~!ko?U>I?RY4TTzvtdN zSMf)b`pjGf<&VgAZtM?e-NIeaCYLRP+N3Q?0)rL>L9GHexP$awmE<9)t&)@NqUeCGW4GJqxX19jJvYQYZ{hcGj91PL?8)PT+fCF zgj6h~n#YI5i-$QQrt0_QM5&sB4!!#bMyAs1s=+mG*+AQ83q) zItUNIRw3(&%mqIku8BZxh?&fTQUy^B;B(=V2LPc4FZ#?vQUc)uIJUo~ z1`u&$?8Z-oR7RxLEIuutPdW$(z$O4)tPTXmNulGno(yxo4Bug#o01Bm{vTSR7(`qK z_lP>BgjCyJusEEw@wxP?%q@*60H`8zE(xjF57bK4$ISH|y@ga3gG+!h7@)7(iLRid z^>z7z_(R&j0Q^3x$@Z2th~I3%sW7nLEGr-?ASxhAWs<@WctTgL_jqM@dtwHhIgP0A zcO6k4AP69X6^i*zwyXKxPX7e|HQkw_LgNrpxd8eB+yLNPOMGCP*&WGsHeSx>!^Bop zwE#r%52862{4~3s8bmj3=DW^jX%thzt^oW}3-2aQHGIkKVFQ~Wa{FBOSq8lkE)8s* zZX7FgJDVWZer5&j{q8W#n1IVey1n_?e$urGqGgW|7{DwW=dnK1a34fbF@m!KLU*Q& z2r|teqFucH5*W{;4c(9K*09(y+RTWJJbX3*9@U!raqjq}O{tNDsgWbl7zl$=N7Ji= z(p(3nEE$Y6JNz)$o_Yi#g9A2eAPBT&fCYzXIP$*TMqArzl2rH)MuG_ucY?Pi00000 LNkvXXu0mjf3GW}q diff --git a/examples/grpc/vehicle/grpc_vehicle_server/CMakeLists.txt b/examples/grpc/vehicle/server/CMakeLists.txt similarity index 70% rename from examples/grpc/vehicle/grpc_vehicle_server/CMakeLists.txt rename to examples/grpc/vehicle/server/CMakeLists.txt index 3dabe89f..c64aa195 100644 --- a/examples/grpc/vehicle/grpc_vehicle_server/CMakeLists.txt +++ b/examples/grpc/vehicle/server/CMakeLists.txt @@ -2,18 +2,16 @@ # SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause cmake_minimum_required(VERSION 3.16) -project(VehicleServerRunner LANGUAGES CXX) +project(VehicleServer LANGUAGES CXX) set(CMAKE_FIND_PACKAGE_PREFER_CONFIG TRUE) -# Qt6::Grpc module is not used directly in this project. But this allows to find Qt6::Grpc's -# dependencies without setting extra cmake module paths. find_package(protobuf) find_package(gRPC) if(NOT TARGET gRPC::grpc_cpp_plugin OR NOT TARGET WrapProtoc::WrapProtoc OR NOT TARGET gRPC::grpc++) - message(WARNING "Dependencies of SimpleVehicleServer not found. Skipping.") + message(WARNING "Dependencies of VehicleServer not found. Skipping.") return() endif() @@ -23,16 +21,19 @@ if(MINGW) " guaranteed otherwise.") endif() +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + set(proto_files "${CMAKE_CURRENT_LIST_DIR}/../proto/vehicleservice.proto" - "${CMAKE_CURRENT_LIST_DIR}/../proto/naviservice.proto") + "${CMAKE_CURRENT_LIST_DIR}/../proto/navigationservice.proto") set(out_dir ${CMAKE_CURRENT_BINARY_DIR}) set(generated_files "${out_dir}/vehicleservice.pb.h" "${out_dir}/vehicleservice.pb.cc" "${out_dir}/vehicleservice.grpc.pb.h" "${out_dir}/vehicleservice.grpc.pb.cc" - "${out_dir}/naviservice.pb.h" "${out_dir}/naviservice.pb.cc" - "${out_dir}/naviservice.grpc.pb.h" "${out_dir}/naviservice.grpc.pb.cc") + "${out_dir}/navigationservice.pb.h" "${out_dir}/navigationservice.pb.cc" + "${out_dir}/navigationservice.grpc.pb.h" "${out_dir}/navigationservice.grpc.pb.cc") add_custom_command( OUTPUT ${generated_files} @@ -53,24 +54,22 @@ add_custom_command( set_source_files_properties(${generated_files} PROPERTIES GENERATED TRUE) -add_executable(SimpleVehicleServer +qt_add_executable(vehicle_server ${generated_files} - ${CMAKE_CURRENT_LIST_DIR}/serverrunner.cpp - ${CMAKE_CURRENT_LIST_DIR}/serverrunner.h ${CMAKE_CURRENT_LIST_DIR}/main.cpp ) -target_include_directories(SimpleVehicleServer +target_include_directories(vehicle_server PRIVATE ${out_dir} ) -target_link_libraries(SimpleVehicleServer PRIVATE +target_link_libraries(vehicle_server PRIVATE protobuf::libprotobuf gRPC::grpc++ ) -install(TARGETS SimpleVehicleServer +install(TARGETS vehicle_server RUNTIME DESTINATION "${INSTALL_EXAMPLEDIR}" BUNDLE DESTINATION "${INSTALL_EXAMPLEDIR}" LIBRARY DESTINATION "${INSTALL_EXAMPLEDIR}" diff --git a/examples/grpc/vehicle/server/main.cpp b/examples/grpc/vehicle/server/main.cpp new file mode 100644 index 00000000..0216d0c1 --- /dev/null +++ b/examples/grpc/vehicle/server/main.cpp @@ -0,0 +1,157 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#include "navigationservice.grpc.pb.h" +#include "vehicleservice.grpc.pb.h" + +#include + +#include +#include +#include +#include + +using namespace qtgrpc::examples; + +class VehicleServiceImpl final : public VehicleService::Service +{ + constexpr static auto speedStreamPeriod = std::chrono::milliseconds(250); + constexpr static auto rpmStreamPeriod = std::chrono::milliseconds(400); + + int speed = 0; + int rpm = 900; + int autonomy = 162; + + grpc::Status getSpeedStream(grpc::ServerContext *, const ::google::protobuf::Empty *, + grpc::ServerWriter *writer) override + { + SpeedMsg msg; + bool speedRequired = true; + bool speedIncreasing = true; + + while (speedRequired) { + if (speed > 180) { + speedIncreasing = false; + } else if (speed < 60) { + speedIncreasing = true; + } + + int deltaSpeed = rand() % 6 + 1; // Random number between 1 and 6 + if (speedIncreasing) { + speed += deltaSpeed; + } else { + speed -= deltaSpeed; + } + + msg.set_speed(speed); + std::this_thread::sleep_for(speedStreamPeriod); + speedRequired = writer->Write(msg); + } + return grpc::Status(); + } + + grpc::Status getRpmStream(grpc::ServerContext *, const ::google::protobuf::Empty *, + grpc::ServerWriter *writer) override + { + RpmMsg msg; + bool rpmRequired = true; + bool rpmIncreasing = true; + while (rpmRequired) { + if (rpm > 4800) { + rpmIncreasing = false; + } else if (rpm < 2200) { + rpmIncreasing = true; + } + + if (rpmIncreasing) { + rpm += 100; + } else { + rpm -= 100; + } + + msg.set_rpm(rpm); + std::this_thread::sleep_for(rpmStreamPeriod); + rpmRequired = writer->Write(msg); + } + + return grpc::Status(); + } + + grpc::Status getAutonomy(grpc::ServerContext *, const ::google::protobuf::Empty *, + AutonomyMsg *response) override + { + response->set_autonomy(autonomy); + if (autonomy > 10) { + autonomy -= 1; + } + return grpc::Status(); + } +}; + +class NavigationServiceImpl final : public NavigationService::Service +{ + constexpr static std::chrono::seconds navigationPeriod = std::chrono::seconds(1); + + int totaldistance = 1200; + inline static std::array streets = { "Friedrichstraße", "Kurfürstendamm", + "Erich-Thilo Straße" }; + + grpc::Status getNavigationStream(grpc::ServerContext *, const ::google::protobuf::Empty *, + grpc::ServerWriter *writer) override + { + volatile bool navigationRequired = true; + NavigationMsg msg; + msg.set_totaldistance(totaldistance); + + for (auto street : streets) { + msg.set_street(street); + + int traveledDistance = 0; + msg.set_traveleddistance(traveledDistance); + + while (navigationRequired) { + if (traveledDistance < totaldistance) { + traveledDistance += 50; + } else { + traveledDistance = 0; + } + msg.set_traveleddistance(traveledDistance); + + if (traveledDistance <= 300) { + msg.set_direction(qtgrpc::examples::DirectionEnum::STRAIGHT); + } else if (traveledDistance <= 600) { + msg.set_direction(qtgrpc::examples::DirectionEnum::RIGHT); + } else if (traveledDistance <= 900) { + msg.set_direction(qtgrpc::examples::DirectionEnum::LEFT); + } + + std::this_thread::sleep_for(navigationPeriod); + navigationRequired = writer->Write(msg); + } + } + + return grpc::Status(); + } +}; + +int main() +{ + VehicleServiceImpl vehicleService; + NavigationServiceImpl navigationService; + + grpc::ServerBuilder builder; + builder.AddListeningPort("127.0.0.1:50051", grpc::InsecureServerCredentials()); + builder.RegisterService(&vehicleService); + builder.RegisterService(&navigationService); + + std::unique_ptr server(builder.BuildAndStart()); + if (!server) { + std::cerr << "Creating grpc::Server failed."; + return -1; + } + + std::cout << "Server listening on port 50051"; + server->Wait(); + + return 0; +} diff --git a/examples/grpc/vehicle/vehiclemanager.cpp b/examples/grpc/vehicle/vehiclemanager.cpp new file mode 100644 index 00000000..392562d9 --- /dev/null +++ b/examples/grpc/vehicle/vehiclemanager.cpp @@ -0,0 +1,99 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#include "vehiclemanager.h" +#include "navigationthread.h" +#include "vehiclethread.h" + +using namespace qtgrpc::examples; + +VehicleManager::VehicleManager(QObject *parent) : QObject(parent) +{ + startVehicleClient(); + startNavigationClient(); +} + +VehicleManager::~VehicleManager() +{ + if (m_vehicleThread && m_vehicleThread->isRunning()) { + m_vehicleThread->quit(); + m_vehicleThread->wait(); + } + + if (m_navigationThread && m_navigationThread->isRunning()) { + m_navigationThread->quit(); + m_navigationThread->wait(); + } +} + +void VehicleManager::startNavigationClient() +{ + m_navigationThread = std::make_unique(); + + connect(m_navigationThread.get(), &NavigationThread::totalDistanceChanged, this, + &VehicleManager::totalDistanceChanged, Qt::QueuedConnection); + connect(m_navigationThread.get(), &NavigationThread::traveledDistanceChanged, this, + &VehicleManager::traveledDistanceChanged, Qt::QueuedConnection); + + connect( + m_navigationThread.get(), &NavigationThread::directionChanged, this, + [this](qtgrpc::examples::DirectionEnumGadget::DirectionEnum direction) { + VehicleManager::directionChanged(qToUnderlying(direction)); + }, + Qt::QueuedConnection); + + connect(m_navigationThread.get(), &NavigationThread::streetChanged, this, + &VehicleManager::streetChanged, Qt::QueuedConnection); + + connect(m_navigationThread.get(), &NavigationThread::connectionError, this, + &VehicleManager::addNavigationError, Qt::QueuedConnection); + + m_navigationThread->start(); +} + +void VehicleManager::startVehicleClient() +{ + m_vehicleThread = std::make_unique(); + + connect(m_vehicleThread.get(), &VehicleThread::speedChanged, this, + &VehicleManager::speedChanged, Qt::QueuedConnection); + connect(m_vehicleThread.get(), &VehicleThread::autonomyChanged, this, + &VehicleManager::autonomyChanged, Qt::QueuedConnection); + connect(m_vehicleThread.get(), &VehicleThread::rpmChanged, this, &VehicleManager::rpmChanged, + Qt::QueuedConnection); + + connect(m_vehicleThread.get(), &VehicleThread::connectionError, this, + &VehicleManager::addVehicleError, Qt::QueuedConnection); + + m_vehicleThread->start(); +} + +void VehicleManager::addVehicleError(QString error) +{ + m_vehicleErrors = m_vehicleErrors.isEmpty() ? error : m_vehicleErrors + "\n" + error; + emit vehicleErrorsChanged(m_vehicleErrors); +} + +void VehicleManager::addNavigationError(QString error) +{ + m_navigationErrors = m_navigationErrors.isEmpty() ? error : m_navigationErrors + "\n" + error; + emit navigationErrorsChanged(m_navigationErrors); +} + +void VehicleManager::restart() +{ + m_vehicleErrors = QString(); + m_navigationErrors = QString(); + + if (m_vehicleThread->isRunning()) { + m_vehicleThread->quit(); + m_vehicleThread->wait(); + m_vehicleThread->start(); + } + + if (m_navigationThread->isRunning()) { + m_navigationThread->quit(); + m_navigationThread->wait(); + m_navigationThread->start(); + } +} diff --git a/examples/grpc/vehicle/vehiclemanager.h b/examples/grpc/vehicle/vehiclemanager.h new file mode 100644 index 00000000..a5d6b8c2 --- /dev/null +++ b/examples/grpc/vehicle/vehiclemanager.h @@ -0,0 +1,68 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#ifndef VEHICLEMANAGER_H +#define VEHICLEMANAGER_H + +#include +#include +#include +#include +#include +#include +#include "navigationservice_client.grpc.qpb.h" +#include "navigationservice.qpb.h" +#include "navigationthread.h" +#include "vehiclethread.h" + +class VehicleManager : public QObject +{ + Q_OBJECT + QML_ELEMENT + QML_SINGLETON + +public: + enum NavigationDirection { + RIGHT = qToUnderlying(qtgrpc::examples::DirectionEnumGadget::DirectionEnum::RIGHT), + LEFT = qToUnderlying(qtgrpc::examples::DirectionEnumGadget::DirectionEnum::LEFT), + STRAIGHT = qToUnderlying(qtgrpc::examples::DirectionEnumGadget::DirectionEnum::STRAIGHT), + BACKWARD = qToUnderlying(qtgrpc::examples::DirectionEnumGadget::DirectionEnum::BACKWARD) + }; + + Q_ENUM(NavigationDirection); + + explicit VehicleManager(QObject *parent = nullptr); + ~VehicleManager() override; + + Q_INVOKABLE void restart(); + + void addVehicleError(QString error); + void addNavigationError(QString error); + void removeErrors(); + +signals: + void speedChanged(int speed); + void rpmChanged(int rpm); + void autonomyChanged(int level); + + void totalDistanceChanged(int distance); + void traveledDistanceChanged(int distance); + void directionChanged(int direction); + void streetChanged(QString street); + + void vehicleErrorsChanged(QString vehicleErrors); + void navigationErrorsChanged(QString navigationErrors); + +private: + void clearErrors(); + void startVehicleClient(); + void startNavigationClient(); + + std::unique_ptr m_navigationThread; + std::unique_ptr m_vehicleThread; + + QString m_vehicleErrors; + QString m_navigationErrors; +}; + +#endif // VEHICLEMANAGER_H diff --git a/examples/grpc/vehicle/vehiclethread.cpp b/examples/grpc/vehicle/vehiclethread.cpp index 40e59287..69a63915 100644 --- a/examples/grpc/vehicle/vehiclethread.cpp +++ b/examples/grpc/vehicle/vehiclethread.cpp @@ -12,6 +12,7 @@ using namespace qtgrpc::examples; using namespace google::protobuf; + VehicleThread::VehicleThread(QObject *parent) : QThread(parent) { } @@ -21,51 +22,83 @@ VehicleThread::~VehicleThread() = default; void VehicleThread::run() { if (!m_client) { - auto channel = std::shared_ptr< - QAbstractGrpcChannel>(new QGrpcHttp2Channel(QUrl("http://localhost:50051", - QUrl::StrictMode))); - m_client = std::make_shared(); - m_client->attachChannel(channel); + m_client = std::make_unique(); + m_client + ->attachChannel(std::make_shared(QUrl("http://localhost:50051"))); } - Empty fuelLvlRequest; - std::shared_ptr replyFuel = m_client->getFuelLevel(fuelLvlRequest); + //! [Speed stream] + Empty speedRequest; + m_streamSpeed = m_client->getSpeedStream(speedRequest); - connect(replyFuel.get(), &QGrpcCallReply::finished, [replyFuel, this] (const QGrpcStatus &status) { - if (status.code() == QtGrpc::StatusCode::Ok) { - if (const auto fuelLvl = replyFuel->read()) - emit fuelLevelChanged(fuelLvl->fuelLevel()); - } else { - emit connectionError(true); - emit fuelLevelChanged(0); + connect(m_streamSpeed.get(), &QGrpcServerStream::messageReceived, this, [this]() { + if (const auto speedResponse = m_streamSpeed->read()) { + emit speedChanged(speedResponse->speed()); } }); - Empty speedRequest; - m_streamSpeed = m_client->getSpeedStream(speedRequest); - connect(m_streamSpeed.get(), &QGrpcServerStream::messageReceived, this, [this] { - if (const auto speedResponse = m_streamSpeed->read()) - emit speedChanged(speedResponse->speed()); + connect( + m_streamSpeed.get(), &QGrpcServerStream::finished, this, + [this](const QGrpcStatus &status) { + if (!status.isOk()) { + auto error = QString("Stream error fetching speed %1 (%2)") + .arg(status.message()) + .arg(QVariant::fromValue(status.code()).toString()); + emit connectionError(error); + qWarning() << error; + return; + } + }, + Qt::SingleShotConnection); + //! [Speed stream] + + Empty rpmRequest; + m_streamRpm = m_client->getRpmStream(rpmRequest); + connect(m_streamRpm.get(), &QGrpcServerStream::messageReceived, this, [this]() { + if (const auto rpmResponse = m_streamRpm->read()) { + emit rpmChanged(rpmResponse->rpm()); + } }); - connect(m_streamSpeed.get(), &QGrpcServerStream::finished, this, - [this](const QGrpcStatus &status) { - emit speedChanged(0); - if (status.code() != QtGrpc::StatusCode::Ok) { - emit connectionError(true); - emit fuelLevelChanged(0); - qWarning() << "Stream error(" << status.code() << "):" << status.message(); - } - }); + connect( + m_streamRpm.get(), &QGrpcServerStream::finished, this, + [this](const QGrpcStatus &status) { + if (!status.isOk()) { + auto error = QString("Stream error fetching RPM %1 (%2)") + .arg(status.message()) + .arg(QVariant::fromValue(status.code()).toString()); + emit connectionError(error); + qWarning() << error; + return; + } + }, + Qt::SingleShotConnection); - Empty gearRequest; - m_streamGear = m_client->getGearStream(gearRequest); - connect(m_streamGear.get(), &QGrpcServerStream::messageReceived, this, [this] { - if (const auto gearResponse = m_streamGear->read()) - emit rpmChanged(gearResponse->rpm()); - }); + //! [Autonomy call] + Empty autonomyRequest; + std::unique_ptr autonomyReply = m_client->getAutonomy(autonomyRequest); + const auto *autonomyReplyPtr = autonomyReply.get(); + connect( + autonomyReplyPtr, &QGrpcCallReply::finished, this, + [this, autonomyReply = std::move(autonomyReply)](const QGrpcStatus &status) { + if (!status.isOk()) { + auto error = QString("Call error fetching autonomy %1 (%2)") + .arg(status.message()) + .arg(QVariant::fromValue(status.code()).toString()); + emit connectionError(error); + qWarning() << error; + return; + } - connect(m_streamGear.get(), &QGrpcServerStream::finished, this, [this] { emit rpmChanged(0); }); + if (const auto autonomyMsg = autonomyReply->read()) { + emit autonomyChanged(autonomyMsg->autonomy()); + } + }, + Qt::SingleShotConnection); + //! [Autonomy call] QThread::run(); + + // Delete the VehicleService::Client object to shut down the connection + m_client.reset(); } diff --git a/examples/grpc/vehicle/vehiclethread.h b/examples/grpc/vehicle/vehiclethread.h index 91efba1f..11359bf0 100644 --- a/examples/grpc/vehicle/vehiclethread.h +++ b/examples/grpc/vehicle/vehiclethread.h @@ -4,20 +4,11 @@ #ifndef VEHICLETHREAD_H #define VEHICLETHREAD_H +#include "vehicleservice_client.grpc.qpb.h" #include #include -QT_BEGIN_NAMESPACE -class QGrpcServerStream; -QT_END_NAMESPACE - -namespace qtgrpc { -namespace examples { -namespace VehicleService { -class Client; -} -} -} +namespace qtgrpc::examples { class VehicleThread : public QThread { @@ -26,17 +17,22 @@ class VehicleThread : public QThread public: explicit VehicleThread(QObject *parent = nullptr); ~VehicleThread() override; + void run() override; + signals: void speedChanged(int speed); void rpmChanged(int rpm); - void fuelLevelChanged(int level); - void connectionError(bool value); + void autonomyChanged(int level); + + void connectionError(QString error); private: - std::shared_ptr m_client; - std::shared_ptr m_streamSpeed; - std::shared_ptr m_streamGear; + std::unique_ptr m_client; + std::unique_ptr m_streamSpeed; + std::unique_ptr m_streamRpm; }; +} + #endif // VEHICLETHREAD_H