This project is the result of some free time I used to learn a little bit of Rust. The core thing to me is to learn Rust. Nevertheless, you always need a topic to write a software for, and so I decided to write a client for the Kuksa Databroker.
The client is using the Tonic framework to access Kuksa via pure gRPC. Doing this, it relies on latest API version kuksa.val.v2.
Please make sure that you have all needed tools installed on your machine, like:
- Rust compiler, Cargo, ...
- Protocol Buffers tooling
- Podman or Docker
Create a new Rust project with Cargo:
cargo new KuksaClientProtoCreate a folder named proto in the root directory of the new Rust project and copy the needed .proto definition files of kuksa.val.v2 to the directory.
To work with Protocol Buffers and gRPC, we need to add some frameworks to the dependencies: Prost to generate Rust code from *.proto files, Tonic for using gRPC, and Tokio to support asynchronous programming paradigms. The following code adds them to the TOML file:
[dependencies]
# Support for gRPC
tonic = "0.12"
# Support for Protobuf
prost = "0.13"
prost-types = "0.13"
# Asynchronous programming
tokio = { version = "1.0", features = ["full"] }
tokio-stream = "0.1"
[build-dependencies]
# Support for gRPC
tonic-build = "0.12"Create a file named build.rs in the root directory of the new Rust project. Cargo will build and execute this code before it will build the actual program. We need this to generate Rust code from the .proto files, which will be included into our program. This is the place where the protoc compiler will be invoked. Put the following code in it:
fn main() -> Result<(), Box<dyn std::error::Error>> {
tonic_build::configure()
.compile_protos(
&["proto/kuksa/val/v2/val.proto"],
&["proto"], // Include-Path
)?;
Ok(())
}compile_protos takes two arrays as parameters. The first one contains the actual .proto files we want to use for Rust code generation, and the second one contains the include path for other *.proto files. The val.proto file contains the following line:
import "kuksa/val/v2/types.proto";This includes another file named types.proto within the val.proto file, and include path helps to find this file.
Starting Kuksa and its command line interface is described well in the Kuksa databroker repository. Here is a short wrap up.
First, create a custom Docker bridge network:
docker network create kuksaTo start Databroker in a container attached to the kuksa bridge network using hostname Server and exposing its port to 55556:
docker run -it --rm --name Server --network kuksa -p 55556:55555 ghcr.io/eclipse-kuksa/kuksa-databroker:main --insecureExposing the port is needed to access to Kuksa from the Rust program running on localhost.
Start the CLI in a new terminal:
docker run -it --rm --network kuksa ghcr.io/eclipse-kuksa/kuksa-databroker-cli:main --server Server:55555As the first step, we need to add the generated proto code to the src/main.rsfile:
use kuksa::val::v2::val_client::ValClient;
use kuksa::val::v2::GetValueRequest;
use kuksa::val::v2::SignalId;
use kuksa::val::v2::signal_id::Signal;
// Include the generated Kuksa modules
pub mod kuksa {
pub mod val {
pub mod v2 {
tonic::include_proto!("kuksa.val.v2");
}
}
}The use commands include some Rust code which has been generated by the protoc compiler based on the *.proto files. Afterwards, pub mod declares a public module containing the generated Rust code from the protobuf files.
Next, we already can start the main function. Since we want to do networking, we will use the famous Tokio framework. Tokio is an asynchronous runtime for the Rust programming language. It provides the building blocks needed for writing network applications.
Actually, a main function in Rust must not be async. Using the #[tokio::main] macro, asynchronous behavior becomes possible even in main:
#[tokio::main]
async fn main() {
// Some code containing .await
}Additionally, we add some error handling code to function.
In this part, we create the gRPC client and connect to the Kuksa Server. The gRPC client has been generated earlier from the *.proto files using the Tonic framework:
// Connect to Kuksa gRPC server
let addr = "http://127.0.0.1:55556";
let mut client = ValClient::connect(addr).await?;
println!("Connected to KUKSA VAL v2 Broker at address {}", addr);The generated code of the ValClient::connect function internally creates a new connection endpoint and establishes the actual connection. The generated code should look like this:
{
let conn = tonic::transport::Endpoint::new(dst)?.connect().await?;
Ok(Self::new(conn))
}At this point, we are ready to do the actual gRPC request to Kuksa. We will use the simple GetValueRequest function, which has been generated from the *.proto files. See the code first:
// Create request - Vehicle.Speed
let request = tonic::Request::new(GetValueRequest {
signal_id: Some(SignalId {
signal: Some(Signal::Path("Vehicle.Speed".to_string())),
}),
});Now it is getting a little bit interesting. The actual Protobuf message is declared as follows:
message SignalID {
oneof signal {
// Numeric identifier to the signal
// As of today Databroker assigns arbitrary unique numbers to each registered signal
// at startup, meaning that identifiers may change after restarting Databroker.
// A mechanism for static identifiers may be introduced in the future.
int32 id = 1;
// Full VSS-style path to a specific signal, like "Vehicle.Speed"
// Wildcards and paths to branches are not supported.
// The given path must be known by the Databroker.
string path = 2;
}
}
message GetValueRequest {
SignalID signal_id = 1;
}The oneof keyword in Protobuf means, that SignalID either is an id of the type int32, or a string containing the path (in our case Vehicle.Speed). The numbers behind the = sign are tags. They are used in Protobuf to minimize the data to be transferred. Instead of sending something like path = Vehicle.Speed, Protobuf will just send something like 2 = Vehicle.Speed.