1 - Quickstart

Learn how to setup and explore the provided development environment.

The following information describes how to setup and configure the Development Container (DevContainer), and how to build, customize and test the sample Vehicle App, which is included in this repository. You will learn how to use the Vehicle App SDK, how to interact with the vehicle API and how to do CI/CD using the pre-configured GitHub workflows that come with the repository.

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 develpment project.

Creating Vehicle App Repository

For the orginization and Vehicle App repository the name MyOrg/MyFirstVehicleApp is used as a reference during the rest of the document.

Create your own repository copy from the template repository of your choice Python/C++ 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.

Starting Development Environment

In the following you will learn different possibilities to work with the repo. Basically you can work on your own machine using just Visual Studio Code or you can set up the environment on a remote agent, using GitHub Codespaces.

Visual Studio Code

The Visual Studio Code Development Containers makes it possible to package a complete Vehicle App development environment, including Visual Studio Code extensions, Vehicle App SDK, Vehicle App runtime and all other development & testing tools into a container that is then started within your Visual Studio Code session.

To be able to use the DevContainer, you have to make sure that you fulfill the following prerequisites:

With following steps you will clone and set up your development environment on your own machine using just Visual Studio Code.

  1. Clone the repo locally using your favorite Git tooling
  2. Start Visual Studio Code
  3. Select Open Folder from the File menu
  4. Open the root of the cloned repo
  5. A popup appears on the lower left side of Visual Studio Code. If the popup does not appear, you can also hit F1 and run the command Dev-Containers: Open Folder in Container
  6. Click on Reopen in Container
  7. Wait for the container to be set up

The first time initializing the container will take a few minutes to build the image and to provision the tools inside the container.

Codespaces

Another possibility to use your newly created repository is via GitHub Codespaces. You can either try it out directly in the browser or also use it inside Visual Studio Code. The main thing to remember is that everything is executed on a remote agent and the browser or Visual Studio Code just act as frontends.

