skycore_cli.py is a standalone, host-run MAVLink/ArduPilot maintenance CLI for a SkyCore drone. An engineer runs it directly on the Jetson host (not inside a container) to set the EKF navigation source and origin, read and write ArduPilot parameters, stream live MAVLink messages, pull dataflash logs, upload terrain tiles, and manage the payload camera’s SD card.
This is not the drone OS entrypoint. Despite living at the repo root and being named skycore_cli.py, it does not boot, orchestrate, or supervise the drone services. That job belongs to docker-compose profiles plus supervisord (inside the skyhub container), driven by the separate skycore installer CLI documented at the bottom of this page. skycore_cli.py is an out-of-band diagnostic tool with a completely separate lifecycle — it talks to the flight controller over a local MAVLink link, independent of the running containers. A refactor must never merge the two. See Drone OS Overview and Microservices & Container Profiles.
Source: skycore_cli.py (2,778 lines). The whole tool is three moving parts: MAVLinkConnection (the raw pymavlink client), NavigationToggle (a higher-level wrapper that holds the GPS parameter set and orchestrates reboots), and a main() / execute_command() dispatcher that maps ~30 command strings onto those two classes. A second, trimmed copy (869 lines) ships to vehicles as installer/skycore_cli.py inside sc.tar.gz, and is what skycore cli runs.

Running the CLI

The tool has two modes, selected by whether you pass an argument:
In REPL mode you type bare command names at the prompt; menu/help re-prints the command list and q quits (skycore_cli.py:2266). In one-shot mode the first argv token is the command and the rest are its arguments (skycore_cli.py:2249).
A live MAVLink heartbeat is mandatory. On startup the CLI constructs NavigationToggle → MAVLinkConnection, which probes a fixed list of endpoints for a heartbeat. If none answers, it raises ConnectionError, prints the error, and exits before running any command (skycore_cli.py:2277). The tool only works when a real flight controller or a SITL instance is reachable.

Connection auto-probe

MAVLinkConnection._connect() (skycore_cli.py:163) tries these endpoints in order and keeps the first that returns a heartbeat: On a production drone the FC is reached over serial /dev/ttyACM0. Note that mavp2p (the MAVLink router) also fans the link out over UDP 14550 to core MAVROS — so if you run the CLI while the stack is up, it can compete for the serial device. Prefer running it against a free UDP endpoint or when the router is stopped.

Command reference

Grouping below matches show_help() (skycore_cli.py:2714) and the dispatcher in execute_command() (skycore_cli.py:2292).
Arguments vs. interactive prompts. Some commands read positional argv (stream, export_params, download_log, download_last_log, download_all_logs, upload_terrain, list_camera_sd, clear_camera_sd). Others always prompt via input() even in one-shot mode — get_param, set_param, custom_ekf, and filter_baro ignore argv and ask for their values interactively. Destructive commands (clean_sd, reset_params, delete_logs, download_all_logs, upload_terrain) additionally require a typed yes/y confirmation.
Which sensor the ArduPilot EKF3 trusts for position, velocity, and yaw. On this build there is exactly one selectable source: GPS. See RTK NTRIP GPS Corrections for the GPS side.
status still reports SLAM when EK3_SRC1_POSXY != 3 even though nothing can select it. That is deliberate — a vehicle can be left in that state by hand, and an operator needs to see it before flying. Pointing the EKF at an external nav source that nothing publishes leaves it with no position source at all, which is why slam and toggle refuse rather than comply.
The source switch writes each parameter with a 100 ms gap, then reboots ArduPilot, waits ~20 s, and reconnects so the change takes effect (set_source_set, skycore_cli.py:1602). The GPS parameter set is hardcoded in NavigationToggle.__init__ (skycore_cli.py:1566):
skycore_cli.py:1566
EK3_SRC_OPTIONS is forced to 0 at construction time to disable velocity fusion (skycore_cli.py:1600).
set_ekf_and_home() first reboots the FC and waits ~20 s, then sends SET_GPS_GLOBAL_ORIGIN (set_ekf_origin, skycore_cli.py:278) and MAV_CMD_DO_SET_HOME (set_home_position, skycore_cli.py:256), then reads back GLOBAL_POSITION_INT and warns if the position drifted beyond ~1e-6° / 0.1 m tolerance (skycore_cli.py:1654). The ekf and full_restart commands call it with (0, 0, 0); the function’s own default coordinates (42.160952, 24.767237, 380.0) are only used if it is invoked directly.

Telemetry inspection

Parameters

Naming trap: the filter_baro command invokes filter_out_baro_params() (skycore_cli.py:82), which excludes baro-matching lines (BARO, GND_BARO, EK3_ALT, EK3_BARO, COMPASS_BARO, ARSPD_BARO, MS5, BMP, LPS, altitude, pressure) and keeps the rest. The sibling filter_baro_params() (skycore_cli.py:23), which keeps only baro params, exists in the file but is not wired to any command.

System / reboot

Dataflash logs

