This is the multi-page printable view of this section. Click here to print.

Return to the regular view of this page.

gRPC service generation

Learn how to generate and fill your own gRPC services.

This tutorial shows how to generate a basic gRPC service like a seat service. For this example the proto file at https://raw.githubusercontent.com/eclipse-kuksa/kuksa-incubation/0.4.0/seat_service/proto/sdv/edge/comfort/seats/v1/seats.proto is used.

All files included from services/seats are auto-generated and added to the app project as Conan dependency. For writing a complete gRPC service you need two velocitas apps/projects. One is implementing a client and the other one is for providing the server.

Networking

The examples shown in this tutorial are based on three components running:

  • A client using the API defined in the proto file
  • A server providing the API and communicating with Databroker to read or modify datapoints
  • A Kuksa Databroker .

As a Velocitas developer you may use the Velocitas devenv-runtimes to deploy and run the Databroker instance, but it is also possible to connect to a Databroker running on localhost. The following setup was used for the examples:

For this to work the .devcontainer/devcontainer.json was changed. In the setup --network=host was added to allow the containers to use the host network. For the server forwardPorts": [ 5555 ] was additionally used to forward port 5555.

    "forwardPorts": [ 5555 ],
	"runArgs": [
		"--init",
		"--privileged",
		"--cap-add=SYS_PTRACE",
		"--network=host",
		"--security-opt",
		"seccomp=unconfined"
	],

Note that changes to .devcontainer/devcontainer.json may be overwritten when velocitas sync is performed.

Running the examples

To run the examples the following actions need to be performed in the shown order:

  • Databroker needs to be started. If using a Databroker on host, make sure that it is compatible with the Velocitas version you are using. The catalog used must also be compatible with the signals used in the example.
  • Set the current value of Vehicle.Cabin.Seat.Row1.DriverSide.Position to a valid value, for example 12, using a Databroker Client ( Kuksa Python Client or Databroker CLI ).
  • Start the server.
  • Start the client.
  • Verify that no (unexpected) errors are reported.
  • Use the Databroker Client to verify that the target value of Vehicle.Cabin.Seat.Row1.DriverSide.Position has been set to 75.

1 - Create a server

Learn how to create a server for a service definition.

Introduction

This example assumes that you have used the Velocitas App C++ Template to create a new repository and now want to modify it to be a grpc service server. The example files can also be found in the Vehicle App C++ SDK Github repository .

Velocitas components

Dependning on how you intend to deploy the Application and Databroker the number of Velocitas components required varies. Below is the minimum set needed in .velocitas.json if deploying Databroker on localhost.

    "components": [
        "devcontainer-setup",
        "vehicle-signal-interface",
        "grpc-interface-support",
        "sdk-installer",
        "build-system"
    ],

App configuration

In the AppManifest.json you need to specify that your server will provide the interfaces defined in the proto file. If it requires access to signals from the Databroker, they must also be specified. In this example the server declares to provide the interfaces from the Seats service defined in seats.proto and that it needs write access to the VSS signal Vehicle.Cabin.Seat.Row1.DriverSide.Position.


    "manifestVersion": "v3",
    "name": "SampleApp",
    "interfaces": [
        {
            "type": "grpc-interface",
            "config": {
                "src": "https://raw.githubusercontent.com/eclipse-kuksa/kuksa-incubation/0.4.0/seat_service/proto/sdv/edge/comfort/seats/v1/seats.proto",
                "provided": { }
            }
        },
        {
            "type": "vehicle-signal-interface",
            "config": {
                "src": "https://github.com/COVESA/vehicle_signal_specification/releases/download/v4.0/vss_rel_4.0.json",
                "datapoints": {
                    "required": [
                        {
                            "path": "Vehicle.Cabin.Seat.Row1.DriverSide.Position",
                            "access": "write"
                        }
                    ]
                }
            }
        }
    ]
}

File Generation

When rebuilding the devcontainer with the configuration above two files will be automatically created:

  • SeatsServiceImpl.cpp
  • SeatsServiceImpl.h