To get started with Codespaces, you just have to follow a few steps:

  1. Open your repository on GitHub (e.g. https://github.com/MyOrg/MyFirstVehicleApp)
  2. Click on the green Code button and select Codespaces on the top
  3. Configure your Codespace if needed (defaults to the main branch and a standard agent)
  4. Click on create

A new window will open where you see the logs for setting up the container. On this window you could now also choose to work with Visual Studio Code. The environment remains on a remote agent and Visual Studio 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 Visual Studio Code.

Starting runtime services

The runtime services (like KUKSA Data Broker or Vehicle Services) are required to develop vehicle apps and run integration tests.

A Visual Studio Code task called Start Vehicle App runtime is available to run these in the correct order.

  1. Press F1
  2. Select command Tasks: Run Task
  3. Select Start VehicleApp runtime
  4. Choose Continue without scanning the output

You should see the tasks run-mosquitto, run-vehicledatabroker, run-vehicleservices and run-feedercan being executed in the Visual Studio Code output panel.

More information about the tasks are available here.

Debugging Vehicle App

Now that the runtime services are all up and running, let’s start a debug session for the Vehicle App as next step.

  1. Open the main source file and set a breakpoint in the given method:
    • Python main source file: /app/src/main.py, set breakpoint in method: on_get_speed_request_received
    • C++: Continue on the Seat Adjuster tab.
  2. 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.

  1. Open VSMqtt extension in Visual Studio Code and connect to mosquitto (local)
  2. Set Subscribe Topic = sampleapp/getSpeed/response and click subscribe
  3. Set Publish Topic = sampleapp/getSpeed
  4. Press publish with an empty payload field.

For Python: Follow the guide provided in: Import examples and import seat-adjuster.
For C++: Continue with the next steps.

  1. Open the main source file and set a breakpoint in the given method:
    • Python main source file: /app/src/main.py, set breakpoint in method: on_set_position_request_received
    • C++ main source file: /app/src/VehicleApp.cpp, set breakpoint in method: onSetPositionRequestReceived
  2. 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.

  1. Open VSMqtt extension in Visual Studio Code and connect to mosquitto (local)

  2. Set Subscribe Topic = seatadjuster/setPosition/response and click subscribe

  3. Set Subscribe Topic = seatadjuster/currentPosition and click subscribe

  4. Set Publish Topic = seatadjuster/setPosition/request

  5. Set and publish a dummy payload:

    { "position": 300, "requestId": "xyz" }
    
Now your breakpoint in the Vehicle App gets hit and you can inspect everything in your debug session. After resuming execution (F5), a response from your Vehicle App is published to the response topic. You can see the response in the MQTT window.

Triggering CI Workflow

The provided GitHub workflows are used to build the container image for the Vehicle App, run unit and integration tests, collect the test results and create a release documentation and publish the Vehicle App. A detailed description of the workflow you can find here.

By pushing a change to GitHub the CI Workflow will be triggered:

  1. Make modification in any of your files

  2. Commit and push your change

    git add .
    git commit -m "removed emtpy line"
    git push
    

To see the results open the Actions page of your repository on GitHub, go to CI Workflow and check the workflow output.

Releasing Vehicle App

Now that the CI Workflow was successful, you are ready to build your first release. Your goal is to build a ready-to-deploy container image that is published in the GitHub container registry

  1. Open the Code page of your repository on GitHub
  2. Click on Create a new release in the Releases section on the right side
  3. 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. The release workflow creates a release documentation and publish the container image of the Vehicle App to the GitHub container registry. Open Actions on the repoitory and see the result.

Deploying Vehicle App

After releasing the Vehicle App to the GitHub container registry you might ask how to bring the Vehicle App on a device and have the required Runtime Stack on the device. Here Eclipse Leda comes into the game.

Please checkout the documentation of Eclipse Leda to get more information.

Next steps

1.1 - Import examples

Learn how to import examples provided by the Vehicle App SDK.

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.

  1. Press F1
  2. Select command Tasks: Run Task
  3. Select Import example app from SDK
  4. Choose Continue without scanning the output
  5. Select seat-adjuster

Run the example Vehicle App

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.

1.2 - Install a working container runtime

Overview about the setup of tested container runtimes

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.

macOS

Since the Docker Engine is not working out of the box on macOS, a virtualisation 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 neccessary 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.

Drawbacks

The only drawback noticed so far is, that K9S is not working properly on M1 Macs. Since the container runtime and deployment are working also without K9S, this is just a minor issue. Nevertheless, the team is working on a solution.

1.3 - Working behind proxy

Learn how to setup your docker desktop and Visual Studio Code behind a coorperate 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 organisation 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 recommented 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 recomment 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, select Proxies
  • 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
  • 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://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

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 transfered 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: https://www.c2labs.com/post/overcoming-proxy-issues-with-docker-containers https://technotes.shemyak.com/posts/docker-behind-ssl-proxy/

Troubleshooting

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 choose Clean / Purge data

GitHub rate limit exceeded

How to fix can be found at Lifecycle Management Troubleshooting.

2 - Prototyping Integration

Learn how to start a prototype with the playground of digital.auto and integrate it into Velocitas.

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.

digital.auto vehicle-models

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.

selected-model cvi-catalogue

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.

wishlist

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, prototype-library
  • 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, select-prototype
  • Go to the Code section and start your prototype right away. code-section

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.

run-section

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’.

generate

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 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 succesful 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 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 application 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.1 - Service Integration

Learn how to integrate a Vehicle Service that executes the request on vehicle side

Services can make sure, that when you write a VSS datapoint, something is actually happening. Eclipse Velocitas has an example seat, hvac or light service. If your Vehicle App makes use of e.g. Vehicle.Cabin.Seat.Row1.Pos1.Position, Vehicle.Body.Lights.IsBackupOn, Vehicle.Body.Lights.IsHighBeamOn, Vehicle.Body.Lights.IsLowBeamOn you are in for some real action. To learn more, visit Vehicle Services.

You can validate the interaction of the service with your Vehicle App by adding a Vehicle Service to the /app/AppManifest.json, start the services locally and debug it.

Modify services

For more advanced usage you can als try modifying 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.val Services repository contains examples illustrating how such kind of vehicle services can be built. You need to write an application that talks to KUKSA.val listening to changes of a target value of some VSS datapoint and then do whatever you want. You can achieve this by using the KUKSA.val GRPC API with any programming language of your choice (learn more about GRPC).

3 - Vehicle App Development

Learn how to develop a new Vehicle App.

3.1 - Python Vehicle App Development

Learn how to develop and test the Vehicle App using Python.

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 template repository. You will learn how to use the Vehicle App 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 positions of the seats in the car and also provide their current positions to other applications.

A detailed explanation of the use case and the example is available here.

At first, you have to create the main python script called main.py in /app/src. All the relevant code for new Vehicle App goes there. Afterwards, there are several steps you need to consider when developing the app:

  1. Manage your imports
  2. Enable logging
  3. Initialize your class
  4. Start the app

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

import grpc
from sdv.util.log import (  # type: ignore
    get_opentelemetry_log_factory,
    get_opentelemetry_log_format,
)
from sdv.vehicle_app import VehicleApp, subscribe_topic
from vehicle import Vehicle, vehicle  # type: ignore
from sdv_model.proto.seats_pb2 import BASE, SeatLocation  # 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 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.

Start the app

Here’s an example of how to start the MyVehicleApp App 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()

The app is now running. In order to use it properly, we will enhance the app 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 models.

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 for the vehicle speed, this can be done via

vehicle_speed = await self.Vehicle.Speed.get()

As the get-method of the Datapoint-class there is a coroutine you have to use the await keyword when using it.

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_client.Cabin.Seat.Row1.Pos1.Position.get()

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 on_start-method in your app.

    async def on_start(self):
        """Run when the vehicle app starts"""
        await self.Vehicle.Cabin.Seat.Row(1).Pos(1).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 datapoint update. Subscribed data is available in the respective DataPointReply object and need to be accessed via the reference to the subscribed datapoint. 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 data broker. 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.Row(1).Pos(1).Position).value
        # ...

Services

Services are used to communicate with other parts of the vehicle via remote function calls (RPC). Please read the basics about them here.

The following lines show you how to use the 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 know which seat to move, you have to pass a SeatLocation object as the first parameter. The second argument specifies the component 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 use the await keyword when calling service methods, since these methods are coroutines.

MQTT

Interaction with other Vehicle Apps or the cloud is enabled by using Mosquitto MQTT Broker. The MQTT broker runs inside a docker image, which is started automatically after starting the DevContainer.

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 @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:
        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 to json (data = json.loads(data_str)).

In order to publish data to topics, the SDK provides the appropriate convenience method: self.publish_mqtt_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.Row(1).Pos(1).Position).value
        await self.publish_mqtt_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
    # pylint: disable=no-value-for-parameter

    with mock.patch.object(
        VehicleApp, "publish_mqtt_event", new_callable=mock.AsyncMock, return_value=-1
    ):
        response = await VehicleApp.publish_mqtt_event(
            str("sampleTopic"), get_sample_request_data()  # type: ignore
        )
        assert response == -1


def get_sample_request_data():
    return {"position": 330, "requestId": "123456789"}

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.

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 you have the seatservice configured as a dependency in your ./AppManifest.json. Read more about it in the run runtime services section.

