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 00000000..ba83a9e2 Binary files /dev/null and b/examples/grpc/vehicle/doc/images/vehicle.webp differ 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 0a187c44..00000000 Binary files a/examples/grpc/vehicle/forward.png and /dev/null differ diff --git a/examples/grpc/vehicle/fuel_lvl.png b/examples/grpc/vehicle/fuel_lvl.png deleted file mode 100644 index de1abe24..00000000 Binary files a/examples/grpc/vehicle/fuel_lvl.png and /dev/null differ 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 5b11fb0c..00000000 Binary files a/examples/grpc/vehicle/left.png and /dev/null differ diff --git a/examples/grpc/vehicle/navigationthread.cpp b/examples/grpc/vehicle/navigationthread.cpp new file mode 100644 index 00000000..ef90ace0 --- /dev/null +++ b/examples/grpc/vehicle/navigationthread.cpp @@ -0,0 +1,68 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#include "navigationthread.h" +#include + +#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 c1433b4a..00000000 Binary files a/examples/grpc/vehicle/right.png and /dev/null differ 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