> ## 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.

# Contributing Guide

> How to set up the dev environment, add features, and submit pull requests.

# Contributing Guide

ADOS Mission Control and ADOS Drone Agent are open-source under GPLv3. Contributions are welcome. This guide covers the dev setup for both repos, the patterns for adding common feature types, and the pull request process.

## Dev environment setup

### Mission Control (GCS)

<Steps>
  <Step title="Clone the repo">
    ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
    git clone https://github.com/altnautica/ADOSMissionControl.git
    cd ADOSMissionControl
    ```
  </Step>

  <Step title="Install dependencies">
    ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
    npm install
    ```

    Requires Node.js 20+ and npm 10+.
  </Step>

  <Step title="Start the dev server">
    ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
    npm run dev
    ```

    Opens at `http://localhost:4000`. Turbopack provides fast hot module replacement.
  </Step>

  <Step title="Launch demo mode">
    Open the app in your browser. The welcome modal offers a "Try Demo" button that starts 7 simulated drones. No hardware needed.
  </Step>
</Steps>

### SITL testing

To test with a real ArduPilot simulator:

<Steps>
  <Step title="Build ArduPilot from source">
    Follow the [ArduPilot build docs](https://ardupilot.org/dev/docs/building-setup-linux.html). The SITL tool expects the ArduPilot repo at `~/.ardupilot`.
  </Step>

  <Step title="Launch SITL">
    ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
    cd tools/sitl
    npm install
    npm start
    ```

    This starts ArduPilot SITL with full physics simulation and a TCP-to-WebSocket bridge. Mission Control connects to `ws://localhost:5760`.
  </Step>

  <Step title="Connect from Mission Control">
    In Mission Control, click Connect > WebSocket and enter `ws://localhost:5760`. You now have a real autopilot with simulated GPS, IMU, and battery.
  </Step>
</Steps>

The agent is a Rust and Python hybrid. The long-running services are Rust binaries in `crates/`; Python carries AI and vision, the plugin runtime, setup, HAL detection, and the residual web API. A full dev setup needs both a Rust toolchain and Python.

<Steps>
  <Step title="Clone the repo">
    ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
    git clone https://github.com/altnautica/ADOSDroneAgent.git
    cd ADOSDroneAgent
    ```
  </Step>

  <Step title="Create a Python virtual environment">
    ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
    python3 -m venv venv
    source venv/bin/activate
    pip install -e ".[dev]"
    ```

    Requires Python 3.11+. (`uv` works too if you prefer it.)
  </Step>

  <Step title="Build the Rust services">
    ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
    cd crates
    cargo build
    cargo test
    ```

    Requires a recent stable Rust toolchain. The Cargo workspace lives in `crates/`.
  </Step>

  <Step title="Run the CLI">
    ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
    ados --help
    ados status
    ```
  </Step>

  <Step title="Run the terminal status page">
    ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
    ados
    ```

    The read-only terminal page shows setup URLs and agent status over SSH.
  </Step>
</Steps>

## Adding a configure panel (Mission Control)

Configure panels are the most common contribution. Each panel lets the user adjust a group of flight controller parameters.

<Steps>
  <Step title="Create the component">
    Add a new file in `src/components/configure/`:

    ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
    // src/components/configure/MyNewPanel.tsx
    import { usePanelParams } from "@/hooks/use-panel-params"

    const PARAMS = [
      "MY_PARAM_1",
      "MY_PARAM_2",
      "MY_PARAM_3",
    ]

    export function MyNewPanel() {
      const { values, setParam, isLoading } = usePanelParams(PARAMS)

      if (isLoading) return <PanelSkeleton />

      return (
        <div>
          <h3>My New Panel</h3>
          <NumberInput
            label="Parameter 1"
            value={values.MY_PARAM_1}
            onChange={(v) => setParam("MY_PARAM_1", v)}
          />
          {/* ... more inputs */}
        </div>
      )
    }
    ```
  </Step>

  <Step title="Register the panel">
    Add the panel to the navigation in `src/components/configure/DroneConfigureTab.tsx`:

    ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
    {
      id: "my-new-panel",
      label: "My New Panel",
      icon: MyIcon,
      component: lazy(() => import("./MyNewPanel")),
      requiredCapability: "supportsParams",
    }
    ```
  </Step>

  <Step title="Test with SITL">
    Launch SITL, connect, and verify the panel loads, reads parameters, and writes them back.
  </Step>
</Steps>

The `usePanelParams` hook handles all protocol details. It works with MAVLink native parameters (ArduPilot, PX4) and MSP virtual parameters (Betaflight) without any changes to your panel code.

## Adding a MAVLink decoder (Mission Control)

When you need to handle a new MAVLink message type:

<Steps>
  <Step title="Add constants">
    In `src/lib/protocol/mavlink-crc-extra.ts`, add the message ID, CRC\_EXTRA, and payload length:

    ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
    export const MSG_MY_NEW_MSG = 999
    export const CRC_EXTRA: Record<number, number> = {
      // ... existing entries
      [MSG_MY_NEW_MSG]: 0xAB,  // from MAVLink XML definition
    }
    export const PAYLOAD_LENGTHS: Record<number, number> = {
      // ... existing entries
      [MSG_MY_NEW_MSG]: 24,
    }
    ```
  </Step>

  <Step title="Add the decoder">
    In `src/lib/protocol/mavlink-adapter.ts`, add a case to `handleMessage()`:

    ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
    case MSG_MY_NEW_MSG: {
      const field1 = view.getFloat32(0, true)
      const field2 = view.getUint16(4, true)
      this._onMyNewMsg.forEach((cb) => cb({ field1, field2 }))
      break
    }
    ```
  </Step>

  <Step title="Add the callback to DroneProtocol">
    In `src/lib/protocol/drone-protocol.ts`:

    ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
    onMyNewMsg(cb: (msg: MyNewMsg) => void): Unsubscribe
    ```
  </Step>

  <Step title="Subscribe in DroneManager">
    In `src/stores/drone-manager.ts` inside `bridgeTelemetry()`:

    ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
    protocol.onMyNewMsg?.((msg) => {
      myStore.update(msg)
    })
    ```
  </Step>
</Steps>

## Adding a Zustand store (Mission Control)

<Steps>
  <Step title="Create the store">
    ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
    // src/stores/my-new-store.ts
    import { create } from "zustand"

    interface MyNewState {
      value: number
      setValue: (v: number) => void
    }

    export const useMyNewStore = create<MyNewState>((set) => ({
      value: 0,
      setValue: (v) => set({ value: v }),
    }))
    ```
  </Step>

  <Step title="Use selectors in components">
    ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
    const value = useMyNewStore((s) => s.value)
    ```

    Always select specific fields. Never destructure the entire store.
  </Step>
</Steps>

If the store needs to persist across page reloads, use the `persist` middleware with a version number:

```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { persist } from "zustand/middleware"

export const useMyNewStore = create<MyNewState>()(
  persist(
    (set) => ({
      value: 0,
      setValue: (v) => set({ value: v }),
    }),
    {
      name: "my-new-store",
      version: 1,
    }
  )
)
```

## Adding a board profile (Drone Agent)

<Steps>
  <Step title="Create the YAML profile">
    ```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
    # src/ados/hal/boards/my-board.yaml
    name: "My Board Name"
    vendor: "Board Manufacturer"
    soc: "RK3566"
    arch: "aarch64"
    model_patterns:
      - "My Board Model String"
    default_tier: 3
    uart_paths:
      - /dev/ttyS0
    gpio_pins: [5, 6, 13, 19]
    hw_video_codecs:
      - h264_enc
      - h264_dec
    video:
      csi_ports: 1
      max_encode_resolution: "1920x1080"
      encoder_api: rkmpp
    buses:
      i2c:
        - id: i2c3
    ```

    `model_patterns` are matched against `/proc/device-tree/model` and `/proc/cpuinfo` for auto-detection.
  </Step>

  <Step title="Test detection">
    On the target board, run:

    ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
    ados status --json
    ```

    This prints setup, service, board, and runtime status for inspection.
  </Step>
</Steps>

## Adding an agent service (Drone Agent)

New long-running or safety-critical services are written as Rust crates under `crates/` and run a binary from `/opt/ados/bin/`. An ancillary Python-backed service is still fine for ecosystem-bound work (AI, drivers, setup glue); the steps below show the Python case. Either way, the unit is registered in the supervisor catalog.

<Steps>
  <Step title="Create the service module">
    Add a new file in `src/ados/services/my_service/`:

    ```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
    # src/ados/services/my_service/__main__.py
    import asyncio
    import structlog

    log = structlog.get_logger()

    async def main():
        log.info("my_service.started")
        while True:
            # Service logic here
            await asyncio.sleep(1)

    if __name__ == "__main__":
        asyncio.run(main())
    ```
  </Step>

  <Step title="Create the systemd unit">
    Add `data/systemd/ados-my-service.service`:

    ```ini theme={"theme":{"light":"github-light","dark":"github-dark"}}
    [Unit]
    Description=ADOS My Service
    After=ados-supervisor.service
    PartOf=ados-supervisor.service

    [Service]
    Type=simple
    User=ados
    ExecStart=/opt/ados/venv/bin/python -m ados.services.my_service
    Restart=on-failure
    RestartSec=3
    MemoryMax=64M
    CPUQuota=25%

    [Install]
    WantedBy=ados-supervisor.service
    ```
  </Step>

  <Step title="Register in the supervisor">
    Add the unit to the `SERVICE_REGISTRY` in `crates/ados-supervisor/src/registry.rs` with its category (core, hardware, on-demand), its profile gate, and any ground-station role gate. The supervisor drives the unit through `systemctl`; it does not spawn the process itself.
  </Step>
</Steps>

## Pull request guidelines

### Before submitting

* Run `tsc --noEmit` for Mission Control (zero errors required)
* Run `python -m py_compile` on any modified Python files
* Run `cargo fmt --check`, `cargo clippy`, and `cargo test` for any Rust crate changes
* Check for em dashes in any user-facing strings (there should be none)
* Test with demo mode or SITL for Mission Control changes
* Bump the version in `src/ados/__init__.py` for agent changes

### PR format

```markdown theme={"theme":{"light":"github-light","dark":"github-dark"}}
## What

Brief description of the change.

## Why

What problem does this solve or what feature does it add.

## How to test

Steps to verify the change works.

## Screenshots

If the change affects the UI, include before/after screenshots.
```

### Branch naming

* `feature/short-description` for new features
* `fix/short-description` for bug fixes
* `docs/short-description` for documentation

### Review process

1. Open a PR against `main`
2. Automated checks run (TypeScript build, linting)
3. A maintainer reviews the code
4. Once approved, the maintainer merges

<Tip>
  For large features, open a draft PR early with a description of what you plan to build. This helps avoid wasted effort if the design needs changes.
</Tip>

## Code style

**Mission Control:** Follow the existing patterns. Zustand for state, `usePanelParams` for configure panels, selectors for subscriptions. Tailwind for styling. No CSS modules.

**Drone Agent (Python):** Use `structlog` for logging, `asyncio` for async code, Pydantic for config models. Type hints on all function signatures. `black` for formatting.

**Drone Agent (Rust):** Format with `cargo fmt`, lint with `cargo clippy`, and keep the IPC wire contracts defined in the `ados-protocol` crate as the single source of truth.

## Getting help

* [Discord](https://discord.gg/uxbvuD4d5q) for questions, architecture discussion, and PR review
* [GitHub Issues](https://github.com/altnautica/ADOSMissionControl/issues) for bug reports and feature requests with a clear use case
