Repository navigation
Guide Plugin SDK
Plugins let other people add decoders ("modes") and actions to OnAir without rebuilding it. A plugin is a shared library plus a manifest.json. OnAir
loads it, gives it IQ samples (or automation events), and draws whatever the plugin reports with one generic screen, so a plugin author writes no UI
code. The SDK is in sdk/ (header, CMake helper, three C++ examples, a Rust crate); the host is in core/src/plugin/.
- Concepts
- Writing a plugin
- manifest.json reference
- Host API reference
- Plugin functions
- Security model
- Out-of-process plugins
- In the program
- Distributing a plugin
- Licences
- Limits
| Kind | What it is | Gets |
|---|---|---|
mode |
a decoder: appears in the mode list under PLUGINS | IQ blocks of the tuned channel |
source |
like mode, meant for plugins that mostly analyse the stream |
IQ blocks |
action |
reacts to what other modes decode (like an automation rule, but in code) | automation events |
panel-lite |
only a manifest and settings, no code | nothing |
Each running plugin mode is one instance. OnAir calls create() when you open the mode and destroy() when you leave it. All calls into the
plugin come from one worker thread of its own, so a plugin needs no locking of its own for those calls. A plugin can never block the radio: OnAir
queues IQ for it (8 MiB at most) and drops blocks when it is too slow (the count is shown on the plugin's screen).
What a plugin reports goes into a bounded store that the generic Plugin screen draws:
- tables of typed rows (text, integer, float, frequency, time, flag), newest first
- status lines with a lamp (off, ok, warning, bad)
- a log of text lines
- plots of a float array: spectrum (dB against frequency), time series, or constellation (I/Q pairs)
- images (RGBA or PNG)
- map points
It can also emit automation events (plugin.<type>, usable in Automations rules), play audio, and declare settings (slider, checkbox,
combo, text) that OnAir draws and saves.
- Copy
sdk/template/(a working IQ plugin), renamemy-pluginin the folder,manifest.json,CMakeLists.txtand the id inplugin.cpp. cmake -S . -B build -DONAIR_SDK_DIR=/path/to/OnAir/sdk && cmake --build build- Copy
build/plugins/my-plugin/into OnAir's plugins folder (View > Plugins > Open folder), press Refresh, enable it, open it from the mode list.
The examples in sdk/examples/ are built with the repository (build/plugins/<id>/) and shipped next to the program as bundled examples:
-
tone-detector: finds the strongest tone in the channel (offset in Hz, level, spectrum plot). About 100 lines. -
burst-counter: counts bursts above a threshold over the noise floor, with start time, length and peak; emitsplugin.burst. Has two settings. -
log-to-file: an action plugin that appends automation events toevents.login its data folder (needs the filesystem permission).
Rust: sdk/rust/onair-plugin is a dependency-free crate with the C ABI written out by hand, a safe Host wrapper, a Plugin trait and the
export_plugin! macro; sdk/rust/examples/level-meter is a complete plugin (cargo build --release, then copy the library and manifest).
Other languages work the same way: export onair_plugin_entry with the C ABI.
{
"id": "burst-counter", // required: a-z 0-9 - _ . (starts with a letter or digit), at most 64; also the folder name
"name": "Burst counter", // required
"version": "1.0.0", // required: dotted numbers, optional suffix
"author": "OnAir contributors", // required
"license": "CC0-1.0", // required: an SPDX id or a short text
"homepage": "https://example.org", // optional, http(s) only
"description": "What it does.", // optional, at most 2000 characters
"kind": "mode", // required: mode | action | source | panel-lite
"abi_version": 1, // required: must equal the ABI OnAir speaks
"minimum_onair": "0.2.7", // optional
"library": {"macos": "x.dylib", "linux": "x.so", "windows": "x.dll"}, // file names inside the folder (no paths)
"permissions": ["filesystem"], // network | filesystem | audio-out | record | spawn-process
"modes": [ // required for mode and source (at most 16)
{"id": "bursts", "name": "Burst counter", "group": "Plugins",
"default_frequency_hz": 433920000, "bandwidth_hz": 500000, "sample_rates": [1000000, 2000000]}
],
"settings": [ // optional, at most 64
{"key": "threshold_db", "label": "Threshold (dB)", "type": "slider", "min": 3, "max": 40, "default": 10},
{"key": "on", "label": "On", "type": "checkbox", "default": true},
{"key": "kind", "label": "Kind", "type": "combo", "options": ["a", "b"], "default": 0},
{"key": "prefix", "label": "Prefix", "type": "text", "default": ""}
]
}(Comments are not valid JSON; they are only here.) The manifest is read with a bounded parser: at most 64 KiB and 32 levels deep. Anything malformed, any id or library name that could point outside the plugin's folder, an unknown permission or kind, or a different ABI version makes the plugin show up in the manager with the reason and refuses to run it.
Settings are stored per plugin id. A combo's value is the index of the chosen option. The plugin reads values with get_setting_* and is told
about changes through setting_changed().
sdk/include/onair_plugin.h is the contract; every function of OnAirHostApi takes the table's ctx first and returns 0 for success, a negative
number when refused (bad argument, limit reached, permission missing).
| Function | Purpose |
|---|---|
log(level, msg) |
diagnostics, shown in the plugin's log in the manager |
get_setting_text / _number / _flag |
current value of a declared setting (the default when unset) |
request_iq(centre_hz, sample_rate) |
ask for a centre frequency (0 = keep). The frequency is applied; the real values arrive in OnAirIqMeta
|
table_define / table_row / table_clear |
tables; rows beyond 2000 push out the oldest |
status(key, text, lamp) |
a status line (at most 64) |
result_log(line) |
a line in the results log (500 kept) |
plot(id, title, kind, data, n, x0, dx) |
up to 8 plots of up to 65536 floats each |
image(id, format, w, h, data, size) |
up to 4 images, 4 MiB, 4096 pixels per side |
map_point(id, label, lat, lon) |
up to 2000 points |
emit_event(type, n, fields) |
automation event plugin.<type> (100 a second per plugin) |
audio_out(pcm, n, rate) |
mono float PCM; needs audio-out
|
data_dir(out, cap) |
the plugin's own data folder; needs filesystem
|
Text longer than the limits is cut, non-finite numbers are replaced, control characters become spaces. Memory passed to any call is copied.
onair_plugin_entry(host_abi) returns an OnAirPluginVTable: create(host, mode_id) (return NULL to refuse), destroy, configure(centre, rate),
process_iq(iq, n, meta) (interleaved float I, Q), on_event(event), tick(now), setting_changed(key). Optional entries are NULL; caps
says which are implemented (ONAIR_CAP_IQ_SINK is required for modes, ONAIR_CAP_EVENTS for actions). Structs start with struct_size; read only
fields that fit inside it. Do not let C++ exceptions or Rust panics leave a callback.
Be clear about what this protects and what it does not.
-
Stability. A plugin runs on its own thread. A call that does not return within 2 seconds (the watchdog) switches the plugin off: OnAir stops
feeding it and stops waiting for it. A thread stuck in foreign code cannot be killed safely, so an in-process plugin that hangs is abandoned
(its thread and library stay until the program exits). A plugin that throws, or whose
createreturns NULL, is reported and switched off. The manager switches off a failed plugin and logs why. - Crashes. A crash (segmentation fault) of an in-process plugin takes the program down. While plugin code runs OnAir keeps a marker file in the plugins folder; if the marker is found at the next start, OnAir starts in safe mode: no plugin is loaded, and the plugin that was running is left disabled. View > Plugins > Try plugins again (or the Safe mode box) leaves safe mode. Plugins running in a helper process leave no marker, since they cannot take the program down.
-
Permissions. A plugin declares what it wants; the first time it is turned on, OnAir shows the list and you allow or refuse each. OnAir
hands out what the permissions control (
data_dirandaudio_outreturn an error without them). It does not sandbox the operating system for an in-process plugin: the library is native code with the program's own rights and can open files or sockets itself. Treat installing a plugin like installing a program: install only ones you trust, read what they ask for, and prefer the helper process for ones you do not fully trust. A sandboxed helper (macOS sandbox profile, Linux seccomp) is a planned hardening step, not part of this version. -
Packages.
.onairpluginfiles are zip archives. OnAir reads them with a bounded reader (64 entries, 128 MiB per file, 256 MiB in all, CRC-32 checked, no encryption, no zip64), refuses names with.., absolute paths, backslashes or drive letters, validatesmanifest.jsonand that a library for this platform is present, and unpacks into a temporary folder before replacing the plugin. Nothing is downloaded by OnAir: there is no store and no auto-update; Community plugins only opens the wiki page in your browser. - Encrypted traffic is never decoded by OnAir, and plugins should not do it either. Patented codecs are not bundled; a plugin that needs one must have the user install it, as OnAir's own voice modes do.
Tick Run in a helper process for a plugin in the manager (macOS and Linux; Windows is not supported yet). OnAir then starts
onair-plugin-host --plugin-host <library> (shipped next to the program) and talks to it over pipes: the helper reads length-prefixed messages on
fd 0 and writes results on fd 3 (its stdout and stderr are discarded). Messages carry the plugin id, mode, permissions and settings (Init), then
Configure, Iq, Event, Tick, Setting, Quit down, and Hello, Ack, results, Event, Audio, RequestIq, Log, Fatal up (core/include/dect2/plugin/results.h,
Wire). Every call is acknowledged; a call not acknowledged within the watchdog time gets the helper killed (SIGKILL), and a crash ends only the
helper. IQ is copied through the pipe (about 8 bytes per sample), so very high sample rates cost CPU; the in-process path is the default.
-
View > Plugins (and the Plugins row in the PLUGINS group of the mode list) is the manager: the installed and bundled plugins with
version, author, licence, kind and permissions; enable/disable; the first-use permission prompt; settings; each plugin's log; Install from
file... (a
.onairplugin); Open folder; Remove; Safe mode; Community plugins. - Plugin modes appear in the mode list under PLUGINS with a stable number (family 1000 and up, stored in
plugins/state.json: assigned in order of first appearance, never reused, kept when a plugin is removed and reinstalled). A plugin's settings are saved per plugin id. - Plugin modes run on receiver 1. The plugins folder is
plugins/in OnAir's data folder. The bundled examples andonair-plugin-hostare looked for next to the program (plugins/,../Resources/plugins/in an app bundle,../lib/onair/plugins/for the Linux packages, which install them; the macOS and Windows packaging scripts do not copy them yet, so there only the build folder'splugins/has them).
Zip the plugin's files (manifest.json and the library for each platform you support, plus anything else the library needs, at most four folder
levels) as <id>-<version>.onairplugin, either at the top of the zip or inside one folder. One package can hold the libraries of all three
platforms; the manifest's library object names each. Sign nothing and promise nothing about trust: users decide whom to trust. Put the source
and the licence where people can read them.
OnAir is GPL-3.0-or-later. This is not legal advice; read the licences yourself.
-
The ABI header (
sdk/include/onair_plugin.h) is dedicated to the public domain (CC0-1.0) and the examples and the Rust crate are CC0, so they can be copied into a plugin of any licence. - A plugin is a separate work when it only uses the stable C ABI of that header: it does not include OnAir's other headers, link OnAir's code or use OnAir internals. Then it is your choice of licence (including closed source), and OnAir does not make it a derivative work by loading it through the C ABI. That is the intent of the ABI.
- A plugin that is loaded into OnAir's process and uses OnAir's own headers or code (
core/include/dect2/...,app/...) is, in all likelihood, a derivative work of OnAir under GPL-3.0: distribute it under GPL-3.0-or-later (or a compatible licence) with its source. Copying OnAir source into a plugin is the same. - To keep a plugin clearly separate, use only the header in
sdk/include/, or run the plugin in the helper process. - Plugin authors keep the notices of any third-party code they include, as OnAir does (
THIRD_PARTY.md).
| What | Limit |
|---|---|
| manifest | 64 KiB, depth 32, 16 modes, 64 settings, 64 combo options |
| IQ queue per plugin | 8 MiB (in-process) |
| watchdog | 2 s per call; start-up 3.5 s |
| tables / columns / rows / cell text | 8 / 24 / 2000 / 512 bytes |
| status / log / log line | 64 / 500 / 1000 bytes |
| plots / floats | 8 / 65536 |
| images / bytes / side | 4 / 4 MiB / 4096 |
| map points | 2000 |
| events | 32 fields, 100 a second per plugin |
| install package | 64 entries, 128 MiB per file, 256 MiB total |
See also: Tutorials and guides, FAQ, Troubleshooting.
Getting started
Using OnAir
Tools and guides (0.2.7)
- Band survey
- Message database
- Watchlists
- TV recorder
- Time machine
- Pass scheduler
- Plugin SDK
- Headless server
- All guides
Radios
Modes by group
- TV
- Broadcast radio
- Aviation
- Maritime
- Satellite & navigation
- Two-way radio
- Cellular & rail
- Pagers
- IoT & sensors
- Weather & science
- Amateur: digital
- Amateur: voice & packet
- HF utility
- General & tools
Tutorials
- DVB-T2 TV with a HackRF
- FM radio with RDS
- Aircraft with ADS-B and the map
- Ships with AIS
- DAB+ digital radio
- Weather satellite images
- HF digital: FT8 and WSPR
- Digital voice: DMR, D-STAR, P25
- Pagers: POCSAG and FLEX
- Time stations and the clock
- Record IQ, then the Signal workbench
- The signal sample library
- Control a rig with Hamlib
- Two receivers and the multi-decoder
Reference
Help
Legal and privacy
More