If you want to run the app together with a Dapr sidecar and use the Dapr middleware, you have to use the “dapr run …” command to start your app:

dapr run --app-id seatadjuster --app-protocol grpc --app-port 50008 --config ./.dapr/config.yaml --components-path ./.dapr/components  python3 ./app/src/main.py

You already have seen this command and how to check if it is working in the general setup.

2 parameters may be unclear in this command:

  • the config file config.yaml
  • the components-path

For now, you just need to know that these parameters are needed to make everything work together.

The config.yaml has to be placed in the folder called .daprand has the following content:

apiVersion: dapr.io/v1alpha1
kind: Configuration
metadata:
 name: config
spec:
 tracing:
   samplingRate: "1"
   zipkin:
     endpointAddress: http://localhost:9411/api/v2/spans
 features:
   - name: proxy.grpc
     enabled: true

An important part is the enabling of the GRPC proxy, to make the communication work.

Inside the .dapr folder you find another folder called components. There you only find one configuration file for the MQTT communication with the following content:

apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: mqtt-pubsub
  namespace: default
spec:
  type: pubsub.mqtt
  version: v1
  metadata:
  - name: url
    value: "mqtt://localhost:1883"
  - name: qos
    value: 1
  - name: retain
    value: "false"
  - name: cleanSession
    value: "false"

If you want to know more about dapr and the configuration, please visit https://dapr.io

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",
        "preLaunchTask": "dapr-VehicleApp-run",
        "postDebugTask": "dapr-VehicleApp-stop",
        "env": {
            "DAPR_HTTP_PORT": "3500",
            "DAPR_GRPC_PORT": "${input:DAPR_GRPC_PORT}",
            "SERVICE_DAPR_APP_ID": "${input:SERVICE_NAME}",
            "VEHICLEDATABROKER_DAPR_APP_ID": "vehicledatabroker"
        }
    }
]

We specify which python-script to run using the program key. With the preLaunchTask and postDebugTask keys, you can also specify tasks to run before or after debugging. In this example, DAPR is set up to start the app before and stop it again after debugging. Below you can see the 2 tasks to find in /.vscode/tasks.json.

{
    "label": "dapr-VehicleApp-run",
    "appId": "vehicleapp",
    "appPort": 50008,
    "componentsPath": "./.dapr/components",
    "config": "./.dapr/config.yaml",
    "appProtocol": "grpc",
    "type": "dapr",
    "args": [
        "--dapr-grpc-port",
        "50001",
        "--dapr-http-port",
        "3500"
    ],
}
{
    "label": "dapr-VehicleApp-stop",
    "type": "shell",
    "command": [
        "dapr stop --app-id vehicleapp"
    ],
    "presentation": {
        "close": true,
        "reveal": "never"
    },
}

Lastly, the environment variables can also be specified.

You can adapt the configuration in /.vscode/launch.json to your needs (e.g., change the ports, add new tasks) or even add a completely new configuration for another Vehicle App.

Environment Variables can also be configured on the central /app/AppManifest.json and read out by the launch configuration in Visual Studio Code through a preinstalled extension in the devcontainer Tasks Shell Input.

"inputs": [
    {
        "id": "DAPR_GRPC_PORT",
        "type": "command",
        "command": "shellCommand.execute",
        "args": {
            "useSingleResult": true,
            "command": "cat ./app/AppManifest.json | jq .[].DAPR_GRPC_PORT | tr -d '\"'",
            "cwd": "${workspaceFolder}",
        }
    },
    {
        "id": "SERVICE_NAME",
        "type": "command",
        "command": "shellCommand.execute",
        "args": {
            "useSingleResult": true,
            "command": "cat ./app/AppManifest.json | jq .[].Name | tr -d '\"'",
            "cwd": "${workspaceFolder}",
        }
    }
]

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

3.2 - C++ Vehicle App Development

Learn how to develop and test a Vehicle App using C++.

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 template repository. You will learn how to use the Vehicle App 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 template repository: The Vehicle App allows to change the positions of the seats in the car and also provide their current positions to other applications.

A detailed explanation of the use case and the example is available here.

At first, you have to create the main c++ file which we will call App.cpp in /app/src. All the relevant code for new Vehicle App goes there. Afterwards, there are several steps you need to consider when developing the app:

  1. Manage your includes
  2. Initialize your class
  3. Start the app

Manage your imports

Before you start development in the App.cpp you just created, it will be necessary to include all required 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 SDK.

class MyVehicleApp : public VehicleApp {
public:
    // <remaining code in this tutorial goes here>
private:
    ::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 dapr-app-id of the KUKSA Databroker in the VAL.
                 IPubSubClient::createInstance("localhost:1883", "MyVehicleApp")) // the URI to the MQTT broker and the client ID of the MQTT client.
    {}
{}

Now, you have initialized the app and can continue developing relevant methods.

Start the app

Here’s an example of how to start the MyVehicleApp app that we just developed:

int main(int argc, char** argv) {
    MyVehicleApp app;
    app.run();
    return 0;
}

The app is now running. In order to use it properly, we will enhance the app 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 models.

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 in the folder app/vehicle_model/include/. This folder is already configured as “include folder” of the CMake tooling. 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 for the vehicle speed, this can be done via

auto vehicleSpeedBlocking = getDataPoint(Vehicle.Speed)->await();
getDataPoint(Vehicle.Speed)->onResult([](auto vehicleSpeed){
    logger().info("Got speed!");
})

getDataPoint() 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 becomes available.

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:

auto driverSeatPosition = getDataPoint(Vehicle.Cabin.Seat.Row(1).Pos(1).Position)->await();

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.Row(1).Pos(1).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.Row(1).Pos(1).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 many 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 any error during communication with the KUKSA data broker.

