Writing a Mission Planner Plugin

This page covers the anatomy of a Mission Planner plugin source code, and some basic things you can do with one.

There are two types of plugins that you can create: source-code .cs plugins, which get compiled in the background by Mission Planner every time it launches; and compiled .dll plugins, which are compiled into a dll by Visual Studio. .cs plugins are good for simple plugins that you want to be easily adjustable, much like lua scripting for ArduPilot. .dll plugins are good for more complicated plugins, where you want to have more than one source file, or where it would take too long for Mission Planner to compile at runtime.

To add a .cs plugin, simply write your code in any text editor, save it as a .cs file, and place it in the plugins folder for your Mission Planner installation (C:\Program Files (x86)\Mission Planner on Windows).

In my next post, I’ll add instructions on how to create a project for a .dll plugin.

Minimal Code

The following is an example of the minimum required variable/function definitions. This is a blank plugin that does nothing.

using MissionPlanner.Plugin;
namespace Carbonix
{
    public class CarbonixPlugin : Plugin
    {
        public override string Name { get; } = "Carbonix Addons";
        public override string Version { get; } = "0.1";
        public override string Author { get; } = "Bob Long";
        public override bool Init() { return true; }
        public override bool Loaded()
        {
            return true;
        }
        public override bool Exit() { return true; }
    }
}

The Name, Version, and Author fields are self-explanatory.

Init() and Loaded() each run once at startup, and Exit() is called once when the PluginLoader’s thread ends. What’s the difference between Init() and Loaded()? Good question. I see no clear rationale for the distinction — one is called by InitPlugin, the other by PluginInit. Here are my observations and recommendations:

  • Init() gets called first, then Loaded()
    • Specifically, Init() gets called for every .cs plugin, then Loaded() gets called for every .cs plugin, then Init() gets called for every .dll plugin, then Loaded() gets called for every .dll plugin. If your plugin depends on another (which you probably shouldn’t do), this order could be useful to know, but I doubt it.
  • Most plugins seem to do the bulk of the work in Loaded(). Some use Init() to set loopratehz (more on that later) and/or use it as a simple way to disable the plugin by having it return false. I follow this convention in my plugins, keeping Init() to a couple basic lines max. Not all plugins follow this convention, and ultimately it does not matter which function you use primarily.

Periodic Work

Most plugins work by adding or modifying the UI in Loaded() and defining event handlers that get assigned to the new UI. Sometimes you want to have code that runs periodically. There are a few ways to do this. Here are four simple examples of how to periodically write to the console.

MissionPlanner.Plugin.Plugin.Loop()

The easiest and safest way to run something periodically is to define the optional Loop() function in your plugin and set loopratehz in Init() or Loaded(). This loop function will be called periodically by a background thread shared by all plugins. There are some things to know:

  • The fastest you are allowed to run is 1Hz. Anything faster will get overridden to 1Hz.
  • Within the Loop() function itself, you can change NextRun to a specific datetime, which will override the loopratehz (but loopratehz still needs to be non-zero for this to work). You are still prevented from setting this faster than 1s in the future.
  • Any unhandled exceptions get caught and logged, instead of crashing the main program.
  • If your Loop() function hangs, or takes longer than expected, other plugins using this thread will hang until your plugin finishes, but the main program will not be impacted.
public override bool Loaded()
{
    loopratehz = 1;
    return true;
}
public override bool Loop()
{
    Console.WriteLine("Hello from Carbonix!");
    return true;
}

System.Threading.Thread

You can create a thread that infinitely loops and use a sleep command in it to control the rate that it runs. This minimizes the risk of freezing main program or any other plugins, but an unhandled exception can still crash the main program. This is the best method if you need faster loops than 1 Hz.

public override bool Loaded()
{
    // Create and launch a thread that periodically prints a message.
    Thread thread = new Thread(new ThreadStart(
        delegate ()
        {
            while (true)
            {
                Console.WriteLine("Hello from Carbonix!");
                Thread.Sleep(1000);
            }
        }
        ));
    thread.Start();
    return true;
}

