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, thenLoaded()
- Specifically,
Init()
gets called for every.cs
plugin, thenLoaded()
gets called for every.cs
plugin, thenInit()
gets called for every.dll
plugin, thenLoaded()
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.
- Specifically,
- Most plugins seem to do the bulk of the work in
Loaded()
. Some useInit()
to setloopratehz
(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, keepingInit()
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 changeNextRun
to a specific datetime, which will override theloopratehz
(butloopratehz
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).