The result passed to the callback registered via onItem is an object of type DataPointsResult which holds all data points that have changed. Individual data points can be accessed directly by their reference: result.get(Vehicle.Cabin.Seat.Row(1).Pos(1).Position))

Services

Services are used to communicate with other parts of the vehicle via remote function calls (RPC). Please read the basics about them here.

The following few lines show you how to use the moveComponent-method of the SeatService you have created:

vehicle::cabin::SeatService::SeatLocation location{1, 1};
Vehicle.Cabin.SeatService.moveComponent(
    location, vehicle::cabin::SeatService::Component::Base, 300
    )->await();

In order to know which seat to move, you have to pass a SeatLocation object as the first parameter. The second argument specifies the component 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 via onResult() otherwise you don’t know when your asynchronous call will finish.

MQTT

Interaction with other Vehicle Apps or the cloud is enabled by using Mosquitto MQTT Broker. The MQTT broker runs inside a docker image, which is started automatically after starting the DevContainer.

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 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 (string) is provided as 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.Row(1).Pos(1).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.

Run your App

If you want to run the app together with a Dapr sidecar and use the Dapr middleware, you have to use the “dapr run …” command to start your app:

dapr run --app-id myvehicleapp --app-port 50008 --config ./.dapr/config.yaml --components-path ./.dapr/components build/bin/App

You already have seen this command and how to check if it is working in the general setup.

2 parameters may be unclear in this command:

  • the config file config.yaml
  • the components-path

For now, you just need to know that these parameters are needed to make everything work together.

The config.yaml has to be placed in the folder called .dapr and has the following content:

apiVersion: dapr.io/v1alpha1
kind: Configuration
metadata:
 name: config
spec:
 tracing:
   samplingRate: "1"
   zipkin:
     endpointAddress: http://localhost:9411/api/v2/spans
 features:
   - name: proxy.grpc
     enabled: true

An important part is the enabling of the GRPC proxy, to make the communication work.

Inside the .dapr folder you find another folder called components. There you only find one configuration file for the MQTT communication with the following content:

apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: mqtt-pubsub
  namespace: default
spec:
  type: pubsub.mqtt
  version: v1
  metadata:
  - name: url
    value: "mqtt://localhost:1883"
  - name: qos
    value: 1
  - name: retain
    value: "false"
  - name: cleanSession
    value: "false"

If you want to know more about dapr and the configuration, please visit the dapr documentation.

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 (dapr)",
        "type": "cppdbg",
        "request": "launch",
        "program": "${workspaceFolder}/build/bin/App",
        "args": [],
        "stopAtEntry": false,
        "cwd": "${workspaceFolder}",
        "environment": [
            {
                "name": "DAPR_GRPC_PORT",
                "value": "50001"
            },
            {
                "name": "DAPR_HTTP_PORT",
                "value": "3500"
            }
        ],
        "externalConsole": false,
        "MIMode": "gdb",
        "setupCommands": [
            {
                "description": "Enable pretty-printing for gdb",
                "text": "-enable-pretty-printing",
                "ignoreFailures": true
            },
            {
                "description": "Set Disassembly Flavor to Intel",
                "text": "-gdb-set disassembly-flavor intel",
                "ignoreFailures": true
            }
        ],
        "preLaunchTask": "dapr-VehicleApp-run",
        "postDebugTask": "dapr-VehicleApp-stop"
    }
]

We specify which binary to run using the program key. With the preLaunchTask and postDebugTask keys, you can also specify tasks to run before or after debugging. In this example, DAPR is set up to start the app before and stop it again after debugging. Below you can see the 2 tasks.

{
    "label": "dapr-VehicleApp-run",
    "appId": "myvehicleapp",
    "componentsPath": "./.dapr/components",
    "config": "./.dapr/config.yaml",
    "grpcPort": 50001,
    "httpPort": 3500,
    "type": "dapr",
    "presentation": {
        "close": true,
        "reveal": "never"
    },
}
{
    "label": "dapr-VehicleApp-stop",
    "type": "shell",
    "command": [
        "dapr stop --app-id myvehicleapp"
    ],
    "presentation": {
        "close": true,
        "reveal": "never"
    },
}

Lastly, the environment variables can also be specified.

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

4 - Vehicle Model Creation

Learn how creation of vehicle models work and how to adapt it to your needs.

A Vehicle Model makes it possible to easily get vehicle data from the KUKSA Data Broker and to execute remote procedure calls over gRPC against Vehicle Services and other Vehicle Apps. It is generated from the underlying semantic models based 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

Learn how to refer a model source and how the automated model lifecycle is working.

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"
}

Instead of an URI you could also reference a local file containing the specification.

Model Creation

The generation of the model is taking place:

  • through a onPostInit hook when velocitas init is called:
  • when you trigger the VS Code task (Re-)generate vehicle model explicitly.

The model generation is a three step process:

  1. The model generator is installed as a Python package (if not already present)
  2. The referenced model specification is downloaded (if no local reference)
  3. The model code is generated and installed.

Model lifecycle overview

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

4.2 - Manual Vehicle Model Creation

Learn how to manually create a vehicle model to access vehicle data or execute remote procedure calls.

This tutorial will show you how to:

  • Create a Vehicle Model
  • Add a Vehicle Service to the Vehicle Model
  • Distribute your Python Vehicle Model

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.

  1. Clone the vehicle-model-generator repository in a container volume.

  2. 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.

  3. 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
    

    Depending on the value of lang, which can assume the values python and cpp, this creates a sdv_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 package
    cpp 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.

  4. For python: Change the version of package in setup.py manually (defaults to 0.1.0).

  5. 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