They contain the stubs for the server. You still need to fill in the actual implementation of the services.

SeatsServiceImpl.h

The service will access the vehicle model, in this tutorial we manage that by adding a private class variable vehicle to the file SeatsServiceImpl.h.

#ifndef VELOCITAS_SERVICE_IMPL_Seats_H
#define VELOCITAS_SERVICE_IMPL_Seats_H

#include "vehicle/Vehicle.hpp"
#include <grpc/grpc.h>
#include <services/seats/seats.grpc.pb.h>

namespace velocitas {

class SeatsService final : public sdv::edge::comfort::seats::v1::Seats::Service {
public:
    SeatsService(){};
    virtual ~SeatsService(){};

    // <auto-generated>
    ::grpc::Status Move(::grpc::ServerContext*                              context,
                        const ::sdv::edge::comfort::seats::v1::MoveRequest* request,
                        ::sdv::edge::comfort::seats::v1::MoveReply*         response) override;
    ::grpc::Status
    MoveComponent(::grpc::ServerContext*                                       context,
                  const ::sdv::edge::comfort::seats::v1::MoveComponentRequest* request,
                  ::sdv::edge::comfort::seats::v1::MoveComponentReply*         response) override;
    ::grpc::Status
    CurrentPosition(::grpc::ServerContext*                                         context,
                    const ::sdv::edge::comfort::seats::v1::CurrentPositionRequest* request,
                    ::sdv::edge::comfort::seats::v1::CurrentPositionReply* response) override;
    // </auto-generated>

private:
    vehicle::Vehicle vehicle;
};

} // namespace velocitas

#include "SeatsServiceImpl.cpp"

#endif // VELOCITAS_SERVICE_IMPL_Seats_H

SeatsServiceImpl.cpp

In the file SeatsServiceImpl.cpp we need to implement the services. In this example we only implement two of them. The example also shows basic error handling.

The business logic of the server is to wait for Move and CurrentPosition requests and when received just forward the request to the Databroker and return the result. You do not need to add SeatsServiceImpl.cpp to src/CMakeLists.txt as the header includes the *.cpp file and the header file will be included in the Launcher.cpp.

#include "SeatsServiceImpl.h"
#include <sdk/VehicleApp.h>
#include <sdk/VehicleModelContext.h>
#include <sdk/vdb/IVehicleDataBrokerClient.h>

namespace velocitas {

::grpc::Status SeatsService::Move(::grpc::ServerContext*                              context,
                                  const ::sdv::edge::comfort::seats::v1::MoveRequest* request,
                                  ::sdv::edge::comfort::seats::v1::MoveReply*         response) {
    (void)context;
    (void)request;
    (void)response;
    auto seat     = request->seat();
    auto location = seat.location();
    auto row      = location.row();
    auto pos      = location.index();

    std::cout << "Got Move Request!" << std::endl;

    try {
        auto status =
            vehicle.Cabin.Seat.Row1.DriverSide.Position.set(seat.position().base())->await();

        if (status.ok()) {

            std::cout << "OK!" << std::endl;
            return ::grpc::Status(::grpc::StatusCode::OK, "");
        } else {
            std::cout << "Some error!" << std::endl;
            // This could for instance happen if datapoint is not known by databroker
            // then message will be UNKNOWN_DATAPOINT.
            return ::grpc::Status(::grpc::StatusCode::CANCELLED, status.errorMessage());
        }
    } catch (AsyncException& e) {
        std::cout << "Async Exception!" << std::endl;
        // This could typically be that Databroker is not running or not reachable
        // or that your Velocitas environment uses an API incompatible with what your Databroker
        // instance supports
        return ::grpc::Status(::grpc::StatusCode::UNAVAILABLE, "");
    }
}

::grpc::Status
SeatsService::MoveComponent(::grpc::ServerContext*                                       context,
                            const ::sdv::edge::comfort::seats::v1::MoveComponentRequest* request,
                            ::sdv::edge::comfort::seats::v1::MoveComponentReply*         response) {
    (void)context;
    (void)request;
    (void)response;
    // This is an example of an unimplemented method
    return ::grpc::Status(::grpc::StatusCode::UNIMPLEMENTED, "");
}

::grpc::Status SeatsService::CurrentPosition(
    ::grpc::ServerContext*                                         context,
    const ::sdv::edge::comfort::seats::v1::CurrentPositionRequest* request,
    ::sdv::edge::comfort::seats::v1::CurrentPositionReply*         response) {
    (void)context;
    (void)request;

    std::cout << "Got CurrentPosition Request!" << std::endl;

    auto seat          = response->mutable_seat();
    auto seat_position = seat->mutable_position();
    try {
        auto seatPos = vehicle.Cabin.Seat.Row1.DriverSide.Position.get()->await().value();

        std::cout << "Success!!" << std::endl;
        seat_position->set_base(seatPos);
        return ::grpc::Status(::grpc::StatusCode::OK, "");
    } catch (AsyncException& e) {
        std::cout << "Async Exception!" << std::endl;
        // This could typically be that Databroker is not running or not reachable
        // or that your Velocitas environment uses an API incompatible with what your Databroker
        // instance supports
        return ::grpc::Status(::grpc::StatusCode::OK, "");
    } catch (InvalidValueException& e) {
        std::cout << "Invalid Value!" << std::endl;
        // This could be given if Databroker has no current value for Position
        // (Have you set it manually using kuksa-client or databroker-cli?)
        return ::grpc::Status(::grpc::StatusCode::OK, "");
    }
}

} // namespace velocitas

