Complete step-by-step guide for initializing a Linux-based companion computer for the MDS drone swarm platform.
The mds_node_init.sh script is an enterprise-grade bootstrap system that configures a fresh companion-computer node for use in MDS drone swarm operations. It handles:
- System prerequisites and validation
- Repository cloning with SSH/HTTPS support (custom repo selection included)
- Hardware identity configuration
- Python virtual environment setup
- MAVSDK binary installation
- Systemd service installation
- Firewall configuration with SSH port detection
- NTP time synchronization
- Candidate announce to the GCS enrollment registry when the API is reachable
- Optional: NetBird VPN (official or self-hosted), Static IP, Smart Wi-Fi Manager
- Raspberry Pi 4 or 5 (4GB+ RAM recommended)
- Other Debian-family companion computers are also supported when they can host the same services and MAVLink routing stack
- 16GB+ SD card (32GB recommended)
- Stable power supply (5V 3A minimum)
- Network connection (Ethernet or WiFi)
- Raspberry Pi OS or another Debian-family Linux distribution (64-bit recommended)
- Python 3.11 or later (included in latest Raspberry Pi OS)
- Internet connectivity for package downloads
Use this on a fresh companion-computer host that does not already have the MDS repo cloned locally. This is the public bootstrap wrapper.
The fastest way to set up a fresh companion-computer node:
curl -fsSL https://raw.githubusercontent.com/alireza787b/mavsdk_drone_show/main/tools/install_companion.sh | sudo bashBy default the wrapper creates the droneshow runtime user and clones the repo
into /home/droneshow/<repo-dir>. Override those wrapper-level defaults with
MDS_USER and MDS_INSTALL_DIR if you need a different runtime identity or
install location:
curl -fsSL https://raw.githubusercontent.com/alireza787b/mavsdk_drone_show/main/tools/install_companion.sh | \
sudo env MDS_USER=companion MDS_INSTALL_DIR=/srv/customer-mds bash -s -- -d 1 -yWhen you run the wrapper from a local checkout instead of the raw GitHub URL, it
also loads repo defaults from deployment/defaults.env before applying any
environment overrides.
With drone ID (non-interactive):
curl -fsSL https://raw.githubusercontent.com/alireza787b/mavsdk_drone_show/main/tools/install_companion.sh | sudo bash -s -- -d 1 -yUsing your own fork or org repo:
curl -fsSL https://raw.githubusercontent.com/alireza787b/mavsdk_drone_show/main/tools/install_companion.sh | sudo bash -s -- --fork yourusername -d 1 -yFor confidentiality-sensitive customers, prefer an org-owned private repo instead of assuming a normal GitHub fork will be private.
Using a customer org/private repo path:
curl -fsSL https://raw.githubusercontent.com/alireza787b/mavsdk_drone_show/main/tools/install_companion.sh | sudo bash -s -- --fork yourorg/customer-mds --branch customer-demo -d 1 -yFor a first-time private SSH bootstrap, omit -y unless the deploy key is already authorized on GitHub. Non-interactive -y is safe only after that prerequisite is already satisfied.
Using an explicit repository URL:
curl -fsSL https://raw.githubusercontent.com/alireza787b/mavsdk_drone_show/main/tools/install_companion.sh | sudo bash -s -- --repo-url git@github.com:yourorg/customer-mds.git --branch customer-demo -d 1 -yUsing a private HTTPS repository with a read-only token file:
install -m 600 /dev/null /home/<mds-user>/.mds_git_read_token
printf '%s' 'YOUR_READ_ONLY_GITHUB_TOKEN' > /home/<mds-user>/.mds_git_read_token
curl -fsSL https://raw.githubusercontent.com/alireza787b/mavsdk_drone_show/main/tools/install_companion.sh | \
sudo bash -s -- \
--repo-url https://github.com/yourorg/customer-mds.git \
--branch customer-demo \
--git-auth-token-file /home/<mds-user>/.mds_git_read_token \
-d 1 -yUsing a private SSH repository with an existing read-only key:
curl -fsSL https://raw.githubusercontent.com/alireza787b/mavsdk_drone_show/main/tools/install_companion.sh | \
sudo bash -s -- \
--repo-url git@github.com:yourorg/customer-mds.git \
--branch customer-demo \
--git-ssh-key-file /home/<mds-user>/.ssh/customer_git_read_key \
-d 1 -yinstall_companion.sh is the public entrypoint for fresh hardware.
install_mds_node.sh remains an equivalent packaged entrypoint for automation
that already references the node-specific filename.
The normal companion-node model has two identities:
- a human/admin SSH user, used to log into the board and run
sudo; and - the
droneshowservice user, which owns the MDS checkout, venv, and services.
Use the human/admin account for SSH, then run service-user commands explicitly:
ssh <human-user>@<node-ip>
sudo -u droneshow -H bash -lc 'cd ~/mavsdk_drone_show && git status --short --branch'That sudo -u droneshow step uses the human/admin account's sudo permission.
It does not require or check a droneshow password. Depending on the site's
sudo policy, it may ask for the human/admin user's password; if that account has
passwordless sudo, it will not prompt. For automation, test with sudo -n -u droneshow ...; sudo -n fails instead of prompting, which makes credential
gaps visible.
Do not create a shared password for droneshow as a normal fleet practice. Keep
it locked or key-only. If a deployment needs direct service-user SSH, install a
named authorized key for that deployment and record it in the site handoff.
Use this when the repo is already present on the node, or when you are repairing, resuming, or deliberately re-running configuration on an existing node.
| Scenario | Recommended entrypoint |
|---|---|
| Fresh OS image, no repo cloned yet | install_companion.sh (install_mds_node.sh also works) |
| Repo already cloned on the node | mds_node_init.sh |
| Resume or repair an interrupted init on the same node | mds_node_init.sh --resume |
| Provisioned node could not reach GCS during bootstrap | mds_node_announce.sh |
| Replace / recover / reassign a drone in the fleet manifest | Use Fleet Enrollment after announce; do not rerun full bootstrap unless the node itself needs reprovisioning |
- Prepare a supported Debian-family image for your companion computer
- If you are using Raspberry Pi, Raspberry Pi Imager is the recommended path
- Configure network access and SSH
- Boot the companion computer
SSH into the companion computer:
ssh pi@raspberrypi.localUpdate the system:
sudo apt update && sudo apt upgrade -yOnly do a manual clone if the repo is already present on the node or you are deliberately repairing an existing checkout. Fresh hosts should start from the official wrapper so first-time private deploy-key authorization happens before the target repo clone.
Manual clone example for an already-provisioned host:
git clone -b customer-demo git@github.com:yourorg/customer-mds.git
cd customer-mdsInteractive mode (recommended for first-time setup):
sudo ./tools/mds_node_init.shNon-interactive mode with drone ID:
sudo ./tools/mds_node_init.sh -d 1 -yUsing your own fork or custom repo:
sudo ./tools/mds_node_init.sh -d 1 --fork yourusername -yOr fully explicit:
sudo ./tools/mds_node_init.sh -d 1 \
--repo-url git@github.com:yourorg/customer-mds.git \
--branch customer-demo \
-yPrivate read-only HTTPS is also supported explicitly:
sudo ./tools/mds_node_init.sh -d 1 \
--repo-url https://github.com/yourorg/customer-mds.git \
--branch customer-demo \
--git-auth-token-file /home/droneshow/.mds_git_read_token \
-yPrivate read-only SSH is also supported explicitly:
sudo ./tools/mds_node_init.sh -d 1 \
--repo-url git@github.com:yourorg/customer-mds.git \
--branch customer-demo \
--git-ssh-key-file /home/droneshow/.ssh/customer_git_read_key \
-yFor private customer repositories, provision repo access before expecting reboot sync to work unattended:
- Generate a separate read-only SSH key for each drone or provisioned image.
- Add the public key under GitHub repository Settings -> Deploy keys without write access.
- Put the private key on the node with owner
droneshowand mode0600. - Set
MDS_GIT_SSH_KEY_FILE=/home/droneshow/.ssh/<key-name>in/etc/mds/local.envor pass--git-ssh-key-fileduring init. - Test from the service account:
sudo -u droneshow -H bash -lc 'GIT_SSH_COMMAND="ssh -i /home/droneshow/.ssh/<key-name> -o IdentitiesOnly=yes" git ls-remote git@github.com:yourorg/customer-mds.git HEAD'If GitHub does not let the repository owner add deploy keys, the organization or enterprise may restrict them. Ask the org owner to check Organization Settings -> Member privileges -> Deploy keys. If that policy should remain disabled, use a GitHub App or approved machine-user/token model instead.
The GCS may use a separate write-capable credential when it must publish changes. Never copy the GCS write credential onto drone nodes.
With an explicit GCS API URL for candidate announce:
sudo ./tools/mds_node_init.sh -d 1 \
--gcs-api-url https://gcs.example/api \
-yThe script runs through these phases automatically:
| Phase | Description |
|---|---|
| 1. Prerequisites | Validates system requirements, creates directories |
| 2. MAVLink Router Setup | Auto-configures mavlink-anywhere when requested, or preserves intentional manual routing |
| 3. Repository | Clones/updates repository, manages SSH keys |
| 4. Identity | Configures drone ID, local.env, and node_identity.json |
| 5. Environment | Sets up environment variables |
| 6. Firewall | Configures UFW rules for MDS services |
| 7. Python Environment | Creates venv, installs requirements |
| 8. MAVSDK | Downloads and installs MAVSDK binary |
| 9. Services | Installs and enables systemd services |
| 10. NTP | Configures time synchronization |
| 11. Netbird | (Optional) Configures VPN access |
| 12. Static IP | (Optional) Configures static IP address |
| 13. Connectivity Backend | (Optional) Installs Smart Wi-Fi Manager or keeps manual networking |
| 14. Verify | Final verification of installation |
| 15. Candidate Announce | Sends the node identity to the GCS enrollment registry when reachable |
For a single drone with ID 1:
sudo ./tools/mds_node_init.sh -d 1 -yFor using a forked repository or org-owned repo (simple shorthand):
sudo ./tools/mds_node_init.sh -d 1 --fork yourusername -yOr with a customer org/private repo:
sudo ./tools/mds_node_init.sh -d 1 \
--repo-url git@github.com:yourorg/customer-mds.git \
--branch customer-demo \
-yFor the full cross-target workflow, see Custom Repo Workflow.
sudo ./tools/mds_node_init.sh -d 5 \
--netbird-key "YOUR_NETBIRD_SETUP_KEY" \
--static-ip 192.168.1.105/24 \
--gateway 192.168.1.1 \
-yNetBird is optional. For same-LAN or static-IP deployments, omit
--netbird-key and use the node's reachable LAN IP for QGroundControl. If the
topology requires device-side push, configure its destination separately with
--mavlink-push-endpoint HOST:PORT.
If the script was interrupted, resume from the last checkpoint:
sudo ./tools/mds_node_init.sh --resumeSee what would happen without making changes:
sudo ./tools/mds_node_init.sh -d 1 --dry-runIf the node is provisioned but the GCS was unavailable during bootstrap:
sudo ./tools/mds_node_announce.sh --gcs-api-url http://192.0.2.75:5030Check service status:
systemctl status coordinator
systemctl status git_sync_mdsBootstrap and candidate announce do not finish fleet enrollment on their own.
The announce payload includes the node's canonical runtime_mode from
/etc/mds/node_identity.json, so a SITL node cannot pollute the REAL enrollment
queue and a REAL node cannot pollute the SITL queue.
After the node appears in Fleet Enrollment:
- accept, replace, or recover the candidate on GCS
- commit/push the updated fleet repo state on GCS if auto-push is disabled
- run a node repo sync before relying on the new assignment at runtime
Operational rule:
- GCS enrollment updates the fleet manifest immediately
- the node still runs from its local repo/config state until the sync step applies that change
Check the installation log:
cat /var/log/mds/mds_init.logCurrent best practice:
- use
mds_node_init.sh --mavlink-autofor the default managed path - keep the managed
mavlink-anywhererepo/ref/install defaults indeployment/defaults.env - use
/etc/mds/local.envonly for node-specific MAVLink ownership overrides when a board needs different runtime handling - interactive bootstrap lets the operator choose between recommended defaults, a guided
mavlink-anywherewizard, or fully manual routing - or provide explicit headless routing flags such as
--mavlink-uartand--mavlink-endpoints - keep
--gcs-ipfor the MDS HTTP/control plane; add--mavlink-push-endpoint HOST:PORTonly when the node must continuously push MAVLink to a known remote consumer - use
--mavlink-input udp --mavlink-input-port ...when the node should ingest MAVLink from a network source instead of a serial FC link - use manual routing only when you intentionally manage
mavlink-anywhereyourself
Managed examples:
sudo ./tools/mds_node_init.sh -d 1 --mavlink-auto --gcs-ip 192.0.2.75 -ysudo ./tools/mds_node_init.sh \
-d 1 \
--mavlink-auto \
--gcs-ip 192.0.2.75 \
--mavlink-push-endpoint 192.0.2.75:24550 \
-ysudo ./tools/mds_node_init.sh \
-d 1 \
--mavlink-input udp \
--mavlink-input-port 14550 \
--mavlink-endpoints "127.0.0.1:14540,127.0.0.1:14569,127.0.0.1:12550" \
-yIf you intentionally keep routing manual, bootstrap and enrollment still work.
In that case, manage mavlink-anywhere yourself and keep that routing profile
documented for the fleet.
Managed runtime ownership and local overrides:
deployment/defaults.envMDS_DEFAULT_MAVLINK_MANAGEMENT_MODEMDS_DEFAULT_MAVLINK_ANYWHERE_REPO_URL_HTTPSMDS_DEFAULT_MAVLINK_ANYWHERE_REFMDS_DEFAULT_MAVLINK_ANYWHERE_INSTALL_DIR
/etc/mds/local.envMDS_MAVLINK_MANAGEMENT_MODE- optional
MDS_MAVLINK_ANYWHERE_REPO_URL - optional
MDS_MAVLINK_ANYWHERE_REF MDS_MAVLINK_ANYWHERE_INSTALL_DIRMDS_MAVLINK_ANYWHERE_DASHBOARD_LISTENMDS_MAVLINK_ANYWHERE_SKIP_DASHBOARD
After changing only the managed runtime ownership settings on an existing node:
sudo ./tools/reconcile_mavlink_runtime.sh apply --forceCurrent mavlink-anywhere defaults include a device-side GCS listener on
14550/udp, so the normal QGC workflow is to connect to the node / CM4 IP
on port 14550. --gcs-ip does not create a full-rate route. Use
--mavlink-push-endpoint HOST:PORT (or an explicitly managed endpoint profile)
only when you intentionally need remote push-mode delivery.
If you are using the Holybro Pixhawk RPi CM4 baseboard, the PX4/Holybro docs wire the CM4 to the FC through TELEM2 and expect PX4 to use:
MAV_1_CONFIG = TELEM2 (102)MAV_1_MODE = Onboard (2)SER_TEL2_BAUD = 921600
If you want the web dashboard reachable from the LAN or VPN, expose it explicitly:
sudo ./configure_mavlink_router.sh --install-dashboard \
--dashboard-listen 0.0.0.0:9070After installation, reboot to start all services:
sudo rebootAfter installation, key configuration files are:
| File | Purpose |
|---|---|
/etc/mds/local.env |
Per-node runtime overrides (hardware ID, GCS routing, optional GCS API URL, repo/branch overrides, etc.) |
/etc/mds/node_identity.json |
Structured machine-readable node manifest for automation, enrollment, and diagnostics |
/var/lib/mds/init_state.json |
Installation state tracking |
~/mavsdk_drone_show/config.json |
Fleet manifest source of truth for real-mode membership and per-node transport settings |
~/mavsdk_drone_show/swarm.json |
Smart Swarm / follow-chain source of truth for real mode |
~/mavsdk_drone_show/src/params.py |
Transitional code defaults and fallback values while the typed settings layer is rolled out |
Use these ownership rules consistently:
config.json/swarm.json: fleet manifest and mission-structure source of truth on GCSFleet Enrollment: canonical workflow that mutatesconfig.jsonfor accept / replace / recover/etc/mds/local.env: node-local runtime identity and GCS routing source of truth on each companion computersrc/params.py: fallback defaults only; not the normal operator customization point for real hardware- See Runtime Config Sources for the full precedence table and ownership model.
Practical consequence:
- changing
MDS_GCS_IPorMDS_GCS_API_BASE_URLbelongs in/etc/mds/local.env - enrolling a new node belongs in
Fleet Enrollment, which updatesconfig.json - replacing a failed airframe also belongs in
Fleet Enrollment; replacement rewrites the affectedconfig.jsonrow and any relevantswarm.jsonreferences - editing
src/params.pyshould be reserved for code-level defaults that apply repo-wide, not for per-deployment runtime state
To change drone-specific settings:
sudo nano /etc/mds/local.env
sudo systemctl restart coordinatorTo preview or resend the candidate announce payload:
sudo ./tools/mds_node_announce.sh --dry-run --report-json -See MDS Init Troubleshooting Guide for common issues.
Script fails to start:
chmod +x tools/mds_node_init.shPermission denied:
sudo ./tools/mds_node_init.shCheck installation state:
cat /var/lib/mds/init_state.json | jq- Confirm MAVLink routing mode (
--mavlink-auto, explicit headless flags, or intentional manual mode) - Set up WiFi manager if using wireless networks
- Test drone connectivity with the GCS
- Run first system test
- CLI Reference - All command-line options
- Headless Automation - Fleet provisioning
- Troubleshooting - Common issues
- Service Architecture - Systemd services
Version: 5.5 | Last Updated: 2026-05-17