4.2.1 - C++ Manual Vehicle Model Creation

Learn how to create a Vehicle Model manually for C++

Not yet done for C++

4.2.2 - Python Manual Vehicle Model Creation

Learn how to create a Vehicle Model manually for python

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.

  1. Start Visual Studio Code

  2. Select File > Open Folder (File > Open… on macOS) from the main menu.

  3. In the Open Folder dialog, create a my_vehicle_model folder and select it. Then click Select Folder (Open on macOS).

  4. Create a new file setup.py under my_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.

  5. Create an empty folder my_vehicle_model under my_vehicle_model.

  6. Create a new file __init__.py under my_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

  1. 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, a Seatmodel and has the following tree structure:

     Vehicle
     └── Cabin
         └── Seat (Row, Pos)
    
  2. Create a new file Seat.py under my_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 named Position.

  3. Create a new file Cabin.py under my_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 six Seat 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
  4. Create a new file vehicle.py under my_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 internals networks like CAN or Ethernet, which are connected to actuators, electronic control units (ECUs) and other vehicle computers (VCs). They may provide a simulation mode to run without a network interface. Vehicle Services may feed data to the Data Broker and may expose gRPC endpoints, which can be invoked by Vehicle Apps over a Vehicle Model.

In this section, we add a Vehicle Service to the Vehicle Model.

  1. Create a new folder proto under my_vehicle_model/my_vehicle_model.

  2. 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 VAL services which describes a seat control service.

  3. Install the grpcio tools including mypy types to generate the python classes out of the proto-file:

    pip3 install grpcio-tools mypy_protobuf
    
  4. 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
  5. 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 the Service class provided by the Python SDK.
    • The SeatService class must use the grpc channel from the Service 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 the self.metadata argument to the metadata of the gRPC call.

4.2.3 - Vehicle Model Distribution

Learn how to distribute a Vehicle Model.

4.2.3.1 - C++ Vehicle Model Distribution

Learn how to distribute a Vehicle Model written in C++.

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.

  1. Create a new git repository on i.e. Github
  2. Clone it locally, add the created vehicle_model folder to the git repository
  3. 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

Learn how to distribute a Vehicle Model written in Python.

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.

  1. Create a new folder my_vehicle_model under /app/src in your Vehicle App repository.
  2. Copy the my_vehicle_model folder to the /app/src folder of your Vehicle App repository.
  3. 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.

  1. Create new Git repository called my_vehicle_model

  2. Copy the content under my_vehicle_model to the repository.

  3. Release the Vehicle Model by creating a version tag (e.g., v1.0.0).

  4. Install the Vehicle Model package to your Vehicle App:

    pip3 install git+https://github.com/<yourorg>/my_vehicle_model.git@v1.0.0
    
  5. 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 - Run Vehicle App Runtime Services

Learn how to run the Vehicle App Runtime Services locally or in Kubernetes.

5.1 - Run runtime services locally

Using tasks in Visual Studio Code

Overview: If you are developing in Visual Studio Code, the runtime components (like KUKSA Data Broker or Vehicle Services) are available for local execution as Tasks, a feature of the Visual Studio Code. Additional information on tasks can be found here.

Quick Start: Each component has a task that is defined in .vscode/tasks.json:

  • Dapr (Local - Ensure Dapr): installs Dapr CLI and initializes Dapr if required
  • Mosquitto (Local - Mosquitto): runs Mosquitto as a container (docker run)
  • KUKSA Data Broker (Local - VehicleDataBroker): runs KUKSA Data Broker as a container
  • (Optional) Vehicle Services (Local - VehicleServices): runs the Vehicle Services (e.g. the Seat Service) configured in the AppManifest.json each as a separate container
  • (Optional) Feeder Can (Local - FeederCan): runs FeederCAN as a container

Run as Bundle: To orchestrate these tasks, a task called Start Vehicle App runtime is available. 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 Start Vehicle App runtime.

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.

Scripting: The tasks itself are executing scripts that are located in .vscode/scripts. These scripts download the specified version of the runtime components and execute them along with Dapr. The same mechanism can be used to register additional services or prerequisites by adding new task definitions in the tasks.json file.

Add/Change service configuration

The configuration for the services is defined in the file ./AppManifest.json. If you want to add a new service, adapt ./AppManifest.json. If you want to update the version, change it within the file and re-run the runtime services by restarting the tasks or the script.

Add/Change service configuration helper

{
  "name": "<NAME>",
  "image": "<IMAGE>",
  "version": "<VERSION>"
}

Using KUKSA Data Broker CLI

A CLI tool is provided for the interact with a running instance of the KUKSA Data Broker. It can be started by running the task Local - VehicleDataBroker CLI(by pressing F1, type Run Task followed by Local - VehicleDataBroker CLI). The KUKSA Data Broker needs to be running for you to be able to use the tool.

Using KUKSA FeederCan

FeederCan is a provider of a certain set of data points to the data broker. To run FeederCan as task please use [F1 -> Tasks: Run Task -> Local - FeederCan] and it will be run as a docker container.

By default it will use the same file, that is used for the k3d environment: deploy/runtime/k3d/volume/dbcfileDefault.dbc

For more flexible configuration please follow CAN feeder (KUKSA DBC Feeder)

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 script based on template script .vscode/scripts/run-vehicledatabroker.sh
  • In .vscode/tasks.json, duplicate section from task run-vehicledatabroker
  • Correct names in a new code block
  • Disclaimer: Problem Matcher defined in tasks.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 name>]
  • Task should be visible in Terminal section of Visual Studio Code

Task CodeBlock helper

