> ## Documentation Index
> Fetch the complete documentation index at: https://docs.altnautica.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Your first plugin

> Walk through the Battery Health Panel from manifest to packed archive, line by line.

The Battery Health Panel is the first first-party extension on the
new plugin host. It is a GCS-only plugin that reads the host's
normalized battery telemetry, runs an anomaly engine, and emits
notifications when a rule fires. Source lives at
[`altnautica/ADOSExtensions/extensions/battery-health-panel`](https://github.com/altnautica/ADOSExtensions/tree/main/extensions/battery-health-panel).

This page walks through the parts that matter for plugin authors.

## Manifest

```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
schema_version: 1
id: com.altnautica.battery-health-panel
name: "ADOS Battery Health Panel"
version: "1.0.2"
description: "Cell-level battery diagnostics, predictive time-to-min, and anomaly alerts."
author: "Altnautica"
homepage: "https://github.com/altnautica/ADOSExtensions/tree/main/extensions/battery-health-panel"
license: "GPL-3.0-or-later"
risk: low

compatibility:
  ados_version: ">=0.10.0"
  gcs_version: ">=0.5.0"
  supported_boards: ["*"]

gcs:
  entrypoint: "gcs/plugin.bundle.js"
  isolation: iframe
  permissions:
    - id: ui.slot.fc-tab
    - id: ui.slot.notification-channel
    - id: ui.slot.settings-section
    - id: telemetry.subscribe.battery
    - id: telemetry.subscribe.mavlink
    - id: recording.write
  contributes:
    panels:
      - id: battery-health-tab
        slot: fc.tab
        title: "Battery Health"
        icon: "battery"
        order: 30
    notifications:
      - id: battery-anomaly
        title: "Battery anomaly"
        severity: warning
  locales:
    - en
```

Manifest fields are flat at the root (`id`, `name`, `version`,
`compatibility`, then the `gcs` half). Permissions are a list of
`- id: <capability>` entries. Six permissions, two contributes blocks
(panels and notifications), no agent half. See the
[permissions](/developers/permissions) reference for the full set of
GCS slot ids. Risk band: `low` (no `vehicle.command`, no host file
system, no network).

## Entry point

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { definePlugin } from "@altnautica/plugin-sdk";
import { createBatteryStore } from "./batteryStore";
import type { BatterySample } from "./types";

const store = createBatteryStore();

definePlugin({
  id: "com.altnautica.battery-health-panel",
  version: "1.0.0",
  async mount(ctx) {
    await ctx.telemetry.subscribe<BatterySample>(
      "battery",
      (sample) => store.ingest(sample),
    );
    store.onAnomaly(async (fired) => {
      for (const event of fired) {
        await ctx.notifications.publish({
          channelId: "battery-anomaly",
          severity: event.severity,
          title: event.title,
          body: event.body,
        });
        await ctx.recording.mark({ label: event.title });
      }
    });
  },
});
```

`definePlugin` is the only SDK call. Everything else is plain
TypeScript: a store, a rule engine, a render loop. The SDK gets out
of the way.

## Anomaly rule

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
const ruleCellLow: Rule = ({ curr, config }) => {
  const min = minDefined(curr.cellVoltagesV);
  if (min === null || min >= config.lowCellVoltageV) return null;
  if (min < config.criticalCellVoltageV) return null;
  return mk("cell_low", curr, "warning", "Cell low", `${min.toFixed(2)} V`);
};
```

A rule is a pure function from `(prev, curr, config)` to
`AnomalyEvent | null`. The store calls every rule on every new
sample. Hysteresis is the store's job: an anomaly stays in the live
list until the underlying condition has been clear for at least 5
seconds.

## Testing without a host

The SDK ships a synthetic-host harness:

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { createPluginHarness } from "@altnautica/plugin-sdk/harness";

const harness = createPluginHarness({
  grantedCapabilities: [
    "telemetry.subscribe.battery",
    "ui.slot.notification-channel",
    "recording.write",
  ],
  mount: async (ctx) => {
    await ctx.telemetry.subscribe("battery", (s) => store.ingest(s));
    store.onAnomaly(async (fired) => {
      for (const e of fired) {
        await ctx.notifications.publish({ ... });
      }
    });
  },
});

await harness.start();
harness.pushTelemetry("battery", { cellVoltagesV: [3.9, 3.4, 3.9, 3.9], ... });
expect(harness.notifications).toHaveLength(1);
expect(harness.notifications[0]).toMatchObject({ severity: "warning" });
await harness.teardown();
```

The harness lets a single Vitest run validate the whole plugin
without spinning up Mission Control or a real drone.

## Packing and installing

```sh theme={"theme":{"light":"github-light","dark":"github-dark"}}
pnpm test
scripts/pack.sh battery-health-panel
```

`pack.sh` builds the GCS bundle (esbuild into `plugin.bundle.js`),
computes SHA-256 hashes for every asset, and zips the result into
`extensions/battery-health-panel/dist/<id>-<version>.adosplug`. The
version comes from the manifest, so the archive is named
`com.altnautica.battery-health-panel-1.0.2.adosplug`.

Signing is a separate step. With a publisher Ed25519 key in
`ADOS_SIGNING_KEY`, sign the packed archive:

```sh theme={"theme":{"light":"github-light","dark":"github-dark"}}
ADOS_SIGNING_KEY=/path/to/key.ed25519 \
  scripts/sign.sh dist/com.altnautica.battery-health-panel-1.0.2.adosplug
```

That writes a `*.signed.adosplug` carrying a `SIGNATURE` file (signer
id plus the base64 Ed25519 signature over the canonical payload hash).
The signer id defaults to `altnautica-2026-A`; override it with
`ADOS_SIGNING_KEY_ID`.

Drag the archive into **Mission Control -> Settings -> Plugins ->
Install plugin**. Approve the six declared permissions. The panel
mounts under the FC tab.

## What is next

* See [permissions](/developers/permissions) to plan the smallest
  surface a real plugin needs.
* See [event hooks](/developers/event-hooks) to learn the host RPC
  catalog.
* See [distribution and local install](/developers/distribution-local-install)
  to publish a signed release on GitHub.
