App Tutorial

Tutorial for creating an AuterionOS App

Getting started

Tutorial App functions Overview

The Tutorial App allows the user to access the vehicles position data from a custom UI and even command position changes. Follow this guide to learn how to write an app that can read and influence the state of the vehicle and make it accessible from a custom UI.

These are the main functions this app provides which will be explained in this guide.

  • Telemetry reading

  • Command sending

  • Custom UI

Along with these functions there will also be general explanations on the structure and functions of AuterionOS apps and how to build, install and use those apps.

Setting Up the Development Environment

Install the Auterion CLI

The Auterion CLI can easily be installed using PIP (python’s package manager) with the following command:

pip3 install auterion-cli

To get more detailed Information on the Setup go to

pageDevelopment environment Setup

Get the Tutorial App Code

Clone the app-tutorial-code Repository to use the example code, take a look at one of the example codes in Application Development or build up your own according to the structure outlined in the next section.

Structure

Every AuterionOS app should follow the same file structure in order to be built and deployed using the Auterion CLI. There are 3 things that make up an AuterionOS app and these will be explained in the following 3 subsections. More information about this topic can be found in the Application Development Section.

For this app mavlink and libmav are used so they need to be added as git submodules.

Directory Structure of the App

An AuterionOS app minimally requires an auterion-app.yml needs to be in the root directory, in this case app-tutorial-code. The rest of the structure is optional, but all the referencing in the example code is based on this structure so any changes to the structure need to be changed there as well. The Dockerfile, the submodules that will be added and the source code will be in services/web-navigation. Since this app will be in C++, the CMakeList.txt will be in this directory as well, main.cpp will be in the src folder and the index.html in the website folder.

app-tutorial-code/              # Root directory of the application
    auterion-app.yml            # Auterion app metadata file. Has to be in root dir
    services/                   # Folder to contain the individual services
        web-navigation/         # Folder for the "web-navigation" service
            Dockerfile          # Describes how "web-navigation" image is built
            CMakeLists.txt      # Describes how our C++ app is built
            src/                # Diretory for source code of our C++ app
                main.cpp        # Source code of our C++ app
            website/            # Directory for the HTML content of our web-UI
                index.html      # Source code of our web UI
            libmav[subm]/       # libmav C++ library as submodule
            mavlink[subm]/      # mavlink message definitions as submodule

Git submodules

After creating the directory the app will be created in, use git to add the necessary submodules. If git isn't already installed you can follow this Guide. To turn your created directory into a git repository use git init.

git init

Then add libmav and mavlink as submodules to your repository by going to the directory the submodules should be added to and using git submodule add.

cd services/web-navigation/
git submodule add https://github.com/Auterion/libmav.git
git submodule add https://github.com/mavlink/mavlink.git

auterion-app.yml

Contains all meta-information about the app, like name, version, author etc. This file needs to be in the project root directory. Each build entry points to the location of a Dockerfile that needs to be built and run. More detailed explanations can be found here.

auterion-api-version: 2               # Auterion API version this app is targeting
auterion-app-base: v2                 # Version of the auterion/app-base

app-name: tutorial-web-navigation     # The name of your app
app-version: 0.0.1                    # The version of your app
app-author: com.auterion              # The authoring entity (reverse-domain notation)
target-platform: [skynode, ainode]    # Supported platforms (skynode or ainode)

Like in docker-compose, your app can consist of multiple docker containers. In 'services' you list all the docker containers that make up your app. In most cases, this is just one service. The API configuration for the web interface is also added here.

services:
  web-navigation:                        # Name of the service
    build: services/web-navigation       # Location of the Dockerfile
    ssh: true                            # Enable SSH port forwarding

    http:
      /:                                 # The URI that should be mapped
        static-root: /data/webroot       # Static content points to a directory
        function: 
          type: ui                       # Assign function UI to the endpoint
          label: Tutorial Web Navigation # Make site reachable from vehicle webpage
      
      /api:                              # The URI that should be mapped
        proxy:                           # Proxying the requests
          port: 8080                     # The port in the docker container to proxy to

main.cpp

Create this file in services/web-navigation/src. It contains all the necessary C++ code. First some general setup.

#include <mav/MessageSet.h>
#include <mav/TCPClient.h>
#include <mav/Network.h>
#include <restinio/all.hpp>
#include <cmath>

Then a struct for the vehicle coordinate and one for the system state.

struct Coordinate {
    double latitude;
    double longitude;
};

struct SystemState {
    Coordinate position;
    double altitude;
    double battery_percentage;
};

To later use for the change in position, functions which change the coordinate a given distance. For this example, this uses a crude distance estimation function, but is sufficient for demo purpose.

Coordinate extendSouth(Coordinate point, double distance) {
    double r_earth = 6378;
    double new_latitude = point.latitude - (distance / r_earth) * (180 / M_PI);
    return Coordinate{new_latitude, point.longitude};
}

Coordinate extendWest(Coordinate point, double distance) {
    double r_earth = 6378;
    double new_longitude = point.longitude - (distance / r_earth) * (180 / M_PI) / cos(point.latitude * M_PI / 180);
    return Coordinate{point.latitude, new_longitude};
}

The code from Telemetry Reading and Sending Commands will be added to the "App" class.

class App {
private:
    mav::MessageSet _message_set;
    std::shared_ptr<mav::TCPClient> _phy;
    std::shared_ptr<mav::NetworkRuntime> _runtime;
    std::shared_ptr<mav::Connection> _connection;

    std::mutex _system_state_mtx;
    SystemState _system_state;
    
    //add code here
    
public:
    App() : _message_set("mavlink/common.xml") {
    }
    
    void run() {
        connectMAVLink();
        std::cout << ".. Connected to MAVLink" << std::endl;
        auto global_position_int_id = _message_set.idForMessage("GLOBAL_POSITION_INT");
        auto battery_status_id = _message_set.idForMessage("BATTERY_STATUS");
    
        //add code here
    
    }
};

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

Telemetry Reading

To get flight information or to influence the flight path Mavlink messages can be used. In the Tutorial App a Mavlink connection is established and then used to receive Telemetry data and send commands for a position change to the flight controller. In this App libmav is used.

To create an interface for the Mavlink connection and connect with Mavlink, add this to the private section of main.cpp. It will be called in run().

void connectMAVLink() {
   // Create interface
   _phy = std::make_shared<mav::TCPClient>("10.41.1.1", 5790);

   mav::Message heartbeat = _message_set.create("HEARTBEAT").set({
        {"type",          _message_set.e("MAV_TYPE_GENERIC")},
        {"autopilot",     _message_set.e("MAV_AUTOPILOT_INVALID")},
        {"base_mode",     _message_set.e("MAV_MODE_FLAG_CUSTOM_MODE_ENABLED")},
        {"custom_mode",   0},
        {"system_status", _message_set.e("MAV_STATE_ACTIVE")}
   });
   _runtime = std::make_shared<mav::NetworkRuntime>(_message_set, heartbeat, *_phy);
   _connection = _runtime->awaitConnection(1000);
}

Get the Telemetry information from the vehicle and safe the system state. This is a callback that needs to be added to run() in the public section.

_connection->addMessageCallback([this,
        global_position_int_id, battery_status_id](const mav::Message &message) {
    if (message.id() == global_position_int_id) {
        std::lock_guard<std::mutex> lock(_system_state_mtx);
        _system_state.position.latitude = message["lat"].as<double>() / 1e7;
        _system_state.position.longitude = message["lon"].as<double>() / 1e7;
        _system_state.altitude = message["alt"].as<double>() / 1e3;
    } else if (message.id() == battery_status_id) {
        std::lock_guard<std::mutex> lock(_system_state_mtx);
        _system_state.battery_percentage = message["battery_remaining"].as<double>();
    }
});

Sending Commands

The tutorial App uses a custom interface explained in the index.html section to change the position of the vehicle. The main.cpp handles these requests and communicates them to the flight controller.

Create a router to handle the http requests from the web interface. This router is created with restinio, to learn more go to RESTinio Documentation. Add this to the private section of the code.

std::unique_ptr<restinio::router::express_router_t<>> createRouter() {
    auto router = std::make_unique<restinio::router::express_router_t<>>();
    
    //add code for telemetry data
    //add code for reposition commands
    
    return router;
}

Add this to createRouter() to provide the telemetry data for the web interface.

router->http_get(
    R"(/telemetry)",
    [this](auto req, auto params) {
        std::lock_guard<std::mutex> lock(_system_state_mtx);
        return req->create_response()
            .append_header(restinio::http_field::content_type, "text/json; charset=utf-8")
            .append_header(restinio::http_field::access_control_allow_origin, "*")
            .set_body(fmt::format("{{\"latitude\": {}, \"longitude\": {}, \"altitude\": {}, \"battery_percentage\": {}}}",
                                 _system_state.position.latitude, _system_state.position.longitude, _system_state.altitude, _system_state.battery_percentage))
            .done();
    });