{
    "label": "<__CHANGEIT: Task name__>",
    "type": "shell",
    "command": "./.vscode/scripts/<__CHANGEIT: Script Name.sh__> --task",
    "group": "none",
    "presentation": {
        "reveal": "always",
        "panel": "dedicated"
    },
    "isBackground": true,
    "problemMatcher": {
        "pattern": [
            {
                "regexp": ".",
                "file": 1,
                "location": 2,
                "message": 3
            }
        ],
        "background": {
            "activeOnStart": true,
            "beginsPattern": "^<__CHANGEIT: Regex log from your app, decision to send process in background__>",
            "endsPattern": "."
        }
    }
},

Integrating a new vehicle service

Integration of a new vehicle service can be done by adding an additional case and following the template run-vehicleservices.sh.

Vehicle Service CodeBlock helper

# Configure Service Specific Requirements
configure_service() {
    case $1 in
        seatservice)
            ...
            ;;
        <NEW_SERVICE>)
            # Configure ports for docker to expose
            DOCKER_PORTS="-p <PORT_TO_EXPOSE>"
            # Configure ENVs need to run docker container
            DOCKER_ENVS="-e <ENV_TO_RUN_CONTAINER>"
            # Configure Dapr App Port
            DAPR_APP_PORT=
            # Configure Dapr Grpc Port
            DAPR_GRPC_PORT=
            ;;
        *)
            echo "Unknown Service to configure."
            ;;
    esac
}      

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. In the Dapr-tab of the sidebar you can check if there are already tasks running. 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

5.2 - Run runtime services in Kubernetes

Besides local execution of the vehicle runtime components, another way is to deploy them as containers in a Kubernetes control plane (like K3D). To create a K3D instance, we provide Visual Studio 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):
    • K3D - Install prerequisites: Install prerequisite components K3D, Helm, KubeCTL and Dapr without configuring them.
    • K3D - Configure control plane: Creates a local container registry used by K3D as well as an K3D cluster with Dapr enabled.
    • K3D - Deploy runtime: Deploys the runtime components (like KUKSA Data Broker, Seat Service, …) within the K3D cluster.
    • K3D - Build VehicleApp: Builds the VehicleApp and pushes it to the local K3D registry.
    • K3D - Deploy VehicleApp: Builds and deploys the VehicleApp via Helm to the K3D cluster.

Each task has the required dependencies defined. If you want to run the runtime in K3D, the task K3D - Deploy VehicleApp will create and configure everything. So it’s enough to run that task.

  • Optional helper tasks:
    • K3D - Deploy VehicleApp (without rebuild): Deploys the VehicleApp via Helm to the K3D cluster (without rebuilding it). That requires, that the task K3D - Build VehicleApp has been executed once before.
    • K3D - Install tooling: Installs tooling for local debugging (K9s)
    • K3D - Uninstall runtime: Uninstalls the runtime components from the K3D cluster (without deleting the cluster).
    • K3D - Reset control plane: Deletes the K3D cluster and the registry with all deployed pods/services.

K3D is configured so that Mosquitto and the KUKSA Data Broker can be reached from outside the container over the ports 31883 (Mosquitto) and 30555(KUKSA Data Broker). The test runner, that is running outside of the cluster, can interact with these services over those ports.

To check the status of your K3D instance (running pods, containers, logs, …) you can either use the kubectl utility or start K9s (after running the task K3D - Install tooling once) in a terminal window to have a UI for interacting with the cluster.

Run as Bundle: To orchestrate these tasks, a task called K3D - Deploy VehicleApp is available. 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 K3D - 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.

Uploading files to persistentVolume

Some applications (e.g. FeederCAN) might make it necessary to load custom files from mounted volume. For that reason, persistentVolume is created in k3d cluster. All the files that are located in deploy/runtime/k3d/volume will be uploaded to the k3d cluster dynamically. In order to mount files to the directory that is accessible by the application, please refer to the deployment configuration file: deploy/runtime/k3d/helm/templates/bash.yaml.

Changes in deploy/runtime/k3d/volume are automatically reflected in PersistentVolume.

Uploading custom candump file to FeederCAN

FeederCAN requires candump file. Pre-defined candump file is part of docker container release, however, if necessary, there is an option to upload the custom file by:

  1. Creating/updating candump file with with name candump in deploy/runtime/k3d/volume
  2. Recreating the feedercan pod: kubectl delete pods -l app=feedercan

More information about FeederCan can be found here

Next steps

6 - Vehicle App Integration Testing

Learn how to test that a Vehicle App together with the KUKSA Data Broker and potentially other dependant Vehicle Services or Vehicle Apps runs as expected.

To be sure that a newly created Vehicle App will run together with the KUKSA Data Broker 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 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.

Quickstart

  1. Make sure that the local execution of runtime components is working and started.
  2. Start the application (Debugger or run as task).
  3. Extend the test file /app/tests/integration/integration_test.py or create a new test file.
  4. Run/debug tests with the Visual Studio Code Test runner.

Runtime components

To be able to test the Vehicle App in an integrated way, the following components should be running:

  • Dapr
  • Mosquitto
  • Data Broker
  • Vehicle Services

We distinguish between two environments for executing the Vehicle App and the runtime components:

  • Local execution: components are running locally in the development environment
  • Kubernetes execution: components (and application) are deployed and running in a Kubernetes control plane (e.g., K3D)

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 task VehicleApp.

When the runtime services and the application are running, integration tests can be executed locally.

Kubernetes execution (K3D)

