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 switch the EKF navigation source between GPS and SLAM, 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 core container) and the gamepad/ws_proxy service. The CLI 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 for the actual orchestration.
Source: skycore_cli.py (~2,757 lines). The whole tool is three moving parts: MAVLinkConnection (the raw pymavlink client), NavigationToggle (a higher-level wrapper that holds the GPS vs SLAM parameter sets and orchestrates reboots), and a main() / execute_command() dispatcher that maps ~30 command strings onto those two classes.

Running the CLI

The tool has two modes, selected by whether you pass an argument:
# From the skyhub_core repo root on the drone host
python skycore_cli.py status
python skycore_cli.py slam
python skycore_cli.py stream filtered
python skycore_cli.py download_last_log ./logs
python skycore_cli.py
# Prints the help menu + current nav source, then:
#   Enter command (type 'menu' for help): status
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:
OrderEndpointTypical use
1–2/dev/ttyACM0, /dev/ttyACM1USB CDC to Pixhawk
3–4/dev/ttyUSB0, /dev/ttyUSB1FTDI/USB serial FC
5–6udpin:127.0.0.1:14550/14551Listen for MAVLink UDP
7–8udpout:127.0.0.1:14550/14551Dial out to a router/SITL
9–10udp://127.0.0.1:14550/14551Generic UDP
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.
These are the CLI’s headline feature: flipping which sensor the ArduPilot EKF3 trusts for position, velocity, and yaw. See Isaac Visual SLAM & Pose Bridge for what actually feeds the external-nav estimate, and RTK NTRIP GPS Corrections for the GPS side.
CommandArgsBehavior
statusPrints current source by reading EK3_SRC1_POSXY (3 = GPS, else SLAM). skycore_cli.py:1647
gpsWrites the GPS source set, then reboots the FC. skycore_cli.py:1624
slamWrites the SLAM (external-nav) source set, then reboots. skycore_cli.py:1629
toggleReads EK3_SRC1_POSXY; if 3 switches to SLAM, otherwise switches to GPS. skycore_cli.py:1634
ekfSets EKF origin and home at (0, 0, 0). skycore_cli.py:2312
custom_ekfPrompts for lat/lon/alt, then sets EKF origin + home there. skycore_cli.py:2317
Every 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 two parameter sets are hardcoded in NavigationToggle.__init__ (skycore_cli.py:1566):
skycore_cli.py:1566
gps_source_set = {
    "EK3_SRC1_POSXY": 3,   # GPS  – horizontal position
    "EK3_SRC1_VELXY": 3,   # GPS  – horizontal velocity
    "EK3_SRC1_POSZ": 1,    # Baro – vertical position
    "EK3_SRC1_VELZ": 3,    # GPS  – vertical velocity
    "EK3_SRC1_YAW": 1,     # Compass – yaw
    "EK3_SRC2_*": 0,       # secondary source disabled
    "GPS_USE": 1, "GPS_TYPE": 9,
    "GPS_DELAY_MS": 50, "GPS_AUTO_CONFIG": 1,
}
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

CommandArgsBehavior
monitorPrints GLOBAL_POSITION_INT lat/lon/alt once per second for 30 s. skycore_cli.py:1718
listenInteractive typed-message monitor (60 s default). skycore_cli.py:1816
streamInteractive continuous stream in compact format. skycore_cli.py:1879
stream allEvery message, detailed format, ~5 Hz. skycore_cli.py:2343
stream filteredAll messages except noisy types (TIMESYNC, PARAM_VALUE, SYSTEM_TIME, RAW_IMU, SCALED_IMU, RC_CHANNELS_RAW, SERVO_OUTPUT_RAW, VELOCITY_ESTIMATE). skycore_cli.py:2384
stream statustextOnly STATUSTEXT — ideal for reading PreArm failure reasons. skycore_cli.py:2335
stream filter X,Ycomma listAll messages except the listed types (uppercased). skycore_cli.py:2432
stream compactAll messages, compact one-line format. skycore_cli.py:2339
recent_msgsDumps the last 30 messages from the in-memory ring buffer (max_log_size = 100). skycore_cli.py:2006
# Diagnose a refusal-to-arm: watch only the PreArm STATUSTEXT lines
python skycore_cli.py stream statustext

