Writing Plugins

The MAVSDK is split into a core and multiple independent plugins.

Plugins that are located in the correct location (a subfolder of /plugins) and have the correct structure are built at compile time. The CMakeLists.txt takes care of including the plugin folders and integration tests.

Plugin Architecture

Plugins should be written so that they are independent of each other (they will still need to be dependent on the core source). This allows plugins to be removed/replaced as needed at the cost of some duplicated functionality across the plugin modules.

The code for each plugin (and its unit test if one has been defined) is stored in a sub-folder of the plugins directory. Integration tests for all plugins in the library are stored in integration_tests.

A simplified view of the folder structure is shown below:

├── MAVSDK
│   ├── core
│   ├── integration_tests
│   └── plugins
│       ├── action
│       ├── ...
│       └── telemetry

Each plugin must have the same files/structure, as shown for the "example" plugin below.

└── plugins
    └── example
        ├── CMakeLists.txt
        ├── example.cpp
        ├── example.h
        ├── example_impl.cpp
        ├── example_impl.h
        └── example_foo_test.cpp  ##optional

Auto-generation

In order to support various language wrappers around MAVSDK without having to write the same things multiple times, once for every language, we opted to use auto-generation as much as possible. The APIs are defined as proto definitions.

From that, several parts are auto-generated, such as:

  • Language wrappers based on gRPC client (formerly called frontend)
  • gRPC mavsdk_server in C++ (formerly called backend)
  • Plugin C++ headers defining the API.

Looking at the plugin structure again, this means that some of the files are auto-generated:

└── plugins
    └── example
        ├── CMakeLists.txt       # auto-generated
        ├── example.cpp          # auto-generated
        ├── example.h            # auto-generated
        ├── example_impl.cpp     # hand-written (can initally be generated)
        ├── example_impl.h       # hand-written (can initally be generated)
        └── example_foo_test.cpp  # optional

Create a Plugin

To create a new plugin do not copy an existing one, instead follow the steps below:

Think about public API

Before writing the API, take a step back and think what a user of it needs and expect, rather than what MAVLink already provides.

Generally, MAVSDK APIs ought to be:

  • Simple and easy to use.
  • Reduced to the essentials; no functionality that is not actually implemented/supported should be exposed.
  • Clearly named and if possible without too much drone jargon and acronyms.
  • Abstracted from the MAVLink implementation and therefore to provide specific functionality instead of just forwarding direct MAVLink.

This advice is important if you are planning to contribute the new plugin back and would like it to get accepted and merged. We are convinced it is also applicable for internal development but - of course - that's up to you.

About proto structure

There are a couple of different API types supported for MAVSDK plugins.

Requests:

A request is a simple one time call with a response. An example would be the takeoff command of the action plugin:

service ActionService {
    rpc Takeoff(TakeoffRequest) returns(TakeoffResponse) {}
    // all other services
}

message TakeoffRequest {}
message TakeoffResponse {
    ActionResult action_result = 1;
}

In this case the request has no argument and no return value except the result but this can vary, e.g. for getters and setters:

service ActionService {
    rpc GetReturnToLaunchAltitude(GetReturnToLaunchAltitudeRequest) returns(GetReturnToLaunchAltitudeResponse) {}
    rpc SetReturnToLaunchAltitude(SetReturnToLaunchAltitudeRequest) returns(SetReturnToLaunchAltitudeResponse) {}
    // all other services
}

message GetReturnToLaunchAltitudeRequest {}
message GetReturnToLaunchAltitudeResponse {
    ActionResult action_result = 1;
    float relative_altitude_m = 2;
}

message SetReturnToLaunchAltitudeRequest {
    float relative_altitude_m = 1;
}
message SetReturnToLaunchAltitudeResponse {
    ActionResult action_result = 1;
}

that requests can defined SYNC, ASYNC, or BOTH using option (mavsdk.options.async_type) = ...;. The choice depends on the functionality that is being implemented and how it would generally be used. There are no hard rules, it's something that makes sense to be discussed one by one in a pull request. The default implementation is BOTH.

Subscriptions:

A subscription is triggered once and will then continuously send responses as a stream. An example would be a the position information of the telemetry plugin:

service TelemetryService {
    rpc SubscribePosition(SubscribePositionRequest) returns(stream PositionResponse) {}
    // all other services
}


message SubscribePositionRequest {}
message PositionResponse {
    Position position = 1;
}

that subscriptions also can defined SYNC, ASYNC, or BOTH using option (mavsdk.options.async_type) = ...;. The sync implementation of a subscription is just a getter for the last received value.

Note that subscriptions can be defined finite using option (mavsdk.options.is_finite) = true;. This means that the stream of messages will end at some point instead of continuing indefinitely. An example would be progress updates about a calibration which eventually finishes.

Add API to proto

The first step should be to define the user API in the proto repository. This repository is part of the MAVSDK as a submodule in the proto/ directory.

You usually want to work from master in the proto/ directory, and then create a feature branch with your additions:

cd proto
git switch master
git pull
git switch -c my-new-plugin
cd ../

Now you can add a folder with your proto file (or copy an existing one and rename it) and draft the API.

Once the API is defined, it makes sense to commit the changes, push them and make a pull request to MAVSDK-Proto to get feedback from the MAVSDK maintainers.

Generate .h and .cpp files

Once the proto file has been created, you can generate all files required for the new plugin.

  1. Run the configure step to prepare the tools required:
    cmake -DBUILD_BACKEND=ON -Bbuild/default -H.
    
  2. Run the auto-generation:
    tools/generate_from_protos.sh
    
  3. Fix style after auto-generation:
    tools/fix_style.sh .
    

the files my_new_plugin.h and my_new_plugin.cpp are generated and overwritten everytime the script is run. However, the files my_new_plugin_impl.h and my_new_plugin_impl.cpp are only generated once. To re-generate them, delete them and run the script again. This approach is used to prevent the script from overwriting your local changes.

You can now add the actual "business logic" which is usually sending and receiving MAVLink messages, waiting for timeouts, etc. All implementation goes into the files my_new_plugin_impl.h and my_new_plugin_impl.cpp or additional files for separete classes required.

You can also add unit tests with unittest_source_files, as discussed below.

The standard plugins can be reviewed for guidance on how to write plugin code, including how to send and process MAVLink messages.

Plugin Code

Plugin Base Class

All plugins should derive their implementation from PluginImplBase (core/plugin_impl_base.h) and override virtual methods as needed.

Plugin Enable/Disable

The SDK provides virtual methods that a plugin should implement to allow the core to better manage resources. For example, to prevent callback being created before the System is instantiated, or messages being sent when a vehicle is not connected.

Plugin authors should provide an implementation of the following PluginImplBase pure virtual methods:

  • init()/deinit(): These are called when a system is created and just before it is destroyed. These should be used for setting up and cleaning everything that depends on having the System instantiated. This includes calls that set up callbacks.
  • enable()/disable(): These are called when a vehicle is discovered or has timed out. They should be used for managing resources needed to access a connected system/vehicle (e.g. getting a parameter or changing a setting).

Additional detail is provided for methods below.

init()
virtual void init() = 0

The init() method is called when a plugin is instantiated. This happens when a System is constructed (this does not mean that the system actually exists and is connected - it might just be an empty dummy system).

Plugins should do initialization steps with other parts of the SDK at this state, e.g. set up callbacks with _parent (DeviceImpl).

deinit()
virtual void deinit() = 0

The deinit() method is called before a plugin is destroyed. This usually happens only at the very end, when a MAVSDK instance is destroyed.

Plugins should cleanup anything that was set up during init().

enable()
virtual void enable() = 0

The enable() method is called when a system is discovered (connected). Plugins should do all initialization/configuration steps that require a system to be connected. For example, setting/getting parameters.

If any threads, call_every or timeouts are needed, they can be started in this method.

disable()
virtual void disable() = 0

The disable() method is called when a system has timed out. The method is also called before deinit() is called to stop any systems with active plugins from communicating (in order to prevent warnings and errors because communication to the system no longer works).

If any threads, call_every, or timeouts are running, they should be stopped in this method.