If you want to execute the integration tests in Kubernetes mode, make sure that K3D is up and running according to the documentation. To ensure that the tests connect to the containers, please execute the following steps in new bash terminal:

  export MQTT_PORT=31883 && export VDB_PORT=30555 && pytest

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 Data Broker (to get and set values for DataPoints), 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. The path describes the property location within the response message, the value the property value to look for.

      Example:

      {
          "status": "success",
          "result": {
              "responsecode": 10
          }
      }
      

      If the responsecode property should be checked for the value 10, the path would be ["result", "responsecode], property value would be 10. 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 of 1883 will be used. It’s recommended to specify no port when initializing that class as it will locally use the default port 1883 and in CI the port set by the environment variable MQTT_PORT. This will prevent a check-in in the wrong port from local development.

  • IntTestHelper: this class provides functionality to interact with the KUKSA Data Broker.

    • register_dapoint: registers a new datapoint with given name and type
    • set_..._datapoint: set the given value for the datapoint with the given name (with given type). If the datapoint 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 of 55555 will be used. It’s recommended to specify no port when initializing that class as it will locally use the default port 55555 and in CI the port set by the environment variable VDB_PORT which is set. This will prevent a check-in in the wrong port from local development.

Please make sure that you don’t check in the test classes with using local ports because then the execution in the CI workflow will fail (as the CI workflow uses Kubernetes execution for running integration tests).

Running Tests locally

Once tests are developed, they can be executed against the running runtime components, either to the local runtime or in Kubernetes mode, by using the test runner in Visual Studio Code. The switch to run against the local components or the Kubernetes components is specified by the port. Local ports for Mosquitto and KUKSA Data Broker are 1883/55555. In Kubernetes mode, the ports would be the locally exposed ports 31883/30555. If using the Kubernetes ports, the tests will be executed against the runtime components/application that run in containers within the Kubernetes cluster.

Running Tests in CI pipeline

The tests will be discovered and executed automatically in the CI pipeline. The job Run Integration Tests contains all steps to set up and execute tests in Kubernetes mode. The results are published as test results to the workflow.

Common Tasks

Run test in local mode

  1. Make sure that the tasks for the runtime components are running (by checking the terminal view).
  2. Make sure that your application is running (via Debugger or task).
  3. Make sure that you are using the right ports for local execution of runtime components.
  4. Run tests from the test runner.

Run tests in Kubernetes mode

  1. Make sure that K3D is set up and all vehicle services and vehicle runtime are deployed and running (by executing the task K3D - Deploy runtime).
  2. Make sure that the tests are using the right ports for Kubernetes execution (see above).
  3. Run tests from the test runner.

Update application when running in Kubernetes mode

  1. Re-run the task K3D - Deploy runtime that rebuilds and deploys the application to K3D.
  2. Re-run tests from the test runner.

Troubleshooting

Check if the services are registered correctly in Dapr

  • When running in local mode, call dapr dashboard in a terminal and open the given URL to see the Dapr dashboard in the browser.
  • When running in Kubernetes mode, call dapr dashboard -k in a terminal and open the given URL to see the Dapr dashboard in the browser.

Troubleshoot IntTestHelper

  • Make sure that the KUKSA Data Broker is up and running by checking the task log.
  • Make sure that you are using the right ports for local/Kubernetes execution.
  • Make sure that you installed the correct version of the SDK (SDV-package).

Troubleshoot Mosquitto (MQTT Broker)

  • Make sure that the Mosquitto up and running by checking the task log.
  • Make sure that you are using the right ports for local/Kubernetes execution.
  • Use VsMqtt extension to connect to MQTT broker (localhost:1883 (local) or localhost:31883 (Kubernetes)) to monitor topics in MQTT broker.

Next steps

7 - Vehicle App Deployment via PodSpecs

Learn how to prepare PodSpecs for the deployment of a Vehicle App.

This tutorial will show you how to:

  • Prepare PodSpecs
  • Deploy your Vehicle App to local K3D

Prerequisites

Use the sample PodSpecs

If the Vehicle App has been created from one of our template repositories, a sample PodSpec is already available under deploy/VehicleApp/podspec and can be used as it is without any modification. Another example can also be found in the documentation of Leda.

Content

Looking at the content of the sample-podspec, it is starting with some general information about the app and the dapr configuration. You can define e.g. the app-port and the log-level. You could also add more labels to your app, which might help to identify the app for later usages.

apiVersion: v1
kind: Pod
metadata:
  name: sampleapp
  annotations:
    dapr.io/enabled: "true"
    dapr.io/app-id: sampleapp
    dapr.io/app-port: "50008"
    dapr.io/app-protocol: grpc
    dapr.io/log-level: info
  labels:
    app: sampleapp

Afterwards the configuration of the container is specified. Please be aware that the container-port should match the app-port from the dapr-config above. In the example the app-id of the VehicleDataBroker is also specified, since the app wants to connect to it. Last but not least the image is defined which should be used for the deployment. In this example the local registry is used, which is created during the configuration of the controlplane (see here for details).

spec:
  containers:
    - name: sampleapp
      imagePullPolicy: IfNotPresent
      ports:
        - containerPort: 50008
      env:
        - name: VEHICLEDATABROKER_DAPR_APP_ID
          value: "vehicledatabroker"
      image: k3d-registry.localhost:12345/sampleapp:local

Local registry or remote registry

In the example above we used the local registry, but you can also define a remote registry in the image tag, e.g.

image: ghcr.io/eclipse-velocitas/vehicle-app-python-template/sampleapp:0.0.1-bcx

If your registry is not public, you might to add secrets to your Kubernets config, see the official documentation for details. Afterwards you have to add the secrets also to the PodSpec:

  imagePullSecrets:
  - name: regcred

Deploy your Vehicle App to local K3D

Prerequisites

  • A local K3D installation must be available. For how to setup K3D, check out this tutorial.

Deploying your app with PodSpecs can be done with one simple command:

kubectl apply -f <podspec.yaml>

In parallel you can check with K9S if the deployment is working correctly.

Next steps

8 - Vehicle App Deployment with Helm

Learn how to prepare a Helm chart for the deployment of a Vehicle App.

This tutorial will show you how to:

  • Prepare a Helm chart
  • Deploy your Vehicle App to local K3D

Prerequisites

Use the sample Helm chart

If the Vehicle App has been created from one of our template repositories, a sample Helm chart is already available under deploy/VehicleApp/helm and can be used as it is without any modification. This sample chart is using the values from deploy/VehicleApp/helm/values.yaml file, during the deployment of the VehicleApp, the neccessary app attributes from the AppManifest.json (e.g. app name and app port) will overwite the default values from the sample helm chart via the .vscode/runtime/k3d/deploy_vehicleapp.sh script.

Prepare a new Helm chart

If you would like to write a new helm chart, this section will guide you to adapt and deploy a new vehicle app, which is called my_vehicle_app for this walkthrough.

  1. Start Visual Studio Code and open the previously created Vehicle App repository.

  2. Create a new folder my_vehicle_app under deploy

  3. Copy all files from the deploy/VehicleApp folder to the new folder deploy/my_vehicle_app.

  4. Rename the file deploy/my_vehicle_app/helm/templates/vehicleapp.yaml to deploy/my_vehicle_app/helm/templates/my_vehicle_app.yaml

  5. Open deploy/my_vehicle_app/helm/Chart.yaml and change the name of the chart to my_vehicle_app and provide a meaningful description.

    apiVersion: v2
    name: my_vehicle_app
    description: Short description for my_vehicle_app
    
    # A chart can be either an 'application' or a 'library' chart.
    #
    # Application charts are a collection of templates that can be packaged into versioned archives
    # to be deployed.
    #
    # Library charts provide useful utilities or functions for the chart developer. They're included as
    # a dependency of application charts to inject those utilities and functions into the rendering
    # pipeline. Library charts do not define any templates and cannot be deployed as a result.
    type: application
    
    # This is the chart version. This version number should be incremented each time you make changes
    # to the chart and its templates, including the app version.
    # Versions are expected to follow Semantic Versioning (https://semver.org/)
    version: 0.1.0
    
    # This is the version number of the application being deployed. This version number should be
    # incremented each time you make changes to the application. Versions are not expected to
    # follow Semantic Versioning. They should reflect the version the application is using.
    appVersion: 1.16.0
    
  6. Open deploy/my_vehicle_app/helm/values.yaml and change name, repository and daprAppid to my_vehicle_app. Rename the root node from imageVehicleApp to imageMyVehicleApp.

    imageMyVehicleApp:
      name: my_vehicle_app
      repository: local/my_vehicle_app
      pullPolicy: Always
      # Overrides the image tag whose default is the chart appVersion.
      tag: "#SuccessfulExecutionOfReleaseWorkflowUpdatesThisValueToReleaseVersionWithoutV#"
      daprAppid: my_vehicle_app
      daprPort: 50008
    
      nameOverride: ""
      fullnameOverride: ""
    
  7. Open deploy/my_vehicle_app/helm/templates/my_vehicle_app.yaml and replace imageVehicleApp with imageMyVehicleApp:

    apiVersion: apps/v1
    kind: Deployment
    metadata:
    name: {{.Values.imageMyVehicleApp.name}}
    labels:
        app: {{.Values.imageMyVehicleApp.name}}
    spec:
    selector:
        matchLabels:
        app: {{.Values.imageMyVehicleApp.name}}
    template:
        metadata:
        annotations:
            dapr.io/enabled: "true"
            dapr.io/app-id: "{{.Values.imageMyVehicleApp.daprAppid}}"
            dapr.io/app-port: "{{.Values.imageMyVehicleApp.daprPort}}"
            dapr.io/log-level: "debug"
            dapr.io/config: "config"
            dapr.io/app-protocol: "grpc"
        labels:
            app: {{.Values.imageMyVehicleApp.name}}
            {{- include "helm.selectorLabels" . | nindent 8 }}
        spec:
        containers:
            - name: {{.Values.imageMyVehicleApp.name}}
            image: "{{ .Values.imageMyVehicleApp.repository }}:{{ .Values.imageMyVehicleApp.tag | default .Chart.AppVersion }}"
            imagePullPolicy: {{ .Values.imageMyVehicleApp.pullPolicy }}
    
  8. update or copy the scripts build_vehicleapp.sh and deploy_vehicleapp.sh in path (.vscode/scripts/runtime/k3d/) for the local Kubernates deployment and adjust the values according to the values AppManifest.json:

    • APP_NAME
    • APP_PORT
    • DOCKERFILE_FILE
  9. update the script .github/scripts/deploy_imagefromghcr.sh for the CI workflow with the correct values from the AppManifest.json as above.

At this point, the Helm chart and updated scripts are ready to use and folder structure under deploy/my_vehicle_app should look like this:

deploy
├── my_vehicle_app
│   └── helm
│       └── templates
│           └── _helpers.tpl
│           └── my_vehicle_app.yaml
│────────── .helmignore
│────────── Chart.yaml
└────────── values.yaml

Deploy your Vehicle App to local K3D

Prerequisites

  • A local K3D installation must be available. For how to setup K3D, check out this tutorial.

After the Helm chart has been prepared, you can deploy it to local K3D. Execute the script:

deploy/my_vehicle_app/deploy-my-vehicle-app.sh

This script builds the local source code of the application into a container, pushes that container to the local cluster registry and deploys the app via a helm chart to the K3D cluster. Rerun this script after you have changed the source code of your application to re-deploy with the latest changes.

Next steps