Skip to content

Commit 1476fa0

Browse files
committed
feat: Ardupilot-Gazebo drone simulation with video stream for agentic tracking, spatial model and warehouse environment
1 parent a499eac commit 1476fa0

9 files changed

Lines changed: 324 additions & 17 deletions

File tree

dimos/agents/skills/osm.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
# limitations under the License.
1414

1515

16+
from typing import Any
17+
1618
from dimos.agents.annotation import skill
1719
from dimos.core.module import Module
1820
from dimos.core.stream import In
@@ -31,8 +33,8 @@ class OsmSkill(Module):
3133

3234
gps_location: In[LatLon]
3335

34-
def __init__(self) -> None:
35-
super().__init__()
36+
def __init__(self, **kwargs: Any) -> None:
37+
super().__init__(**kwargs)
3638
self._latest_location = None
3739
self._current_location_map = CurrentLocationMap(QwenVlModel())
3840

dimos/robot/all_blueprints.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,10 @@
5151
"demo-osm": "dimos.mapping.osm.demo_osm:demo_osm",
5252
"demo-skill": "dimos.agents.skills.demo_skill:demo_skill",
5353
"drone-agentic": "dimos.robot.drone.blueprints.agentic.drone_agentic:drone_agentic",
54+
"drone-agentic-gazebo": "dimos.robot.drone.blueprints.agentic.drone_agentic:drone_agentic_gazebo",
5455
"drone-basic": "dimos.robot.drone.blueprints.basic.drone_basic:drone_basic",
56+
"drone-basic-gazebo": "dimos.robot.drone.blueprints.basic.drone_basic:drone_basic_gazebo",
57+
"drone-basic-gazebo-spatial": "dimos.robot.drone.blueprints.basic.drone_basic:drone_basic_gazebo_spatial",
5558
"dual-xarm6-planner": "dimos.manipulation.blueprints:dual_xarm6_planner",
5659
"keyboard-teleop-piper": "dimos.robot.manipulators.piper.blueprints:keyboard_teleop_piper",
5760
"keyboard-teleop-xarm6": "dimos.robot.manipulators.xarm.blueprints:keyboard_teleop_xarm6",

dimos/robot/drone/README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ dimos run drone-basic --set outdoor=true
1919