The above is the bare minimum working example, but you should follow best practices and clean up your thread on exit.

private Thread _thread;
private volatile bool _stop;

public override bool Loaded()
{
    _stop = false;
    _thread = new Thread(() =>
    {
        while (!_stop)
        {
            Console.WriteLine("Hello from Carbonix!");
            Thread.Sleep(1000);
        }
    });
    _thread.Start();
    return true;
}

public override bool Exit()
{
    _stop = true;
    if (_thread != null && _thread.IsAlive)
    {
        _thread.Join();
    }
    return true;
}

System.Windows.Forms.Timer

This is the best method if you need to periodically modify something in UI (like enabling/disabling a button on certain conditions, or printing text to a box or label). You can still do that with the other loop types, but you need to use Invoke since you are working across different threads. System.Windows.Forms.Timer runs on the main UI thread, which eliminates a lot of those cross-thread issues.

You need to be careful with these though. If your handler takes longer than expected, or if it freezes, the entire Mission Planner UI will freeze. For example:

public override bool Loaded()
{
    var timer = new System.Windows.Forms.Timer();
    timer.Tick += (object sender, EventArgs e) =>
    {
        Console.WriteLine("Hello from Carbonix!");
        while(true) // This infinite-loop bug will freeze the whole UI!!!
        {
            ;
        }
    };
    timer.Interval = 1000;
    timer.Enabled = true;
    return true;
}

Here’s the best-practices example:

private System.Windows.Forms.Timer _timer;

public override bool Loaded()
{
    _timer = new System.Windows.Forms.Timer();
    _timer.Tick += (object sender, EventArgs e) =>
    {
        Console.WriteLine("Hello from Carbonix!");
    };
    _timer.Interval = 1000;
    _timer.Start();
    return true;
}

public override bool Exit()
{
    _timer?.Stop();
    _timer?.Dispose();
    return true;
}

System.Threading.Timer

I don’t recommend this one at all.

You could create a timer (System.Threading.Timer) in the plugin class and an event handler for it. The reason this method is dangerous is that the handler gets run on a new thread each time it is called, which can lead to all sorts of hard-to-debug issues if you are not careful. For example, if the handler is taking longer than expected, or if it freezes, new instances of it will continue to be launched. You can try it out yourself with this example:

public override bool Loaded()
{
    var timer = new System.Threading.Timer(new TimerCallback((object state) =>
    {
        // Print console message
        Console.WriteLine("Hello from Carbonix!");
        // This task will never end, and we will keep spawning new ones until Mission Planner crashes
        while(true)
        {
            ;
        }
    }), null, 0, 1000);
    return true;
}

I’ll skip the best-practices example for this one, as I really think you should avoid it. Use any of the previous methods instead.

Doing Something Useful

Now that you understand the basic structure of a plugin, you’ll probably want to do something more useful than print a static message to the console. The following example is the simplest thing I could think of that actually does something with the UI.

public override bool Loaded()
{
    // Add button under the HUD that launches a pop-up
    var button = new MissionPlanner.Controls.MyButton();
    button.Text = "Press Me";
    button.Click += (sender, e) =>
    {
        CustomMessageBox.Show("Hello from Carbonix!");
    };
    Host.MainForm.FlightData.panel_persistent.Controls.Add(button);
    return true;
}

Notice my use of Host.MainForm instead of MainV2 . There’s no real difference between these, but I try to keep a habit of using the included PluginHost Host member wherever possible. For the most part, these are just nice shortcuts, like Host.cs instead of MainV2.comPort.MAV.cs and Host.FPMenuMapPosition to get the Lat/Lon of where the user last right-clicked on the map on the FlightPlanner page. These also help protect your plugins from future changes in Mission Planner, where something may get renamed or relocated. Look through all the members of PluginHost in the source code to see what else is in there

There are several more examples in Mission Planner’s repo:
MissionPlanner/Plugins at master · ArduPilot/MissionPlanner
MissionPlanner/plugins at master · ArduPilot/MissionPlanner

