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)
Clone the repo
git clone https://github.com/altnautica/ADOSMissionControl.git
cd ADOSMissionControl
Install dependencies
Requires Node.js 20+ and npm 10+. Start the dev server
Opens at http://localhost:4000. Turbopack provides fast hot module replacement. Launch demo mode
Open the app in your browser. The welcome modal offers a “Try Demo” button that starts 5 simulated drones. No hardware needed.
SITL testing
To test with a real ArduPilot simulator:
Build ArduPilot from source
Launch SITL
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:5001.Connect from Mission Control
In Mission Control, click Connect > WebSocket and enter ws://localhost:5001. You now have a real autopilot with simulated GPS, IMU, and battery.
Drone Agent
Clone the repo
git clone https://github.com/altnautica/ADOSDroneAgent.git
cd ADOSDroneAgent
Create a virtual environment
python3 -m venv venv
source venv/bin/activate
pip install -e ".[dev]"
Requires Python 3.11+.Run the TUI
The Textual-based terminal dashboard works in any terminal emulator.
Configure panels are the most common contribution. Each panel lets the user adjust a group of flight controller parameters.
Create the component
Add a new file in src/components/configure/:// 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>
)
}
Register the panel
Add the panel to the navigation in src/components/configure/DroneConfigureTab.tsx:{
id: "my-new-panel",
label: "My New Panel",
icon: MyIcon,
component: lazy(() => import("./MyNewPanel")),
requiredCapability: "supportsParams",
}
Test with SITL
Launch SITL, connect, and verify the panel loads, reads parameters, and writes them back.
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:
Add constants
In src/lib/protocol/mavlink-constants.ts, add the message ID, CRC_EXTRA, and payload length: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,
}
Add the decoder
In src/lib/protocol/mavlink-adapter.ts, add a case to handleMessage():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
}
Add the callback to DroneProtocol
In src/lib/protocol/drone-protocol.ts:onMyNewMsg(cb: (msg: MyNewMsg) => void): Unsubscribe
Subscribe in DroneManager
In src/stores/drone-manager.ts inside bridgeTelemetry():protocol.onMyNewMsg?.((msg) => {
myStore.update(msg)
})
Adding a Zustand store (Mission Control)
Create the store
// 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 }),
}))
Use selectors in components
const value = useMyNewStore((s) => s.value)
Always select specific fields. Never destructure the entire store.
If the store needs to persist across page reloads, use the persist middleware with a version number:
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)
Create the YAML profile
# src/ados/hal/boards/my-board.yaml
vendor: "Board Manufacturer"
name: "My Board Name"
soc: "RK3566"
ram_mb: 2048
tier: 3
uart:
- path: /dev/ttyS0
baud: 921600
purpose: mavlink
i2c:
- bus: 3
purpose: oled
gpio:
buttons: [5, 6, 13, 19]
usb:
ports: 4
video:
encode: "rkmpp"
hdmi:
available: true
usb_gadget:
available: false
Test detection
On the target board, run:This prints the detected board, matched profile, and any fallback to defaults.
Adding an agent service (Drone Agent)
Create the service module
Add a new file in src/ados/services/my_service/:# 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())
Create the systemd unit
Add data/systemd/ados-my-service.service:[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
Register in the supervisor
Add the service to the SERVICE_REGISTRY in src/ados/core/supervisor.py with its profile gate and hardware dependencies.
Pull request guidelines
Before submitting
- Run
tsc --noEmit for Mission Control (zero errors required)
- Run
python -m py_compile on any modified Python files
- Check for em dashes in any user-facing strings (there should be none)
- Make sure new files do not reference any partner company names or internal decision IDs
- Test with demo mode or SITL for Mission Control changes
- Bump the version in
src/ados/__init__.py for agent changes
## 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
- Open a PR against
main
- Automated checks run (TypeScript build, linting)
- A maintainer reviews the code
- Once approved, the maintainer merges
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.
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: Use structlog for logging, asyncio for async code, Pydantic for config models. Type hints on all function signatures. black for formatting.
Getting help