# Watch everything except the two chattiest message types
python skycore_cli.py stream filter TIMESYNC,PARAM_VALUE

Parameters

CommandArgsBehavior
get_param(prompts)Prompts for a name, prints its value. skycore_cli.py:2539
set_param(prompts)Prompts for name + numeric value and writes it. skycore_cli.py:2549
export_params[file]Dumps the full param list to CSV (NAME,value). Default file pixhawk_params_<timestamp>.param. skycore_cli.py:2581, skycore_cli.py:672
filter_baro(prompts)Prompts for input/output files, writes a copy with all barometer params removed (keeps everything else). skycore_cli.py:2564
reset_paramsFactory-reset all params via MAV_CMD_PREFLIGHT_STORAGE (param1=2), after a yes confirmation and reboot. skycore_cli.py:2579, skycore_cli.py:371
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

CommandArgsBehavior
rebootMAV_CMD_PREFLIGHT_REBOOT_SHUTDOWN (param1=1), wait 20 s, reconnect. skycore_cli.py:2480, skycore_cli.py:310
full_restartReboot FC → reconnect → docker restart isaac_ros_dev-aarch64-container → wait 30 s → set EKF/home at (0,0,0). skycore_cli.py:2490
clean_sdFormat the Pixhawk SD via MAV_CMD_PREFLIGHT_STORAGE (param1=2, param3=1). Requires yes. skycore_cli.py:2562, skycore_cli.py:332
full_restart shells out to docker restart isaac_ros_dev-aarch64-container by hardcoded container name (skycore_cli.py:2516). It is tightly coupled to the Isaac SLAM container’s naming — if that container is renamed the restart step prints an error and the sequence aborts before setting the EKF origin.

Dataflash logs

ArduPilot .bin log management over MAVLink LOG_REQUEST_*.
CommandArgsBehavior
list_logsLists log ID, size, and UTC timestamp. skycore_cli.py:2093, skycore_cli.py:728
download_log[id] [file]Downloads one log by ID (prompts for ID if omitted). skycore_cli.py:2589
download_last_log[dir]Downloads the highest-ID log via the fast chunked + resume path to <dir>/log_<id>_<ts>.bin (default dir logs). skycore_cli.py:2608, skycore_cli.py:1032
download_all_logs[dir]Downloads every log to a directory (default logs), after a yes confirmation. skycore_cli.py:2602
delete_logsErases all logs (yes confirmation). skycore_cli.py:2618, skycore_cli.py:1114
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 gamepad service’s ExecutionTracker/LogDownloader (see Executions, Assets & Reports); this CLI is for manual retrieval.

Terrain

CommandArgsBehavior
upload_terrain[dir]Uploads every .DAT tile from a directory to the FC’s /APM/terrain/ over MAVLink-FTP. Default dir /home/skycore/skyhub_core/terrain. skycore_cli.py:2620, skycore_cli.py:1150
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)

CommandArgsBehavior
list_camera_sd[ip]Lists videos + images on the SIYI camera SD with sizes. skycore_cli.py:2673
clear_camera_sd[ip]Deletes videos and images (per-category y/N confirmations). skycore_cli.py:2626
Both use SiyiDownloader, imported from the gamepad service 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, supervisord (core container), and the gamepad service. The CLI 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, slam, toggle, ekf, and full_restart all reboot ArduPilot and block while reconnecting. Never run them mid-flight. full_restart additionally restarts the Isaac SLAM container.
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.

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.

Isaac Visual SLAM

What feeds the external-nav estimate the slam command selects.

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.