-
Notifications
You must be signed in to change notification settings - Fork 32
Expand file tree
/
Copy pathfleet_spawn.py
More file actions
executable file
·184 lines (153 loc) · 7.78 KB
/
Copy pathfleet_spawn.py
File metadata and controls
executable file
·184 lines (153 loc) · 7.78 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
#!/usr/bin/env python
"""Generic Isaac Sim fleet spawner (RFC #380 §2).
Replaces the hardcoded one-/multi-drone example launch scripts when a fleet is
selected: spawn positions, per-robot vehicle (pass-through → the Pegasus Iris
asset today), sensor toggles, and the scene all come from the fleet file named
by ``FLEET_CONFIG_FILE`` (``airstack up --fleet <name>`` exports it and
switches ``ISAAC_SIM_SCRIPT_NAME`` to this script when the default was
untouched).
``FLEET_CONFIG_FILE`` carries the ROBOT-container path
(``/root/AirStack/config/fleets/<name>.yaml``); this container mounts the
checkout at ``/isaac-sim/AirStack``, so the path is remapped onto that mount.
Fleet fields consumed:
- ``robots.<name>.spawn`` — [x, y, z] in meters (default [0, 0, 0.07])
- ``robots.<name>`` — order defines domain_id / vehicle_id (robot N →
domain N, ``network.domain_policy: auto``)
- vehicle manifests (``config/vehicles/<v>/vehicle.yaml``) — any ``lidar*``
sensor entry enables the RTX lidar subgraph for that robot (the
``ENABLE_LIDAR`` env-var equivalent, but per vehicle); any ``stereo_cam``
entry enables the ZED camera subgraph.
- ``sim.scene`` — ``default`` (or unset) = the Pegasus "Default Environment"
(matching the example scripts); any other value: a
``SIMULATION_ENVIRONMENTS`` key, or a ``.usd`` path/URL used verbatim.
Env (see pegasus_app.py): ISAAC_SIM_LIVESTREAM, ISAAC_SIM_HEADLESS,
PLAY_SIM_ON_START.
Import-order contract: everything Isaac/Pegasus (including pegasus_app, which
imports ``carb`` at module scope) is imported inside ``main()`` — the
fleet-parsing helpers below are stdlib+PyYAML only, so this module parses and
imports without an Isaac install (unit tests exercise the mapping directly).
"""
import os
import sys
import yaml
# Robot-container checkout root → this container's checkout mount.
ROBOT_CONTAINER_ROOT = "/root/AirStack"
ISAAC_CONTAINER_ROOT = "/isaac-sim/AirStack"
DEFAULT_SPAWN_Z = 0.07
def remap_fleet_path(fleet_config_file, isaac_root=ISAAC_CONTAINER_ROOT):
"""Map the robot-container FLEET_CONFIG_FILE path onto this container."""
if fleet_config_file.startswith(ROBOT_CONTAINER_ROOT + "/"):
return isaac_root + fleet_config_file[len(ROBOT_CONTAINER_ROOT):]
return fleet_config_file
def load_yaml(path):
with open(path, encoding="utf-8") as f:
return yaml.safe_load(f) or {}
def vehicle_sensor_flags(project_root, vehicle_name):
"""(has_stereo_cam, has_lidar) from the vehicle manifest's sensor list.
A missing/invalid manifest keeps the permissive defaults (camera on,
lidar on) so a mis-mounted config degrades loudly-visibly, not silently
sensor-less.
"""
manifest = os.path.join(project_root, "config", "vehicles", vehicle_name, "vehicle.yaml")
if not os.path.isfile(manifest):
print(f"[fleet_spawn] WARNING: no vehicle manifest at {manifest} — "
f"defaulting to camera+lidar on")
return True, True
sensors = load_yaml(manifest).get("sensors") or []
has_cam = any("cam" in str(s.get("type", "")) for s in sensors if isinstance(s, dict))
has_lidar = any("lidar" in str(s.get("type", "")) for s in sensors if isinstance(s, dict))
return has_cam, has_lidar
def fleet_to_drone_configs(fleet, project_root):
"""Fleet dict → PegasusApp drone_configs (pure function; unit-tested).
Robot N (1-based file order) gets domain_id N — the ``domain_policy: auto``
rule, matching the legacy resolver and ``row_spawn_configs``.
"""
robots = fleet.get("robots") or {}
if not robots:
raise ValueError("fleet has no robots: — nothing to spawn")
defaults = fleet.get("defaults") or {}
configs = []
for i, (name, entry) in enumerate(robots.items(), start=1):
entry = entry or {}
spawn = entry.get("spawn", [0.0, 0.0, DEFAULT_SPAWN_Z])
vehicle = entry.get("vehicle", defaults.get("vehicle", ""))
has_cam, has_lidar = vehicle_sensor_flags(project_root, vehicle)
configs.append({
"domain_id": i, # MAVLink port = 14540 + vehicle_id (= domain_id)
"robot_name": name,
"x_m": float(spawn[0]),
"y_m": float(spawn[1]),
"z_m": float(spawn[2]),
"camera": has_cam,
"lidar": has_lidar,
})
if len(configs) == 1:
# Single-robot fleets must be byte-equivalent to the validated
# example_one script, including the historical single-drone prim and
# node names (multi-style names are for multi-drone scenes).
configs[0]["prim"] = "/World/base_link"
configs[0]["node_name"] = "PX4Multirotor"
return configs
def fleet_env_url(fleet, simulation_environments):
"""Resolve ``sim.scene`` against Pegasus SIMULATION_ENVIRONMENTS.
An exported ``ISAAC_SIM_SCENE`` (from ``airstack up --scene``) wins over
the fleet file's ``sim.scene``.
"""
scene = (fleet.get("sim") or {}).get("scene", "default")
env_scene = os.environ.get("ISAAC_SIM_SCENE", "").strip()
if env_scene:
if scene not in (None, "", "default"):
print(f"[fleet_spawn] --scene override: ISAAC_SIM_SCENE='{env_scene}' "
f"replaces the fleet's sim.scene '{scene}'")
scene = env_scene
if scene in (None, "", "default"):
return simulation_environments["Default Environment"]
if scene in simulation_environments:
return simulation_environments[scene]
if "://" in str(scene) or str(scene).endswith((".usd", ".usda", ".usdc", ".usdz")):
return scene
raise ValueError(
f"sim.scene '{scene}' is neither a SIMULATION_ENVIRONMENTS key nor a USD reference "
f"(keys: {', '.join(sorted(simulation_environments))})"
)
def main():
fleet_config_file = os.environ.get("FLEET_CONFIG_FILE", "")
if not fleet_config_file:
print("[fleet_spawn] ERROR: FLEET_CONFIG_FILE is not set — this script is "
"selected by `airstack up --fleet <name>`; use the example launch "
"scripts for fleetless runs.", file=sys.stderr)
return 1
fleet_path = remap_fleet_path(fleet_config_file)
if not os.path.isfile(fleet_path):
print(f"[fleet_spawn] ERROR: fleet file not found: {fleet_path} "
f"(from FLEET_CONFIG_FILE={fleet_config_file})", file=sys.stderr)
return 1
# <root>/config/fleets/<name>.yaml → <root>
project_root = os.path.dirname(os.path.dirname(os.path.dirname(fleet_path)))
fleet = load_yaml(fleet_path)
drone_configs = fleet_to_drone_configs(fleet, project_root)
print(f"[fleet_spawn] {os.path.basename(fleet_path)}: spawning "
f"{len(drone_configs)} drone(s): "
+ ", ".join(
f"{c['robot_name']}@({c['x_m']:g},{c['y_m']:g},{c['z_m']:g})"
f"{' +cam' if c['camera'] else ''}{' +lidar' if c['lidar'] else ''}"
for c in drone_configs))
# ── Isaac/Pegasus imports — deferred (see module docstring) ──────────────
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from pegasus_app import create_simulation_app
# Must be created before any omni/pegasus imports.
create_simulation_app()
from pegasus.simulator.params import SIMULATION_ENVIRONMENTS # noqa: E402
from pegasus_app import PegasusApp # noqa: E402
PegasusApp(
env_url=fleet_env_url(fleet, SIMULATION_ENVIRONMENTS),
# `airstack up --scene` exports the matching stage scale; default 1.0.
stage_scale=float(os.environ.get("ISAAC_SIM_STAGE_SCALE") or 1.0),
drone_configs=drone_configs,
# Per-robot "camera"/"lidar" keys above override the app-level
# defaults (enable_camera defaults True in PegasusApp).
enable_lidar=False,
).run()
return 0
if __name__ == "__main__":
sys.exit(main())