2020
# Agentic with LLM control
2121
dimos run drone-agentic
22+
23+
# Gazebo + ArduPilot SITL
24+
dimos run drone-basic-gazebo
25+
dimos run drone-agentic-gazebo
26+
dimos run drone-basic-gazebo-spatial
2227
```
2328

2429
To interact with the agent, run `dimos humancli` in a separate terminal.
@@ -48,6 +53,27 @@ Composes on top of `drone-basic`, adding autonomous capabilities:
4853
| `Agent` | LLM agent (default: GPT-4o) |
4954
| `WebInput` | Web/CLI interface for human commands |
5055

56+
### `drone-basic-gazebo` / `drone-agentic-gazebo` / `drone-basic-gazebo-spatial`
57+
Same as the non-Gazebo blueprints but with video from Gazebo (RTP/H264 on UDP 5600). Odometry from MAVLink; when SITL sends `LOCAL_POSITION_NED` it is used for position. Spatial variant adds the semantic map (camera + TF).
58+
59+
## Running with Gazebo + ArduPilot
60+
61+
1. **Install ArduPilot SITL** and a GCS (e.g. MAVProxy). See the [ArduPilot development wiki](https://ardupilot.org/dev/).
62+
2. **Install Gazebo and the ArduPilot Gazebo plugin** (Iris quad, camera streaming, etc.): [Using SITL with Gazebo](https://ardupilot.org/dev/docs/sitl-with-gazebo.html). Set `GZ_SIM_SYSTEM_PLUGIN_PATH` and `GZ_SIM_RESOURCE_PATH` as described there.
63+
3. **Start Gazebo** with the Iris world (camera streams to UDP 5600):
64+
```bash
65+
gz sim -v4 -r iris_runway.sdf
66+
```
67+
Enable camera streaming if needed:
68+
```bash
69+
gz topic -t /world/iris_runway/model/iris_with_gimbal/model/gimbal/link/pitch_link/sensor/camera/image/enable_streaming -m gz.msgs.Boolean -p "data: 1"
70+
```
71+
4. **Start SITL** in another terminal:
72+
```bash
73+
sim_vehicle.py -v ArduCopter -f gazebo-iris --model JSON --map --console
74+
```
75+
5. **Run DimOS** (e.g. `dimos run drone-agentic-gazebo`). MAVLink: default UDP 14550. Video: port 5600.
76+
5177
## Installation
5278

5379
### Python (included with DimOS)

dimos/robot/drone/blueprints/agentic/drone_agentic.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,19 @@
2525
from dimos.agents.skills.osm import OsmSkill
2626
from dimos.agents.web_human_input import web_input
2727
from dimos.core.blueprints import autoconnect
28-
from dimos.robot.drone.blueprints.basic.drone_basic import drone_basic
28+
from dimos.robot.drone.blueprints.basic.drone_basic import drone_basic, drone_basic_gazebo
2929
from dimos.robot.drone.drone_tracking_module import DroneTrackingModule
3030

3131
DRONE_SYSTEM_PROMPT = """\
3232
You are controlling a DJI drone with MAVLink interface.
3333
You have access to drone control skills you are already flying so only run move_twist, set_mode, and fly_to.
3434
When the user gives commands, use the appropriate skills to control the drone.
3535
Always confirm actions and report results. Send fly_to commands only at above 200 meters altitude to be safe.
36+
37+
Motion skills: move(x, y, z, duration) and move_forward(distance, speed) use velocity commands.
38+
For direct position use go_to_position(x, y, z, vx_ff, vy_ff, vz_ff): position in local NED (m; z negative = up) with optional velocity feedforward.
39+
Yaw control: use rotate_to(heading_deg) to turn to a heading (0–360°, 0=North, 90=East). Remember you have this for "turn left/right", "face North", etc.
40+
3641
Here are some GPS locations to remember
3742
6th and Natoma intersection: 37.78019978319006, -122.40770815020853,
3843
454 Natoma (Office): 37.780967465525244, -122.40688342010769
@@ -44,7 +49,21 @@
4449
DroneTrackingModule.blueprint(outdoor=False),
4550
GoogleMapsSkillContainer.blueprint(),
4651
OsmSkill.blueprint(),
47-
agent(system_prompt=DRONE_SYSTEM_PROMPT, model="gpt-4o"),
52+
agent(system_prompt=DRONE_SYSTEM_PROMPT, model="gpt-4o-mini"),
53+
web_input(),
54+
).remappings(
55+
[
56+
(DroneTrackingModule, "video_input", "video"),
57+
(DroneTrackingModule, "cmd_vel", "movecmd_twist"),
58+
]
59+
)
60+
61+
drone_agentic_gazebo = autoconnect(
62+
drone_basic_gazebo,
63+
DroneTrackingModule.blueprint(outdoor=False),
64+
GoogleMapsSkillContainer.blueprint(),
65+
OsmSkill.blueprint(),
66+
agent(system_prompt=DRONE_SYSTEM_PROMPT, model="gpt-4o-mini"),
4867
web_input(),
4968
).remappings(
5069
[
@@ -56,4 +75,5 @@
5675
__all__ = [
5776
"DRONE_SYSTEM_PROMPT",
5877
"drone_agentic",
78+
"drone_agentic_gazebo",
5979
]

dimos/robot/drone/blueprints/basic/drone_basic.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
from dimos.core.blueprints import autoconnect
2222
from dimos.core.global_config import global_config
23+
from dimos.perception.spatial_perception import spatial_memory
2324
from dimos.protocol.pubsub.impl.lcmpubsub import LCM
2425
from dimos.robot.drone.camera_module import DroneCameraModule
2526
from dimos.robot.drone.connection_module import DroneConnectionModule
@@ -44,7 +45,7 @@ def _drone_rerun_blueprint() -> Any:
4445

4546
return rrb.Blueprint(
4647
rrb.Horizontal(
47-
rrb.Spatial2DView(origin="world/video", name="Camera"),
48+
rrb.Spatial2DView(origin="world/color_image", name="Camera"),
4849
rrb.Spatial3DView(
4950
origin="world",
5051
name="3D",
@@ -95,6 +96,25 @@ def _drone_rerun_blueprint() -> Any:
9596
websocket_vis(),
9697
)
9798

99+
drone_basic_gazebo = autoconnect(
100+
_vis,
101+
DroneConnectionModule.blueprint(
102+
connection_string=connection_string,
103+
video_port=video_port,
104+
video_source="gazebo",
105+
outdoor=False,
106+
),
107+
DroneCameraModule.blueprint(camera_intrinsics=[1000.0, 1000.0, 960.0, 540.0]),
108+
websocket_vis(),
109+
)
110+
111+
drone_basic_gazebo_spatial = autoconnect(
112+
drone_basic_gazebo,
113+
spatial_memory(),
114+
)
115+
98116
__all__ = [
99117
"drone_basic",
118+
"drone_basic_gazebo",
119+
"drone_basic_gazebo_spatial",
100120
]

dimos/robot/drone/connection_module.py

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
from dimos.msgs.geometry_msgs.Vector3 import Vector3
3737
from dimos.msgs.sensor_msgs.Image import Image
3838
from dimos.robot.drone.dji_video_stream import DJIDroneVideoStream
39+
from dimos.robot.drone.gazebo_video_stream import GazeboVideoStream
3940
from dimos.robot.drone.mavlink_connection import MavlinkConnection
4041
from dimos.utils.logging_config import setup_logger
4142

@@ -52,6 +53,7 @@ def _add_disposable(composite: CompositeDisposable, item: Disposable | Any) -> N
5253
class Config(ModuleConfig):
5354
connection_string: str = "udp:0.0.0.0:14550"
5455
video_port: int = 5600
56+
video_source: str = "dji"
5557
outdoor: bool = False
5658

5759

@@ -92,7 +94,7 @@ def __init__(self, **kwargs: Any) -> None:
9294
"""
9395
super().__init__(**kwargs)
9496
self.connection: MavlinkConnection | None = None
95-
self.video_stream: DJIDroneVideoStream | None = None
97+
self.video_stream: DJIDroneVideoStream | GazeboVideoStream | None = None
9698
self._latest_video_frame = None
9799
self._latest_telemetry = None
98100
self._latest_status = None
@@ -116,7 +118,10 @@ def start(self) -> None:
116118
)
117119
self.connection.connect()
118120

119-
self.video_stream = DJIDroneVideoStream(port=self.config.video_port)
121+
if self.config.video_source == "gazebo":
122+
self.video_stream = GazeboVideoStream(port=self.config.video_port)
123+
else:
124+
self.video_stream = DJIDroneVideoStream(port=self.config.video_port)
120125

121126
if not self.connection.connected:
122127
logger.error("Failed to connect to drone")
@@ -282,6 +287,56 @@ def move(self, x: float = 0.0, y: float = 0.0, z: float = 0.0, duration: float =
282287
if self.connection:
283288
self.connection.move(Vector3(x, y, z), duration)
284289

290+
@skill
291+
def move_forward(self, distance: float, speed: float = 0.3) -> None:
292+
"""Move forward by a given distance in meters.
293+
294+
Args:
295+
distance: Distance to move in meters (positive = forward).
296+
speed: Forward speed in m/s. Default 0.3.
297+
"""
298+
if self.connection and abs(distance) >= 0.01:
299+
duration = abs(distance) / max(0.01, speed)
300+
vx = speed if distance >= 0 else -speed
301+
self.connection.move(Vector3(vx, 0.0, 0.0), duration)
302+
303+
@skill
304+
def go_to_position(
305+
self, x: float, y: float, z: float,
306+
vx_ff: float = 0.0, vy_ff: float = 0.0, vz_ff: float = 0.0,
307+
) -> bool:
308+
"""Send position target in local NED with optional velocity feedforward.
309+
310+
Args:
311+
x: North position in meters (local NED).
312+
y: East position in meters (local NED).
313+
z: Down position in meters (negative = up).
314+
vx_ff: Velocity feedforward North (m/s). Optional.
315+
vy_ff: Velocity feedforward East (m/s). Optional.
316+
vz_ff: Velocity feedforward Down (m/s). Optional.
317+
318+
Returns:
319+
True if position target sent successfully
320+
"""
321+
if self.connection:
322+
return self.connection.set_position_target(x, y, z, vx_ff, vy_ff, vz_ff)
323+
return False
324+
325+
@skill
326+
def rotate_to(self, heading_deg: float, timeout: float = 60.0) -> bool:
327+
"""Rotate drone to a specific heading (yaw control).
328+
329+
Args:
330+
heading_deg: Target heading in degrees (0–360, 0=North, 90=East).
331+
timeout: Max time to rotate in seconds.
332+
333+
Returns:
334+
True if rotation completed successfully
335+
"""
336+
if self.connection:
337+
return self.connection.rotate_to(heading_deg, timeout)
338+
return False
339+
285340
@skill
286341
def takeoff(self, altitude: float = 3.0) -> bool:
287342
"""Takeoff to specified altitude.

dimos/robot/drone/drone_tracking_module.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ def __init__(
6363
x_pid_params: PIDParams | None = None,
6464
y_pid_params: PIDParams | None = None,
6565
z_pid_params: PIDParams | None = None,
66+
**kwargs: Any,
6667
) -> None:
6768
"""Initialize the drone tracking module.
6869
@@ -75,7 +76,7 @@ def __init__(
7576
If None, uses preset based on outdoor flag.
7677
z_pid_params: Optional PID parameters for altitude control.
7778
"""
78-
super().__init__()
79+
super().__init__(**kwargs)
7980

8081
default_params = OUTDOOR_PID_PARAMS if outdoor else INDOOR_PID_PARAMS
8182
x_pid_params = x_pid_params if x_pid_params is not None else default_params

0 commit comments

Comments
 (0)