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 <alexey.edelev@qt.io>
Reviewed-by: Jaime Resano <Jaime.RESANO-AISA@qt.io>
This commit is contained in:
Jaime Resano 2024-10-21 13:16:50 +02:00
parent 5242c14faf
commit 524ba1e4d0
32 changed files with 939 additions and 788 deletions

View File

@ -2,7 +2,7 @@
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause # SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
cmake_minimum_required(VERSION 3.16) cmake_minimum_required(VERSION 3.16)
project(vehicle_cluster LANGUAGES CXX) project(vehicle LANGUAGES CXX)
if(NOT DEFINED INSTALL_EXAMPLESDIR) if(NOT DEFINED INSTALL_EXAMPLESDIR)
set(INSTALL_EXAMPLESDIR "examples") set(INSTALL_EXAMPLESDIR "examples")
@ -20,58 +20,57 @@ find_package(Qt6 REQUIRED COMPONENTS
qt_standard_project_setup() qt_standard_project_setup()
add_subdirectory(grpc_vehicle_server) qt_add_executable(vehicle_client
qt_add_executable(vehicle_cluster
main.cpp main.cpp
clusterdatamanager.h vehiclemanager.h
clusterdatamanager.cpp vehiclemanager.cpp
vehiclethread.h vehiclethread.h
vehiclethread.cpp vehiclethread.cpp
navithread.h navigationthread.h
navithread.cpp navigationthread.cpp
) )
qt_add_protobuf(vehiclelib qt_add_protobuf(vehiclelib
PROTO_FILES PROTO_FILES
proto/vehicleservice.proto proto/vehicleservice.proto
proto/naviservice.proto proto/navigationservice.proto
) )
qt_add_grpc(vehiclelib CLIENT qt_add_grpc(vehiclelib CLIENT
PROTO_FILES PROTO_FILES
proto/vehicleservice.proto proto/vehicleservice.proto
proto/naviservice.proto proto/navigationservice.proto
) )
target_link_libraries(vehiclelib PRIVATE Qt6::ProtobufWellKnownTypes) target_link_libraries(vehiclelib PRIVATE Qt6::ProtobufWellKnownTypes)
set_target_properties(vehicle_cluster PROPERTIES set_target_properties(vehicle_client PROPERTIES
WIN32_EXECUTABLE TRUE WIN32_EXECUTABLE TRUE
MACOSX_BUNDLE TRUE MACOSX_BUNDLE TRUE
) )
qt_add_qml_module(vehicle_cluster qt_add_qml_module(vehicle_client
URI qtgrpc.examples.vehicle URI qtgrpc.examples.vehicle
VERSION 1.0 VERSION 1.0
RESOURCE_PREFIX "/qt/qml" RESOURCE_PREFIX "/qt/qml"
QML_FILES QML_FILES
"ClusterText.qml" "StyledText.qml"
"ClusterProgressBar.qml" "StyledProgressBar.qml"
"Main.qml" "Main.qml"
) )
qt_add_resources(vehicle_cluster qt_add_resources(vehicle_client
"vehicle_cluster" "resources"
PREFIX "/" PREFIX "/"
BASE "resources"
FILES FILES
"left.png" "resources/direction_left.svg"
"right.png" "resources/direction_right.svg"
"forward.png" "resources/direction_straight.svg"
"fuel_lvl.png" "resources/fuel_icon.svg"
) )
target_link_libraries(vehicle_cluster PRIVATE target_link_libraries(vehicle_client PRIVATE
Qt6::Core Qt6::Core
Qt6::Quick Qt6::Quick
Qt6::Protobuf Qt6::Protobuf
@ -80,8 +79,13 @@ target_link_libraries(vehicle_cluster PRIVATE
vehiclelib vehiclelib
) )
install(TARGETS vehicle_cluster vehiclelib install(TARGETS vehicle_client vehiclelib
RUNTIME DESTINATION "${INSTALL_EXAMPLEDIR}" RUNTIME DESTINATION "${INSTALL_EXAMPLEDIR}"
BUNDLE DESTINATION "${INSTALL_EXAMPLEDIR}" BUNDLE DESTINATION "${INSTALL_EXAMPLEDIR}"
LIBRARY DESTINATION "${INSTALL_EXAMPLEDIR}" LIBRARY DESTINATION "${INSTALL_EXAMPLEDIR}"
) )
add_subdirectory(server)
if(TARGET vehicle_server)
add_dependencies(vehicle_client vehicle_server)
endif()

View File

@ -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
}
}

View File

@ -3,299 +3,286 @@
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Controls.Material import QtQuick.Layouts
import QtQuick.VectorImage
import QtQuick.Controls.Basic
import qtgrpc.examples.vehicle import qtgrpc.examples.vehicle
ApplicationWindow { ApplicationWindow {
id: root id: root
width: 1280
height: 518
minimumWidth: width property int speed: -1
maximumWidth: width property int rpm: -1
minimumHeight: height property int totalDistance: -1
maximumHeight: height property int traveledDistance: -1
property int estimatedAutonomy: -1
property int speed: 0 property string directionImageSource: ""
property int rpm: 0 property string street: ""
property int totalDistance: 0 property string vehicleErrors: ""
property int remainingDistance: 0 property string navigationErrors: ""
property int direction: ClusterDataManager.BACKWARD
property int fuelLevel: 0
property bool availableBtn: false
width: 1200
height: 500
visible: true visible: true
title: qsTr("Cluster Qt GRPC Example") title: qsTr("Vehicle Qt GRPC example")
Material.theme: Material.Light
//! [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;
}
}
// Background
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
color: "#040a16" color: "#160404"
} }
Item { // Information screen
id: background GridLayout {
anchors.fill: parent anchors.fill: parent
visible: !root.availableBtn anchors.margins: 40
visible: !(root.vehicleErrors && root.navigationErrors)
Row { columnSpacing: 20
id: textsBar rowSpacing: 10
anchors.horizontalCenter: background.horizontalCenter columns: 2
anchors.bottom: progressBars.top uniformCellWidths: true
spacing: 130
width: root.width - 110
height: 200
Item { Item {
width: 300 Layout.fillHeight: true
height: 200 Layout.fillWidth: true
ClusterText { StyledText {
width: 200 id: speedText
height: 200
anchors.bottom: parent.bottom
anchors.left: parent.left anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
verticalAlignment: Text.AlignBottom text: root.speed == -1 ? "-" : root.speed
font.pointSize: 90 font.pointSize: 90
text: root.speed
} }
ClusterText { StyledText {
width: 100 id: speedUnitText
height: 200
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.bottomMargin: 27
anchors.right: parent.right anchors.right: parent.right
text: "Km/h"
verticalAlignment: Text.AlignBottom
horizontalAlignment: Text.AlignRight
text: "Kmph"
} }
} }
Item { Item {
width: 300 Layout.fillHeight: true
height: 200 Layout.fillWidth: true
Image { VectorImage {
id: arrow id: directionImage
source: root.directionImageSource
width: 100
height: 100
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left anchors.left: parent.left
anchors.bottom: parent.bottom
anchors.bottomMargin: 33
source: root.getImage()
width: implicitWidth
height: implicitHeight
Timer {
interval: 2000
running: arrow.source !== ""
repeat: true
onTriggered: arrow.visible = !arrow.visible
}
} }
ClusterText { Rectangle {
width: 200 visible: root.directionImageSource
height: 40 anchors.fill: directionImage
anchors.bottom: street.top color: "#363636"
radius: 10
z: -1
}
StyledText {
anchors.right: parent.right anchors.right: parent.right
anchors.bottom: streetText.top
verticalAlignment: Text.AlignBottom font.pointSize: 24
horizontalAlignment: Text.AlignRight horizontalAlignment: Text.AlignRight
text: Number(root.remainingDistance * 0.001).toFixed(2) + " km" text: {
if (root.totalDistance == -1 || root.traveledDistance == -1) {
return "- km";
} }
ClusterText { let remainingDistance = root.totalDistance - root.traveledDistance;
id: street
width: 200
height: 40
anchors.bottom: parent.bottom
anchors.bottomMargin: 27
anchors.right: parent.right
verticalAlignment: Text.AlignBottom if (remainingDistance > 1000) {
return `${Number(remainingDistance * 0.001).toFixed(2)} km`;
}
return `${remainingDistance} m`
}
}
StyledText {
id: streetText
anchors.right: parent.right
anchors.bottom: parent.bottom
horizontalAlignment: Text.AlignRight horizontalAlignment: Text.AlignRight
color: "#828284" color: "#828284"
text: (root.getImage() !== "") ? "Erich-Thilo St" : "None" text: root.street
}
}
StyledProgressBar {
Layout.fillWidth: true
value: root.speed
to: 200
activeColor: "#04e2ed"
bgColor: "#023061"
}
StyledProgressBar {
Layout.fillWidth: true
value: root.traveledDistance
to: root.totalDistance != -1 ? root.totalDistance : 0
activeColor: "#dde90000"
bgColor: "#860000"
}
Item {
Layout.fillHeight: true
Layout.fillWidth: true
StyledText {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
text: root.rpm == -1 ? "-" : root.rpm
font.pointSize: 60
}
StyledText {
anchors.bottom: parent.bottom
anchors.right: parent.right
text: "rpm"
} }
} }
Item { Item {
width: 300 Layout.fillHeight: true
height: 200 Layout.fillWidth: true
ClusterText { VectorImage {
id: rightSide id: fuelIcon
width: 200 width: 28
height: 200 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.bottom: parent.bottom
anchors.bottomMargin: 27
anchors.right: parent.right anchors.right: parent.right
verticalAlignment: Text.AlignBottom
horizontalAlignment: Text.AlignRight
text: root.rpm + " rpm"
}
} }
} }
Row { StyledProgressBar {
id: progressBars Layout.fillWidth: true
value: root.rpm
anchors.horizontalCenter: background.horizontalCenter to: 9000
anchors.verticalCenter: background.verticalCenter
anchors.verticalCenterOffset: 50
spacing: 130
width: root.width - 110
height: 20
ClusterProgressBar {
currentBarValue: root.speed;
totalBarValue: 200
activeColor: "#04e2ed"
bgColor: "#023061"
}
ClusterProgressBar {
currentBarValue: root.remainingDistance;
totalBarValue: root.totalDistance
activeColor: "#04e2ed"
bgColor: "#023061"
}
ClusterProgressBar {
currentBarValue: root.rpm;
totalBarValue: 9000
activeColor: "#f8c607" activeColor: "#f8c607"
bgColor: "#5f3f04" bgColor: "#5f3f04"
} }
}
ClusterProgressBar { StyledProgressBar {
id: fuelLevel Layout.fillWidth: true
anchors.leftMargin: 55 value: root.estimatedAutonomy
anchors.left: parent.left to: 250
anchors.topMargin: 100
anchors.top: progressBars.bottom
currentBarValue: root.fuelLevel
totalBarValue: 250
activeColor: "#05c848" activeColor: "#05c848"
bgColor: "#03511f" 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 { // No connection error screen
anchors.bottom: fuelLevel.bottom ColumnLayout {
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
width: 200
height: 80
font.pointSize: 14
visible: root.availableBtn
text: "Please, start server and press run."
}
Rectangle {
id: restartBtn
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: 60
width: 100
height: 50
radius: 80
color: "#040a16"
border.color: btn.btnPressed ? "#828284" : "white"
border.width: 2
visible: root.availableBtn
ClusterText {
anchors.centerIn: parent anchors.centerIn: parent
color: btn.btnPressed ? "#828284" : "white" visible: root.vehicleErrors && root.navigationErrors
text: "RUN"
StyledText {
Layout.alignment: Qt.AlignHCenter
font.pointSize: 14
text: qsTr("Please, start vehicle server. Then, press try again.")
} }
MouseArea { Button {
id: btn id: runButton
property bool btnPressed: false property string baseColor: "white"
anchors.fill: parent property string clickedColor: "#828284"
property string hoverColor: "#a9a9a9"
enabled: root.availableBtn Layout.alignment: Qt.AlignHCenter
onClicked: ClusterDataManager.restart()
onPressed: btnPressed = true background: Rectangle {
onReleased: btnPressed = false border.color: runButton.down ?
} runButton.clickedColor
} : (runButton.hovered ? runButton.hoverColor : runButton.baseColor)
border.width: 2
radius: 2
color: "transparent"
} }
function getImage() { contentItem: Text {
switch (root.direction) { text: qsTr("Try again")
case ClusterDataManager.RIGHT: font.pointSize: 16
return "qrc:/right.png" horizontalAlignment: Text.AlignHCenter
case ClusterDataManager.LEFT: verticalAlignment: Text.AlignVCenter
return "qrc:/left.png" color: runButton.down ?
case ClusterDataManager.STRAIGHT: runButton.clickedColor
return "qrc:/forward.png" : (runButton.hovered ? runButton.hoverColor : runButton.baseColor)
default:
return ""
}
} }
Connections { onClicked: {
target: ClusterDataManager root.vehicleErrors = "";
root.navigationErrors = "";
function onTotalDistanceChanged(distance) { VehicleManager.restart();
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
} }
} }
} }

View File

@ -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
}
}
}
}
}

View File

@ -4,9 +4,7 @@
import QtQuick import QtQuick
Text { Text {
wrapMode: Text.WordWrap
font.family: "Helvetica" font.family: "Helvetica"
color: "white" color: "white"
style: Text.Sunken
font.pointSize: 18 font.pointSize: 18
} }

View File

@ -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<NaviThread>();
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<VehicleThread>();
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();
}
}

View File

@ -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 <QtCore/QObject>
#include <QtCore/QtTypeTraits>
#include <QtQml/QQmlEngine>
#include <naviservice_client.grpc.qpb.h>
#include <memory>
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<NaviThread> m_naviThread;
std::unique_ptr<VehicleThread> m_vehicleThread;
bool m_threadsAvailable = false;
};
#endif // CLUSTERDATAMANAGER_H

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

@ -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.
*/

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 939 B

View File

@ -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 <memory>
int main()
{
auto server = std::make_unique<VehicleServer>();
server->run();
return 0;
}

View File

@ -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 <grpc++/grpc++.h>
#include <chrono>
#include <iostream>
#include <memory>
#include <thread>
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<SpeedMsg> *writer) override;
grpc::Status getGearStream(grpc::ServerContext *, const ::google::protobuf::Empty *,
ServerWriter<GearMsg> *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<DistanceMsg> *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<DistanceMsg> *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<SpeedMsg> *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<GearMsg> *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<grpc::Server> server(builder.BuildAndStart());
if (!server) {
std::cout << "Creating grpc::Server failed." << std::endl;
return;
}
std::cout << "Server listening on " << serverUri << std::endl;
server->Wait();
}

View File

@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -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 <navigationservice_client.grpc.qpb.h>
#include <QtCore/QDebug>
#include <QtCore/QUrl>
#include <QtGrpc/QGrpcChannelOptions>
#include <QtGrpc/QGrpcHttp2Channel>
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<qtgrpc::examples::NavigationService::Client>();
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<NavigationMsg>();
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();
}

View File

@ -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 <QtCore/QString>
#include <QtCore/QThread>
#include <memory>
#include <navigationservice_client.grpc.qpb.h>
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<QGrpcServerStream> m_stream;
std::shared_ptr<qtgrpc::examples::NavigationService::Client> m_client;
};
}
#endif // NAVIGATIONTHREAD_H

View File

@ -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 <naviservice_client.grpc.qpb.h>
#include <QtCore/QDebug>
#include <QtCore/QUrl>
#include <QtGrpc/QGrpcChannelOptions>
#include <QtGrpc/QGrpcHttp2Channel>
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<qtgrpc::examples::NaviService::Client>();
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<DistanceMsg>();
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();
}

View File

@ -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 <QtCore/QThread>
#include <memory>
#include <naviservice_client.grpc.qpb.h>
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<QGrpcServerStream> m_stream;
std::shared_ptr<qtgrpc::examples::NaviService::Client> m_client;
};
#endif // NAVITHREAD_H

View File

@ -5,12 +5,7 @@ syntax = "proto3";
package qtgrpc.examples; package qtgrpc.examples;
import "google/protobuf/empty.proto"; import "google/protobuf/empty.proto";
message DistanceMsg { //! [Proto types]
int32 totalDistance = 1;
int32 remainingDistance = 2;
DirectionEnum direction = 3;
}
enum DirectionEnum { enum DirectionEnum {
RIGHT = 0; RIGHT = 0;
LEFT = 1; LEFT = 1;
@ -18,6 +13,14 @@ enum DirectionEnum {
BACKWARD = 3; BACKWARD = 3;
} }
service NaviService { message NavigationMsg {
rpc getNaviStream(google.protobuf.Empty) returns (stream DistanceMsg) {} int32 totalDistance = 1;
int32 traveledDistance = 2;
DirectionEnum direction = 3;
string street = 4;
} }
service NavigationService {
rpc getNavigationStream(google.protobuf.Empty) returns (stream NavigationMsg) {}
}
//! [Proto types]

View File

@ -5,20 +5,23 @@ syntax = "proto3";
package qtgrpc.examples; package qtgrpc.examples;
import "google/protobuf/empty.proto"; import "google/protobuf/empty.proto";
//! [Proto types]
message SpeedMsg { message SpeedMsg {
int32 speed = 1; int32 speed = 1;
} }
message GearMsg { message RpmMsg {
int32 rpm = 1; int32 rpm = 1;
} }
message FuelLevelMsg { message AutonomyMsg {
int32 fuelLevel = 1; int32 autonomy = 1;
} }
service VehicleService { service VehicleService {
rpc getSpeedStream(google.protobuf.Empty) returns (stream SpeedMsg) {} rpc getSpeedStream(google.protobuf.Empty) returns (stream SpeedMsg) {}
rpc getGearStream(google.protobuf.Empty) returns (stream GearMsg) {} rpc getRpmStream(google.protobuf.Empty) returns (stream RpmMsg) {}
rpc getFuelLevel(google.protobuf.Empty) returns (FuelLevelMsg) {} rpc getAutonomy(google.protobuf.Empty) returns (AutonomyMsg) {}
} }
//! [Proto types]

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e8eaed">
<path d="M600-160v-360H272l64 64-56 56-160-160 160-160 56 56-64 64h328q33 0 56.5 23.5T680-520v360h-80Z" />
</svg>

After

Width:  |  Height:  |  Size: 226 B

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e8eaed">
<path d="M280-160v-360q0-33 23.5-56.5T360-600h328l-64-64 56-56 160 160-160 160-56-56 64-64H360v360h-80Z" />
</svg>

After

Width:  |  Height:  |  Size: 227 B

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e8eaed">
<path d="M440-120v-567l-64 63-56-56 160-160 160 160-56 56-64-63v567h-80Z" />
</svg>

After

Width:  |  Height:  |  Size: 196 B

View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e8eaed">
<path
d="M160-120v-640q0-33 23.5-56.5T240-840h240q33 0 56.5 23.5T560-760v280h40q33 0 56.5 23.5T680-400v180q0 17 11.5 28.5T720-180q17 0 28.5-11.5T760-220v-288q-9 5-19 6.5t-21 1.5q-42 0-71-29t-29-71q0-32 17.5-57.5T684-694l-84-84 42-42 148 144q15 15 22.5 35t7.5 41v380q0 42-29 71t-71 29q-42 0-71-29t-29-71v-200h-60v300H160Zm80-440h240v-200H240v200Zm480 0q17 0 28.5-11.5T760-600q0-17-11.5-28.5T720-640q-17 0-28.5 11.5T680-600q0 17 11.5 28.5T720-560ZM240-200h240v-280H240v280Zm240 0H240h240Z" />
</svg>

After

Width:  |  Height:  |  Size: 618 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -2,18 +2,16 @@
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause # SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
cmake_minimum_required(VERSION 3.16) cmake_minimum_required(VERSION 3.16)
project(VehicleServerRunner LANGUAGES CXX) project(VehicleServer LANGUAGES CXX)
set(CMAKE_FIND_PACKAGE_PREFER_CONFIG TRUE) 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(protobuf)
find_package(gRPC) find_package(gRPC)
if(NOT TARGET gRPC::grpc_cpp_plugin OR NOT TARGET WrapProtoc::WrapProtoc if(NOT TARGET gRPC::grpc_cpp_plugin OR NOT TARGET WrapProtoc::WrapProtoc
OR NOT TARGET gRPC::grpc++) OR NOT TARGET gRPC::grpc++)
message(WARNING "Dependencies of SimpleVehicleServer not found. Skipping.") message(WARNING "Dependencies of VehicleServer not found. Skipping.")
return() return()
endif() endif()
@ -23,16 +21,19 @@ if(MINGW)
" guaranteed otherwise.") " guaranteed otherwise.")
endif() endif()
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(proto_files set(proto_files
"${CMAKE_CURRENT_LIST_DIR}/../proto/vehicleservice.proto" "${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(out_dir ${CMAKE_CURRENT_BINARY_DIR})
set(generated_files set(generated_files
"${out_dir}/vehicleservice.pb.h" "${out_dir}/vehicleservice.pb.cc" "${out_dir}/vehicleservice.pb.h" "${out_dir}/vehicleservice.pb.cc"
"${out_dir}/vehicleservice.grpc.pb.h" "${out_dir}/vehicleservice.grpc.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}/navigationservice.pb.h" "${out_dir}/navigationservice.pb.cc"
"${out_dir}/naviservice.grpc.pb.h" "${out_dir}/naviservice.grpc.pb.cc") "${out_dir}/navigationservice.grpc.pb.h" "${out_dir}/navigationservice.grpc.pb.cc")
add_custom_command( add_custom_command(
OUTPUT ${generated_files} OUTPUT ${generated_files}
@ -53,24 +54,22 @@ add_custom_command(
set_source_files_properties(${generated_files} PROPERTIES GENERATED TRUE) set_source_files_properties(${generated_files} PROPERTIES GENERATED TRUE)
add_executable(SimpleVehicleServer qt_add_executable(vehicle_server
${generated_files} ${generated_files}
${CMAKE_CURRENT_LIST_DIR}/serverrunner.cpp
${CMAKE_CURRENT_LIST_DIR}/serverrunner.h
${CMAKE_CURRENT_LIST_DIR}/main.cpp ${CMAKE_CURRENT_LIST_DIR}/main.cpp
) )
target_include_directories(SimpleVehicleServer target_include_directories(vehicle_server
PRIVATE PRIVATE
${out_dir} ${out_dir}
) )
target_link_libraries(SimpleVehicleServer PRIVATE target_link_libraries(vehicle_server PRIVATE
protobuf::libprotobuf protobuf::libprotobuf
gRPC::grpc++ gRPC::grpc++
) )
install(TARGETS SimpleVehicleServer install(TARGETS vehicle_server
RUNTIME DESTINATION "${INSTALL_EXAMPLEDIR}" RUNTIME DESTINATION "${INSTALL_EXAMPLEDIR}"
BUNDLE DESTINATION "${INSTALL_EXAMPLEDIR}" BUNDLE DESTINATION "${INSTALL_EXAMPLEDIR}"
LIBRARY DESTINATION "${INSTALL_EXAMPLEDIR}" LIBRARY DESTINATION "${INSTALL_EXAMPLEDIR}"

View File

@ -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 <grpc++/grpc++.h>
#include <array>
#include <chrono>
#include <iostream>
#include <thread>
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<SpeedMsg> *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<RpmMsg> *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<std::string, 3> streets = { "Friedrichstraße", "Kurfürstendamm",
"Erich-Thilo Straße" };
grpc::Status getNavigationStream(grpc::ServerContext *, const ::google::protobuf::Empty *,
grpc::ServerWriter<NavigationMsg> *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<grpc::Server> server(builder.BuildAndStart());
if (!server) {
std::cerr << "Creating grpc::Server failed.";
return -1;
}
std::cout << "Server listening on port 50051";
server->Wait();
return 0;
}

View File

@ -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<NavigationThread>();
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<VehicleThread>();
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();
}
}

View File

@ -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 <QQmlPropertyMap>
#include <QtCore/QObject>
#include <QtCore/QString>
#include <QtCore/QtTypeTraits>
#include <QtQml/QQmlEngine>
#include <memory>
#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<qtgrpc::examples::NavigationThread> m_navigationThread;
std::unique_ptr<qtgrpc::examples::VehicleThread> m_vehicleThread;
QString m_vehicleErrors;
QString m_navigationErrors;
};
#endif // VEHICLEMANAGER_H

View File

@ -12,6 +12,7 @@
using namespace qtgrpc::examples; using namespace qtgrpc::examples;
using namespace google::protobuf; using namespace google::protobuf;
VehicleThread::VehicleThread(QObject *parent) : QThread(parent) VehicleThread::VehicleThread(QObject *parent) : QThread(parent)
{ {
} }
@ -21,51 +22,83 @@ VehicleThread::~VehicleThread() = default;
void VehicleThread::run() void VehicleThread::run()
{ {
if (!m_client) { if (!m_client) {
auto channel = std::shared_ptr< m_client = std::make_unique<qtgrpc::examples::VehicleService::Client>();
QAbstractGrpcChannel>(new QGrpcHttp2Channel(QUrl("http://localhost:50051", m_client
QUrl::StrictMode))); ->attachChannel(std::make_shared<QGrpcHttp2Channel>(QUrl("http://localhost:50051")));
m_client = std::make_shared<qtgrpc::examples::VehicleService::Client>();
m_client->attachChannel(channel);
} }
Empty fuelLvlRequest; //! [Speed stream]
std::shared_ptr<QGrpcCallReply> replyFuel = m_client->getFuelLevel(fuelLvlRequest);
connect(replyFuel.get(), &QGrpcCallReply::finished, [replyFuel, this] (const QGrpcStatus &status) {
if (status.code() == QtGrpc::StatusCode::Ok) {
if (const auto fuelLvl = replyFuel->read<FuelLevelMsg>())
emit fuelLevelChanged(fuelLvl->fuelLevel());
} else {
emit connectionError(true);
emit fuelLevelChanged(0);
}
});
Empty speedRequest; Empty speedRequest;
m_streamSpeed = m_client->getSpeedStream(speedRequest); m_streamSpeed = m_client->getSpeedStream(speedRequest);
connect(m_streamSpeed.get(), &QGrpcServerStream::messageReceived, this, [this] {
if (const auto speedResponse = m_streamSpeed->read<SpeedMsg>())
emit speedChanged(speedResponse->speed());
});
connect(m_streamSpeed.get(), &QGrpcServerStream::finished, this, connect(m_streamSpeed.get(), &QGrpcServerStream::messageReceived, this, [this]() {
[this](const QGrpcStatus &status) { if (const auto speedResponse = m_streamSpeed->read<SpeedMsg>()) {
emit speedChanged(0); emit speedChanged(speedResponse->speed());
if (status.code() != QtGrpc::StatusCode::Ok) {
emit connectionError(true);
emit fuelLevelChanged(0);
qWarning() << "Stream error(" << status.code() << "):" << status.message();
} }
}); });
Empty gearRequest; connect(
m_streamGear = m_client->getGearStream(gearRequest); m_streamSpeed.get(), &QGrpcServerStream::finished, this,
connect(m_streamGear.get(), &QGrpcServerStream::messageReceived, this, [this] { [this](const QGrpcStatus &status) {
if (const auto gearResponse = m_streamGear->read<GearMsg>()) if (!status.isOk()) {
emit rpmChanged(gearResponse->rpm()); 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<RpmMsg>()) {
emit rpmChanged(rpmResponse->rpm());
}
}); });
connect(m_streamGear.get(), &QGrpcServerStream::finished, this, [this] { emit rpmChanged(0); }); 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);
//! [Autonomy call]
Empty autonomyRequest;
std::unique_ptr<QGrpcCallReply> 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;
}
if (const auto autonomyMsg = autonomyReply->read<AutonomyMsg>()) {
emit autonomyChanged(autonomyMsg->autonomy());
}
},
Qt::SingleShotConnection);
//! [Autonomy call]
QThread::run(); QThread::run();
// Delete the VehicleService::Client object to shut down the connection
m_client.reset();
} }

