The Velocitas development model is centered around what are known as Vehicle Apps. Automation allows engineers to make high-impact changes frequently and deploy Vehicle Apps through cloud backends as over-the-air updates. The Vehicle App development model is about speed and agility paired with state-of-the-art software quality.
Development Architecture
Velocitas provides a flexible development architecture for Vehicle Apps. The following diagram shows the major components of the Velocitas stack.
Vehicle Apps
The Vehicle Applications (aka. Vehicle Apps) contain the business logic that needs to be executed on a vehicle. A Vehicle App is implemented on top of a Vehicle Model and its underlying language-specific SDK. Many concepts of cloud-native and twelve-factor applications apply to Vehicle Apps as well and are summarized in the next chapter.
Vehicle Models
A Vehicle Model makes it possible to easily get vehicle data from the Data Broker and to execute remote procedure calls over gRPC against Vehicle Services and other Vehicle Apps. It is generated from the underlying semantic models for a concrete programming language as a graph-based, strongly-typed, intellisense-enabled library. The elements of the vehicle models are defined by the SDKs.
SDKs
To reduce the effort required to implement Vehicle Apps, Velocitas provides a set of SDKs for different programming languages. SDKs are available for Python and C++, further SDKs for Rust and C are planned.
Next to a Vehicle Apps abstraction, the SDKs are Middleware-enabled, provide connectivity to the Data Broker and contain the ontology in the form of base classes to create Vehicle Models.
Vehicle Services
Vehicle Services provide service interfaces to control actuators or to trigger (complex) actions. E.g. they communicate with the vehicle internals networks like CAN or Ethernet, which are connected to actuators, electronic control units (ECUs) and other vehicle computers (VCs). They may provide a simulation mode to run without a network interface. Vehicle services may feed data to the Data Broker and may expose gRPC endpoints, which can be invoked by Vehicle Apps over a Vehicle Model
Data Broker
Vehicle data is stored in the KUKSA Data Broker conforming to an underlying Semantic Model like VSS. Vehicle Apps can either pull this data or subscribe for updates. In addition, it supports rule-based access to reduce the number of updates sent to the Vehicle App.
Semantic models
The Vehicle Signal Specification (VSS) provides a domain taxonomy for vehicle signals and defines the vehicle data semantically, which is exchanged between Vehicle Apps and the Data Broker.
The Vehicle Service Catalog (VSC) extends VSS with functional remote procedure call definitions and semantically defines the gRPC interfaces of Vehicle Services and Vehicle Apps.
As an alternative to VSS and VSC, vehicle data and services can be defined semantically in a general IoT modelling language like Digital Twin Definition Language (DTDL) or BAMM Aspect Meta Model BAMM as well.
The Velocitas SDK is using (VSS) as the sementic model for the Vehicle Model.
Communication Protocols
Asynchronous communication between Vehicle Apps and other vehicle components, as well as cloud connectivity, is facilitated through MQTT messaging. Direct, synchronous communication between Vehicle Apps, Vehicle Services and the Data Broker is based on the gRPC protocol.
Middleware
Velocitas leverages dapr for gRPC service discovery, Open Telemetry tracing and the publish/subscribe building block which provides an abstraction of the MQTT messaging protocol.
Vehicle Edge Operating System
Vehicle Apps are expected to run on a Linux-based operating system. An OCI-compliant container runtime is required to host the Vehicle App containers and the dapr middleware mandates a Kubernetes control plane. For publish/subscribe messaging a MQTT broker must be available (e.g., Eclipse Mosquitto).
Vehicle App Characteristics
The following aspects are important characteristics for Vehicle Apps:
Code base:
Every Vehicle App is stored in its own repository. Tracked by version control, it can be deployed to multiple environments.
Polyglot:
Vehicle Apps can be written in any programming language. System-level programming languages like Rust and C/C++ are particularly relevant for limited hardware resources found in vehicles, but higher-level languages like Python and JavaScript are also considered for special use cases.
OCI-compliant containers:
Vehicle Apps are deployed as OCI-compliant containers. The size of these containers should be minimal to fit on constrained devices.
Isolation:
Each Vehicle App should execute in its own process and should be self-contained with its interfaces and functionality exposed on its own port.
Configurations:
Configuration information is separated from the code base of the Vehicle App, so that the same deployment can propagate across environments with their respective configuration applied.
Disposability:
Favor fast startup and support graceful shutdowns to leave the system in a correct state.
Observability:
Vehicle Apps provide traces, metrics and logs of every part of the application using Open Telemetry.
Over-the-air updatability:
Vehicle Apps are released to cloud backends like the Bosch Mobility Cloud and can be updated in vehicles frequently over the air.
Development Process
The starting point for developing Vehicle Apps is a Semantic Model of the vehicle data and vehicle services. Based on the Semantic Model, language-specific Vehicle Models are generated. Vehicle Models are then distributed as packages to the respective package manager of the chosen programming language (e.g. pip, cargo, npm, …).
After a Vehicle Model is available for the chosen programming language, the Vehicle App can be developed using the generated Vehicle Model and its SDK.
Further information
1 - Vehicle App SDK
Learn more about the provided Vehicle App SDK.
Introduction
The Vehicle App SDK consists of the following building blocks:
Vehicle Model Ontology: The SDK provides a set of model base classes for the creation of vehicle models.
Middleware integration: Vehicle Models can contain gRPC stubs to communicate with Vehicle Services. gRPC communication is integrated with the Dapr middleware for service discovery and OpenTelemetry tracing.
Fluent query & rule construction: Based on a concrete Vehicle Model, the SDK is able to generate queries and rules against the KUKSA Data Broker to access the real values of the data points that are defined in the vehicle model.
Publish & subscribe messaging: The SDK supports publishing messages to a MQTT broker and subscribing to topics of a MQTT broker.
Vehicle App abstraction: Last but not least the SDK provides a Vehicle App base class, which every Vehicle App derives from.
An overview of the Vehicle App SDK and its dependencies is depicted in the following diagram:
Vehicle Model Ontology
The Vehicle Model is a tree-based model where every branch in the tree, including the root, is derived from the Model base class.
The Vehicle Model Ontology consists of the following classes:
Model
A model contains services, data points and other models. It corresponds to branch entries in VSS or interfaces in DTDL or namespaces in VSC.
ModelCollection
Info
The ModelCollection is deprecated since SDK v0.4.0. The generated vehicle model must reflect the actual representation of the data points. Please use the Model base class instead.
Specifications like VSS support a concept that is called Instances. It makes it possible to describe repeating definitions. In DTDL, such kind of structures may be modeled with Relationships. In the SDK, these structures are mapped with the ModelCollection class. A ModelCollection is a collection of models, which make it possible to reference an individual model either by a NamedRange (e.g., Row [1-3]), a Dictionary (e.g., “Left”, “Right”) or a combination of both.
Service
Direct asynchronous communication between Vehicle Apps and Vehicle Services is facilitated via the gRPC protocol.
The SDK has its own Service base class, which provides a convenience API layer to access the exposed methods of exactly one gRPC endpoint of a Vehicle Service or another Vehicle App. Please see the Middleware Integration section for more details.
DataPoint
DataPoint is the base class for all data points. It corresponds to sensors/actuators in VSS or telemetry / properties in DTDL.
Data Points are the signals that are typically emitted by Vehicle Services.
The representation of a data point is a path starting with the root model, e.g.:
Vehicle.Speed
Vehicle.FuelLevel
Vehicle.Cabin.Seat.Row1.Pos1.Position
Data points are defined as attributes of the model classes. The attribute name is the name of the data point without its path.
Typed DataPoint classes
Every primitive datatype has a corresponding typed data point class, which is derived from DataPoint (e.g., DataPointInt32, DataPointFloat, DataPointBool, DataPointString, etc.).
Example
An example of a Vehicle Model created with the described ontology is shown below:
# import ontology classesfromsdvimport(DataPointDouble,Model,Service,DataPointInt32,DataPointBool,DataPointArray,DataPointString,)classSeat(Model):def__init__(self,name,parent):super().__init__(parent)self.name=nameself.Position=DataPointBool("Position",self)self.IsOccupied=DataPointBool("IsOccupied",self)self.IsBelted=DataPointBool("IsBelted",self)self.Height=DataPointInt32("Height",self)self.Recline=DataPointInt32("Recline",self)classCabin(Model):def__init__(self,name,parent):super().__init__(parent)self.name=nameself.DriverPosition=DataPointInt32("DriverPosition",self)self.Seat=SeatCollection("Seat",self)classSeatCollection(Model):def__init__(self,name,parent):super().__init__(parent)self.name=nameself.Row1=self.RowType("Row1",self)self.Row2=self.RowType("Row2",self)defRow(self,index:int):ifindex<1orindex>2:raiseIndexError(f"Index {index} is out of range")_options={1:self.Row1,2:self.Row2,}return_options.get(index)classRowType(Model):def__init__(self,name,parent):super().__init__(parent)self.name=nameself.Pos1=Seat("Pos1",self)self.Pos2=Seat("Pos2",self)self.Pos3=Seat("Pos3",self)defPos(self,index:int):ifindex<1orindex>3:raiseIndexError(f"Index {index} is out of range")_options={1:self.Pos1,2:self.Pos2,3:self.Pos3,}return_options.get(index)classVehicleIdentification(Model):def__init__(self,name,parent):super().__init__(parent)self.name=nameself.VIN=DataPointString("VIN",self)self.Model=DataPointString("Model",self)classCurrentLocation(Model):def__init__(self,name,parent):super().__init__(parent)self.name=nameself.Latitude=DataPointDouble("Latitude",self)self.Longitude=DataPointDouble("Longitude",self)self.Timestamp=DataPointString("Timestamp",self)self.Altitude=DataPointDouble("Altitude",self)classVehicle(Model):def__init__(self,name,parent):super().__init__(parent)self.name=nameself.Speed=DataPointFloat("Speed",self)self.CurrentLocation=CurrentLocation("CurrentLocation",self)self.Cabin=Cabin("Cabin",self)vehicle=Vehicle("Vehicle")
# include "sdk/DataPoint.h"
# include "sdk/Model.h"
usingnamespacevelocitas;classSeat:publicModel{public:Seat(std::stringname,Model*parent):Model(name,parent){}DataPointBooleanPosition{"Position",this};DataPointBooleanIsOccupied{"IsOccupied",this};DataPointBooleanIsBelted{"IsBelted",this};DataPointInt32Height{"Height",this};DataPointInt32Recline{"Recline",this};};classCurrentLocation:publicModel{public:CurrentLocation(Model*parent):Model("CurrentLocation",parent){}DataPointDoubleLatitude{"Latitude",this};DataPointDoubleLongitude{"Longitude",this};DataPointStringTimestamp{"Timestamp",this};DataPointDoubleAltitude{"Altitude",this};};classCabin:publicModel{public:classSeatCollection:publicModel{public:classRowType:publicModel{public:usingModel::Model;SeatPos1{"Pos1",this};SeatPos2{"Pos2",this};};SeatCollection(Model*parent):Model("Seat",parent){}RowTypeRow1{"Row1",this};RowTypeRow2{"Row2",this};};Cabin(Model*parent):Model("Cabin",parent){}DataPointInt32DriverPosition{"DriverPosition",this};SeatCollectionSeat{this};};classVehicle:publicModel{public:Vehicle():Model("Vehicle"){}DataPointFloatSpeed{"Speed",this};::CurrentLocationCurrentLocation{this};::CabinCabin{this};};
Middleware integration
gRPC Services
Vehicle Services are expected to expose their public endpoints over the gRPC protocol. The related protobuf definitions are used to generate method stubs for the Vehicle Model to make it possible to call the methods of the Vehicle Services.
Model integration
Based on the .proto files of the Vehicle Services, the protocol buffers compiler generates descriptors for all rpcs, messages, fields etc for the target language.
The gRPC stubs are wrapped by a convenience layer class derived from Service that contains all the methods of the underlying protocol buffer specification.
Info
The convencience layer of C++ is abit more extensive than in Python. The complexity of gRPC’s async API is hidden behind individual AsyncGrpcFacade implementations which need to be implemented manually. Have a look at the SeatAdjusterApp example’s SeatService and its SeatServiceAsyncGrpcFacade.
The underlying gRPC channel is provided and managed by the Service base class of the SDK. It is also responsible for routing the method invocation to the service through dapr middleware. As a result, a dapr-app-id has to be assigned to every Service, so that dapr can discover the corresponding vehicle services. This dapr-app-id has to be specified as an environment variable named <service_name>_DAPR_APP_ID.
Fluent query & rule construction
A set of query methods like get(), where(), join() etc. are provided through the Model and DataPoint base classes. These functions make it possible to construct SQL-like queries and subscriptions in a fluent language, which are then transmitted through the gRPC interface to the KUKSA Data Broker.
Query examples
The following examples show you how to query data points.
self.rule=(awaitself.vehicle.Cabin.Seat.Row(2).Pos(1).Position.subscribe(self.on_seat_position_change))defon_seat_position_change(self,data:DataPointReply):position=data.get(self.vehicle.Cabin.Seat.Row2.Pos1.Position).valueprint(f'Seat position changed to {position}')# Call to broker# Subscribe(rule="SELECT Vehicle.Cabin.Seat.Row2.Pos1.Position")# If needed, the subscription can be stopped like thisawaitself.rule.subscription.unsubscribe()
autosubscription=subscribeDataPoints(velocitas::QueryBuilder::select(Vehicle.Cabin.Seat.Row(2).Pos(1).Position).build())->onItem([this](auto&&item){onSeatPositionChanged(std::forward<decltype(item)>(item));});// If needed, the subscription can be stopped like this:
subscription->cancel();voidonSeatPositionChanged(constDataPointMap_tdatapoints){logger().info("SeatPosition has changed to: "+datapoints.at(Vehicle.Cabin.Seat.Row(2).Pos(1).Position)->asFloat().get());}
Vehicle.Cabin.Seat.Row(2).Pos(1).Position.where("Cabin.Seat.Row2.Pos1.Position > 50").subscribe(on_seat_position_change)defon_seat_position_change(data:DataPointReply):position=data.get(Vehicle.Cabin.Seat.Row2.Pos1.Position).valueprint(f'Seat position changed to {position}')# Call to broker# Subscribe(rule="SELECT Vehicle.Cabin.Seat.Row2.Pos1.Position WHERE Vehicle.Cabin.Seat.Row2.Pos1.Position > 50")
autoquery=QueryBuilder::select(Vehicle.Cabin.Seat.Row(2).Pos(1).Position).where(vehicle.Cabin.Seat.Row(2).Pos(1).Position).gt(50).build();subscribeDataPoints(query)->onItem([this](auto&&item){onSeatPositionChanged(std::forward<decltype(item)>(item));}));voidonSeatPositionChanged(constDataPointMap_tdatapoints){logger().info("SeatPosition has changed to: "+datapoints.at(Vehicle.Cabin.Seat.Row(2).Pos(1).Position)->asFloat().get());}// Call to broker:
// Subscribe(rule="SELECT Vehicle.Cabin.Seat.Row2.Pos1.Position WHERE Vehicle.Cabin.Seat.Row2.Pos1.Position > 50")
Publish & subscribe messaging
The SDK supports publishing messages to a MQTT broker and subscribing to topics of a MQTT broker. By leveraging the dapr pub/sub building block for this purpose, the low-level MQTT communication is abstracted away from the Vehicle App developer. Especially the physical address and port of the MQTT broker is no longer configured in the Vehicle App itself, but rather is part of the dapr configuration, which is outside of the Vehicle App.
Publish MQTT Messages
MQTT messages can be published easily with the publish_mqtt_event() method, inherited from VehicleApp base class:
In Python subscriptions to MQTT topics can be easily established with the subscribe_topic() annotation. The annotation needs to be applied to a method of the Vehicle App class. In C++ the subscribeToTopic() method has to be called. Callbacks for onItem and onError can be set. The following examples provide some more details.
@subscribe_topic("seatadjuster/setPosition/request")asyncdefon_set_position_request_received(self,data:str)->None:data=json.loads(data)logger.info("Set Position Request received: data=%s",data)
# include <fmt/core.h>
# include <nlohmann/json.hpp>
subscribeToTopic("seatadjuster/setPosition/request")->onItem([this](auto&&item){constautojsonData=nlohmann::json::parse(item);logger().info(fmt::format("Set Position Request received: data={}",jsonData));});
Under the hood, the vehicle app creates a grpc endpoint on port 50008, which is exposed to the dapr middleware. The dapr middleware will then subscribe to the MQTT broker and forward the messages to the vehicle app.
To change the app port, set it in the main() method of the app:
// c++ does not use dapr for Pub/Sub messaging at this point
Vehicle App abstraction
Vehicle Apps are inherited from the VehicleApp base class. This enables the Vehicle App to use the Publish & subscribe messaging and the KUKSA Data Broker.
The Vehicle Model instance is passed to the constructor of the VehicleApp class and should be stored in a member variable (e.g. self.vehicle for Python, std::shared_ptr<Vehicle> m_vehicle; for C++), to be used by all methods within the application.
Finally, the run() method of the VehicleApp class is called to start the Vehicle App and register all MQTT topic and Data Broker subscriptions.
Implementation detail
In Python, the subscriptions are based on asyncio, which makes it necessary to call the run() method with an active asyncio event_loop.
A typical skeleton of a Vehicle App looks like this:
classSeatAdjusterApp(VehicleApp):def__init__(self,vehicle:Vehicle):super().__init__()self.vehicle=vehicleasyncdefmain():# Main functionlogger.info("Starting seat adjuster app...")seat_adjuster_app=SeatAdjusterApp(vehicle)awaitseat_adjuster_app.run()LOOP=asyncio.get_event_loop()LOOP.add_signal_handler(signal.SIGTERM,LOOP.stop)LOOP.run_until_complete(main())LOOP.close()
# include "VehicleApp.h"
# include "vehicle_model/Vehicle.h"
usingnamespacevelocitas;classSeatAdjusterApp:publicVehicleApp{public:SeatAdjusterApp():VehicleApp(IVehicleDataBrokerClient::createInstance("vehicledatabroker")),IPubSubClient::createInstance("localhost:1883","SeatAdjusterApp")){}private:::VehicleVehicle;};intmain(intargc,char**argv){example::SeatAdjusterAppapp;app.run();return0;}
Learn about the main concepts and components of the vehicle abstraction and how it relates to the Eclipse KUKSA project.
Introduction
The Eclipse Velocitas project is using the Vehicle Abstraction Layer (VAL) of the Eclipse KUKSA project, also called KUKSA.VAL.
It is a reference implementation of an abstraction layer that allows Vehicle applications to interact with signals and services in the vehicle.
It currently consists of a data broker, a CAN feeder, and a set of example services.
Architecture
The image below shows the main components of the Vehicle Abstraction Layer (VAL) and its relation to the Velocitas Development Model.
Overview of the vehicle abstraction layer architecture
KUKSA Data Broker
The KUKSA Data Broker is a gRPC service acting as a broker of vehicle data / data points / signals.
It provides central access to vehicle data points arranged in a - preferably standardized - vehicle data model like the COVESA Vehicle Signal Specification (VSS) or others.
It is implemented in Rust, can run in a container and provides services to get datapoints, update datapoints and for subscribing to datapoints.
Filter- and rule-based subscriptions of datapoints can be used to reduce the number of updates sent to the subscriber.
Data Feeders
Conceptually, a data feeder is a provider of a certain set of data points to the data broker.
The source of the contents of the data points provided is specific to the respective feeder.
As of today, the Vehicle Abstraction Layer contains a generic CAN feeder (KUKSA DBC Feeder) implemented in Python,
which reads data from a CAN bus based on mappings specified in e.g. a CAN network description (dbc) file.
The feeder uses a mapping file and data point meta data to convert the source data to data points and injects them into the data broker using its Collector gRPC interface.
The feeder automatically reconnects to the data broker in the event that the connection is lost.
Vehicle Services
A vehicle service offers a gRPC interface allowing vehicle apps to interact with underlying services of the vehicle.
It can provide service interfaces to control actuators or to trigger (complex) actions, or provide interfaces to get data.
It communicates with the Hardware Abstraction to execute the underlying services, but may also interact with the data broker.
Data feeders rely on hardware abstraction. Hardware abstraction is project/platform specific.
The reference implementation relies on SocketCAN and vxcan, see https://github.com/eclipse/kuksa.val.feeders/tree/main/dbc2val.
The hardware abstraction may offer replaying (e.g., CAN) data from a file (can dump file) when the respective data source (e.g., CAN) is not available.
Information Flow
The vehicle abstraction layer offers an information flow between vehicle networks and vehicle services.
The data that can flow is ultimately limited to the data available through the Hardware Abstraction, which is platform/project-specific.
The KUKSA Data Broker offers read/subscribe access to data points based on a gRPC service. The data points which are actually available are defined by the set of feeders providing the data into the broker.
Services (like the seat service) define which CAN signals they listen to and which CAN signals they send themselves, see documentation.
Service implementations may also interact as feeders with the data broker.
Data flow when a Vehicle Application uses the KUKSA Data Broker.
Architectural representation of the KUKSA data broker data flow
Data flow when a Vehicle Application uses a Vehicle Service.
Architectural representation of the vehicle service data flow
Source Code
Source code and build instructions are available in the respective KUKSA.VAL repositories:
Guidelines for best practices on how to specify a gRPC-based service interface and on how to implement a vehicle service can be found in the kuksa.val.services repository.