Skip to main content
These recipes show where code goes for the most common changes. Setting up a development environment, branch names and pull request rules are on Contributing. Directory layouts are on Project structure. To add a board, follow HAL and tiers.

Test against ArduPilot SITL

Most Mission Control recipes end with a test against a simulated autopilot. The SITL tool in tools/sitl/ builds ArduPilot and bridges it to a WebSocket:
The bridge binds the IPv6 loopback and accepts only clients that present a per-run token. Copy the ws://[::1]:5760/?token=... URL it prints. In Mission Control, open the connect dialog, choose Flight Controller, then WebSocket, pick the SITL #1 preset and paste the full URL, token included. The token changes on every start.
SITL accepts arm, takeoff and mission commands like a real autopilot. Before you point the same code at hardware, read Safety.

Add a flight controller panel (Mission Control)

Configuration panels live under src/components/fc/<area>/. Every panel reads and writes parameters through usePanelParams, which works the same for MAVLink parameters and MSP virtual parameters.
1

Create the component

src/components/fc/safety/FailsafePanel.tsx is a complete example, including the unsaved-changes guard.
2

List it in the panel navigation

Add an entry to FC_NAV_ITEMS in src/components/drone-detail/fc-nav-items.tsx:
requiredCapability hides the panel on firmwares that lack the capability. vehicleClasses, requiredVehicleType and excludeFirmware narrow it further.
3

Route the id to the component

In src/components/drone-detail/FcPanelRouter.tsx, add a dynamic() import for the panel and a case "my-new-panel": that returns it.
4

Add the label

Add "myNewPanel" under the fcNav namespace in locales/en.json. English is the source locale.
5

Test with SITL

Connect to SITL, open the panel, and check that it loads, edits and writes the parameters.
1

Register the message's CRC and length

In src/lib/protocol/mavlink-crc-extra.ts, add the message id to both maps:
Without a CRC_EXTRA entry the parser cannot validate the frame.
2

Write the decoder

Add a typed decode* function to the matching file in src/lib/protocol/messages/:
3

Add the callback

Add a callback type in src/lib/protocol/types/callbacks.ts, a myNewMsgCallbacks array to CallbackStore in src/lib/protocol/mavlink-adapter-callbacks.ts, and an onMyNewMsg entry in bindCallbackMethods. Declare onMyNewMsg?(callback: MyNewMsgCallback): () => void; on DroneProtocol in src/lib/protocol/types/protocol.ts. Making it optional means the MSP and mock adapters do not have to implement it.
4

Handle and route the frame

Add a handler to the matching file in src/lib/protocol/handlers/ that decodes the payload and calls each callback, then add a case to routeFrame() in src/lib/protocol/mavlink-adapter-frame-handlers.ts:
5

Bridge it into a store

Subscribe in bridgeTelemetry() in src/stores/drone-manager-bridge.ts and push the data into a store. Add the returned unsubscribe function to the array the bridge returns.

Add a Zustand store (Mission Control)

1

Create the store

2

Select specific fields

Never subscribe to the whole store.
If the store must survive a reload, wrap it in persist with a storage name, a version and a migration:
Bump version and branch on the old version in migrate whenever the stored shape changes. If the store is fed by live telemetry, do not call set() per message. Mutate the data in place and notify through a coalesced version bumper, as telemetry-store does:
A store with a bumper must call bumper.cancelVersionBump() from its clear() action.

Add an agent service (Drone Agent)

New long-running services are Rust binaries in the crates/ workspace, installed to /opt/ados/bin/. The supervisor runs every unit through systemctl; it never spawns processes itself. The design is explained on Agent services.
1

Create the crate

Add crates/ados-my-service/ with a binary target and add it to the workspace members in crates/Cargo.toml. Use ados-protocol for any socket you read or write, so framing and contract versions stay in one place.
2

Write the unit file

Add data/systemd/ados-my-service.service, following the existing units:
Set MemoryMax, CPUQuota and ReadWritePaths to what the service really needs.
3

Register it in the unit table

Add a UnitSpec row to UNITS in crates/ados-protocol/src/units.rs. The supervisor’s catalog, the installer’s enable, start, teardown and health lists, the REST restart allowlist and the armed interlock’s link-dropping class are all derived from that table, and a test fails if a unit file in data/systemd/ has no row (or a row has no unit file):
Set idle_ok: true if the service exits 0 on purpose when the node has nothing for it to do, and drops_link: true if restarting it drops a command, telemetry, video, RC, mesh, access-point or cloud link.
4

Ship the binary

Add the binary to the installer’s prebuilt catalog in crates/ados-installer/src/binaries.rs for the profiles that run it. CI publishes every service binary as an asset of the per-commit rev-<sha> release, and the installer fetches the assets for the active profile from there.

Next steps

Contributing

Development setup, branches and pull requests.

Project structure

Where every module lives.

HAL and tiers

Add a board profile.
Last modified on October 11, 2026