Launcher.cpp

You need to have a file that that starts the server, in this example we modify the file Launcher.cpp that already exist in the template . It has very simple logic, it only starts the service and waits until the service shuts down, if ever.

#include "SeatsServiceImpl.h"
#include <sdk/middleware/Middleware.h>
#include <services/seats/SeatsServiceServerFactory.h>

#include "vehicle/Vehicle.hpp"

#include <memory>

using namespace velocitas;

int main(int argc, char** argv) {
    auto seatsImpl = std::make_shared<SeatsService>();

    velocitas::VehicleModelContext::getInstance().setVdbc(
        velocitas::IVehicleDataBrokerClient::createInstance("vehicledatabroker"));

    auto seatServer = SeatsServiceServerFactory::create(Middleware::getInstance(), seatsImpl);

    seatServer->Wait();

    std::cout << "Waiting!" << std::endl;
    return 0;
}

Building and Running

To (re-)build the App after changing the code you can use the build script . As preparation for running you must also set two environment variables to define where the address/port of the server and the Databroker. The environment variable needs to be set in the same terminal as used for starting the application.

./build.sh
export SDV_SEATS_ADDRESS=grpc://127.0.0.1:5555
export SDV_VEHICLEDATABROKER_ADDRESS=grpc://127.0.0.1:55555

If Databroker is running and has a current value for the wanted signal, then everthing shall work when the client is started. Output similar to below is expected when client has run once.

vscode ➜ /workspaces/erik_server_241021 (main) $ build/bin/app
2024-10-22 09:51:41, INFO  : Connecting to data broker service 'vehicledatabroker' via '127.0.0.1:55555'
2024-10-22 09:51:41, INFO  : Server sdv::edge::comfort::seats::v1::Seats listening on 127.0.0.1:5555
Got Move Request!
OK!
Got CurrentPosition Request!
Success!

2 - Create a client

Learn how to create a client for a service definition.

Introduction

This example assumes that you have used the Velocitas App C++ Template to create a new repository and now want to modify it to be a grpc service client. The example files can also be found in the Vehicle App C++ SDK Github repository .

Velocitas components

Depending on how you intend to deploy the Application and Databroker the number of Velocitas components required varies. Below is the minimum set needed in .velocitas.json if deploying Databroker on localhost.

    "components": [
        "devcontainer-setup",
        "grpc-interface-support",
        "sdk-installer",
        "build-system"
    ],

