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
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()

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.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")
//! [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 {
anchors.fill: parent
color: "#040a16"
color: "#160404"
}
Item {
id: background
// Information screen
GridLayout {
anchors.fill: parent
visible: !root.availableBtn
Row {
id: textsBar
anchors.horizontalCenter: background.horizontalCenter
anchors.bottom: progressBars.top
spacing: 130
width: root.width - 110
height: 200
anchors.margins: 40
visible: !(root.vehicleErrors && root.navigationErrors)
columnSpacing: 20
rowSpacing: 10
columns: 2
uniformCellWidths: true
Item {
width: 300
height: 200
Layout.fillHeight: true
Layout.fillWidth: true
ClusterText {
width: 200
height: 200
anchors.bottom: parent.bottom
StyledText {
id: speedText
anchors.left: parent.left
verticalAlignment: Text.AlignBottom
anchors.verticalCenter: parent.verticalCenter
text: root.speed == -1 ? "-" : root.speed
font.pointSize: 90
text: root.speed
}
ClusterText {
width: 100
height: 200
StyledText {
id: speedUnitText
anchors.bottom: parent.bottom
anchors.bottomMargin: 27
anchors.right: parent.right
verticalAlignment: Text.AlignBottom
horizontalAlignment: Text.AlignRight
text: "Kmph"
text: "Km/h"
}
}
Item {
width: 300
height: 200
Layout.fillHeight: true
Layout.fillWidth: true
Image {
id: arrow
VectorImage {
id: directionImage
source: root.directionImageSource
width: 100
height: 100
anchors.verticalCenter: parent.verticalCenter
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 {
width: 200
height: 40
anchors.bottom: street.top
Rectangle {
visible: root.directionImageSource
anchors.fill: directionImage
color: "#363636"
radius: 10
z: -1
}
StyledText {
anchors.right: parent.right
verticalAlignment: Text.AlignBottom
anchors.bottom: streetText.top
font.pointSize: 24
horizontalAlignment: Text.AlignRight
text: Number(root.remainingDistance * 0.001).toFixed(2) + " km"
text: {
if (root.totalDistance == -1 || root.traveledDistance == -1) {
return "- km";
}
ClusterText {
id: street
width: 200
height: 40
anchors.bottom: parent.bottom
anchors.bottomMargin: 27
anchors.right: parent.right
let remainingDistance = root.totalDistance - root.traveledDistance;
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
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 {
width: 300
height: 200
Layout.fillHeight: true
Layout.fillWidth: true
ClusterText {
id: rightSide
width: 200
height: 200
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.bottomMargin: 27
anchors.right: parent.right
verticalAlignment: Text.AlignBottom
horizontalAlignment: Text.AlignRight
text: root.rpm + " rpm"
}
}
}
Row {
id: progressBars
anchors.horizontalCenter: background.horizontalCenter
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
StyledProgressBar {
Layout.fillWidth: true
value: root.rpm
to: 9000
activeColor: "#f8c607"
bgColor: "#5f3f04"
}
}
ClusterProgressBar {
id: fuelLevel
anchors.leftMargin: 55
anchors.left: parent.left
anchors.topMargin: 100
anchors.top: progressBars.bottom
currentBarValue: root.fuelLevel
totalBarValue: 250
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
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 {
// No connection error screen
ColumnLayout {
anchors.centerIn: parent
color: btn.btnPressed ? "#828284" : "white"
text: "RUN"
visible: root.vehicleErrors && root.navigationErrors
StyledText {
Layout.alignment: Qt.AlignHCenter
font.pointSize: 14
text: qsTr("Please, start vehicle server. Then, press try again.")
}
MouseArea {
id: btn
property bool btnPressed: false
anchors.fill: parent
Button {
id: runButton
property string baseColor: "white"
property string clickedColor: "#828284"
property string hoverColor: "#a9a9a9"
enabled: root.availableBtn
onClicked: ClusterDataManager.restart()
onPressed: btnPressed = true
onReleased: btnPressed = false
}
}
Layout.alignment: Qt.AlignHCenter
background: Rectangle {
border.color: runButton.down ?
runButton.clickedColor
: (runButton.hovered ? runButton.hoverColor : runButton.baseColor)
border.width: 2
radius: 2
color: "transparent"
}
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 ""
}
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)
}
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();
}
}
}
}

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
Text {
wrapMode: Text.WordWrap
font.family: "Helvetica"
color: "white"
style: Text.Sunken
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;
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]

View File

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

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

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 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<qtgrpc::examples::VehicleService::Client>();
m_client->attachChannel(channel);
m_client = std::make_unique<qtgrpc::examples::VehicleService::Client>();
m_client
->attachChannel(std::make_shared<QGrpcHttp2Channel>(QUrl("http://localhost:50051")));
}
Empty fuelLvlRequest;
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);
}
});
//! [Speed stream]
Empty 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,
[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_streamSpeed.get(), &QGrpcServerStream::messageReceived, this, [this]() {
if (const auto speedResponse = m_streamSpeed->read<SpeedMsg>()) {
emit speedChanged(speedResponse->speed());
}
});
Empty gearRequest;
m_streamGear = m_client->getGearStream(gearRequest);
connect(m_streamGear.get(), &QGrpcServerStream::messageReceived, this, [this] {
if (const auto gearResponse = m_streamGear->read<GearMsg>())
emit rpmChanged(gearResponse->rpm());
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<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();
// Delete the VehicleService::Client object to shut down the connection
m_client.reset();
}

View File

@ -4,20 +4,11 @@
#ifndef VEHICLETHREAD_H
#define VEHICLETHREAD_H
#include "vehicleservice_client.grpc.qpb.h"
#include <QtCore/QThread>
#include <memory>
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<qtgrpc::examples::VehicleService::Client> m_client;
std::shared_ptr<QGrpcServerStream> m_streamSpeed;
std::shared_ptr<QGrpcServerStream> m_streamGear;
std::unique_ptr<qtgrpc::examples::VehicleService::Client> m_client;
std::unique_ptr<QGrpcServerStream> m_streamSpeed;
std::unique_ptr<QGrpcServerStream> m_streamRpm;
};
}
#endif // VEHICLETHREAD_H