Test Code

Tests must be created for all new and updated plugin code. The tests should be exhaustive, and cover all aspects of using the plugin API.

The Google Test Primer provides an excellent overview of how tests are written and used.

Writing Unit Tests

Most of the existing plugins do not have unit tests, because we do not yet have the ability to mock MAVLink communications (needed to test most plugins). Unit tests are therefore considered optional!

Comprehensive integration tests should be written instead, with the simulator providing appropriate MAVLink messages.

Adding Unit Tests

Unit test files are stored in the same directory as their associated source code.

In order to include a test in the SDK unit test program (unit_tests_runner), it must be added to the UNIT_TEST_SOURCES variable in the plugin CMakeLists.txt file.

For example, to add the example_foo_test.cpp unit test you would append the following lines to its CMakeLists.txt:

list(APPEND UNIT_TEST_SOURCES
    ${CMAKE_SOURCE_DIR}/src/plugins/mission/example_foo_test.cpp
)
set(UNIT_TEST_SOURCES ${UNIT_TEST_SOURCES} PARENT_SCOPE)

Unit Test Code

Unit tests typically include the file to be tested, mavsdk.h, and gtest.h. There are no standard shared test unit resources so test functions are declared using TEST. All tests in a file should share the same test-case name (the first argument to TEST).

The skeleton example plugin unit test is shown below:

#include "example.h"
#include "mavsdk.h"
#include "global_include.h"
#include <gtest/gtest.h>

namespace mavsdk {

TEST(Example, NoTest)
{
    ASSERT_TRUE(true);
}

} // namespace mavsdk

Writing Integration Tests

MAVSDK provides the integration_tests_runner application for running the integration tests and some helper code to make it easier to log tests and run them against the simulator.

Check out the Google Test Primer and the integration_tests for our existing plugins to better understand how to write your own!

Adding Integration Tests

In order to run an integration test it needs to be added to the integration_tests_runner program.

Integration tests for core functionality and plugins delivered by the project are stored in MAVSDK/src/integration_tests. The files are added to the test program in that folder's CMakeLists.txt file:

# This includes all GTests that run integration tests
add_executable(integration_tests_runner
    ../core/unittests_main.cpp
    simple_connect.cpp
    async_connect.cpp
    telemetry_simple.cpp
    ...
    gimbal.cpp
    transition_multicopter_fixedwing.cpp
    follow_me.cpp
)

Integration Test Files/Code

The main MAVSDK-specific functionality is provided by integration_test_helper.h. This provides access to the Plugin/Test Logger and a shared test class SitlTest for setting up and tearing down the PX4 simulator.

All tests running against SITL can be declared using TEST_F and have a first argument SitlTest as shown. This is required in order to use the shared class to set up and tear down the simulator between tests.

For reference inspect the existing integration tests.

Example Code

It is quicker and easier to write and modify integration tests than examples. Do not write example code until the plugin has been accepted!

A simple example should be written that demonstrates basic usage of its API by 3rd parties. The example need not cover all functionality, but should demonstrate enough that developers can see how it is used and how the example might be extended.

Where possible examples should demonstrate realistic use cases such that the code can usefully be copied and reused by external developers.

Documentation

In-Source Comments

The public API must be fully documented using the proto files.

The in-source comments will be compiled to markdown and included in the API Reference. The process is outlined in Documentation > API Reference.

Internal/implementation classes need not be documented, but should be written using expressive naming of variables and functions to help the reader. Anything unexpected or special however warrants an explanation as a comment.

Example Code Documentation

The plugin example should be documented in markdown following the same pattern as the existing examples.

Generally this involves explaining what the example does and displaying the source. The explanation of how the code works is usually deferred to guide documentation.

Guide Documentation

Ideally, guide documentation should be created. This should be based on example code.

The purpose of the guide is to:

  • Show how different parts of the API can be used together
  • Highlight usage patterns and limitations that may not be obvious from API reference
  • Provide code fragments that can easily be reused
© Dronecode 2017-2019. License: CC BY 4.0            Updated: 2020-06-05 09:10:43

results matching ""

    No results matching ""