Simulation Module¶
ArduPilot SITL + Gazebo Harmonic simulation for Nectar drone development and testing, over either transport (MAVROS or direct MAVLink).
How It Works¶
ArduPilot SITL runs the full ArduCopter firmware on the host machine. Gazebo Harmonic provides physics and sensor simulation. The ArduPilotPlugin bridges Gazebo physics to SITL via JSON over UDP (port 9002). SITL exposes two MAVLink endpoints: TCP 5760 (SERIAL0, for MAVROS) and TCP 5762 (SERIAL1, for a direct pymavlink client / MavlinkDrone) — so both transports can run against the same simulator. The ros_gz_bridge converts Gazebo sensor data to ROS 2 messages.
flowchart LR
subgraph sitl [ArduPilot SITL]
ArduCopter["arducopter binary"]
end
subgraph gazebo [Gazebo Harmonic]
Physics["Physics + Sensors"]
ArduPlugin["ArduPilotPlugin"]
GUI["GUI: 3D + ImageDisplay + TopicEcho"]
end
subgraph bridge [ros_gz_bridge]
SensorBridge["Cameras + Lidar"]
PoseBridge["Pose bridge (indoor)"]
end
subgraph ros2 [ROS 2]
MAVROS["MAVROS"]
VisionSource["gz_vision_source (indoor)"]
VisionNode["vision_pose_node (mavros)"]
SDK["Nectar SDK / MavrosDrone | MavlinkDrone"]
end
ArduPlugin -->|"JSON UDP:9002"| ArduCopter
ArduCopter -->|"TCP:5760 (MAVROS)"| MAVROS
ArduCopter -->|"TCP:5762 (direct MAVLink)"| SDK
Physics --> SensorBridge
SensorBridge --> SDK
PoseBridge --> VisionSource
VisionSource -->|"/visual_slam/tracking/vo_pose_covariance"| VisionNode
VisionSource -->|"/visual_slam/tracking/vo_pose_covariance"| SDK
VisionNode -->|"/mavros/vision_pose/pose_cov"| MAVROS
MAVROS --> SDK
Two Environments¶
Outdoor (GPS)¶
- World:
outdoor_field.sdf-- open field with obstacle zone at x=13..18, fly-through gate - GPS via
gz-sim-navsat-systemplugin with WGS84 coordinates (Canberra default) - ArduPilot params:
copter.parm+gazebo.parm(rangefinder enabled) - Config preset:
SITL_GAZEBO_CONFIG(PoseSource.GPS)
Indoor (Vision)¶
- World:
indoor_room.sdf-- 20x20x12m enclosed room, drone at x=-5, obstacle zone at x=5..9, gate - No GPS. EKF3 uses ExternalNav (vision) for position
gz_vision_source.pypublishes Gazebo ground-truth pose on the canonical VSLAM topic/visual_slam/tracking/vo_pose_covariance;vision_pose_node(mavros) relays it to/mavros/vision_pose/pose_cov, orMavlinkDroneconsumes it directly (mavlink) -- the same bridges as real hardware- ArduPilot params:
copter.parm+gazebo.parm+indoor.parm(GPS disabled, EKF3 ExternalNav) - Config preset:
SITL_VISION_CONFIG(PoseSource.VISION)
Simulated Sensors¶
| Real sensor | Gazebo sensor | Topic (ROS 2) | Notes |
|---|---|---|---|
| RealSense D435i (front) | rgbd_camera |
/front_camera/image, /front_camera/depth_image, /front_camera/points |
640x480, RGB + depth + point cloud |
| Arducam (down) | camera |
/down_camera |
640x480 RGB, downward-facing |
| TFLuna lidar (down) | SITL simulated sonar | /mavros/rangefinder/rangefinder |
RNGFND1_TYPE=1, ground distance from physics |
| TFLuna lidar (down) | gpu_lidar |
/lidar/range |
Direct LaserScan via Gazebo, 1-sample rangefinder |
The table above is the ArduPilot world. On both firmwares the SDK reads the
downward rangefinder from /mavros/rangefinder/rangefinder: ArduPilot derives it
from the SITL sonar, while PX4 fuses the x500_nectar gz gpu_lidar into a
distance_sensor and streams it as MAVLink DISTANCE_SENSOR (see
simulation/config/px4_config_sitl.yaml). ArduPilot additionally exposes the raw
Gazebo gpu_lidar LaserScan on /lidar/range.
Gazebo GUI¶
Both world SDFs include built-in GUI plugins (no extra windows needed):
- ImageDisplay panels for front RGB, front depth, and down camera (start collapsed, click to expand)
- TopicEcho for viewing any Gazebo transport topic live
- WorldStats showing sim time, real time, RTF
Installation¶
ArduPilot — clones ~/ardupilot + builds ArduCopter SITL, then installs Gazebo Harmonic + ArduPilotPlugin + ros_gz_bridge:
PX4 — clones ~/PX4-Autopilot + builds px4_sitl + Gazebo, and symlinks the Nectar shared assets (x500_nectar, outdoor_field_scenery, outdoor_field_px4) into the PX4 tree. Add ARGS=--native for the uXRCE-DDS path:
Both:
Then reload your shell: source ~/.bashrc.
Usage¶
One pattern for both firmwares. The two-terminal split is unavoidable (the autopilot SITL and the ROS stack are separate processes), so it is symmetric:
- Terminal 1 —
sim-start: the simulator (ArduPilot SITL; for PX4 also Gazebo). - Terminal 2 —
sim-bridge: the ROS stack (Gazebo + MAVROS for ArduPilot; for PX4, MAVROS, MicroXRCE-DDS, or camera-only depending onPROTOCOL).
Choose the scenario with three variables (defaults ardupilot / outdoor /
mavros, so bare make sim-start + make sim-bridge = ArduPilot outdoor over
MAVROS). ENV must match between the two terminals.
FIRMWARE=ardupilot|px4ENV=outdoor|indoorPROTOCOL=mavros|mavlink(direct pymavlink). For PX4,ddsselects the native uXRCE-DDS agent.
| Scenario | Terminal 1 | Terminal 2 | Mission config |
|---|---|---|---|
| ArduPilot outdoor, MAVROS | make sim-start FIRMWARE=ardupilot ENV=outdoor |
make sim-bridge FIRMWARE=ardupilot ENV=outdoor |
MavrosDrone / SITL_GAZEBO_CONFIG |
| ArduPilot outdoor, direct MAVLink | (same Terminal 1) | make sim-bridge FIRMWARE=ardupilot ENV=outdoor PROTOCOL=mavlink |
MavlinkDrone / MAVLINK_SITL_GAZEBO_CONFIG |
| ArduPilot indoor, MAVROS | make sim-start FIRMWARE=ardupilot ENV=indoor |
make sim-bridge FIRMWARE=ardupilot ENV=indoor |
MavrosDrone / SITL_VISION_CONFIG |
| ArduPilot indoor, direct MAVLink | (same Terminal 1) | make sim-bridge FIRMWARE=ardupilot ENV=indoor PROTOCOL=mavlink |
MavlinkDrone / MAVLINK_SITL_VISION_CONFIG |
| PX4 outdoor, MAVROS | make sim-start FIRMWARE=px4 ENV=outdoor |
make sim-bridge FIRMWARE=px4 ENV=outdoor |
Px4MavrosDrone / PX4_SITL_GAZEBO_CONFIG |
| PX4 outdoor, direct MAVLink | (same Terminal 1) | make sim-bridge FIRMWARE=px4 ENV=outdoor PROTOCOL=mavlink |
Px4MavlinkDrone / PX4_MAVLINK_SITL_GAZEBO_CONFIG |
| PX4 outdoor, uXRCE-DDS | (same Terminal 1) | make sim-bridge FIRMWARE=px4 ENV=outdoor PROTOCOL=dds |
Px4DdsDrone / PX4_DDS_SITL_CONFIG |
| PX4 indoor (VIO) | make sim-start FIRMWARE=px4 ENV=indoor |
make sim-bridge FIRMWARE=px4 ENV=indoor |
Px4MavrosDrone / PX4_SITL_VISION_CONFIG |
- ArduPilot: Terminal 1 runs the SITL physics; Terminal 2 launches the Gazebo world +
ros_gz_bridge+ (unlessPROTOCOL=mavlink) MAVROS.mavrosuses SERIAL0 (tcp5760); direct MAVLink connects aMavlinkDroneon SERIAL1 (tcp5762), whichstart_sitl.shalways exposes. - Connection strings differ by transport: MAVROS uses a URL (
tcp://host:port,udp://...); the direct-MAVLinkMavlinkDrone(pymavlink) uses a bare string (tcp:127.0.0.1:5762,udp:host:port, or a serial path like/dev/ttyUSB0). Thetcp://URL form is also accepted forMavlinkDroneand normalized. - PX4: Terminal 1 (
start_px4.sh) runs PX4 and its Gazebo.ENV=outdoorspawnsx500_nectarinto the sharedoutdoor_field_px4.sdf(matched sensors);ENV=indooruses PX4'sx500_vision(GPS-denied onboard VIO). Terminal 2 runs MAVROS (+ camera bridges for outdoor; + the external-vision relay for indoor). PX4 exposes the offboard MAVLink API on UDP14540. WithPROTOCOL=mavlink, Terminal 2 skips MAVROS (camera bridges only) and aPx4MavlinkDroneconnects to UDP14540directly (pymavlinkudp:0.0.0.0:14540); the rangefinder then arrives as MAVLinkDISTANCE_SENSOR, no MAVROS. - PX4 uXRCE-DDS (
PROTOCOL=dds): Terminal 2 runsMicroXRCEAgent(udp4 :8888); PX4's onboard uXRCE-DDS client connects to it, exposing/fmu/* topics thatPx4DdsDronereads/writes directly (no MAVROS). One-time setup:make sim-install FIRMWARE=px4 ARGS=--native(buildspx4_msgs+ the agent).px4_msgsmust match the PX4 firmware (topics are versioned, e.g.vehicle_status_v4). - Indoor adds the vision pipeline: ArduPilot uses
gz_vision_source→vision_pose_node→/mavros/vision_pose/pose_cov; PX4 fuses onboard VIO. Same bridges as real hardware. - Forward extra launch/script args with
ARGS=..., e.g. a one-off mavros toggle or rangefinder test world:
- Custom mission arenas (recommended): competition packages ship scenery only (
model.config+model.sdf+ meshes). Nectar composes the vehicle stack (plugins, iris, cameras, lidar).ENVselects indoor ExternalNav vs outdoor GPS; map and spawn are launch args:
make sim-start FIRMWARE=ardupilot ENV=indoor
make sim-bridge FIRMWARE=ardupilot ENV=indoor \
ARGS='scenery:=model://my_arena spawn_pose:="-5 0 0.195 0 0 0" resource_path:=/path/to/pkg/simulation/models'
| Arg | Role |
|---|---|
world:=outdoor\|indoor |
Compose from Nectar vehicle template (+ stock scenery if scenery empty) |
scenery:=model://name |
Replace stock scenery with a mission model |
spawn_pose:="x y z r p y" |
Iris pose (degrees); empty = template default |
resource_path:=a:b |
Extra dirs on GZ_SIM_RESOURCE_PATH (mission simulation/models) |
world:=/abs/path.sdf |
Escape hatch: full custom world (must embed drone/sensors yourself) |
Composed worlds use fixed names nectar_indoor / nectar_outdoor (pose topic /world/<name>/dynamic_pose/info). Mission packages must not hard-code iris/cameras in their SDF.
- Headless ArduPilot without Gazebo (pure MAVROS): run
./scripts/simulation/start_sitl.shthenros2 launch nectar sitl.launch.pydirectly.
Run
make sim-stopbefore relaunching Terminal 2 — it clears every simulation process for both firmwares (arducopter/px4, Gazebo, MAVROS, bridges, vision nodes).
Shared-world architecture¶
Nectar owns the vehicle stack; missions own scenery.
simulation/templates/{indoor,outdoor}_vehicle.sdf.in— ArduPilot compose templates (plugins, iris, Nectar cameras/lidar). Launch substitutes scenery + spawn.simulation/models/indoor_room_scenery/— stock 20×20×12 m indoor room + obstacles.simulation/models/outdoor_field_scenery/— outdoor gate + obstacles. Also used byoutdoor_field.sdf/outdoor_field_px4.sdf.simulation/models/x500_nectar/— PX4x500+ matched sensors. Outdoor PX4 still uses shared scenery via include.simulation/worlds/outdoor_field_px4.sdf— scenery-only world for PX4 (no world<plugin>tags; PX4'sserver.configinjects systems). GPS origin near-zero declination (lat 0, lon 40), not ArduPilot's Canberra — see magnetometer notes in prior docs (gz-sim#2536).
Mission package layout:
No iris, cameras, or Gazebo world plugins in the mission package.
install_px4.sh symlinks the three Nectar assets into PX4's Tools/simulation/gz/{models,worlds} so PX4's launcher can find them while the source of truth stays in nectar-sdk/. start_px4.sh --autostart reuses PX4's existing 4001 (x500) airframe via PX4_SYS_AUTOSTART, so no PX4-tree airframe file is added.
Stop all¶
One stop for both firmwares: kills arducopter, PX4 (px4_sitl/bin/px4), MicroXRCEAgent, Gazebo, MAVROS, ros_gz_bridge, gz_vision_source, and vision_pose_node processes.
Verify sensors¶
| Check | Command |
|---|---|
| State | ros2 topic echo /mavros/state --once |
| GPS (outdoor) | ros2 topic echo /mavros/global_position/global --once --qos-reliability best_effort |
| Vision pose (indoor) | ros2 topic echo /mavros/vision_pose/pose_cov --once |
| Rangefinder | ros2 topic echo /mavros/rangefinder/rangefinder --once |
| Front camera | ros2 topic echo /front_camera/image --once |
| Depth | ros2 topic echo /front_camera/depth_image --once |
Multi-orientation rangefinders¶
rangefinder_test.sdf places an iris with front and back single-ray lidars
between two walls. The iris_with_rangefinders model forwards the front/back
lidars to ArduPilot as rng_2/rng_3, and rangefinder_test.parm exposes them
as RNGFND2/3 (orientations forward/back). The downward rangefinder
(RNGFND1, orientation down) stays on the analog SITL sonar from gazebo.parm:
on the ground the airframe is only ~0.2 m tall, so a downward GPU lidar reads
below its minimum range, while the sonar reports vehicle height above terrain
reliably. ArduPilot emits one DISTANCE_SENSOR per instance, accessible through
drone.distance_sensors and drone.get_distance(orientation).
Terminal 1 — SITL with the three rangefinders:
./scripts/simulation/start_sitl.sh --gazebo \
--params nectar/simulation/params/rangefinder_test.parm
Terminal 2 — Gazebo (and MAVROS) with the test world:
Terminal 3 — inspect the readings (MAVROS topics):
ros2 topic echo /mavros/rangefinder/rangefinder --once
ros2 topic echo /mavros/distance_sensor/rangefinder/front --once
In code, both transports expose every reported unit via drone.distance_sensors
and drone.get_distance(orientation) (the downward unit also updates
drone.rangefinder); see the vehicle core README.
The MAVROS path relies on the distance_sensor plugin entries in
config/apm_config_sitl.yaml (rangefinder/rangefinder, rangefinder/front,
rangefinder/back) mapped to orientations.
Configuration Presets¶
Defined in nectar/control/config.py:
| Preset | Transport | Port | PoseSource | Lidar | Use case |
|---|---|---|---|---|---|
SITL_CONFIG |
mavros | 5760 | GPS | No | Headless SITL, no sensors |
SITL_GPS_CONFIG |
mavros | 5760 | GPS | No | Headless SITL with GPS |
SITL_GAZEBO_CONFIG |
mavros | 5760 | GPS | Yes | Gazebo outdoor |
SITL_VISION_CONFIG |
mavros | 5760 | VISION | Yes | Gazebo indoor |
MAVLINK_SITL_CONFIG |
mavlink | 5760 | GPS | No | Headless SITL, direct pymavlink |
MAVLINK_SITL_GAZEBO_CONFIG |
mavlink | 5762 | GPS | No | Gazebo outdoor, direct (SERIAL1, alongside MAVROS) |
MAVLINK_SITL_VISION_CONFIG |
mavlink | 5762 | VISION | No | Gazebo indoor, direct (vision feed from /visual_slam/tracking/vo_pose_covariance) |
PX4_SITL_CONFIG |
px4 | 14540 | GPS | No | PX4 SITL headless (offboard over MAVROS) |
PX4_SITL_GAZEBO_CONFIG |
px4 | 14540 | GPS | Yes | PX4 SITL + Gazebo (x500_nectar, outdoor) |
PX4_SITL_VISION_CONFIG |
px4 | 14540 | VISION | No | PX4 SITL + Gazebo indoor (EKF2 external vision) |
PX4_MAVLINK_SITL_CONFIG |
px4_mavlink | 14540 | GPS | No | PX4 SITL headless, direct pymavlink |
PX4_MAVLINK_SITL_GAZEBO_CONFIG |
px4_mavlink | 14540 | GPS | Yes | PX4 SITL + Gazebo (x500_nectar, outdoor), direct pymavlink |
PX4_MAVLINK_SITL_VISION_CONFIG |
px4_mavlink | 14540 | VISION | No | PX4 SITL + Gazebo indoor, direct pymavlink (VISION_POSITION_ESTIMATE) |
PX4_DDS_SITL_CONFIG |
px4_dds | 8888 | GPS | Yes | PX4 SITL native uXRCE-DDS (MicroXRCEAgent on 8888), outdoor |
from nectar.control import (
DroneFactory,
SITL_GAZEBO_CONFIG,
MAVLINK_SITL_GAZEBO_CONFIG,
PX4_SITL_GAZEBO_CONFIG,
PX4_MAVLINK_SITL_GAZEBO_CONFIG,
)
Outdoor over MAVROS (port 5760):
Outdoor over direct MAVLink (port 5762, sim-bridge ... PROTOCOL=mavlink):
PX4 over MAVROS (offboard UDP 14540, sim-start/sim-bridge FIRMWARE=px4 ENV=outdoor):
PX4 over direct pymavlink (offboard UDP 14540, sim-bridge ... PROTOCOL=mavlink):
Test Suite¶
Per-distro outdoor protocol status lives in docs/COMPATIBILITY.md (Simulation section). Tier-3 gate per protocol: make verify-sitl FIRMWARE=<ardupilot|px4> PROTOCOL=<mavros|mavlink|dds> — maps to the scenario rows below.
sitl_test.py runs atomic navigation tests. Each test starts from a clean hover and verifies a specific capability.
Usage¶
# All outdoor tests over MAVROS (37 tests, tcp 5760)
python3 nectar/nectar/examples/simulation/sitl_test.py
# Same suite over direct pymavlink (MavlinkDrone, tcp 5762)
python3 nectar/nectar/examples/simulation/sitl_test.py --mavlink
# Indoor-compatible subset (vision config, skips the 5 GPS-only tests -> 32 tests)
python3 nectar/nectar/examples/simulation/sitl_test.py --indoor
# Land between tests for a full reset
python3 nectar/nectar/examples/simulation/sitl_test.py --fresh pid_fwd
# Specific tests / a group / list everything
python3 nectar/nectar/examples/simulation/sitl_test.py pid_fwd setpoint_fwd
python3 nectar/nectar/examples/simulation/sitl_test.py --group vel
python3 nectar/nectar/examples/simulation/sitl_test.py --list
Flags: --mavlink (direct pymavlink on tcp 5762), --px4 (PX4 over MAVROS, offboard on udp 14540), --indoor (vision config, skip GPS-only tests), --fresh (land between tests).
Test groups¶
| Group | Tests | Description |
|---|---|---|
vel |
vel_fwd, vel_lat, vel_up, vel_yaw, vel_takeoff, vel_world, vel_world_north, vel_world_rotated, brake | Velocity in BODY/WORLD/TAKEOFF frames |
pid |
pid_fwd, pid_lat, pid_alt, pid_yaw | PID navigation with raw GPS |
pid_local |
pid_local_fwd, pid_local_lat, pid_local_yaw | PID navigation with EKF local position |
setpoint |
setpoint_fwd, setpoint_lat, setpoint_yaw | Local position setpoint publishing |
setpoint_global |
setpoint_global, setpoint_global_yaw | GPS global setpoint (outdoor only) |
wpnav |
setpoint_wpnav | AC_WPNav waypoint setpoint |
rtl |
rtl_pid, rtl_ardupilot | Return to launch |
yaw |
vel_yaw, pid_yaw, pid_local_yaw, setpoint_yaw, setpoint_global_yaw, yaw_direction, yaw_takeoff_ref | Yaw handling across methods |
world |
vel_world, vel_world_north, vel_world_rotated | WORLD-frame velocity |
nav |
pid_fwd, pid_lat, pid_local_fwd, pid_local_lat | Core PID navigation |
compound |
sequential, takeoff_ref | Multi-step sequences |
square |
sq_pid, sq_pid_takeoff, sq_pid_local, sq_setpoint, sq_setpoint_global, sq_wpnav | 3m square patterns |
Pre-flight tests (sensors, heading_enu) run without takeoff. GPS-only tests (skipped with --indoor): heading_enu, setpoint_global, setpoint_global_yaw, sq_setpoint_global, rtl_ardupilot.
The set_speed test exercises MAV_CMD_DO_CHANGE_SPEED (horizontal/climb/descent). It is not
in the pre-flight set because ArduCopter accepts that command only in a nav-capable mode
(GUIDED), so it must run airborne.
ArduPilot Parameters¶
gazebo.parm (loaded for all Gazebo sessions)¶
| Parameter | Value | Purpose |
|---|---|---|
SIM_SONAR_SCALE |
10 | SITL sonar scaling factor |
RNGFND1_TYPE |
1 | Analog rangefinder driven by SIM_SONAR |
RNGFND1_SCALING |
10 | Voltage-to-distance scaling |
RNGFND1_PIN |
0 | Analog pin |
RNGFND1_MAX |
40 | Max range (m) |
RNGFND1_MIN |
0.10 | Min range (m) |
indoor.parm (loaded additionally for indoor)¶
| Parameter | Value | Purpose |
|---|---|---|
GPS1_TYPE |
0 | Disable GPS |
EK3_SRC1_POSXY |
6 | ExternalNav for XY position |
EK3_SRC1_VELXY |
6 | ExternalNav for XY velocity |
EK3_SRC1_POSZ |
1 | Barometer for Z (default) |
EK3_SRC1_YAW |
6 | ExternalNav for yaw |
VISO_TYPE |
1 | Enable visual odometry input |
ARMING_CHECK |
388598 | Disable GPS-related arming checks |
Indoor Vision Pose Pipeline¶
flowchart LR
GzPose["Gazebo SceneBroadcaster<br/>/world/nectar_indoor/dynamic_pose/info<br/>gz.msgs.Pose_V"]
Bridge["ros_gz_bridge<br/>gz.msgs.Pose_V → tf2_msgs/TFMessage"]
VPB["gz_vision_source.py<br/>Selects iris model pose"]
Topic["/visual_slam/tracking/vo_pose_covariance<br/>(canonical VSLAM topic)"]
VPN["vision_pose_node (mavros)<br/>or MavlinkDrone VisionPoseBridge"]
FCU["ArduPilot EKF3<br/>ExternalNav fusion"]
GzPose --> Bridge --> VPB --> Topic --> VPN --> FCU
The gz_vision_source.py node replaces the real RealSense D435i + Isaac ROS VSLAM pipeline. It selects the iris model pose from the Gazebo ground-truth TFMessage and publishes PoseWithCovarianceStamped on the same canonical topic Isaac ROS Visual SLAM uses, /visual_slam/tracking/vo_pose_covariance. The downstream delivery is then identical to real hardware: vision_pose_node (backend mavros) relays it to /mavros/vision_pose/pose_cov, or MavlinkDrone's VisionPoseBridge (backend mavlink) forwards it as VISION_POSITION_ESTIMATE.
Frame-name fallback: it first matches the transform whose child_frame_id equals the model name. On ROS 2 Jazzy the ros_gz_bridge Pose_V → TFMessage conversion strips the frame ids, leaving child_frame_id empty. When all names are empty the node falls back to the transform at model_index (default 0, the iris model root) and logs a one-time warning, so the indoor pipeline publishes reliably on Jazzy.
Layout¶
Simulation assets (nectar/simulation/):
params/— ArduPilot SITL parameter files:gazebo.parm(rangefinders),indoor.parm(no-GPS + EKF3 ExternalNav),rangefinder_test.parm(RNGFND2/3 for the test world)config/— MAVROS bridge profiles:apm_config_sitl.yaml/apm_pluginlists_sitl.yaml(ArduPilot),px4_config_sitl.yaml/px4_pluginlists_sitl.yaml(PX4)models/—indoor_room_scenery,outdoor_field_scenery,iris_with_rangefinders,x500_nectartemplates/—indoor_vehicle.sdf.in,outdoor_vehicle.sdf.in(composed bysitl_gazebo.launch.py)worlds/—outdoor_field.sdf,outdoor_field_px4.sdf,indoor_room.sdf,rangefinder_test.sdf
Install and start scripts (scripts/simulation/): install_sitl.sh, install_gazebo.sh,
install_px4.sh, start_sitl.sh, start_px4.sh, and gz_vision_source.py (Gazebo ground-truth
to canonical VSLAM pose topic).
Launch files (nectar/launch/): sitl.launch.py (MAVROS-only), sitl_gazebo.launch.py
(ArduPilot Gazebo + ros_gz_bridge), px4_sitl.launch.py (PX4 bridge). The navigation test suite
is examples/simulation/sitl_test.py.