Add this to createRouter() to handle the reposition commands from the web interface

router->http_post(
    R"(/commands/reposition)",
    [this](auto req, auto params) {

        std::string direction = req->body();
        std::lock_guard<std::mutex> lock(_system_state_mtx);
        Coordinate new_position{};
        if (direction == "north") {
            new_position = extendSouth(_system_state.position, -0.001);
        } else if (direction == "south") {
            new_position = extendSouth(_system_state.position, 0.001);
        } else if (direction == "east") {
            new_position = extendWest(_system_state.position, -0.001);
        } else if (direction == "west") {
            new_position = extendWest(_system_state.position, 0.001);
        } else {
            return req->create_response(restinio::status_not_found())
                .append_header(restinio::http_field::content_type, "text/json; charset=utf-8")
                .set_body("{ \"status\": \"error\", \"message\": \"unknown direction\" }")
                .done();
        }
        std::cout << "Old position: " << _system_state.position.latitude << ", " << _system_state.position.longitude << std::endl;
        std::cout << "New position: " << new_position.latitude << ", " << new_position.longitude << std::endl;
        sendMAVLinkDoReposition(new_position.latitude, new_position.longitude, _system_state.altitude);
        return req->create_response()
            .append_header(restinio::http_field::content_type, "text/json; charset=utf-8")
            .set_body("{ \"status\": \"ok\" }")
            .done();
    });

Create and send a Mavlink message to send commands like a reposition to the vehicle. Add this to the private section.

void sendMAVLinkDoReposition(double latitude, double longitude, double altitude) {

    auto message = _message_set.create("COMMAND_INT").set({
        {"target_system", 1},
        {"target_component", 1},
        {"command", _message_set.e("MAV_CMD_DO_REPOSITION")},
        {"param1", 0},
        {"param2", 1}, // enforce mode change
        {"param3", 0},
        {"param4", 0},
        {"x", latitude * 1e7},
        {"y", longitude * 1e7},
        {"z", altitude},
        {"frame", _message_set.e("MAV_FRAME_GLOBAL_RELATIVE_ALT")},
        {"current", 0},
        {"autocontinue", 0}
    });
    auto exp = _connection->expect("COMMAND_ACK");
    _connection->send(message);
    _connection->receive(exp, 1000);
}

Add this to run() in the public section to start the server.

struct my_server_traits : public restinio::default_single_thread_traits_t {
    using request_handler_t = restinio::router::express_router_t<>;
};

std::cout << "Starting server on port 8080" << std::endl;
restinio::run(
    restinio::on_this_thread<my_server_traits>()
        .port(8080)
        .address("0.0.0.0")
        .request_handler(this->createRouter())
);

index.html

Create this file in services/web-navigation/website. It contains the scripts for the UI. The Interface can be designed with html and will be accessible in the local updater.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>WEB Navigation example</title>

<script lang="javascript">

// add javascript code here

</script>

<!-- add html layout here --> 

A javascript function using the API is used to get the telemetry data and display it on the web interface.

function init() {
    setInterval(function() {
        const statusInfo = document.getElementById("status-info");

        fetch("api/telemetry").then(function(response) {
            return response.json();
        }).then(function(json) {
            statusInfo.innerHTML = `
             <table>
                <tr>
                    <th>Latitude</th>
                    <td>${json.latitude} deg</td>
                </tr>
                <tr>
                    <th>Longitude</th>
                    <td>${json.longitude} deg </td>
                </tr>
                <tr>
                    <th>Altitude</th>
                    <td>${json.altitude} m</td>
                </tr>
            </table>
            `
        }).catch(function(error) {
            statusInfo.innerHTML = "Error: " + error;
        });

    }, 1000);
}

window.onload = init;

A second javascript function which uses the Reposition command written in the API is used to command the change in position.

function goDirection(direction) {
    fetch("api/commands/reposition",
        {
            method: "POST",
            headers: {
                "Content-Type": "application/json"
            },
            body: direction
        }
    ).then(function(response) {
        return response.json();
    }).then(function(json) {
        console.log(json);
    }).catch(function(error) {
        console.log(error);
    });
}

To create the page and call the reposition function html is used.

<style>
    body {
        font-family: Arial, Helvetica, sans-serif;

    }
