This is the multi-page printable view of this section. Click here to print.
Tutorials
- 1: Getting Started
- 1.1: Quickstart
- 1.2: Import examples
- 1.3: Install a working container runtime
- 1.4: Working behind proxy
- 2: Prototyping
- 2.1: digital.auto
- 2.2: Service Integration
- 3: Vehicle App Development
- 3.1: Python Vehicle App Development
- 3.2: C++ Vehicle App Development
- 3.3: Vehicle App Integration Testing
- 4: Vehicle Model Creation
- 4.1: Automated Vehicle Model Lifecycle
- 4.2: Manual Vehicle Model Creation
- 4.2.1: C++ Manual Vehicle Model Creation
- 4.2.2: Python Manual Vehicle Model Creation
- 4.2.3: Vehicle Model Distribution
- 4.2.3.1: C++ Vehicle Model Distribution
- 4.2.3.2: Python Vehicle Model Distribution
- 5: Vehicle App Runtime
- 5.1: Local Runtime
- 5.2: Kanto Runtime
- 6: gRPC service generation
- 6.1: Create a server
- 6.2: Create a client
- 7: Vehicle App Deployment
1 - Getting Started
1.1 - Quickstart
This page describes
- how to create a GitHub repository for your Vehicle App development,
- how to set up and configure the DevContainer-based development environment , and
- how to build, customize and test the sample Vehicle App included in your freshly created Vehicle App repository.
You will learn how to use the Vehicle App SDK, interact with the Vehicle API and work with CI/CD using the pre-configured GitHub Workflows that come with the template repository.
Once you have completed all steps, you will have a solid understanding of the development workflow, and you will be able to use one of our template repositories as a starting point for your own Vehicle App development project.
Note
Before you start, we recommend familiarizing yourself with our
Basic Concept to understand all mentioned terms.
Prerequisites
Please make sure you did all the prerequisite steps to create comprehensive development environment for your Vehicle App:
-
Install VS Code
-
Add the Remote-Containers extension to VS Code via the marketplace or using the command line:
code --install-extension ms-vscode-remote.remote-containers
How to create your Vehicle App repository?
For your (GitHub) organization and Vehicle App repository the name MyOrg/MyFirstVehicleApp is used as a place holder during the rest of the document.
You can create your own repository using one of our provided templates or start prototyping via digital.auto.
Create your own repository copy from the template repository of your choice:
by clicking the green button Use this template. You don’t have to include all branches. For more information on Template Repositories take a look at this GitHub Tutorial .
Create your Vehicle App project via our Velocitas CLI create command from within vehicle-app-template’s devcontainer:
velocitas create
interactive mode.velocitas create -n MyApp -l python
for a skeleton vehicle application.velocitas create -n MyApp -l python -e seat-adjuster
for a vehicle application based on the seat adjuster example .
To learn how to start prototyping with the playground of digital.auto and integrate it into Velocitas please take a look here .
How to start developing?
In this section you will learn different possibilities to start developing based on your repository. Basically you can work on your own machine using VS Code’s DevContainer or you can set up the environment on a remote agent, using GitHub Codespaces .
The VS Code DevContainer makes it possible to package a complete Vehicle App development environment, including VS Code extensions, Vehicle App SDK , Vehicle App Runtimes and all other development and testing tools into a container which is started directly in VS Code.
Proxy Configuration
A non proxy configuration is used by default. If you are working behind a corporate proxy you will need to specify proxy settings:
With following steps you will clone and set up your development environment on your own machine using VS Code.
- Clone created MyOrg/MyFirstVehicleApp repository locally using your favorite Git tool
- Switch the directory to the cloned repository folder, e.g.
$ cd MyFirstVehicleApp
- Open the repository in VS Code via
$ code .
or via VS Code user interface . - A popup appears in the lower right corner with the button Reopen in Container.
- Click on Reopen in Container. If the popup does not appear, you can also hit F1 and perform the command
Dev-Containers: Reopen in Container
- Wait for the container to be set up
The first initializing of the container will take some minutes to build the image and provision all the integrated tools.
If the DevContainer build process fails, press F1 and run the command Dev-Containers: Rebuild Container Without Cache
.
The DevContainer is using the
docker-in-docker feature to run docker containers within the container.
One of the possibilities to use your newly created repository is to use it inside a GitHub Codespace . You can either try it out directly in the browser or also use it inside VS Code. The main thing to remember is that everything is executed on a remote agent and the browser or VS Code just acts as a “thin-client”.
To get started with GitHub Codespaces, you just have to follow a few steps:
- Open your repository on GitHub (e.g. https://github.com/MyOrg/MyFirstVehicleApp )
- Click on the green
Code
button and select Codespaces on the top - Configure your Codespace if needed (defaults to the main branch and a standard agent)
- Click on
create
A new window will open where you can see logs for setting up the container. On this window you could now also choose to work with VS Code. The environment remains on a remote agent and VS Code establishes a connection to this machine.
Once everything is set up in the Codespace, you can work with it in the same way as with the normal DevContainer inside VS Code.
Be careful with using GitHub Codespaces in a browser and VS Code locally at the same time: Tasks that are started using a browser session will not show in VS Code environment and vice versa. This might lead to problems.
You can find more information about the Vehicle App development in the respective pages .
How to start the runtime services?
Velocitas has packaged a number of services as runtime services. The services (like KUKSA Databroker or Vehicle Services) can be used when developing Vehicle Apps and to run integration test.
Currently, the supported options to run these services is either locally or via the Kanto runtime .
A VS Code task called Local Runtime - Up
is available to start the included services in the correct order.
- Press F1
- Select command
Tasks: Run Task
- Select
Local Runtime - Up
You should see the task Local Runtime - Up
being executed on a separate VS Code terminal with the following content:
$ velocitas exec runtime-local up
Hint: Log files can be found in your workspace's logs directory
> mqtt-broker running
> vehicledatabroker running
> seatservice running
> feedercan running
✅ Runtime is ready to use!
To stop the runtime simply press Ctrl + C
.
A VS Code task called Kanto Runtime - Up
is available to start all necessary services in the correct order.
- Press F1
- Select command
Tasks: Run Task
- Select
Kanto Runtime - Up
You should see the task Kanto Runtime - Up
being executed on a separate VS Code terminal with the following content:
$ velocitas exec runtime-kanto up
Hint: Log files can be found in your workspace's logs directory
> Checking Kanto registry... registry already exists.
> Checking Kanto registry... starting registry.
> Checking Kanto registry... started.
✅ Configuring controlplane for Kanto...
⠇ Starting Kanto...waiting
✅ Kanto is ready to use!
To stop the runtime simply press Ctrl + C
or execute the task Kanto Runtime - Down
.
More information about the runtimes are available here .
How to debug your Vehicle App?
Warning
Debugging functionality is only available when using the
Local Runtime . Both given examples are available as part of template.
Now that the runtime services are all up and running, let’s start a debug session for the Vehicle App.
- Open the main source file
/app/src/main.py
and set a breakpoint in the given methodon_get_speed_request_received
- Press F5 to start a debug session of the Vehicle App and see the log output on the
DEBUG CONSOLE
To trigger this breakpoint, let’s send a message to the Vehicle App using the mqtt broker that is running in the background.
- Open
VSMqtt
extension in VS Code and connect tomosquitto (local)
- Set
Subscribe Topic
=sampleapp/getSpeed/response
and click subscribe - Set
Publish Topic
=sampleapp/getSpeed
- Press publish with an empty payload field.
- Open the main source file
/app/src/VehicleApp.cpp
and set a breakpoint in the given methodonSetPositionRequestReceived
- Press F5 to start a debug session of the Vehicle App and see the log output on the
DEBUG CONSOLE
To trigger this breakpoint, let’s send a message to the Vehicle App using the mqtt broker that is running in the background.
- Open
VSMqtt
extension in VS Code and connect tomosquitto (local)
- Set
Subscribe Topic
=seatadjuster/setPosition/response
and click subscribe - Set
Subscribe Topic
=seatadjuster/currentPosition
and click subscribe - Set
Publish Topic
=seatadjuster/setPosition/request
- Set and publish a dummy payload:
{ "position": 300, "requestId": 123 }
How to trigger the CI Workflow?
The provided GitHub workflows are used to build the container image for the Vehicle App, run unit and integration tests and collect the test results.
The CI Workflow will be triggered by pushing a change to the main branch of your repository:
-
Make modification in any of your files
-
Navigate in your terminal to your repository
-
Commit and push your change
git add . git commit -m "<explain your changes>" git push origin
To see the results open the Actions
page of your repository on GitHub, go to CI Workflow
and check the workflow output.
How to release your Vehicle App?
Now that the CI Workflow
was successful, you are ready to build your first release. The goal is to build a ready-to-deploy container image that is published in the GitHub container registry.
- Open the
Code
page of your repository on GitHub - Click on
Create a new release
in the Releases section on the right side - Enter a version (e.g. v1.0.0) and click on
Publish release
- GitHub will automatically create a tag using the version number
The provided release workflow will be triggered by the release. It creates a release documentation and publishes the container image of the Vehicle App to the GitHub container registry. A detailed description of the workflow can be found here .
How to deploy your Vehicle App?
After releasing the Vehicle App to the GitHub container registry you might ask how to bring the Vehicle App and the required runtime stack on a device. Here, Eclipse Leda comes into the game.
Please read the documentation of Eclipse Leda to get more information.
Next steps
- Tutorial: Creating a Vehicle Model
- Tutorial: Create a Vehicle App
- Tutorial: Develop and run integration tests for a Vehicle App
1.2 - Import examples
This guide will help you to import examples provided by the SDK package into your template repository.
A Visual Studio Code task called Import example app from SDK
is available in the /.vscode/tasks.json
which can replace your /app
directory in your template repository with some example Vehicle Apps from the
SDK
package.
/app
directory, commit or stash changes before importing the example app.
- Press F1
- Select command
Tasks: Run Task
- Select
Import example app from SDK
- Choose
Continue without scanning the output
- Select
seat-adjuster
Run the Vehicle App from SDK example
The launch settings are already prepared for the VehicleApp
in the template repository /.vscode/launch.json
. The configuration is meant to be as generic as possible to make it possible to run all provided example apps.
Every example app comes with its own /app/AppManifest.json
to see which Vehicle Services are configured and needed as a dependency.
To start the app: Just press F5 to start a debug session of the example Vehicle App.
To debug example, please check How to debug Vehicle App?
1.3 - Install a working container runtime
In the past the recommended runtime would for sure be Docker Desktop . But since Docker Inc. changed their license model it is fair enough for an open source project to look for free alternatives.
Linux
The obvious (and our recommended) “alternative” to Docker Desktop on Linux is to just use the Docker Engine (without Docker Desktop), a pure CLI-based solution available for most popular Linux distributions licensed under the Apache License, version 2.0. Installation instructions can be found here .
MacOS
Since the Docker Engine is not working out of the box on MacOS, a virtualizations tool which helps emulating linux is needed. Fortunately there are several solutions on the market. Good results could be achieved using Colima .
Setup Colima
Please uninstall or at least quit Docker Desktop if you already used it, before starting the setup.
For Colima to work properly you need Colima itself and a container client e.g. the Docker client, which is still free to use:
brew install colima
brew install docker
After the installation you need to start the runtime:
colima start --cpu x --memory y
For M1 Macs it might be necessary to add --arch aarch64
Docker Desktop uses 5 cores and 12 GB of RAM by default on an M1 MacBook Pro. The equivalent in Colima can be achieved with
colima start --cpu 5 --memory 12
That’s all you have to do. After these few steps you can go on with the devcontainer setup.
Microsoft Windows
There is currently no recommended alternative for Windows except using GitHub codespaces, a cloud-based development environment.
An option would be to setup a VM (e.g. with VirtualBox or VMWare) running a Linux system with Docker Engine (see above).
Other alternatives
Besides our recommendations above, there are further alternatives, which are not yet evaluated by this project or have some other drawbacks, blocking a recommendation.
For example, you could try
Podman
/
Buildah
, which can replace docker run
and docker build
, respectively.
Podman is available for MacOS, Windows, and several Linux distributions.
Buildah seems just being available for several Linux distributions.
1.4 - Working behind proxy
We know what a pain and how time consuming it can be to setup your environment behind a cooperate proxy. This guide will help you to set it up correctly.
Be aware that correct proxy configuration depends on the setup of your organization and of course of your personal development environment (hardware, OS, virtualization setup, …). So, we most probably do not cover all issues out there in the developers world. So, we encourage you to share hints and improvements with us.
HTTP(s) proxy server
Install and configure the proxy server as recommended or required by your company. For example you could use PX , which is a HTTP(s) proxy server that allows applications to authenticate through an NTLM or Kerberos proxy server, typically used in corporate deployments, without having to deal with the actual handshake. Px leverages Windows SSPI or single sign-on and automatically authenticates using the currently logged in Windows user account. It is also possible to run Px on Windows, Linux and MacOS without single sign-on by configuring the domain, username and password to authenticate with. (Source: PX )
- Install your HTTP(s) proxy server
- Start your HTTP(s) proxy server
Docker Desktop
You need to install Docker Desktop using the right version. As we recognized a proxy issue in Docker Desktop #12672 we strongly recommend to use a Docker Desktop version >= 4.8.2. In case you have an older version on your machine please update to the current version.
In the next step you need to enter your proxy settings:
- Open Docker Desktop and go to the Settings
- From
Resources
, selectProxies
- Enable
Manual proxy configuration
- Enter your proxy settings, this depends on the configuration you did while setting up your proxy tool e.g.:
- Web Server (HTTP):
http://localhost:3128
- Secure Web Server (HTTPS):
http://localhost:3128
- Bypass:
localhost,127.0.0.1
- Web Server (HTTP):
- Apply & Restart.
Docker daemon
You also have to configure the Docker daemon, which is running the containers basically, to forward the proxy settings. For this you have to add the proxy configuration to the ~/.docker/config.json
. Here is an example of a proper config (Port and noProxy settings might differ for your setup):
{
"proxies":{
"default":{
"httpProxy":"http://host.docker.internal:3128",
"httpsProxy":"http://host.docker.internal:3128",
"noProxy":"host.docker.internal,localhost,127.0.0.1"
}
}
}
{
"proxies":{
"default":{
"httpProxy":"http://host.docker.internal:3128",
"httpsProxy":"http://host.docker.internal:3128",
"noProxy":"host.docker.internal,localhost,127.0.0.1"
}
}
}
{
"proxies":{
"default":{
"httpProxy":"http://172.17.0.1:3128",
"httpsProxy":"http://172.17.0.1:3128",
"noProxy":"host.docker.internal,localhost,127.0.0.1"
}
}
}
For more details see: Docker Documentation
Environment Variables
It is required to set the following environment variables:
HTTP_PROXY
- proxy server, e.g.http://localhost:3128
HTTPS_PROXY
- secure proxy server, e.g.http://localhost:3128
set
setx HTTP_PROXY "http://localhost:3128"
setx HTTPS_PROXY "http://localhost:3128"
echo "export HTTP_PROXY=http://localhost:3128" >> ~/.bash_profile
echo "export HTTPS_PROXY=http://localhost:3128" >> ~/.bash_profile
source ~/.bash_profile
echo "export HTTP_PROXY=http://localhost:3128" >> ~/.bash_profile
echo "export HTTPS_PROXY=http://localhost:3128" >> ~/.bash_profile
source ~/.bash_profile
Troubleshooting
Solving issues with TLS (SSL) certificate validation using https connections from containers
If you are behind a so-called intercept proxy (which you most probably are), you can run into certificate issues: Your corporate proxy works as a “man-in-the-middle” to be able to check the transferred data for malicious content. Means, there is a protected connection between the application in your local runtime environment and the proxy and another from the proxy to the external server your application wants to interact with.
For the authentication corporate proxies often use self-signed certificates (certificates which are not signed by a (well-known official) certificate authority. Those kind of certificates need to be added to the database of trusted certificates of your local runtime environment. This task is typically handled by the IT department of your corporation (if the OS and software installed on it is managed by them) and you will not run into problems, normally.
If it comes to executing containers, those are typically not managed by your IT department and the proxy certificate(s) is/are missing. So, you need to find a way to install those into the (dev) container you want to execute.
See (one of) those articles to get how to achieve that:
Initial DevContainer build issue
If you experience issues during initial DevContainer build, clean all images and volumes otherwise cache might be used:
- Open Docker Desktop
- From
Troubleshooting
chooseClean / Purge data
GitHub rate limit exceeded
How to fix can be found at Lifecycle Management Troubleshooting .
2 - Prototyping
2.1 - digital.auto
The open and web based digital.auto offers a rapid prototyping environment to explore and validate ideas of a Vehicle App. digital.auto interacts with different vehicle sensors and actuators via standardized APIs specified by the COVESA Vehicle Signal Specification (VSS) without custom setup requirements. Within the platform you can:
- browse, navigate and enhance vehicle signals (sensors, actuators and branches) in the Vehicle API Catalogue mapped to a 3D model of the vehicle.
- build Vehicle App prototypes in the browser using Python and the Vehicle API Catalogue.
- test the Vehicle App prototype in a dashboard with 3D animation for API calls.
- create new plugins, which usually represent UX widgets or remote server communication to enhance the vehicle mockup experience in the playground.
- collect and evaluate user feedback to prioritize your development portfolio.
Start the journey of a Vehicle App
As first step open digital.auto , select Get Started in the prototyping section of the landing page and use the Vehicle Model of your choice.
You now have the possibility to browse existing vehicle signals for the selected vehicle model which you can use for prototyping your Vehicle App by clicking on Vehicle APIs.
Add additional Vehicle APIs
If the ideation of your Vehicle App prototype comes with any new Vehicle API which is not part of the standard VSS you also have the option to include it into your pre-selected model by clicking the + New Wishlist API button. After filling out all required fields, simply click the create button - this will commit the new API to the existing model.
Note
For this feature, a digital.auto account is required. Get in touch with us or the digital.auto team in case you want to explore this feature.Prototype an idea of a Vehicle App
The next step would be to prototype your idea. To do so:
- Click on Prototype Library of your selected model.
- Create a new prototype, by clicking on New Prototype and filling out the information or select one from the list.
- Click on the Open button.
- Go to the Code section and start your prototype right away.
Test the prototype of a Vehicle App
Testing of your prototype starts in the Run section. You will find a dashboard consisting all vehicle and application components similar to mockups. The control center on the right side has an integrated terminal showing all of your prototyped outputs as well as a list of all called VSS API’s. The Run button executes all your prototype code from top to bottom. The Debug button allows you to step through your prototype line by line.
To get started quickly, the digital.auto team has added a number of widgets to simulate related elements of the vehicle – like doors, seats, light, etc. – and made them available in the playground.
Feel free to add your own Plugins with widgets for additional car features (maybe an antenna waving a warm “welcome”…?).
Transfer your prototype into a Velocitas Vehicle App
In the previous steps you started with envisioning and prototyping your Vehicle App idea and tested it against mocked vehicle components in digital.auto. The Velocitas team provides a project generator to transfer the prototype from digital.auto into your own development environment where you are able to test it with real Vehicle Services . The generator creates a Vehicle App GitHub repository using your prototype code based on our vehicle-app-python-template . In the ‘Code’ section of your prototype in digital.auto you have the button ‘Create Eclipse Velocitas Project’.
After pressing the button you will be forwarded to GitHub . Login with your GitHub Account and authorize velocitas-project-generator to create the repository for you. You will be redirected to digital.auto and asked for a repository name (equals to the name of the Vehicle App). By clicking on “Create repository”:
- the project generator takes over your prototype code.
- the code is adapted to the structure in the vehicle-app-python-template .
- a new private repository under your specified GitHub User will be created.
A successful generation of the repository is followed by a pop-up dialogue with the URL of your repository.
Among other things the newly created repository will contain:
Files | Description |
---|---|
/app/src/main.py | Main class of the Vehicle App, containing your modified prototype code |
/app/AppManifest.json | Settings file defining required services |
/app/requirements.txt | Requirements file defining all Python dependencies |
/.devcontainer/ | Required scripts and settings to setup the devcontainer in Microsoft Visual Studio Code |
/.github/workflows/ | All required CI/CD pipelines to build, test and deploy the Vehicle App as container image to the GitHub container registry |
/gen/vehicle_model/ | The generated model classes. If your prototype includes any exceptional API you added beforehand our automated vehicle model lifecycle takes care of handling the custom VSS vspec file coming from digital.auto and generates a vehicle_model when starting the devContainer |
Your prototype Vehicle App transferred into a GitHub repository is now ready to be extended. Clone your newly created repository and open the Vehicle App in Microsoft Visual Studio Code and start to extend it.
You can proceed with the following topics:
2.2 - Service Integration
Services can make sure, that when you write a VSS data point, something is actually happening. Eclipse Velocitas has an example seat or hvac service. If your Vehicle App makes use of e.g. Vehicle.Cabin.Seat.Row1.Pos1.Position
or other seat/hvac specific data points you are in for some real action. To learn more, visit
Vehicle Services
.
Our maintained
devenv-runtimes
package (
Velocitas Lifecycle Management
) comes with the support of adding further Vehicle Services to the runtime.json
of a package. More information
here
.
Modify existing services
For more advanced usage you can also try to modify existing services. Check out the seat service for example, modify it and integrate it into your Vehicle App repository.
Create your own services
If you want to create your own service the KUKSA Incubation repository contains examples illustrating how such kind of vehicle services can be built. You need to write an application that talks to KUKSA listening to changes of a target value of some VSS data point and then do whatever you want. You can achieve this by using the KUKSA gRPC API with any programming language of your choice (learn more about gRPC ).
Mock Provider and Mock Provider Integration
The Vehicle Mock Provider is a dummy service allowing to control all specified actuator- and sensor-signals via a configuration file. These configuration files are expressed in a Python-based domain-specific language (DSL). The default behavior is predefined in mock.py
The Mock Provider is already integrated in all our
Vehicle Runtimes
. To be able to configure it, you need to add a custom mock.py
in the root of your Vehicle App Project. The Mock Provider Container will pick it up automatically.
3 - Vehicle App Development
Please visit first Getting Started page if you don’t know where to start.
3.1 - Python Vehicle App Development
We recommend that you make yourself familiar with the Vehicle App SDK first, before going through this tutorial.
The following information describes how to develop and test the sample Vehicle App that is included in the Python template repository . You will learn how to use the Vehicle App Python SDK and how to interact with the Vehicle Model.
Once you have completed all steps, you will have a solid understanding of the development workflow and you will be able to reuse the template repository for your own Vehicle App development project.
Develop your first Vehicle App
This section describes how to develop your first Vehicle App. Before you start building a new Vehicle App, make sure you have already read this manual:
Once you have established your development environment, you will be able to start developing your first Vehicle App.
For this tutorial, you will recreate the Vehicle App that is included with the SDK repository : The Vehicle App allows to change the position of the driver’s seat in the car and also provides its current positions to other applications. A detailed explanation of the use case and the example is available here .
Note
If you don’t like to do the following steps by yourself, you can use the Import example app from SDK
task within VS Code to get a working copy of this example into your repository.
For details about the import of an example from the SDK look
Setting up the basic skeleton of your app
At first, you have to create the main Python script called main.py
in /app/src
. All the relevant code for your new Vehicle App goes there.
If you’ve created your app development repository from our Python template repository , the Velocitas CLI create command or via digital.auto prototyping a file with this name is already present and can be adjusted to your needs.
Setting up the basic skeleton of an app consists of the following steps:
Manage your imports
Before you start development in the main.py
you just created, it will be necessary to include the imports required, which you will understand better later through the development:
import asyncio
import json
import logging
import signal
from velocitas_sdk.util.log import ( # type: ignore
get_opentelemetry_log_factory,
get_opentelemetry_log_format,
)
from velocitas_sdk.vdb.reply import DataPointReply
from velocitas_sdk.vehicle_app import VehicleApp, subscribe_topic
from vehicle import Vehicle, vehicle # type: ignore
Enable logging
The following logging configuration applies the default log format provided by the SDK and sets the log level to INFO:
logging.setLogRecordFactory(get_opentelemetry_log_factory())
logging.basicConfig(format=get_opentelemetry_log_format())
logging.getLogger().setLevel("INFO")
logger = logging.getLogger(__name__)
Initialize your class
The main class of your new Vehicle App needs to inherit the VehicleApp
provided by the
Python SDK
.
class MyVehicleApp(VehicleApp):
In class initialization, you have to pass an instance of the Vehicle Model:
def __init__(self, vehicle_client: Vehicle):
super().__init__()
self.Vehicle = vehicle_client
We save the vehicle object to use it in our app. Now, you have initialized the app and can continue developing relevant methods.
Entry point of your app
Here’s an example of an entry point to the MyVehicleApp that we just developed:
async def main():
"""Main function"""
logger.info("Starting my VehicleApp...")
vehicle_app = MyVehicleApp(vehicle)
await vehicle_app.run()
LOOP = asyncio.get_event_loop()
LOOP.add_signal_handler(signal.SIGTERM, LOOP.stop)
LOOP.run_until_complete(main())
LOOP.close()
With this your app can now be started. In order to provide some meaningful behaviour of the app, we will enhance it with more features in the next sections.
Vehicle Model Access
In order to facilitate the implementation, the whole vehicle is abstracted into model classes. Please check tutorial about creating models for more details about this topic. In this section, the focus is on using the model.
The first thing you need to do is to get access to the Vehicle Model. If you derived your project repository from our template, we already provide a generated model installed as a Python package named vehicle
. Hence, in most cases no additional setup is necessary. How to tailor the model to your needs or how you could get access to vehicle services is described in the tutorial linked above.
If you want to access a single DataPoint e.g. for the vehicle speed, this can be done via
vehicle_speed = (await self.Vehicle.Speed.get()).value
As the get()
method of the DataPoint-class there is a coroutine you have to use the await
keyword when using it and access its .value
.
If you want to get deeper inside the vehicle, to access a single seat for example, you just have to go the model-chain down:
self.DriverSeatPosition = await self.Vehicle.Cabin.Seat.Row1.DriverSide.Position.get()
Subscription to Data Points
If you want to get notified about changes of a specific DataPoint
, you can subscribe to this event, e.g. as part of the on_start()
method in your app.
async def on_start(self):
"""Run when the vehicle app starts"""
await self.Vehicle.Cabin.Seat.Row1.DriverSide.Position.subscribe(
self.on_seat_position_changed
)
Every DataPoint
provides a .subscribe() method that allows for providing a callback function which will be invoked on every data point update. Subscribed data is available in the respective DataPointReply object and need to be accessed via the reference to the subscribed data point. The returned object is of type TypedDataPointResult
which holds the value
of the data point
and the timestamp
at which the value was captured by the Databroker.
Therefore the on_seat_position_changed
callback function needs to be implemented like this:
async def on_seat_position_changed(self, data: DataPointReply):
# handle the event here
response_topic = "seatadjuster/currentPosition"
position = data.get(self.Vehicle.Cabin.Seat.Row1.DriverSide.Position).value
# ...
Subscription using Annotations
The Python SDK also supports annotations for subscribing to data point changes with @subscribe_data_points
defined by the whole path to the DataPoint
of interest. This would replace the implementation of the
Subscription to Data Points
@subscribe_data_points("Vehicle.Cabin.Seat.Row1.DriverSide.Position")
async def on_seat_position_changed(self, data: DataPointReply):
response_topic = "seatadjuster/currentPosition"
response_data = {"position": data.get(self.Vehicle.Cabin.Seat.Row1.DriverSide.Position).value}
await self.publish_event(response_topic, json.dumps(response_data))
Similarly, subscribed data is available in the respective DataPointReply object and needs to be accessed via the reference to the subscribed data point.
Services
Services are used to communicate with other parts of the vehicle via remote function calls (RPC). Please read the basics about them here .
Note
Services are not supported by our
automated vehicle model lifecycle for the time being. If you need access to services please read
here how you can create a model and add services to it manually.
MoveComponent()
method of the SeatService
from the vehicle model:
location = SeatLocation(row=1, index=1)
await self.vehicle_client.Cabin.SeatService.MoveComponent(
location, BASE, data["position"]
)
In order to define which seat you like to move, you have to pass a SeatLocation
object as the first parameter. The second argument specifies the component of the seat to be moved. The possible components are defined in the proto files. The last parameter to be passed into the method is the desired position of the component.
Make sure to use the
await
keyword when calling service methods, since these methods are asynchronously working coroutines.
MQTT
Interaction with other Vehicle Apps or with the cloud is enabled by using the Mosquitto MQTT Broker. The MQTT broker runs inside a docker container, which is started as part of one of our predefined runtimes .
In the
quickstart section
about the Vehicle App, you already tested sending MQTT messages to the app.
In the previous sections, you generally saw how to use Vehicle Models
, DataPoints
and Services
. In this section, you will learn how to combine them with MQTT.
In order to receive and process MQTT messages inside your app, simply use the @subscribe_topic
annotations from the SDK for an additional method on_set_position_request_received()
you have to implement:
@subscribe_topic("seatadjuster/setPosition/request")
async def on_set_position_request_received(self, data_str: str) -> None:
logger.info(f"Got message: {data_str!r}")
data = json.loads(data_str)
response_topic = "seatadjuster/setPosition/response"
response_data = {"requestId": data["requestId"], "result": {}}
# ...
The on_set_position_request_received
method will now be invoked every time a message is published to the subscribed topic "seatadjuster/setPosition/response"
. The message data (string) is provided as parameter. In the example above the data is parsed from json (data = json.loads(data_str)
).
In order to publish data to topics, the SDK provides the appropriate convenience method: self.publish_event()
which will be added to the on_seat_position_changed
callback function from before.
async def on_seat_position_changed(self, data: DataPointReply):
response_topic = "seatadjuster/currentPosition"
position = data.get(self.Vehicle.Cabin.Seat.Row1.DriverSide.Position).value
await self.publish_event(
response_topic,
json.dumps({"position": position}),
)
The above example illustrates how one can easily publish messages. In this case, every time the seat position changes, the new position is published to seatadjuster/currentPosition
Your main.py
should now have a full implementation for class MyVehicleApp(VehicleApp):
containing:
__init__()
on_start()
on_seat_position_changed()
on_set_position_request_received()
and last but not least a main()
method to run the app.
Check the
seat-adjuster
example to see a more detailed implementation including error handling.
UnitTests
Unit testing is an important part of the development, so let’s have a look at how to do that. You can find some example tests in /app/tests/unit
.
First, you have to import the relevant packages for unit testing and everything you need for your implementation:
from unittest import mock
import pytest
from sdv.vehicle_app import VehicleApp
from sdv_model.Cabin.SeatService import SeatService # type: ignore
from sdv_model.proto.seats_pb2 import BASE, SeatLocation # type: ignore
@pytest.mark.asyncio
async def test_for_publish_to_topic():
# Disable no-value-for-parameter, seems to be false positive with mock lib
with mock.patch.object(
VehicleApp, "publish_event", new_callable=mock.AsyncMock, return_value=-1
):
response = await VehicleApp.publish_event(
str("sampleTopic"), get_sample_request_data() # type: ignore
)
assert response == -1
def get_sample_request_data():
return {"position": 330, "requestId": 123}
Looking at a test you notice the annotation @pytest.mark.asyncio
. This is required if the test is defined as a coroutine. The next step is to create a mock from all the external dependencies. The method takes 4 arguments: first is the object to be mocked, second the method for which you want to modify the return value, third a callable and the last argument is the return value.
After creating the mock, you can test the method and check the response. Use asserts to make your test fail if the response does not match.
Check the
seat-adjuster
unit tests
to see a more detailed implementation.
See the results
Once the implementation is done, it is time to run and debug the app.
Run your App
In order to run the app:
- Make sure the
devenv-runtimes
&devenv-devcontainer-setup
packages are part of your.velocitas.json
(which should be the default). - Have a correctly configured
app/AppManifest.json
. See more - Trigger our
automated vehicle model lifecycle
. (e. g.
velocitas init
) - A runtime needs to be up and running. Read more about it in the run runtime services section.
Now chose one of the options to start the VehicleApp under development:
- Press F5
or:
- Press F1
- Select command
Tasks: Run Task
- Select
Local Runtime - Run VehicleApp
Debug your Vehicle App
In the introduction about debugging , you saw how to start a debugging session. In this section, you will learn what is happening in the background.
The debug session launch settings are already prepared for the VehicleApp
in /.vscode/launch.json
.
{
"configurations": [
{
"type": "python",
"justMyCode": false,
"request": "launch",
"name": "VehicleApp",
"program": "${workspaceFolder}/app/src/main.py",
"console": "integratedTerminal",
"env": {
"SDV_MIDDLEWARE_TYPE": "native",
"SDV_VEHICLEDATABROKER_ADDRESS": "grpc://127.0.0.1:55555",
"SDV_MQTT_ADDRESS": "mqtt://127.0.0.1:1883"
}
}
]
}
We specify which python-script to run using the program
key.
You can adapt the configuration in /.vscode/launch.json
and in /.vscode/tasks.json
to your needs (e.g., change the ports, add new tasks) or even add a completely new configuration for another Vehicle App.
Once you are done, you have to switch to the debugging tab (sidebar on the left) and select your configuration using the dropdown on the top. You can now start the debug session by clicking the play button or F5. Debugging is now as simple as in every other IDE, just place your breakpoints and follow the flow of your Vehicle App.
Next steps
- Concept: SDK Overview
- Tutorial: Deploy runtime services in Kanto
- Tutorial: Start runtime services locally
- Tutorial: Creating a Python Vehicle Model
- Tutorial: Develop and run integration tests for a Vehicle App
- Concept: Deployment Model
3.2 - C++ Vehicle App Development
We recommend that you make yourself familiar with the Vehicle App SDK first, before going through this tutorial.
The following information describes how to develop and test the sample Vehicle App that is included in the C++ template repository . You will learn how to use the Vehicle App C++ SDK and how to interact with the Vehicle Model.
Once you have completed all steps, you will have a solid understanding of the development workflow and you will be able to reuse the template repository for your own Vehicle App development project.
Develop your first Vehicle App
This section describes how to develop your first Vehicle App. Before you start building a new Vehicle App, make sure you have already read this manual:
For this tutorial, you will recreate the Vehicle App that is included in the template repository : The Vehicle App allows you to change the position of the driver’s seat in the car and also provides its current positions to other applications. A detailed explanation of the use case and the example is available here .
Setting up the basic skeleton of your app
At first, you have to create the main C++ file which we will call App.cpp
in /app/src
. All the relevant code for your new Vehicle App goes there. Afterwards, there are several steps you need to consider when developing the app:
Manage your includes
Before you start development in the App.cpp
you just created, it will be necessary to include all required header files, which you will understand better later through the development:
#include "sdk/VehicleApp.h"
#include "sdk/IPubSubClient.h"
#include "sdk/IVehicleDataBrokerClient.h"
#include "sdk/Logger.h"
#include "vehicle/Vehicle.hpp"
#include <memory>
using namespace velocitas;
Initialize your class
The main class of your new Vehicle App needs to inherit the VehicleApp
provided by the
C++ SDK
.
class MyVehicleApp : public VehicleApp {
public:
// <remaining code in this tutorial goes here>
private:
vehicle::Vehicle Vehicle; // this member exists to provide simple access to the vehicle model
}
In your constructor, you have to choose which implementations to use for the VehicleDataBrokerClient and the PubSubClient. By default we suggest you use the factory methods to generate the default implementations: IVehicleDataBrokerClient::createInstance
and IPubSubClient::createInstance
. These will create a VehicleDataBrokerClient which connects to the VAL via gRPC and an MQTT-based pub-sub client.
MyVehicleApp()
: VehicleApp(IVehicleDataBrokerClient::createInstance("vehicledatabroker"), // this is the app-id of the KUKSA Databroker in the VAL.
IPubSubClient::createInstance("MyVehicleApp")) // the clientId identifies the client at the pub/sub broker
{}
{}
Note
The URI of the MQTT broker is by defaultlocalhost:1883
and can be set to another address via the environment variable SDV_MQTT_ADDRESS
(beginning with C++ SDK v0.3.3) or MQTT_BROKER_URI
(SDKs before v0.3.3).
Now, you have initialized the app and can continue developing relevant methods.
Entry point of your app
Here’s an example of an entry point to the MyVehicleApp
that we just developed:
int main(int argc, char** argv) {
MyVehicleApp app;
app.run();
return 0;
}
With this your app can now be started. In order to provide some meaningful behaviour of the app, we will enhance it with more features in the next sections.
Vehicle Model Access
In order to facilitate the implementation, the whole set of vehicle signals is abstracted into model classes. Please check the tutorial about creating models for more details. In this section, the focus is on using the model.
The first thing you need to do is to get access to the Vehicle Model. If you derived your project repository from our template, we already provide a generated model as a Conan source package. The library is already referenced as “include folder” in the CMake files. Hence, in most cases no additional setup is necessary. How to tailor the model to your needs or how you could get access to vehicle services is described in the tutorial linked above. In your source code the model is included via #include "vehicle/Vehicle.hpp"
(as shown above).
If you want to read a single signal/data point e.g. for the vehicle speed, the simplest way is to do it via a blocking call and directly accessing the value of the speed:
auto vehicleSpeed = Vehicle.Speed.get()->await().value();
Lets have a look, what this line contains:
- The term
Vehicle.Speed
addresses the signal we like to query, i.e. the current speed of the vehicle. - The term
.get()
tells that we want to get/read the current state of that signal from the Data Broker. Behind the scenes this triggers a request-response flow via IPC with the Data Broker. - The term
->await()
blocks the execution until the response was received. - Finally, the term
.value()
tries to access the returned speed value.
The get()
returns a shared_ptr
to an AsyncResult
which, as the name implies, is the result of an asynchronous operation. We have two options to access the value of the asynchronous result. First we can use await()
and block the calling thread until a result is available or use onResult(...)
which allows us to inject a function pointer or a lambda which is called once the result is available:
Vehicle.Speed.get()
->onResult([](auto vehicleSpeed){
logger().info("Got speed!");
})
->onError(auto status){
logger().info("Something went wrong communicating to the data broker!");
});
If you want to get deeper inside to the vehicle, to access a single seat for example, you just have to go the model-chain down:
auto driverSeatPosition = Vehicle.Cabin.Seat.Row1.Pos1.Position.get()->await();
Class TypedDataPointValue
If you have a detailed look at the AsyncResult
class, you will observe that the object returned by the await()
or passed to the onResult
callback is not directly the current value of the signal, but instead an object of type TypedDataPointValue
. This object does not only contain the current value of the signal but also some additional metadata accessible via these functions:
getPath()
provides the signal name, i.e. the complete path,getType()
provides the data type of the signal,getTimeStamp()
provides the timestamp when the current state was received by the data broker,isValid()
returnstrue
if the current state represents a valid value of the signal orfalse
otherwise,getFailure()
returns the reason, why the current state does not represent a valid value (it returnsNONE
if the value is valid),getValue()
returns the a valid current value. It will throw anInvalidValueException
if the current value is invalid for whatever reason.
The latter three points lead us to the next chapter.
Failure Handling
As indicated above, there might be reasons/situations why the get operation is not able to deliver a valid value for the requested signal. Those shall be handled properly by any application (that wants “to be more” than a prototype).
There are two ways to handle the failure situations:
- Either you can catch the exception thrown by the
.value()
function:
try {
auto vehicleSpeed = Vehicle.Speed.get()->await().value();
// use the speed value
} catch (AsyncException& e) {
// thrown by the await(): Something went wrong on communication level with the data broker
} catch (InvalidValueException& e) {
// thrown by .value(): The vehicle speed signal does not contain a valid value, currently
}
- Throwing the
InvalidValueException
can be avoided if you first check that.isValid()
returns true before calling.value()
:
auto vehicleSpeed = Vehicle.Speed.get()->await();
if (vehicleSpeed.isValid())
// Accessing .value() now wont throw an exception
auto speed = vehicleSpeed.value()
...
} else {
// Do your failure handling here
switch (vehicleSpeed.getFailure()) {
case Failure::INVALID_VALUE:
...
break;
case ...
default:
...
}
}
(isValid()
is a convenience function for checking .getFailure() == Failure::NONE
.)
Note
If you use the asynchroneous variant, the callback passed toonError
is just called to report errors on communication level with the data broker. The validity of the returned signal’s/data point’s value needs to be checked separatly (e.g. via ‘isValid()’)!
Failure Reasons
There are two levels where errors accessing signal/data points might occure.
Communication with the Data Broker (IPC Level)
The data broker might be (temporarly) unavailable because
- it’s not yet started up,
- temporary “stopped” due to a crash or a “live update”,
- some temporary network issues (if running on a different hardware node),
- …
Errors on the IPC level between the application and the data broker will be reported either via
- an
AsyncException
thrown by theawait()
function of theAsyncResult
class or - calling the function passed to the
onError
function of theAsyncResult
/AsyncSubscription
class.
Errors on this level always make the overall call fail: If getting/setting multiple data points in a single call, the overall operation will fail. In case of setting multiple signals/data points, this means that none of the signals/data points are set. In case of an error on a subscription, this means that the overall subscription could not be established or is terminated now.
Signal / Data Point Level
Failures on signal/data point level are always reported individually per signal/data point. If accessing multiple signals/data points in a single call, getting or setting some certain signal might be successfully done but another one will report an error or failure.
The reasons why a valid value of signal/data point can be missing are explained here:
Reported failure | Reason | Explanation |
---|---|---|
Failure::UNKNOWN_DATAPOINT |
The addressed signal/data point is “unknown” on the system. | This can be a hint for a misconfiguration of the overall system, because no provider is installed in that system which will provide this signal. It can be acceptable, if an application does just “optionally” require access to that signal and would work properly without it being present. |
Failure::ACCESS_DENIED |
The application does not have the necessary access rights to the addressed signal/data point. | This can be a hint for a misconfiguration of the overall system, but could be also a “normal” situation if the user of the vehicle blocks access to certain signals for that application. |
Failure::NOT_AVAILABLE |
The addressed signal/data point is temporary not available. | This is a normal situation which will arise, while the provider of that signal is - not yet started up or has not yet passed a value to the data broker, - temporary “stopped” due to a crash or a “live update”, - some temporary network issues (e.g. if the provider is running on a different hardware node), - … |
Failure::INVALID_VALUE |
The addressed signal/data point might currently not represent a valid value. | This situation means, that the signal is currently provided but just the value itself is not representable, e.g. because the hardware sensor delivers implausible values. |
Failure::INTERNAL_ERROR |
The value is missing because of some internal issue in the data broker. | This typically points out some misbehaviour within the broker’s implementation - call it “bug”. |
Failure::NONE |
No failure state - a valid value is provided. | This “failure” reason is used to represent a signal state where a valid value is provided. |
It is the application developer’s decision if it makes sense to distinguish between the different failure reasons or if some or all of them can be handled as “just one”.
Subscription to DataPoints
If you want to get notified about changes of a specific DataPoint
, you can subscribe to this event, e.g. as part of the onStart()
method in your app:
void onStart() override {
subscribeDataPoints(QueryBuilder::select(Vehicle.Cabin.Seat.Row1.Pos1.Position).build())
->onItem([this](auto&& item) { onSeatPositionChanged(std::forward<decltype(item)>(item)); })
->onError([this](auto&& status) { onError(std::forward<decltype(status)>(status)); });
}
void onSeatPositionChanged(const DataPointsResult& result) {
const auto dataPoint = result.get(Vehicle.Cabin.Seat.Row1.Pos1.Position);
logger().info(dataPoint->value());
// do something with the data point value
}
The VehicleApp
class provides the subscribeDataPoints()
method which allows to listen for changes on one or multiple data points. Once a change in any of the data points is registered, the callback registered via AsyncSubscription::onItem()
is called. Conversely, the callback registered via AsyncSubscription::onError()
is called once there is an error during communication with the KUKSA Databroker.
The result passed to the callback registered via onItem()
is an object of type DataPointsResult
which holds the current state of all data points that were part of the respective subscription. The state of individual data points can be accessed by their reference: result.get(Vehicle.Cabin.Seat.Row1.Pos1.Position)
)
Note
If you select multiple signals/data points in a single subscription be aware that:
- The update notification will not only contain those data points whose states were updated, but the state of all data points selected in the belonging subscription. If you don’t want this behaviour, you must subscribe to change notifications for each signal/data point separately.
- A possible failure state will be reported individually per signal/data point. The reason is, that each signal/data point might come from a different provider, has individual access rights and individual reasons to become invalid. This is also true, if requesting multiple signal/data point states via a single get call.
Services
Services are used to communicate with other parts of the vehicle via remote procedure calls (RPC). Please read the basics about them here .
Note
This description is outdated!
Services were not supported by our
automated vehicle model lifecycle for some time and could be made available via the
description how you can create a model and add services to it manually.
In-between we provide a way to refer gRPC based services by referencing the required proto files from the AppManifest and auto-generated the language-specific stubs. The necessary steps need being described here.
The following code snippet shows how to use the moveComponent()
method of the SeatService
from the vehicle model:
vehicle::cabin::SeatService::SeatLocation location{1, 1};
Vehicle.Cabin.SeatService.moveComponent(
location, vehicle::cabin::SeatService::Component::Base, 300
)->await();
In order to define which seat you like to move, you have to pass a SeatLocation
object as the first parameter. The second argument specifies the component of the seat to be moved. The possible components are defined in the proto-files. The last parameter to be passed into the method is the final position of the component.
Make sure to call the
await()
method when calling service methods or register a callback viaonResult()
otherwise you don’t know when your asynchronous call will finish.
MQTT
Interaction with other Vehicle Apps or with the cloud is enabled by using the Mosquitto MQTT Broker. When using the provided template repository you can start a MQTT Broker as part the local runtime. More information can be found here .
In the
quickstart section
about the Vehicle App, you already tested sending MQTT messages to the app.
In the previous sections, you generally saw how to use Vehicle Models
, DataPoints
and GRPC Services
. In this section, you will learn how to combine them with MQTT.
In order to receive and process MQTT messages inside your app, simply use the VehicleApp::subscribeTopic(<topic>)
method provided by the SDK:
void onStart() override {
subscribeTopic("seatadjuster/setPosition/request")
->onItem([this](auto&& item){ onSetPositionRequestReceived(std::forward<decltype(item)>(item);)});
}
void onSetPositionRequestReceived(const std::string& data) {
const auto jsonData = nlohmann::json::parse(data);
const auto responseTopic = "seatadjuster/setPosition/response";
nlohmann::json respData({{"requestId", jsonData["requestId"]}, {"result", {}}});
}
The onSetPositionRequestReceived
method will now be invoked every time a message is created on the subscribed topic seatadjuster/setPosition/response
. The message data is provided as a string parameter. In the example above the data is parsed to json (data = json.loads(data_str)
).
In order to publish data to other subscribers, the SDK provides the appropriate convenience method: VehicleApp::publishToTopic(...)
void MyVehicleApp::onSeatPositionChanged(const DataPointsResult& result):
const auto responseTopic = "seatadjuster/currentPosition";
nlohmann::json respData({"position": result.get(Vehicle.Cabin.Seat.Row1.Pos1.Position)->value()});
publishToTopic(
responseTopic,
respData.dump(),
);
The above example illustrates how one can easily publish messages. In this case, every time the seat position changes, the new position is published to seatadjuster/currentPosition
See the results
Once the implementation is done, it is time to run and debug the app.
Build your App
Before you can run the Vehicle App you need to build it first. To do so, simply run the provided build.sh
script found in the root of the SDK. It does accept some arguments, but that is out of scope for this tutorial.
Warning
If this is your first time building, you might have to runinstall_dependencies.sh
first.
Run your App
In order to run the app make sure the devenv-runtimes
package is part of your
.velocitas.json
(which should be the default) and the runtime is up and running. Read more about it in the
run runtime services
section.
Now chose one of the options to start the VehicleApp under development:
- Press F5
or:
- Press F1
- Select command
Tasks: Run Task
- Select
Local Runtime - Run VehicleApp
Debug your Vehicle App
In the introduction about debugging , you saw how to start a debugging session. In this section, you will learn what is happening in the background.
The debug session launch settings are already prepared for the VehicleApp
.
{
"configurations": [
{
"name": "VehicleApp - Debug (Native)",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceFolder}/build/bin/app",
"args": [ ],
"stopAtEntry": false,
"cwd": "${workspaceFolder}",
"environment": [
{
"name": "SDV_MIDDLEWARE_TYPE",
"value": "native"
},
{
"name": "SDV_VEHICLEDATABROKER_ADDRESS",
"value": "127.0.0.1:55555"
},
{
"name": "SDV_MQTT_ADDRESS",
"value": "127.0.0.1:1883"
}
],
"externalConsole": false,
"MIMode": "gdb",
"setupCommands": [ ],
}
]
}
We specify which binary to run using the program
key. In the environment
you can specify all needed environment variables.
You can adapt the JSON to your needs (e.g., change the ports, add new tasks) or even add a completely new configuration for another Vehicle App.
Once you are done, you have to switch to the debugging tab (sidebar on the left) and select your configuration using the dropdown on the top. You can now start the debug session by clicking the play button or F5. Debugging is now as simple as in every other IDE, just place your breakpoints and follow the flow of your Vehicle App.
Next steps
- Concept: SDK Overview
- Concept: Deployment Model
- Tutorial: Deploy runtime services in Kanto
- Tutorial: Start runtime services locally
- Tutorial: Creating a Vehicle Model
- Tutorial: Develop and run integration tests for a Vehicle App
3.3 - Vehicle App Integration Testing
To be sure that a newly created Vehicle App will run together with the KUKSA Databroker and potentially other dependant Vehicle Services or Vehicle Apps, it’s essential to write integration tests along with developing the app.
To execute an integration test, the dependant components need to be running and be accessible from the test runner. This guide will describe how integration tests can be written and integrated in the CI pipeline so that they are executed automatically when building the application.
Note
This guide is currently only available for development of integration tests with Python.Writing Test Cases
To write an integration test, you should check the sample that comes with the template (
/app/tests/integration/integration_test.py
). To support interacting with the MQTT broker and the KUKSA Databroker (to get and set values for data points), there are two classes present in Python SDK that will help:
-
MqttClient
: this class provides methods for interacting with the MQTT broker. Currently, the following methods are available:-
publish_and_wait_for_response
: publishes the specified payload to the given request topic and waits (till timeout) for a message to the response topic. The payload of the first message that arrives in the response topic will be returned. If the timeout expires before, an empty string ("") is returned. -
publish_and_wait_for_property
: publishes the specified payload to the given request topic and waits (till timeout) until the given property value is found in an incoming message to the response topic. Thepath
describes the property location within the response message, thevalue
the property value to look for.Example:
{ "status": "success", "result": { "responsecode": 10 } }
If the
responsecode
property should be checked for the value10
, the path would be["result", "responsecode"]
, property value would be10
. When the requested value has been found in a response message, the payload of that message will be returned. If the timeout expires before receiving a matching message, an empty string ("") is returned.
This class can be initialized with a given port. If no port is specified, the environment variable
MQTT_PORT
will be checked. If this is not possible either, the default value of1883
will be used. It’s recommended to specify no port when initializing that class as it will locally use the default port1883
and in CI the port is set by the environment variableMQTT_PORT
. This will prevent a check-in in the wrong port during local development. -
-
IntTestHelper
: this class provides functionality to interact with the KUKSA Databroker.register_datapoint
: registers a new data point with given name and type ( here you can find more information about the available types)set_..._datapoint
: set the given value for the data point with the given name (with given type). If the data point does not exist, it will be registered.
This class can be initialized with a given port. If no port is specified, the environment variable
VDB_PORT
will be checked. If this is not possible either, the default value of55555
will be used. It’s recommended to specify no port when initializing that class as it will locally use the default port55555
and in CI the port is set by the environment variableVDB_PORT
. This will prevent a check-in in the wrong port during local development.
Runtime components
To be able to test the Vehicle App in an integrated way, the following components should be running:
- Mosquitto
- Databroker
- Vehicle Mock Provider
We distinguish between two environments for executing the Vehicle App and the runtime components:
- Local execution: components are running locally in the development environment
- Kanto execution: components (and application) are deployed and running in a Kanto control plane
Local execution
First, make sure that the runtime services are configured and running like described here .
The application itself can be executed by using a Visual Studio Launch Config (by pressing F5) or by executing the provided task Local Runtime - Run VehicleApp
.
When the runtime services and the application are running, integration tests can be executed locally via
pytest ./app/tests/integration
or using the testing tab in the sidebar to the left.
Kanto runtime
First, make sure that the runtime and the services are up and running, like described here .
The application itself can be deployed by executing the provided task Kanto Runtime - Deploy VehicleApp
or Kanto Runtime - Deploy VehicleApp (without rebuild)
. Depending on whether your app is already available as a container or not.
When the runtime services and the application are running, integration tests can be executed locally via
pytest ./app/tests/integration
or using the testing tab in the sidebar to the left.
Integration Tests in CI pipeline
The tests will be discovered and executed automatically in the provided
CI pipeline
. The job Run Integration Tests
contains all steps to set up and execute all integration tests in the Kanto runtime. Basically it is doing the same steps as you saw above:
- start the Kanto runtime
- deploy the Vehicle App container
- set the correct MQTT and Databroker ports
- execute the integration tests
Finally the test results are collected and published as artifacts of the workflow.
Troubleshooting
Troubleshoot IntTestHelper
- Make sure that the KUKSA Databroker is up and running by checking the task log.
- Make sure that you are using the right ports.
- Make sure that you installed the correct version of the SDK (SDV-package).
Troubleshoot Mosquitto (MQTT Broker)
- Make sure that Mosquitto is up and running by checking the task log.
- Make sure that you are using the right ports.
- Use VsMqtt extension to connect to MQTT broker locally (
localhost:1883
) to monitor topics in MQTT broker by subscribing to all topics using#
.
Next steps
- Concept: Deployment Model
- Concept: Build and release process
4 - Vehicle Model Creation
Info
On Friday, 2023-03-03 we released our new
model lifecycle approach . With the update of the documentation the previous content of this page can be found in the section
A Vehicle Model makes it possible to easily get vehicle data from the KUKSA Databroker and to execute remote procedure calls over gRPC against Vehicle Services and other Vehicle Apps. It is generated from the underlying semantic models based e.g. on the COVESA Vehicle Signal Specification (VSS) . The model is generated for a concrete programming language as a graph-based, strongly-typed, intellisense-enabled library providing vehicle abstraction “on code level”.
By default our app templates now generate the vehicle model during the devContainer initialization - managed by the Velocitas life cycle management. The respective VSS-based model source is referenced in the app manifest allowing to freely choose the model being used in your project. You will find more details about this in section Automated Model Lifecycle .
The previous approach, using pre-generated model repositories, is deprecated as of now. But is still available and is described in section Manual Vehicle Model Creation . Please be aware, that you would either have to use template versions before the above mentioned release, or you need to adapt the newer versions of the template using the old approach.
4.1 - Automated Vehicle Model Lifecycle
Info
This article describes our new model lifecycle approach released on Friday, 2023-03-03. With that, the model is now automatically generated with the instantiation of the devContainer. It is generated from the vehicle model source file referenced in the AppManifest.
For the time being, the integration of services is not supported by the new approach.
The previous approach, using pre-generated model repositories, is now deprecated. But it is still available and
This tutorial will show you how:
- the vehicle API used as the source to generate the model is referenced in the app manifest,
- the automatic generation of the model works,
- you can trigger manual recreation of the model (after adding extensions to the API required by your project)
How to Reference a Model Specification
The model specification defines the vehicle API to be used by your project. It is referenced in the AppManifest.json
via a URI or local file path like this:
"vehicleModel": {
"src": "<https://github.com/COVESA/vehicle_signal_specification/releases/download/v3.0/vss_rel_3.0.json>"
}
"vehicleModel": {
"src": "./my_model/my_model.json"
}
Info
The reference must point to a JSON file containing the model specification as VSS vspec. References to a VSS.vspec
file hierarchy are not supported as of now.
Model Creation
The generation of the model is taking place:
- through a
onPostInit hook
when
velocitas init
is called:- either triggered manually or
- automatically during the instantiation of the devContainer through our Velocitas Lifecycle Management , or
- when you trigger the VS Code task
(Re-)generate vehicle model
explicitly.
The model generation is a three step process:
- The model generator is installed as a Python package (if not already present)
- The referenced model specification is downloaded (if no local reference)
- The model code is generated and installed.
The model is generated using our
Velocitas vehicle-model-generator
.
The used version and also the repository of the generator can be altered via the variables
section of the project configuration in the .velocitas.json
.
The default values for those are defined in the
manifest.json
of the
devContainer setup package
.
Also, the target folder for the generated model source code is specified here:
{
"variables": {
"modelGeneratorGitRepo": "https://github.com/eclipse-velocitas/vehicle-model-generator.git",
"modelGeneratorGitRef": "v0.3.0",
"generatedModelPath": "./gen/vehicle_model"
}
}
In Python template based projects the generated model is finally installed in the site-packages folder, while in C++ projects it is made available as a CMake include folder.
Further information
- Concept: SDK Overview
- Tutorial: Quickstart
- Tutorial: Create a Vehicle App
4.2 - Manual Vehicle Model Creation
Info
With the release of our new
model lifecycle approach on Friday, 2023-03-03, the model is now automatically generated with the instantiation of the devContainer from a model source referenced in the app manifest.
The approach described here, using pre-generated model repositories, is deprecated as of now. But it is still available and must be used if you need access to vehicle services. Please be aware, that you would either have to use template versions before the above mentioned release, or you need to adapt the newer versions of the template using the old approach.
This tutorial will show you how to:
- Create a Vehicle Model
- Add a Vehicle Service to the Vehicle Model
- Distribute your Python Vehicle Model
Note
A Vehicle Model should be defined in its own package. This makes it possible to distribute the Vehicle Model later as a standalone package and to use it in different Vehicle App projects.
The creation of a new vehicle model is only required if the vehicle signals (like sensors and actuators) defined in the current version of the
COVESA Vehicle Signal Specification (VSS) is not sufficient for the definition of your vehicle API. Otherwise you could use the default vehicle model we already generated for you, see
Create a Vehicle Model from VSS specification
A Vehicle Model can be generated from a COVESA Vehicle Signal Specification (VSS). VSS introduces a domain taxonomy for vehicle signals, in the sense of classical attributes, sensors and actuators with the raw data communicated over vehicle buses and data. The Velocitas vehicle-model-generator creates a Vehicle Model from the given specification and generates a package for use in Vehicle App projects.
Follow the steps to generate a Vehicle Model.
-
Clone the vehicle-model-generator repository in a container volume.
-
In this container volume, clone the vehicle-signal-specification repository and if required checkout a particular branch:
git clone https://github.com/COVESA/vehicle_signal_specification cd vehicle_signal_specification git checkout <branch-name>
In case the VSS vspec doesn’t contain the required signals, you can create a vspec using the VSS Rule Set .
-
Execute the command
python3 gen_vehicle_model.py -I ./vehicle_signal_specification/spec ./vehicle_signal_specification/spec/VehicleSignalSpecification.vspec -l <lang> -T sdv_model -N sdv_model
or if you want to generate it from a .json file
python3 gen_vehicle_model.py <path_to_your_json_file> -l <lang> -T sdv_model
Depending on the value of
lang
, which can assume the valuespython
andcpp
, this creates asdv_model
directory in the root of repository along with all generated source files for the given programming language.Here is an overview of what is generated for every available value of
lang
:lang output python
Python sources and a setup.py
ready to be used as Python packagecpp
C++ sources, headers and a CMakeLists.txt ready to be used as a CMake project To have a custom model name, refer to README of vehicle-model-generator repository.
-
For Python: Change the version of package in
setup.py
manually (defaults to 0.1.0). -
Now the newly generated
sdv_model
can be used for distribution. (See Distributing your Vehicle Model )
Create a Vehicle Model Manually
Alternative to the generation from a VSS specification you could create the Vehicle Model manually. The following sections describing the required steps.
Distributing your Vehicle Model
Once you have created your Vehicle Model either manually or via the Vehicle Model Generator, you need to distribute your model to use it in an application. Follow the links below for language specific tutorials on how to distribute your freshly created Vehicle Model.
Further information
- Concept: SDK Overview
- Tutorial: Quickstart
- Tutorial: Create a Vehicle App
4.2.1 - C++ Manual Vehicle Model Creation
Not yet done for C++
4.2.2 - Python Manual Vehicle Model Creation
Setup a Python Package manually
A Vehicle Model should be defined in its own Python Package. This allows to distribute the Vehicle Model later as a standalone package and to use it in different Vehicle App projects.
The name of the Vehicle Model package will be my_vehicle_model
for this walkthrough.
-
Start Visual Studio Code
-
Select File > Open Folder (File > Open… on macOS) from the main menu.
-
In the Open Folder dialog, create a
my_vehicle_model
folder and select it. Then click Select Folder (Open on macOS). -
Create a new file
setup.py
undermy_vehicle_model
:from setuptools import setup setup(name='my_vehicle_model', version='0.1', description='My Vehicle Model', packages=['my_vehicle_model'], zip_safe=False)
This is the Python package distribution script.
-
Create an empty folder
my_vehicle_model
undermy_vehicle_model
. -
Create a new file
__init__.py
undermy_vehicle_model/my_vehicle_model
.
At this point the source tree of the Python package should look like this:
my_vehicle_model
├── my_vehicle_model
│ └── __init__.py
└── setup.py
To verify that the package is created correctly, install it locally:
pip3 install .
The output of the above command should look like this:
Defaulting to user installation because normal site-packages is not writeable
Processing /home/user/projects/my-vehicle-model
Preparing metadata (setup.py) ... done
Building wheels for collected packages: my-vehicle-model
Building wheel for my-vehicle-model (setup.py) ... done
Created wheel for my-vehicle-model: filename=my_vehicle_model-0.1-py3-none-any.whl size=1238 sha256=a619bc9fbea21d587f9f0b1c1c1134ca07e1d9d1fdc1a451da93d918723ce2a2
Stored in directory: /home/user/.cache/pip/wheels/95/c8/a8/80545fb4ff73c974ac1716a7bff6f7f753f92022c41c2e376f
Successfully built my-vehicle-model
Installing collected packages: my-vehicle-model
Successfully installed my-vehicle-model-0.1
Finally, uninstall the package again:
pip3 uninstall my_vehicle_model
Add Vehicle Models manually
-
Install the Python Vehicle App SDK:
pip3 install git+https://github.com/eclipse-velocitas/vehicle-app-python-sdk.git
The output of the above command should end with:
Successfully installed sdv-x.y.z
Now it is time to add some Vehicle Models to the Python package. At the end of this section you will have a Vehicle Model, that contains a
Cabin
model, aSeat
model and has the following tree structure:Vehicle └── Cabin └── Seat (Row, Pos)
-
Create a new file
Seat.py
undermy_vehicle_model/my_vehicle_model
:from sdv.model import Model class Seat(Model): def __init__(self, parent): super().__init__(parent) self.Position = DataPointFloat("Position", self)
This creates the Seat model with a single data point of type
float
namedPosition
. -
Create a new file
Cabin.py
undermy_vehicle_model/my_vehicle_model
:from sdv.model import Model class Cabin(Model): def __init__(self, parent): super().__init__(parent) self.Seat = SeatCollection("Seat", self) class SeatCollection(Model): def __init__(self, name, parent): super().__init__(parent) self.name = name self.Row1 = self.RowType("Row1", self) self.Row2 = self.RowType("Row2", self) def Row(self, index: int): if index < 1 or index > 2: raise IndexError(f"Index {index} is out of range") _options = { 1 : self.Row1, 2 : self.Row2, } return _options.get(index) class RowType(Model): def __init__(self, name, parent): super().__init__(parent) self.name = name self.Pos1 = Seat("Pos1", self) self.Pos2 = Seat("Pos2", self) self.Pos3 = Seat("Pos3", self) def Pos(self, index: int): if index < 1 or index > 3: raise IndexError(f"Index {index} is out of range") _options = { 1 : self.Pos1, 2 : self.Pos2, 3 : self.Pos3, } return _options.get(index)
This creates the
Cabin
model, which contains a set of sixSeat
models, referenced by their names or by rows and positions:- row=1, pos=1
- row=1, pos=2
- row=1, pos=3
- row=2, pos=1
- row=2, pos=2
- row=2, pos=3
-
Create a new file
vehicle.py
undermy_vehicle_model/my_vehicle_model
:from sdv.model import Model from my_vehicle_model.Cabin import Cabin class Vehicle(Model): """Vehicle model""" def __init__(self, name): super().__init__() self.name = name self.Speed = DataPointFloat("Speed", self) self.Cabin = Cabin("Cabin", self) vehicle = Vehicle("Vehicle")
The root model of the Vehicle Model tree should be called Vehicle by convention and is specified, by setting parent to None
. For all other models a parent model must be specified as the 2nd argument of the Model
constructor, as can be seen by the Cabin
and the Seat
models above.
A singleton instance of the Vehicle Model called vehicle
is created at the end of the file. This instance is supposed to be used in the Vehicle Apps. Creating multiple instances of the Vehicle Model should be avoided for performance reasons.
Add a Vehicle Service
Vehicle Services provide service interfaces to control actuators or to trigger (complex) actions. E.g. they communicate with the vehicle internal 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 Databroker and may expose gRPC endpoints, which can be invoked by Vehicle Apps over a Vehicle Model.
In this section, we add a Vehicle Service to the Vehicle Model.
-
Create a new folder
proto
undermy_vehicle_model/my_vehicle_model
. -
Copy your proto file under
my_vehicle_model/my_vehicle_model/proto
As example you could use the protocol buffers message definition seats.proto provided by the KUKSA services which describes a seat control service .
-
Install the grpcio tools including mypy types to generate the Python classes out of the proto-file:
pip3 install grpcio-tools mypy_protobuf
-
Generate Python classes from the
SeatService
message definition:python3 -m grpc_tools.protoc -I my_vehicle_model/proto --grpc_python_out=./my_vehicle_model/proto --python_out=./my_vehicle_model/proto --mypy_out=./my_vehicle_model/proto my_vehicle_model/proto/seats.proto
This creates the following gRPC files under the
proto
folder:- seats_pb2.py
- seats_pb2_grpc.py
- seats_pb2.pyi
-
Create the
SeatService
class and wrap the gRPC service:from sdv.model import Service from my_vehicle_model.proto.seats_pb2 import ( CurrentPositionRequest, MoveComponentRequest, MoveRequest, Seat, SeatComponent, SeatLocation, ) from my_vehicle_model.proto.seats_pb2_grpc import SeatsStub class SeatService(Service): "SeatService model" def __init__(self): super().__init__() self._stub = SeatsStub(self.channel) async def Move(self, seat: Seat): response = await self._stub.Move(MoveRequest(seat=seat), metadata=self.metadata) return response async def MoveComponent( self, seatLocation: SeatLocation, component: SeatComponent, position: int, ): response = await self._stub.MoveComponent( MoveComponentRequest( seat=seatLocation, component=component, # type: ignore position=position, ), metadata=self.metadata, ) return response async def CurrentPosition(self, row: int, index: int): response = await self._stub.CurrentPosition( CurrentPositionRequest(row=row, index=index), metadata=self.metadata, ) return response
Some important remarks about the wrapping
SeatService
class shown above:- The
SeatService
class must derive from theService
class provided by the Python SDK. - The
SeatService
class must use the gRPC channel from theService
base class and provide it to the_stub
in the__init__
method. This allows the SDK to manage the physical connection to the gRPC service and use service discovery of the middleware. - Every method needs to pass the metadata from the
Service
base class to the gRPC call. This is done by passing theself.metadata
argument to the metadata of the gRPC call.
- The
4.2.3 - Vehicle Model Distribution
4.2.3.1 - C++ Vehicle Model Distribution
Now that you have created your own Vehicle Model, we can distribute it to make use of it in Vehicle Apps.
Copying the folder to your Vehicle App repo
The easiest way to get started quickly is to copy the created model, presumably stored in vehicle_model
into your Vehicle App repository to use it. To do so, simply copy and paste the directory into the <sdk_root>/app
directory and replace the existing model.
Using a git submodule
A similar approach to the one above but a bit more difficult to set up is to create a git repository for the created model. The advantage of this approach is that you can share the same model between multiple Vehicle Apps without any manual effort.
- Create a new git repository on i.e. Github
- Clone it locally, add the created
vehicle_model
folder to the git repository - Commit everything and push the branch
In your Vehicle App repo, add a new git submodule via
git submodule add <checkout URL of your new repo> app/vehicle_model
git submodule init
Now you are ready to develop new Vehicle Apps with your custom Vehicle Model!
4.2.3.2 - Python Vehicle Model Distribution
Now you a have a Python package containing your first Python Vehicle Model and it is time to distribute it. There is nothing special about the distribution of this package, since it is just an ordinary Python package. Check out the Python Packaging User Guide to learn more about packaging and package distribution in Python.
Distribute to single Vehicle App
If you want to distribute your Python Vehicle Model to a single Vehicle App, you can do so by copying the entire folder my_vehicle_model
under the /app/src
folder of your Vehicle App repository and treat it as a sub-package of the Vehicle App.
- Create a new folder
my_vehicle_model
under/app/src
in your Vehicle App repository. - Copy the
my_vehicle_model
folder to the/app/src
folder of your Vehicle App repository. - Import the package
my_vehicle_model
in your Vehicle App:
from <my_app>.my_vehicle_model import vehicle
...
my_app = MyVehicleApp(vehicle)
Distribute inside an organization
If you want to distribute your Python Vehicle Model inside an organization and use it to develop multiple Vehicle Apps, you can do so by creating a dedicated Git repository and copying the files there.
-
Create new Git repository called
my_vehicle_model
-
Copy the content under
my_vehicle_model
to the repository. -
Release the Vehicle Model by creating a version tag (e.g.,
v1.0.0
). -
Install the Vehicle Model package to your Vehicle App:
pip3 install git+https://github.com/<yourorg>/my_vehicle_model.git@v1.0.0
-
Import the package
my_vehicle_model
in your Vehicle App and use it as shown in the previous section.
Distribute publicly as open source
If you want to distribute your Python Vehicle Model publicly, you can do so by creating a Python package and distributing it on the
Python Package Index (PyPI)
. PyPi is a repository of software for the Python programming language and helps you find and install software developed and shared by the Python community. If you use the pip
command, you are already using PyPI.
Detailed instructions on how to make a Python package available on PyPI can be found here .
5 - Vehicle App Runtime
5.1 - Local Runtime
Using tasks in Visual Studio Code
Overview: If you are developing in Visual Studio Code, the runtime components (like KUKSA Databroker or Vehicle Services) are available for local execution coming from our devenv-runtimes package and are accessible as Tasks, a feature of the Visual Studio Code. Additional information on tasks can be found here .
Start local runtime: To start local runtime, a task called Local Runtime - Up
is available. This task runs the runtime services in the correct order. You can run this task by clicking F1
and choose Tasks: Run task
, then select Local Runtime - Up
.
Stop local runtime: To stop local runtime, a task called Local Runtime - Down
is available. This task stops running runtime services gracefully. You can run this task by clicking F1
and choose Tasks: Run task
, then select Local Runtime - Down
.
Tasks Management: Visual Studio Code offers various other commands concerning tasks like Start/Terminate/Restart/… You can access them by pressing F1 and typing task
. A list with available task commands will appear.
Logging: Running tasks appear in the Terminals View of Visual Studio Code. From there, you can see the logs of each running task. More detailed logs can be found inside your workspace’s logs directory ./logs/*
Add/Change runtime service configuration
The configuration for services of our provided local runtime are defined in the
runtime.json
at the root of the repository
devenv-runtimes
.
For a more detailed view on how to change or add runtime service configuration, please visit:
Lifecycle Management Package Development
Using KUKSA Databroker CLI
A CLI tool is provided for interacting with a running instance of the KUKSA Databroker. It can be started by running the task Local Runtime - VehicleDataBroker CLI
(by pressing F1, type Run Task followed by Local Runtime - VehicleDataBroker CLI
). The Runtime Local needs to be running for you to be able to use the tool.
Integrating a new runtime service into Visual Studio Code Task
Integration of a new runtime service can be done by duplicating one of the existing tasks.
- Create a new service in either a new created Package or branch/fork of devenv-runtimes as already explained above
- In
.vscode/tasks.json
, duplicate section from task e.g.Local Runtime - Up
,Local Runtime - Run VehicleApp
orLocal Runtime - VehicleDataBroker CLI
- Correct names in a new code block
- Disclaimer:
Problem Matcher
defined intasks.json
is a feature of the Visual Studio Code Task, to ensure that the process runs in background - Run task using
[F1 -> Tasks: Run Task -> <Your new task label>]
- Task should be visible in Terminal section of Visual Studio Code
Task CodeBlock helper
{
"label": "<task_name>",
"detail": "<task_description>",
"type": "shell",
"command": [
"velocitas exec runtime-local <service_id> <args>"
],
"presentation": {
"close": true,
"reveal": "never"
},
"problemMatcher": []
}
Troubleshooting
Problem description: When integrating new services into an existing dev environment, it is highly recommended to use the Visual Studio Code Task Feature. A new service can be easily started by calling it from bash script, however restarting the same service might lead to port conflicts (GRPC Port or APP port). That can be easily avoided by using the Visual Studio Code Task Feature.
Codespaces
If you are using Codespaces, remember that you are working on a remote agent. That’s why it could happen that the tasks are already running in the background. If that’s the case a new start of the tasks will fail, since the ports are already in use. Another possibility to check if the processes are already running, is to check which ports are already open. Check the Ports-tab to view all open ports (if not already open, hit F1
and enter View: Toggle Ports
).
Next steps
- Tutorial: Quickstart
- Concept: Deployment Model
- Concept: Build and release process
5.2 - Kanto Runtime
Besides starting the vehicle runtime components locally , another way is to deploy them as containers using Kanto . To start the runtime, we provide VS Code Tasks, a feature of Visual Studio Code. Additional information on tasks can be found here .
Quick Start: Each step has a task that is defined in /.vscode/tasks.json
:
-
Core tasks (dependent on each other in the given order):
Kanto - Runtime Up
: Starts up the Kanto runtime and deploys the runtime components.Kanto - Build VehicleApp
: Builds the VehicleApp.Kanto - Deploy VehicleApp
: Deploys the VehicleApp as container in the Kanto runtime.
-
Optional helper tasks:
Kanto - Deploy VehicleApp (without rebuild)
: Deploys the VehicleApp as container in the Kanto runtime but does not build it upfront. That requires, that the taskKanto - Build VehicleApp
has been executed once before.Kanto - Runtime Down
: Stops the Kanto runtime and all deployed containers.
Run as Bundle: To orchestrate these tasks, you can use the task Kanto - Deploy VehicleApp
. This task runs the other tasks in the correct order. You can run this task by clicking F1
and choose Tasks: Run task
, then select Kanto - Deploy VehicleApp
.
Tasks Management: Visual Studio Code offers various other commands concerning tasks like Start/Terminate/Restart/… You can access them by pressing F1 and typing task
. A list with available task commands will appear.
Logging: Running tasks appear in the Terminals View of Visual Studio Code. From there, you can see the logs of each running task. More detailed logs can be found inside your workspace’s logs directory ./logs/*
KantUI
The Leda team developed a tool to easily work with Kanto. It is similar to K9S for Kubernetes. You can find more details about KantUI in the documentation of Leda .
In the devcontainer KantUI is already installed and it can be started via:
sudo kantui
After starting the Kanto runtime with the mentioned tasks above, you will directly see all the running containers in KantUI. Now you could also take a look at the logs, delete or stop single containers. After you deployed your application to Kanto, this container will also show up and can be handled with KantUI.
Mounting folders for FeederCAN
Some applications (e.g. FeederCAN) might make it necessary to load custom files from a mounted volume.
All the files that are located in [./config/feedercan](https://github.com/eclipse-velocitas/devenv-runtimes/tree/main/config/feedercan)
will be automatically mounted into the container. In order to mount files to the directory that is accessible by the application, please refer to the deployment configuration file:
runtime-kanto/src/runtime/deployment/feedercan.json
.
Uploading custom candump file to FeederCAN
FeederCAN requires a candump file. A pre-defined candump file is already part of our delivery, however, if necessary, there is an option to upload a custom file by:
- Creating/updating candump file with the name
candumpDefault.log
in./config/feedercan
- Restarting Kanto (execute the tasks
Kanto - Runtime Down
andKanto - Runtime Up
)
More information about the CAN Provider can be found here
Next steps
- Concept: Deployment Model
- Concept: Build and release process
- Tutorial: Start runtime services locally
- Tutorial: Quickstart
6 - gRPC service generation
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:
- One Velocitas Devcontainer running the Client, based on the Vehicle App C++ Template .
- One Velocitas Devcontainer running the Server, based on the Vehicle App C++ Template .
- Databroker running on localhost.
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.
6.1 - Create a server
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!
6.2 - Create a client
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:
7 - Vehicle App Deployment
See the Kanto container configuration for details how to write Kanto deployment files.