Skip to content

Commit 1cdabea

Browse files
committed
packaging: add Docker runtime images for SIH and Gazebo SITL
Add minimal Dockerfiles and a platform-adaptive entrypoint script that enables cross-platform Docker support (Linux, macOS, Windows). On Docker Desktop (macOS/Windows), the entrypoint detects host.docker.internal and patches MAVLink targets (-t flag) and uXRCE-DDS agent address so UDP packets reach the host. Verified on macOS: MAVSDK, mavsim-viewer, and uXRCE-DDS agent all connect successfully through Docker port mapping. Images: px4io/px4-sitl-sih: 107MB (ubuntu:24.04, stripped binary) px4io/px4-sitl-gazebo: 2.2GB (ubuntu:24.04 + gz-harmonic) Signed-off-by: Ramon Roche <mrpollo@gmail.com>
1 parent f5315c9 commit 1cdabea

File tree

4 files changed

+292
-0
lines changed

4 files changed

+292
-0
lines changed

Tools/packaging/Dockerfile.gazebo

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# PX4 SITL Gazebo Harmonic runtime image
2+
# Runs PX4 SITL with Gazebo Harmonic. Supports X11 forwarding for GUI.
3+
#
4+
# Build:
5+
# make px4_sitl_default && cd build/px4_sitl_default && cpack -G DEB && cd ../..
6+
# docker build -f Tools/packaging/Dockerfile.gazebo -t px4io/px4-sitl-gazebo:v1.17.0 build/px4_sitl_default/
7+
#
8+
# Run (headless):
9+
# docker run --rm -it --network host px4io/px4-sitl-gazebo:v1.17.0
10+
#
11+
# Run (X11 GUI):
12+
# xhost +local:docker
13+
# docker run --rm -it --network host \
14+
# -e DISPLAY=$DISPLAY -v /tmp/.X11-unix:/tmp/.X11-unix \
15+
# --gpus all px4io/px4-sitl-gazebo:v1.17.0
16+
17+
FROM ubuntu:24.04 AS extract
18+
COPY px4-gazebo_*.deb /tmp/
19+
RUN apt-get update && apt-get install -y --no-install-recommends binutils \
20+
&& dpkg -x /tmp/px4-gazebo_*.deb /staging \
21+
&& strip /staging/opt/px4-gazebo/bin/px4 \
22+
&& rm -rf /var/lib/apt/lists/*
23+
24+
FROM ubuntu:24.04
25+
LABEL maintainer="PX4 Development Team"
26+
LABEL description="PX4 SITL with Gazebo Harmonic simulation"
27+
28+
ENV DEBIAN_FRONTEND=noninteractive
29+
ENV RUNS_IN_DOCKER=true
30+
31+
# Install Gazebo Harmonic and simulation dependencies (no NuttX toolchain)
32+
COPY --from=extract /staging /staging
33+
RUN apt-get update \
34+
&& apt-get install -y --no-install-recommends \
35+
bc \
36+
ca-certificates \
37+
gnupg \
38+
lsb-release \
39+
wget \
40+
&& wget -q https://packages.osrfoundation.org/gazebo.gpg \
41+
-O /usr/share/keyrings/pkgs-osrf-archive-keyring.gpg \
42+
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/pkgs-osrf-archive-keyring.gpg] http://packages.osrfoundation.org/gazebo/ubuntu-stable $(lsb_release -cs) main" \
43+
> /etc/apt/sources.list.d/gazebo-stable.list \
44+
&& apt-get update \
45+
&& apt-get install -y --no-install-recommends \
46+
gz-harmonic \
47+
&& rm -rf /var/lib/apt/lists/*
48+
49+
# Install PX4 files from .deb
50+
RUN cp -a /staging/opt/px4-gazebo /opt/px4-gazebo && rm -rf /staging
51+
RUN ln -sf /opt/px4-gazebo/bin/px4-gazebo /usr/bin/px4-gazebo
52+
53+
# Create the DART physics engine symlink (avoids needing the -dev package)
54+
RUN GZ_PHYSICS_DIR=$(echo /usr/lib/*/gz-physics-7/engine-plugins) \
55+
&& if [ -d "$GZ_PHYSICS_DIR" ]; then \
56+
VERSIONED=$(ls "$GZ_PHYSICS_DIR"/libgz-physics*-dartsim-plugin.so.* 2>/dev/null | head -1) \
57+
&& [ -n "$VERSIONED" ] \
58+
&& ln -sf "$(basename "$VERSIONED")" "$GZ_PHYSICS_DIR/libgz-physics-dartsim-plugin.so"; \
59+
fi
60+
61+
# Gazebo resource paths
62+
ENV GZ_SIM_RESOURCE_PATH=/opt/px4-gazebo/share/gz/models:/opt/px4-gazebo/share/gz/worlds
63+
ENV GZ_SIM_SYSTEM_PLUGIN_PATH=/opt/px4-gazebo/lib/gz/plugins
64+
ENV GZ_SIM_SERVER_CONFIG_PATH=/opt/px4-gazebo/share/gz/server.config
65+
ENV PX4_GZ_MODELS=/opt/px4-gazebo/share/gz/models
66+
ENV PX4_GZ_WORLDS=/opt/px4-gazebo/share/gz/worlds
67+
68+
ENV PX4_SIM_MODEL=gz_x500
69+
ENV HOME=/root
70+
71+
# MAVLink, MAVSDK, DDS
72+
EXPOSE 14550/udp 14540/udp 8888/udp
73+
74+
WORKDIR /root
75+
76+
ENTRYPOINT ["/opt/px4-gazebo/bin/px4-gazebo"]
77+
CMD []

Tools/packaging/Dockerfile.sih

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# PX4 SITL SIH runtime image
2+
# Minimal container that runs PX4 with the SIH physics engine (no Gazebo).
3+
#
4+
# Build:
5+
# make px4_sitl_sih && cd build/px4_sitl_sih && cpack -G DEB && cd ../..
6+
# docker build -f Tools/packaging/Dockerfile.sih -t px4io/px4-sitl-sih:v1.17.0 build/px4_sitl_sih/
7+
#
8+
# Run (Linux):
9+
# docker run --rm -it --network host px4io/px4-sitl-sih:v1.17.0
10+
#
11+
# Run (macOS / Windows):
12+
# docker run --rm -it -p 14550:14550/udp -p 14540:14540/udp -p 19410:19410/udp -p 8888:8888/udp px4io/px4-sitl-sih:v1.17.0
13+
14+
FROM ubuntu:24.04 AS build
15+
COPY px4_*.deb /tmp/
16+
RUN apt-get update \
17+
&& apt-get install -y --no-install-recommends binutils \
18+
&& dpkg -x /tmp/px4_*.deb /staging \
19+
&& strip /staging/opt/px4/bin/px4 \
20+
&& rm -rf /var/lib/apt/lists/*
21+
22+
FROM ubuntu:24.04
23+
LABEL maintainer="PX4 Development Team"
24+
LABEL description="PX4 SITL with SIH physics (no simulator dependencies)"
25+
26+
RUN apt-get update && apt-get install -y --no-install-recommends bc \
27+
&& rm -rf /var/lib/apt/lists/* /usr/share/doc /usr/share/man /usr/share/info
28+
29+
COPY --from=build /staging/opt/px4 /opt/px4
30+
RUN ln -sf /opt/px4/bin/px4 /usr/bin/px4
31+
32+
# Platform-adaptive entrypoint: detects Docker Desktop (macOS/Windows) via
33+
# host.docker.internal and configures MAVLink + DDS to target the host.
34+
COPY px4-entrypoint.sh /opt/px4/bin/px4-entrypoint.sh
35+
RUN chmod +x /opt/px4/bin/px4-entrypoint.sh
36+
37+
ENV PX4_SIM_MODEL=sihsim_quadx
38+
ENV HOME=/root
39+
40+
# MAVLink (QGC, MAVSDK), DDS (ROS 2), jMAVSim/viewer display
41+
EXPOSE 14550/udp 14540/udp 19410/udp 8888/udp
42+
43+
WORKDIR /root
44+
45+
ENTRYPOINT ["/opt/px4/bin/px4-entrypoint.sh"]
46+
CMD []

Tools/packaging/px4-entrypoint.sh

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
#!/bin/sh
2+
# Docker entrypoint for PX4 SITL containers.
3+
#
4+
# On Docker Desktop (macOS/Windows), host.docker.internal resolves to the
5+
# host machine. We detect this and configure MAVLink + DDS to send to the
6+
# host IP instead of localhost (which stays inside the container VM).
7+
#
8+
# On Linux with --network host, host.docker.internal does not resolve and
9+
# PX4 defaults work without modification.
10+
11+
set -e
12+
13+
if getent hosts host.docker.internal >/dev/null 2>&1; then
14+
DOCKER_HOST_IP=$(getent hosts host.docker.internal | awk '{print $1}')
15+
16+
# MAVLink: replace default target (127.0.0.1) with the Docker host IP
17+
sed -i "s/mavlink start -x -u/mavlink start -x -t $DOCKER_HOST_IP -u/g" \
18+
/opt/px4/etc/init.d-posix/px4-rc.mavlink
19+
20+
# DDS: point uXRCE-DDS client at the host
21+
sed -i "s|uxrce_dds_client start -t udp|uxrce_dds_client start -t udp -h $DOCKER_HOST_IP|" \
22+
/opt/px4/etc/init.d-posix/rcS
23+
fi
24+
25+
exec /opt/px4/bin/px4 "$@"
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
#!/usr/bin/env python3
2+
"""
3+
MAVSDK mission test for PX4 SIH SITL in Docker.
4+
5+
Takes off to 100m, flies a short 4-waypoint box mission, then lands.
6+
Validates that the SIH Docker container works end-to-end with MAVSDK.
7+
8+
Prerequisites:
9+
- Docker container running:
10+
docker run --rm --network host px4io/px4-sitl-sih:v1.17.0-alpha1
11+
- pip install mavsdk
12+
- mavsim-viewer running (optional):
13+
/path/to/mavsim-viewer -n 1
14+
15+
Usage:
16+
python3 Tools/packaging/test_sih_mission.py
17+
python3 Tools/packaging/test_sih_mission.py --speed 10 # faster-than-realtime
18+
"""
19+
20+
import asyncio
21+
import argparse
22+
import sys
23+
import time
24+
25+
from mavsdk import System
26+
from mavsdk.mission import MissionItem, MissionPlan
27+
28+
29+
async def run_mission(speed_factor: int = 1):
30+
drone = System()
31+
print(f"Connecting to drone on udp://:14540 ...")
32+
await drone.connect(system_address="udp://:14540")
33+
34+
print("Waiting for drone to connect...")
35+
async for state in drone.core.connection_state():
36+
if state.is_connected:
37+
print(f"Connected (UUID: {state.uuid if hasattr(state, 'uuid') else 'N/A'})")
38+
break
39+
40+
print("Waiting for global position estimate...")
41+
async for health in drone.telemetry.health():
42+
if health.is_global_position_ok and health.is_home_position_ok:
43+
print("Global position OK")
44+
break
45+
46+
# Get home position for reference
47+
async for pos in drone.telemetry.position():
48+
home_lat = pos.latitude_deg
49+
home_lon = pos.longitude_deg
50+
print(f"Home position: {home_lat:.6f}, {home_lon:.6f}")
51+
break
52+
53+
# Build a small box mission at 100m AGL
54+
# ~100m offset in each direction
55+
offset = 0.001 # roughly 111m at equator
56+
mission_items = [
57+
MissionItem(
58+
home_lat + offset, home_lon,
59+
100, 10, True, float('nan'), float('nan'),
60+
MissionItem.CameraAction.NONE,
61+
float('nan'), float('nan'), float('nan'),
62+
float('nan'), float('nan'),
63+
MissionItem.VehicleAction.NONE,
64+
),
65+
MissionItem(
66+
home_lat + offset, home_lon + offset,
67+
100, 10, True, float('nan'), float('nan'),
68+
MissionItem.CameraAction.NONE,
69+
float('nan'), float('nan'), float('nan'),
70+
float('nan'), float('nan'),
71+
MissionItem.VehicleAction.NONE,
72+
),
73+
MissionItem(
74+
home_lat, home_lon + offset,
75+
100, 10, True, float('nan'), float('nan'),
76+
MissionItem.CameraAction.NONE,
77+
float('nan'), float('nan'), float('nan'),
78+
float('nan'), float('nan'),
79+
MissionItem.VehicleAction.NONE,
80+
),
81+
MissionItem(
82+
home_lat, home_lon,
83+
100, 10, True, float('nan'), float('nan'),
84+
MissionItem.CameraAction.NONE,
85+
float('nan'), float('nan'), float('nan'),
86+
float('nan'), float('nan'),
87+
MissionItem.VehicleAction.NONE,
88+
),
89+
]
90+
91+
mission_plan = MissionPlan(mission_items)
92+
93+
print(f"Uploading mission ({len(mission_items)} waypoints, 100m AGL)...")
94+
await drone.mission.upload_mission(mission_plan)
95+
print("Mission uploaded")
96+
97+
print("Arming...")
98+
await drone.action.arm()
99+
print("Armed")
100+
101+
t0 = time.time()
102+
print("Starting mission...")
103+
await drone.mission.start_mission()
104+
105+
# Monitor mission progress
106+
async for progress in drone.mission.mission_progress():
107+
elapsed = time.time() - t0
108+
print(f" [{elapsed:6.1f}s] Waypoint {progress.current}/{progress.total}")
109+
if progress.current == progress.total:
110+
print(f"Mission complete in {elapsed:.1f}s (speed factor: {speed_factor}x)")
111+
break
112+
113+
print("Returning to launch...")
114+
await drone.action.return_to_launch()
115+
116+
# Wait for landing
117+
async for in_air in drone.telemetry.in_air():
118+
if not in_air:
119+
print("Landed")
120+
break
121+
122+
print("Disarming...")
123+
await drone.action.disarm()
124+
print("Test PASSED")
125+
126+
127+
def main():
128+
parser = argparse.ArgumentParser(description="PX4 SIH Docker mission test")
129+
parser.add_argument("--speed", type=int, default=1,
130+
help="PX4_SIM_SPEED_FACTOR (must match container)")
131+
args = parser.parse_args()
132+
133+
try:
134+
asyncio.run(run_mission(args.speed))
135+
except KeyboardInterrupt:
136+
print("\nInterrupted")
137+
sys.exit(1)
138+
except Exception as e:
139+
print(f"Test FAILED: {e}")
140+
sys.exit(1)
141+
142+
143+
if __name__ == "__main__":
144+
main()

0 commit comments

Comments
 (0)