</style>
</head>
<body>
    <h1>
        Tutorial app example
    </h1>
    <div id="status-info">
        Loading...
    </div>
    <hr />
    <button onclick="goDirection('north')">Go north (1m)</button>
    <button onclick="goDirection('east')">Go east (1m)</button>
    <button onclick="goDirection('south')">Go south (1m)</button>
    <button onclick="goDirection('west')">Go west (1m)</button>
</body>
</html>

After installing the App on a Skynode, open 10.41.1.1 and click "Show installed apps" to show all apps installed on the Skynode and click on the "Tutorial Web Navigation" Link to open the web interface.

CMakeList.txt

The CMakeList file contains the information on how to compile.

cmake_minimum_required(VERSION 3.22)
project(web-navigation)

set(CMAKE_CXX_STANDARD 17)

find_package(unofficial-http-parser REQUIRED)
find_package(fmt REQUIRED)
find_package(restinio CONFIG REQUIRED)
if (restinio_FOUND)
    message(STATUS "Found restinio: ${restinio_VERSION}")
endif ()

add_executable(web-navigation src/main.cpp)
target_include_directories(web-navigation PRIVATE ${CMAKE_SOURCE_DIR}/libmav/include)
target_link_libraries(web-navigation PRIVATE restinio::restinio)
target_link_libraries(web-navigation PRIVATE fmt::fmt)
target_link_libraries(web-navigation PRIVATE unofficial::http_parser::http_parser)

file(GLOB MAVLINK_XML ${CMAKE_CURRENT_SOURCE_DIR}/mavlink/message_definitions/v1.0/*.xml)
file(COPY ${MAVLINK_XML} DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/mavlink)

Dockerfile

Add a file named "Dockerfile" to services/web-navigation.Describes how to actually build and run the app. Any docker command can be used in here. Apps shall inherit from the auterion/app-base image. The Dockerfile gets discovered in the build process by the tool looking at the locations indicated in the auterion-app.yml file.

This app will inherit from version 2 of the App Base.

FROM auterion/app-base:v2

This will install restinio. To learn more about restinio, and how to use it, go to the RESTinio Documentation.

ARG RESTINIO_VERSION=0.6.18


RUN apt update && \
    apt install -y \
    libasio-dev \
    libfmt-dev && \
    apt clean && \
    rm -rf /var/lib/apt/lists/*

# Install restinio
RUN curl -L https://github.com/Stiffstream/restinio/releases/download/v.${RESTINIO_VERSION}/restinio-${RESTINIO_VERSION}-full.tar.bz2 -o restinio.tar.bz2 && \
    tar xf restinio.tar.bz2 && \
    cd restinio-${RESTINIO_VERSION}/dev && \
    mkdir build && \
    cd build && \
    cmake -DCMAKE_BUILD_TYPE=Release -DRESTINIO_TEST=no -DRESTINIO_SAMPLE=no -DRESTINIO_INSTALL_SAMPLES=no -DRESTINIO_BENCH=no  .. && \
    make -j8 install && \
    cd ../../.. && \
    rm -rf restinio.zip restinio-${RESTINIO_VERSION}

This will copy the code to the specified folder, build it and then run the app.

COPY . /app

WORKDIR /app

RUN cmake . && make -j8

CMD mkdir -p /data/webroot && cp  website/index.html /data/webroot/ && ./web-navigation

Installation on Skynode

Discover Skynode using Auterion CLI

To install on a Skynode the device must be selected in the Auterion CLI. First discover the devices connected. When no device is selected and the Skynode is connected with USB it will be selected by default so this step can be skipped.

auterion-cli device discover

To select a device the serial number is needed. The "*" in the first column shows if the device is already selected and the serial number is in the second column.

selected       serial  version    addresses
----------  ---------  ---------  -------------
*           009128332  v2.15.0    {'10.41.1.1'}

Use the serial number to select the device.

auterion-cli device select <device serial number>

If the selection was successful this is the shown output.

$ auterion-cli device select 009128332
Selected device with ID 009128332 on address 10.41.1.1

Build and Install app on Skynode using Auterion CLI

First the App needs to be built. Execute this command in the folder the App is in.

auterion-cli app build

To build an app for Virtual Skynode, you must also provide the --simulation flag to the build command. Refer to the Application Development page for more context.

The App base version specified in the auterion-app.yml needs to be installed on the device first. The App can be installed either over the local updater on 10.41.1.1 or by using this command.

auterion-cli app install build/com.auterion.tutorial-web-navigation.auterionos

Get status and logs from app running on Skynode using Auterion CLI

Auterion CLI can also be used to get the log output from the app.

auterion-cli app logs -f <APP NAME>

Last updated