ArduPilot .bin log management over MAVLink LOG_REQUEST_*.
MAVLink offers no per-log delete — delete_logs erases the entire dataflash. To keep one log, download_log it first. Automatic post-flight log pulls are a separate feature of the agent half’s ExecutionTracker/LogDownloader (see Executions, Assets & Reports); this CLI is for manual retrieval.

Terrain

upload_terrain_files() uses pymavlink.mavftp to mkdir /APM then /APM/terrain, and uploads each .DAT with a progress callback; a hand-rolled FTP-opcode implementation (_upload_terrain_files_direct, skycore_cli.py:1275) exists as a fallback. It prompts for a yes confirmation and prints the file list and total size first.

Camera SD card (SIYI)

Both use SiyiDownloader, imported from the agent’s source tree via a sys.path hack at the top of the file (skycore_cli.py:11). If that import fails, SIYI_AVAILABLE is False and the commands print an error and return. The camera IP defaults to CAMERA_IP env (or <camera-ip>) and can be overridden by the positional arg.

Command surface at a glance

Gotchas & guardrails

The name implies an orchestrator; it is not. Boot and supervision are handled by docker-compose profiles and supervisord inside the skyhub container, driven from the host by the skycore installer CLI. skycore_cli.py runs on the host over local MAVLink and shares nothing with those runtimes. Do not merge them in a refactor.
MAVLinkConnection raises ConnectionError at construction if no device/UDP endpoint answers, and main() exits before running the requested command (skycore_cli.py:2277). Ensure a FC or SITL is reachable on one of the probed endpoints first.
gps, ekf, custom_ekf, and full_restart all reboot ArduPilot and block while reconnecting. Never run them mid-flight.
get_param, set_param, custom_ekf, and filter_baro call input() and ignore argv, so python skycore_cli.py set_param still stops to ask for name and value. Don’t script them expecting positional args — only the argv-driven commands in the table above are scriptable.
The wired command excludes barometer parameters and keeps the rest. The keep-only-baro variant (filter_baro_params) is dead code.

The skycore installer CLI

skycore is a different program with a confusingly similar name. It is the Bash installer and lifecycle tool (installer/sc.sh), shipped to vehicles in sc.tar.gz, installed as /usr/local/bin/sc.sh with /usr/local/bin/skycore pointing at it. Unlike skycore_cli.py, this one does orchestrate: it installs itself and its dependencies, activates the vehicle against a token, downloads the compose file, and pulls and restarts container images. Everything that changes the system needs root.
For the pilot’s step-by-step path, see Set up your drone.

Bootstrap: install.sh

installer/install.sh is the one-line bootstrap that fetches sc.tar.gz and runs sc.sh install (and, with a token, skycore activate):
It asks for the sudo password up front, works in a temporary directory that is removed on exit (also on failure), and is wrapped in one function so a truncated download never runs half a script. Re-running it is safe and is how a vehicle is repaired.
Replaced command. Older docs published curl -sL https://skyhub.ai/sc.tar.gz | tar xz && sudo bash sc.sh install. It still works, but it unpacks sc.sh and skycore_cli.py into the current directory and the && silently skips the install when the download fails. Use install.sh.

Commands

There is no uninstall command; see Start over or remove SkyCore.

Trying a pre-release

update installs a different archive when SKYCORE_ARCHIVE_URL is set: an https:// URL (fetched with curl) or an s3://<bucket>/<key> URL (fetched unsigned with the AWS CLI). Anything else, or a failed download, fails the update:
The status block then names the archive it used (archive:). The next plain skycore update goes back to the published release.
install used to exit 0 after failing. Steps that failed were reported as errors and then ignored, so a vehicle could finish provisioning “successfully” without jq — which skycore update needs to read its registry credentials. Required packages are now a hard failure. Unexpected arguments are ignored with a warning rather than silently, because an older published command passed a documentation URL to install.
Old service names are translated once and the translation is persisted. core, ws_proxy, ws-proxy, gamepad, and drone-mavros all resolve to skyhub; camera-proxy, isaac-slam, and slam resolve to nothing and are dropped with a warning. A vehicle activated years ago replays the list it stored then, and that list must not be able to brick a later release — so an unknown name warns instead of failing. update writes the resolved list back to skycore.conf, so each vehicle converges and the translation table becomes deletable once no vehicle reports a version below 0.2.0.
Before 0.2.0 nothing in the installer pulled a container image except activate, and activation consumes a single-use token. A vehicle therefore stayed on whatever images it happened to pull on its activation day, permanently. skycore update is the fix; skycore status is how you confirm it worked.

Drone OS Overview

Where the CLI sits relative to the containerized service stack.

Microservices & Profiles

The real orchestrator: docker-compose profiles and supervisord.

MAVLink Routing (mavp2p)

The 14550/14777/14560/14900 port map the CLI’s UDP probes share.

RTK NTRIP Corrections

The GPS side of the nav-source toggle.

Local Dev with SITL

Run the CLI against ArduPilot SITL over UDP 14550/14551.