(yes, thanks Windows folder case insensitivity, these are split into two separate folders in git; when you clone the repo, they show up as the same folder though, so it’s all good).

2 Likes

This next post covers how to create a new blank plugin project for compiling a plugin as a dll.

(edit: @Yuri_Rage pointed me to EosBandi/PluginSkeleton, which is another good resource on this subject. You should check that out too)

Instructions

  1. Add a new project in the Plugins folder.
    a. Create a WinForms project.
    b. Move the project location within the plugins folder of the repository and give it a name.
    c. .NET version selection does not matter (we will override the project file in the next step)
    d. Double click the project to open its .csproj file.
    CarbonixNewPlugin
  2. Replace the contents of the .csproj project file with this: BlankProject.zip (640 Bytes). I’ll break that down in a later section.
  3. Create the base boilerplate code.
    a. Delete the existing code files.

    b. Add a new Class item to your project for the _plugin class definition, name it something like Carbonix_Plugin.cs.

    c. Edit the file to look like the example plugins we discussed in the first post:
using MissionPlanner.Plugin;
namespace Carbonix
{
    public class CarbonixPlugin : Plugin
    {
        public override string Name { get; } = "Carbonix Addons";
        public override string Version { get; } = "0.1";
        public override string Author { get; } = "Carbonix";
        public override bool Init() { return true; }
        public override bool Loaded()
        {
            return true;
        }
        public override bool Exit() { return true; }
    }
}
  1. Create a launch profile for the plugin. This makes it so the plugin compiles automatically when launching the debugger (when launching the Mission Planner project, only the dependent projects get built).
    a. Open “Configure Startup Projects”.

    b. Select your plugin and hit “OK”.

    c. Select “Debug Properties” for your plugin.

    d. Create a new executable profile that executes ..\MissionPlanner.exe
    CarbonixLaunchProfile

Project File Modification Breakdown

Earlier, I had you copy in that blank csproj file. I’ll walk you through what those changes do:

The first section sets the .NET target and enables WinForms components to added through the new-item wizard:

  <PropertyGroup>
    <TargetFramework>net472</TargetFramework>
    <UseWindowsForms>true</UseWindowsForms>
    <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
  </PropertyGroup>

Next we suppress some nuisance errors/warnings and sets the output location for the build dll:

  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
    <NoWarn>1701;1702;NU1605</NoWarn>
    <DebugType>portable</DebugType>
    <outputPath>..\..\bin\Debug\net461\plugins\</outputPath>
  </PropertyGroup>

Finally, we add some dependencies to other projects in the solution. These three are a good starting point and cover the majority of plugins.

  <ItemGroup>
    <ProjectReference Include="..\..\ExtLibs\MissionPlanner.Drawing\MissionPlanner.Drawing.csproj">
      <Aliases>Drawing</Aliases>
      <Private>false</Private>
    </ProjectReference>
    <ProjectReference Include="..\..\ExtLibs\Utilities\MissionPlanner.Utilities.csproj">
      <Private>false</Private>
    </ProjectReference>
    <ProjectReference Include="..\..\MissionPlanner.csproj">
      <Private>false</Private>
    </ProjectReference>
  </ItemGroup>

If you need to add more dependencies later, don’t worry about doing it in this file, you can use the Solution Explorer and Properties panes instead. When you do this, there are two important settings to get right:

  1. MissionPlanner.Drawing needs the Alias Drawing, or you will get build errors complaining about the ambiguity between System.Drawing and MissionPlanner.Drawing
  2. “Copy Local | No” (or, in the csproj file: <Private>false</Private>). This stops Visual Studio from making unnecessary copies of dll files for each of these references and dumping them into the plugins folder. Don’t forget to change this setting every time you add a new reference (whether it’s a project reference or an assembly reference).

Next Steps

Now, you should be able to build/launch your plugin, and you should be able to set breakpoints to debug it. You can now create new forms/controls/whatever as separate files in this project and use them within your plugin. The OpenDroneID plugin is an example of this.

3 Likes