aruco_landing module inside the skyhub container’s ROS half, reads the SIYI down-camera over
RTSP, and commands the flight controller directly with pymavlink over mavp2p:14561 — bypassing
MAVROS.
For the video pipeline the landing camera rides on, see
/drone-os/robotics/video-streaming. For the GUIDED-velocity
safety model that manual movement (and the landing correction commands) build on, see
/drone-os/safe-control. For the Redis channel contract and MAVLink port
map, see /drone-os/message-bus and
/drone-os/mavlink-topology.
The module
The landing module implements Embedded ArUco (Khazetdinov et al., 2021): a large outer marker with a small inner marker printed into its center cell, both fromDICT_7X7_250. The
outer marker is visible from high altitude; the inner marker gives centimeter precision on final
approach. The module is off by default and lives entirely in the ROS half:
| File | Responsibility |
|---|---|
docker/core/src/modules/aruco_landing/module.py | ROS2 module: RTSP capture, altitude/GPS subscriptions, Redis command loop, RTL auto-trigger, gimbal lock, tracking publish (1742 lines) |
docker/core/src/modules/aruco_landing/detector.py | EArucoDetector — detects outer/inner markers, pose (rvec/tvec), yaw, distance |
docker/core/src/modules/aruco_landing/controller.py | LandingController — LandingState machine, yaw-align-then-descend, per-zone speeds |
docker/core/src/modules/aruco_landing/drone_controller.py | DroneController — direct pymavlink body-NED velocity, yaw, land, mode |
Printing a marker
Generate a print-ready e-ArUco marker withutils/generate_e_aruco_marker.py. The inner marker
is always 1/9 of the outer size and is embedded in the outer marker’s center cell:
utils/generate_e_aruco_marker.py
Detection and the outer→inner switch
EArucoDetector.detect_both() (detector.py:110) detects both markers each frame; the module’s
_detect_with_altitude_switch() (module.py:1292) picks which one to act on based on current
altitude. Below EARUCO_SWITCH_ALTITUDE it uses the inner marker, otherwise the outer:
docker/core/src/modules/aruco_landing/module.py:1327
cv2.aruco.estimatePoseSingleMarkers using a camera matrix derived from
CAMERA_HFOV; each detection yields center (pixels), center_normalized (−1..1), distance
(m), and yaw_degrees (0–360, compass). Frames come from a dedicated RTSP capture thread that
opens rtsp://{CAMERA_IP}:{CAMERA_PORT}/{CAMERA_PATH} only while landing is active.
Descent state machine
LandingController (controller.py) drives the descent. It corrects yaw first (no descent
during yaw alignment), then descends continuously while nudging XY to keep the marker centered.
Speeds are chosen per altitude “zone”, so descent is fast up high and gentle near the ground.
The three movement primitives all come from DroneController and bypass MAVROS entirely,
talking pymavlink to mavp2p on UDP 14561:
| Action | MAVLink message | Frame / notes |
|---|---|---|
| XY correction + descent | SET_POSITION_TARGET_LOCAL_NED | MAV_FRAME_BODY_NED, type-mask 0b0000111111000111 (velocity only). vx=forward, vy=right, vz=down (drone_controller.py:117) |
| Yaw align | MAV_CMD_CONDITION_YAW | relative rotation, speed capped at 60°/s (controller.py:531) |
| Touchdown | MAV_CMD_NAV_LAND | at ARUCO_LANDING_DISTANCE (drone_controller.py:195) |
| Mode change | MAV_CMD_DO_SET_MODE | GUIDED with retry before descent (drone_controller.py:267) |
Auto-trigger on RTL / AUTO
Landing can be started manually (adock_start command on {ip}:gamepad_input, subscribed in
_redis_command_loop, module.py:1398) or automatically during a return-to-launch. When
RTL_DOCK_ENABLED, _check_rtl_dock_trigger() (module.py:1481) fires once the drone descends
to RTL_DOCK_ALTITUDE and is within RTL_DOCK_HOME_DISTANCE of home (haversine distance
from /mavros/home_position/home vs /mavros/global_position/global). The home-distance guard
prevents a dock from starting at a mid-mission RTL waypoint far from the pad. It works for both a
manual RTL and an AUTO mission whose final leg is an RTL.
Gimbal lock and the canvas overlay
When landing activates,_activate_landing() (module.py:1629) locks the camera: points the
gimbal down, sets zoom to minimum (widest view), and triggers autofocus — issued both directly
over the SIYI TCP protocol and as a Redis camera_command with source: "aruco_landing" to the
agent half.
Marker tracking is not burned into the video stream (the “CANVAS approach”). Instead
_publish_tracking_data() publishes marker position/distance/yaw to Redis {ip}:aruco_tracking
at ~10 Hz; the agent half forwards it to WS clients and the Dashboard renders it as a canvas
overlay on top of the WebRTC video. This keeps the encoded stream clean and saves Jetson
GPU — see /dashboard/features/video-and-control.
Configuration
All values are read via theEARUCO_* / ARUCO_* / RTL_DOCK_* env prefixes. The table shows
the shipped .env.example values (what actually runs). Note the module’s hard-coded fallback
defaults differ where called out — always trust .env.example.
| Env var | .env.example | Purpose |
|---|---|---|
ARUCO_LANDING_ENABLED | false | Enable the aruco_landing module |
EARUCO_OUTER_MARKER_ID / _INNER_MARKER_ID | 228 / 11 | e-ArUco marker IDs (DICT_7X7_250) |
EARUCO_OUTER_MARKER_SIZE / _INNER_MARKER_SIZE | 0.87 / 0.077 m | Physical marker sizes (code fallback 0.45 / 0.05) |
EARUCO_SWITCH_ALTITUDE | 2.5 m | Outer→inner switch altitude (code fallback 1.3) |
RTL_DOCK_ENABLED | true | Auto-trigger landing on RTL/AUTO descent |
RTL_DOCK_ALTITUDE | 30 m | Altitude that arms the auto-dock (code fallback 15) |
RTL_DOCK_HOME_DISTANCE | 15 m | Max distance from home to allow auto-dock |
ARUCO_LANDING_DISTANCE | 1.2 m | Distance at which MAV_CMD_NAV_LAND fires (code fallback 1.0) |
ARUCO_LAND_LOCK_DURATION | 2.0 s | Fallback: centered-lock time before LAND |
ARUCO_LAND_LOCK_RADIUS | 0.15 | Fallback: normalized frame radius around center |
ARUCO_LAND_LOCK_MAX_DISTANCE | 1.5 m | Fallback: max marker distance allowed |
ARUCO_LAND_LOCK_MIN_CONFIDENCE | 0.5 | Fallback: minimum marker confidence |
ARUCO_LAND_LOCK_MAX_GAP | 1.0 s | Fallback: max detection gap before the lock timer restarts |
ARUCO_TIMER_FRAMES | 15 | Frames the detector averages before acting |
ARUCO_TARGET_YAW | 0 | Compass heading to align the airframe to (code fallback 270) |
ARUCO_LEVEL_0..3 | 20 / 10 / 3 / 1.5 m | Zone altitude thresholds (code fallbacks 10 / 5 / 3 / 1.5) |
ARUCO_XY_SPEED_LVL0..3 | 0.5 / 0.3 / 0.15 / 0.08 m/s | Per-zone XY correction speed |
ARUCO_DOWN_SPEED_LVL0..3 | 1.0 / 0.5 / 0.3 / 0.15 m/s | Per-zone descent speed |
ARUCO_MAVLINK_CONNECTION | udpout:127.0.0.1:14561 | pymavlink endpoint into mavp2p (controller.py:45) |
CAMERA_HFOV | 81.0 | Camera horizontal FOV for pose intrinsics |
ARUCO_TEST_MODE | false | Detect/track only, suppress movement commands |
The
mavp2p udps:14561 endpoint the landing module uses is defined only in the main
docker-compose.yml. The minimal docker/docker-compose.installer.yml router omits 14561 (and
the RTK port), so precision landing will not send velocity commands under the installer compose.Gotchas a future editor must preserve
OpenCV is pinned to 4.5.4 on purpose
OpenCV is pinned to 4.5.4 on purpose
detector.py calls cv2.aruco.estimatePoseSingleMarkers, removed in OpenCV 4.9. The image
holds apt’s 4.5.4 and keeps pip’s opencv-python out (siyi-sdk installed --no-deps);
docker/skyhub/test/check-deps.sh fails the build if the function disappears. Do not “fix” a
dependency resolver complaint by unpinning it.Gimbal is locked during precision landing
Gimbal is locked during precision landing
aruco_landing sends camera_command with source: aruco_landing, which sets
gimbal_locked in the agent half. Operator center/move gimbal commands are dropped until
dock_stop or disarm. Removing this lock lets operator input fight the landing controller.Landing tracking is a canvas overlay, never burned into video
Landing tracking is a canvas overlay, never burned into video
Marker overlays go to Redis
{ip}:aruco_tracking and are drawn on the frontend. The module
deliberately does not composite them into the encoded stream (preserves quality + Jetson
GPU). Do not re-add a server-side overlay..env.example values override the code fallbacks
.env.example values override the code fallbacks
Several
aruco_landing fallback defaults in module.py (marker sizes 0.45/0.05, switch 1.3 m,
RTL altitude 15 m, land distance 1.0 m, target yaw 270°, zones 10/5/3/1.5) differ from the
shipped .env.example (0.87/0.077, 2.5, 30, 1.2, 0, 20/10/3/1.5). Production runs the
.env.example values — quote those when documenting behavior.Related pages
Video Streaming
The camera drivers, encode selection and WHIP/WebRTC pipeline the down-camera feed shares.
Guided Velocity & Safety
The GUIDED-mode velocity model the landing corrections ultimately execute through.
MAVLink Routing (mavp2p)
The single MAVLink hub and the 14550/14561/14777 port map this module’s
14561 endpoint sits in.Redis Message Bus
The
{ip}:gamepad_input and {ip}:aruco_tracking channel contract.