App configuration

In the AppManifest.json file you need to specify which interfaces your App wants to use. In this case it declares that it wants to use Move and CurrentPosition from the Seats service defined in seats.proto.

{
    "manifestVersion": "v3",
    "name": "SampleApp",
    "interfaces": [
        {
            "type": "grpc-interface",
            "config": {
                "src": "https://raw.githubusercontent.com/eclipse-kuksa/kuksa-incubation/0.4.0/seat_service/proto/sdv/edge/comfort/seats/v1/seats.proto",
                "required": {
                    "methods": [
                        "Move", "CurrentPosition"
                    ]
                }
            }
        }
    ]
}

File Generation

When rebuilding the devcontainer with the configuration no new files will appear in your repository, but the SDK has been updated in the background so you can use it in the file containing main(). You can also regenerate the SDK with the (Re-)generate gRPC SDKs task.

Launcher.cpp

You need to have a file that implements the client behavior. In this example we modify the file Launcher.cpp that already exists in the template . The logic of the example client is simple. It tries to set the target position for the seat and if it succeeds it tries to read current position.

#include <sdk/middleware/Middleware.h>
#include <services/seats/SeatsServiceClientFactory.h>
#include <services/seats/seats.grpc.pb.h>

#include <iostream>

using namespace velocitas;

int main(int argc, char** argv) {
    // The default Velocitas Middleware class performs service discovery by
    // environment variables.
    // For this client it expects SDV_SEATS_ADDRESS to be defined
    // for example:
    // export SDV_SEATS_ADDRESS=grpc://127.0.0.1:5556

    std::cout << "Starting " << std::endl;
    auto serviceClient = SeatsServiceClientFactory::create(Middleware::getInstance());

    ::grpc::ClientContext                        context;
    ::sdv::edge::comfort::seats::v1::MoveRequest request;
    ::sdv::edge::comfort::seats::v1::MoveReply   response;

    ::sdv::edge::comfort::seats::v1::Seat seat;

    ::sdv::edge::comfort::seats::v1::SeatLocation seat_location;
    seat_location.set_row(1);
    seat_location.set_index(1);

    ::sdv::edge::comfort::seats::v1::Position seat_position;

    seat_position.set_base(75.0);

    seat.set_allocated_location(&seat_location);
    seat.set_allocated_position(&seat_position);

    request.set_allocated_seat(&seat);

    auto status = serviceClient->Move(&context, request, &response);

    std::cout << "gRPC Server returned code: " << status.error_code() << std::endl;
    std::cout << "gRPC error message: " << status.error_message().c_str() << std::endl;

    if (status.error_code() != ::grpc::StatusCode::OK) {
        // Some error
        return 1;
    } else {
        ::grpc::ClientContext                                   context;
        ::sdv::edge::comfort::seats::v1::CurrentPositionRequest request;
        ::sdv::edge::comfort::seats::v1::CurrentPositionReply   response;

        request.set_row(1);
        request.set_index(1);

        auto status_curr_pos = serviceClient->CurrentPosition(&context, request, &response);
        std::cout << "gRPC Server returned code: " << status_curr_pos.error_code() << std::endl;
        std::cout << "gRPC error message: " << status_curr_pos.error_message().c_str() << std::endl;
        if (status_curr_pos.ok())
            std::cout << "current Position:" << response.seat().position().base() << std::endl;
        return 0;
    }
}

Building and Running

To (re-)build the App after changing the code you can use the build script . As preparation for running the client you must also set an environment variable to define the address/port of the server. The environment variable needs to be set in the same terminal as used for starting the application.

./build.sh
export SDV_SEATS_ADDRESS=grpc://127.0.0.1:5555

If Databroker and the Server are running and have a valid value for the wanted signal, everything should work when the client is started. Output similar to below is expected.

vscode ➜ /workspaces/erik_vapp_241018 (main) $ build/bin/app
Starting
gRPC Server returned code: 0
gRPC error message:
gRPC Server returned code: 0
gRPC error message: