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.