com.example.hello-panel, the smallest useful
plugin: a GCS-only tab on a drone’s node-detail panel that subscribes to
the host’s battery telemetry and prints the pack voltage. There is no
agent half. The plugin runs inside Mission Control in a sandboxed
iframe, and the drone’s agent stores and serves it.
By the end you have a manifest the host accepts, a bundle that mounts, a
unit test that runs without Mission Control, and a signed .adosplug
installed on a drone.
Battery health (per-cell analysis, sag detection, time-to-reserve and
alerts) is built into the Drone Agent. See
Battery health. This tutorial uses the
battery topic only because every drone publishes it.
Prerequisites
- Node.js 20+ and pnpm 9+.
- bash with
rsyncandzipforscripts/pack.sh(Linux or macOS; on Windows use WSL). - Python 3.11+ with the
cryptographypackage, for signing. - Mission Control connected to a drone that runs the ADOS Drone Agent, and root access on that drone to enrol your signing key. Demo mode does not install extensions.
1. Clone and scaffold
The SDK and thecreate-ados-plugin scaffolder live in the
altnautica/ADOSExtensions
monorepo. Neither is published to npm, so develop the plugin as a
workspace member there:
@altnautica/plugin-sdk to
workspace:^, so the plugin builds against the SDK in the tree. The
GCS-only template lays out:
gcs/src/plugin.ts mounts a one-line greeting from the
locale bundle:
2. Manifest
Replace the generatedmanifest.yaml with:
- No
agentblock. A manifest needs at least one half; this one ships only the GCS half, so no process runs on the drone. The agent still checkscompatibilityand stores the bundle. tabsmounts the bundle as a tab on the node-detail panel. A tab always lives in thenode.detail.tabslot.profile: ["drone"]offers it on drones only; leaveprofileout to offer it on every node profile.ui.slot.node-detail-tablets the tab mount. Every visual contribution needs its matchingui.slot.*permission, or the slot skips it.telemetry.subscribe.batteryis the topic-scoped telemetry grant. The bridge checks it on every subscribe to thebatterytopic; the baretelemetry.subscribegrants no stream.risk: low: the plugin reads telemetry and draws a tab. It sends no commands and stores nothing.
3. The panel
Keep the logic in its own module so it can be tested without a browser. Creategcs/src/panel.ts:
ctx.telemetry.subscribe asks the host to open the battery stream for
the drone the tab is mounted on. It resolves with an unsubscribe
function once the host accepts the subscription. If the grant is
missing, the promise rejects and no handler is left behind.
Every frame on the battery topic is a BatterySample:
Values the flight controller does not report arrive as
null.
4. The entry point
Replacegcs/src/plugin.ts with:
definePlugin is the only call a plugin module must make. The host
loads the bundle inside the tab’s iframe, and the SDK calls mount with
a PluginContext wired to the host bridge. When the tab closes or the
operator switches node, Mission Control destroys the iframe, which ends
the subscription with it. See Lifecycle for the
teardown rules.
5. Test without a host
The SDK ships a synthetic host at@altnautica/plugin-sdk/harness. It
runs the plugin against an in-memory transport and lets you push
telemetry frames by hand. Create gcs/src/panel.test.ts:
harness.calls: each entry records the method and the capability.
6. Pack
From the monorepo root:pack.sh builds the GCS bundle (esbuild into gcs/plugin.bundle.js),
checks that every entrypoint the manifest declares is present, and zips
the result into
extensions/hello-panel/dist/com.example.hello-panel-0.1.0.adosplug.
The name comes from the manifest id and version.
7. Sign and enrol your key
A default agent installs only archives signed by a key it trusts. Create a key once, sign the archive, and enrol the public key on the drone:sign.sh writes com.example.hello-panel-0.1.0.signed.adosplug next to
the input, with a SIGNATURE entry: the signer id, then the base64
Ed25519 signature over the canonical payload hash. Copy
keys/my-publisher-2026-A.pem to /etc/ados/plugin-keys/ on the drone
(root). Keep the .priv.pem out of git.
Where to run keygen, how to enrol the key on a node, and the bench-only
unsigned mode are covered in
Bench install and signing.
8. Install
In Mission Control, open the drone from the fleet, switch to its Extensions tab, and choose Install from file. Drop incom.example.hello-panel-0.1.0.signed.adosplug. The review shows the
signer, the board compatibility and the two permissions; choose
Install, which reads grants 2 permissions. The Hello tab appears on the drone’s
node-detail panel and updates with every battery frame.
On the drone itself, ados plugin install does the same from a shell:
What just happened
- The agent parsed the archive in memory, verified the signature
against your enrolled key, checked
compatibility, and returned a preview. Nothing was written yet. - After you approved, the agent unpacked the archive, recorded the install, and granted the two permissions.
- Mission Control fetched the bundle from the agent, mounted an
<iframe sandbox="allow-scripts">on the Hello tab, and wired thepostMessagebridge, which checkstelemetry.subscribe.batteryon every subscribe.
Next steps
Scaffolder walkthrough
A hybrid plugin with an agent half, a flight skill and parameters.
Permissions
Plan the smallest capability set a real plugin needs.
TypeScript SDK
Everything on the GCS
PluginContext.Distribution
Ship a signed release.