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: