Tutorial - Get a vehicle managed with Hermes using C++
— In this tutorial we will create a C++ vehicle simulator that will use HermesV2 to receive commands, execute them and report the status back.
HermesV2 is a protocol based on gRPC. All services and objects are described in proto
files. gRPC provides tools to compile those proto files to C++ code, that will handle serialization, deserialization and transport mechanisms. The client code just need to call this generated code.
Get the HermesV2 protocol definitions
First, we need to get the proto
files for HermesV2. They are available from the bestmile/hermes
repo on github (https://github.com/bestmile/hermes) under the src
folder of the hermes-v2.0
branch. Copy them to your current directory:
git clone --depth 1 --branch hermes-v2.0 git@github.com:Bestmile/hermes.git
Compile the HermesV2 protocol definition to C++ code
The .proto
files contain definitions of messages and procedure call that we want to compile to C++ code to be able to use them.
To do that we need Protocol Buffer v3 with the gRPC C++ plugin, we can use our distribution to install them or follow the instructions here:
Now we can compile the HermesV2 protocol:
cd ./examples/cpp
mkdir generated
protoc -I ../../src/main/protobuf --grpc_out=generated --plugin=protoc-gen-grpc=`which grpc_cpp_plugin` ../../src/main/protobuf/com/bestmile/vehicle/service/v2/telemetry.proto ../../src/main/protobuf/com/bestmile/vehicle/service/v2/common.proto ../../src/main/protobuf/deprecated.proto ../../src/main/protobuf/com/bestmile/vehicle/model/v2/unit.proto ../../src/main/protobuf/com/bestmile/vehicle/model/v2/id.proto ../../src/main/protobuf/com/bestmile/vehicle/model/v2/command.proto ../../src/main/protobuf/com/bestmile/vehicle/model/v2/command_details.proto ../../src/main/protobuf/com/bestmile/vehicle/model/v2/command_report.proto ../../src/main/protobuf/com/bestmile/vehicle/model/v2/command_signature.proto ../../src/main/protobuf/com/bestmile/vehicle/service/v2/orchestration.proto
protoc -I ../../src/main/protobuf --cpp_out=generated ../../src/main/protobuf/com/bestmile/vehicle/service/v2/common.proto ../../src/main/protobuf/com/bestmile/vehicle/service/v2/telemetry.proto ../../src/main/protobuf/deprecated.proto ../../src/main/protobuf/com/bestmile/vehicle/model/v2/unit.proto ../../src/main/protobuf/com/bestmile/vehicle/model/v2/id.proto ../../src/main/protobuf/com/bestmile/vehicle/model/v2/command.proto ../../src/main/protobuf/com/bestmile/vehicle/model/v2/command_details.proto ../../src/main/protobuf/com/bestmile/vehicle/model/v2/command_report.proto ../../src/main/protobuf/com/bestmile/vehicle/model/v2/command_signature.proto ../../src/main/protobuf/com/bestmile/vehicle/model/v2/telemetry.proto ../../src/main/protobuf/com/bestmile/vehicle/service/v2/orchestration.proto
The first command will generate the service call (RPC) code. The last two commands generate the serialization and deserialization code.
Create a main event loop to run the management stream
In the previous tutorial (Tutorial - Post vehicle telemetry) we saw how to use gRPC to do a simple procedure call that publishes the vehicle telemetry data. In this example we show how to receive messages from the orchestrator and send back report messages. Messages can flow in both directions at any time, so we use a bidirectional stream from gRPC.
There are two ways to handle streams with gRPC and C++, we will use and explain the Async way (https://grpc.io/docs/tutorials/async/helloasync-cpp/).
We create a channel, a stub, a completion queue, and some tags to know what is ready on the completion queue:
#include <grpcpp/grpcpp.h>
#include "grpc.grpc.pb.h"
using grpc::Channel;
using grpc::ClientAsyncReaderWriter;
using grpc::ClientContext;
using grpc::ClientReaderWriter;
using grpc::CompletionQueue;
using grpc::Status;
using namespace std;
auto credentials = grpc::SslCredentials(grpc::SslCredentialsOptions())));
auto credentials = grpc::InsecureChannelCredentials();
auto channel = grpc::CreateChannel(server, credentials);
enum class CQTAG { CONNECTED, READ, WRITE };
std::unique_ptr<Management::Stub> stub(Management::NewStub(channel));
CompletionQueue cq;
ClientContext context;
Next, we register the event we care about in the completion queue. First we establish a connection via a stream, and then register for incoming messages.
auto rw(stub->AsyncVehicleStream(&context, &cq, (void *)CQTAG::CONNECTED));
VehicleMessage vhcMsg;
rw->Read(&supMsg, (void *)CQTAG::READ);
Then we can loop forever on the completion queue and react on the incoming events:
void *got_tag;
bool ok = false;
gpr_timespec timeout;
timeout.tv_sec = 1;
timeout.tv_nsec = 0;
timeout.clock_type = GPR_TIMESPAN;
while (true) {
auto asyncStatus = cq.AsyncNext(&got_tag, &ok, timeout);
switch (asyncStatus) {
case CompletionQueue::SHUTDOWN:
cout << "Connection is being shutdown" << endl;
return;
case CompletionQueue::GOT_EVENT:
switch (reinterpret_cast<CQTAG &>(got_tag)) {
case CQTAG::CONNECTED: {
cout << "Connection is established" << endl;
break;
}
case CQTAG::READ: {
cout << "Received an orchestration message" << endl;
break;
}
case CQTAG::WRITE: {
cout << "Write is done, completion queue is ready for writing next message" << endl;
break;
}
default: {
break;
}
}
break;
case CompletionQueue::TIMEOUT: {
break;
}
}
}
When we need to send a message, we can do so with a call that will send the vhcMsg
passed as argument and register an event on the completion queue with the CQTAG::WRITE
tag. This tag can be handled on the main loop to know when the transmission has been done. You should wait for acknowledgement before sending the next message.
rw->Write(vhcMsg, (void *)CQTAG::WRITE);
Use HermesV2 to get commands and send statuses
Now that we have the wiring ready to handle the stream messages, we can use HermesV2 within it.
The first thing to do is send vehicle identification information when the connection establishes. This will allow the orchestrator (Bestmile) to know which vehicle is talking and identify it. We send this message as soon as the connection channel is established, when we receive the CQTAG::CONNECTED
event.
The VehicleMessage
as been allocated already and is re-used every time we need to send it. We fill it, send it, wait for write acknowledgement (CQTAG::WRITE
) and repeat.
Let’s build the announce message, and set the vehicle telemetry too as we should already know what the state of the vehicle is.
using com::bestmile::vehicle::service::v2::VehicleMessage;
using com::bestmile::vehicle::model::v2::Telemetry;
void State::setVehicleTelemetry(VehicleMessage &message) {
auto telemetry = message.mutable_telemetry();
telemetry->mutable_location()->set_latitude(position->latitude);
telemetry->mutable_location()->set_longitude(position->longitude);
telemetry->mutable_speed()->set_value(speed);
}
vhcMsg.mutable_announce()->mutable_vehicle_id()->set_value(state.vehicle_id);
state.setVehicleTelemetry(vhcMsg);
rw->Write(vhcMsg, (void *)CQTAG::WRITE);
From this moment on, we will receive an OrchestrationMessage
containing the instructions for the vehicle, and we will send back a VehicleMessage
that reports the progress and status of the vehicle activities.
We will focus on the happy path to understand the mechanisms and let the error handling as an exercise for the reader.
We have already seen how to send a VehicleMessage, the rest of the simulator keeps doing this, but instead of reusing the identification and telemetry information, it will build telemetry and command status reports. We can focus on reading and parsing the OrchestrationMessage
that are received.
case CQTAG::READ:
/* When a message is received, we enter this case branch and the `orchestrationMsg`
variable is filled with the received message (that's what we asked)
*/
// We are interested in the orchestration commands, so let's accumulate them
vector<Command> commands;
for (auto command : orchestrationMsg.commands()) {
if (command.has_details())
commands.push_back(command);
}
/* this vector of commands will probably end up being saved in a
state class as it represents what the vehicle has to do.
*/
// Now let's assume we want to know what needs to be done on the first command
assert(commands.size() > 1)
switch (command.details().sealed_value_case()) {
case CommandDetails::kDrive:
// The command is a drive
if (command.details().drive().has_route() && command.details().drive().route().has_destination()) {
float latitude = command.details().drive().route().destination().latitude();
float longitude = command.details().drive().route().destination().longitude();
// We have the latitude and longitude of the destination
}
break;
case CommandDetails::kPickup:
// the command is a pickup
break;
case CommandDetails::kDropoff:
// the command is a dropoff
break;
case CommandDetails::kOpenAccess:
// the command is an open access
break;
default:
break;
}
Protobuf generates getters and setters based on the names of the fields used in the proto files definitions.
Compile
We can compile everything to an executable:
g++ -std=c++11 vehicle.cc generated/*.cc generated/com/bestmile/vehicle/model/v2/*.cc generated/com/bestmile/vehicle/service/v2/*.cc -I./generated -L./generated `pkg-config --libs protobuf grpc++ grpc` -o client
Complete example
This tutorial introduced some basic knowledge to start working with gRPC and protobuf for Hermes. This is not a complete example but it provides the initial bricks that should help integrate the protocol quickly.
If you want a more advanced example (which is based on the content of this tutorial), look at the examples/cpp/vehicle.cc
file in the Hermes github repository. The code presents a vehicle simulator that receives commands from the orchestrator, executes them, and reports the vehicle status back to the orchestrator.