View File

@ -4,20 +4,11 @@
#ifndef VEHICLETHREAD_H #ifndef VEHICLETHREAD_H
#define VEHICLETHREAD_H #define VEHICLETHREAD_H
#include "vehicleservice_client.grpc.qpb.h"
#include <QtCore/QThread> #include <QtCore/QThread>
#include <memory> #include <memory>
QT_BEGIN_NAMESPACE namespace qtgrpc::examples {
class QGrpcServerStream;
QT_END_NAMESPACE
namespace qtgrpc {
namespace examples {
namespace VehicleService {
class Client;
}
}
}
class VehicleThread : public QThread class VehicleThread : public QThread
{ {
@ -26,17 +17,22 @@ class VehicleThread : public QThread
public: public:
explicit VehicleThread(QObject *parent = nullptr); explicit VehicleThread(QObject *parent = nullptr);
~VehicleThread() override; ~VehicleThread() override;
void run() override; void run() override;
signals: signals:
void speedChanged(int speed); void speedChanged(int speed);
void rpmChanged(int rpm); void rpmChanged(int rpm);
void fuelLevelChanged(int level); void autonomyChanged(int level);
void connectionError(bool value);
void connectionError(QString error);
private: private:
std::shared_ptr<qtgrpc::examples::VehicleService::Client> m_client; std::unique_ptr<qtgrpc::examples::VehicleService::Client> m_client;
std::shared_ptr<QGrpcServerStream> m_streamSpeed; std::unique_ptr<QGrpcServerStream> m_streamSpeed;
std::shared_ptr<QGrpcServerStream> m_streamGear; std::unique_ptr<QGrpcServerStream> m_streamRpm;
}; };
}
#endif // VEHICLETHREAD_H #endif // VEHICLETHREAD_H