Skip to content

Commit 00893d8

Browse files
committed
Update python client to create world with just client parameter and previous configs
1 parent 66df900 commit 00893d8

File tree

4 files changed

+197
-32
lines changed

4 files changed

+197
-32
lines changed
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
"""
2+
Loads the existing world from saved config and flies a drone.
3+
"""
4+
5+
import asyncio
6+
from projectairsim import ProjectAirSimClient, Drone, World
7+
from projectairsim.utils import projectairsim_log
8+
from projectairsim.image_utils import ImageDisplay
9+
10+
CONFIG_PATH = "./world_config.json"
11+
12+
async def main():
13+
client = ProjectAirSimClient()
14+
image_display = ImageDisplay()
15+
16+
try:
17+
client.connect()
18+
world = World(client) # sin argumentos
19+
drone = Drone(client, world, "Drone1")
20+
21+
# --- Set up image windows ---
22+
chase_cam_window = "ChaseCam"
23+
image_display.add_chase_cam(chase_cam_window)
24+
client.subscribe(
25+
drone.sensors["Chase"]["scene_camera"],
26+
lambda _, chase: image_display.receive(chase, chase_cam_window),
27+
)
28+
29+
rgb_name = "RGB-Image"
30+
image_display.add_image(rgb_name, subwin_idx=0)
31+
client.subscribe(
32+
drone.sensors["DownCamera"]["scene_camera"],
33+
lambda _, rgb: image_display.receive(rgb, rgb_name),
34+
)
35+
36+
depth_name = "Depth-Image"
37+
image_display.add_image(depth_name, subwin_idx=2)
38+
client.subscribe(
39+
drone.sensors["DownCamera"]["depth_camera"],
40+
lambda _, depth: image_display.receive(depth, depth_name),
41+
)
42+
43+
image_display.start()
44+
45+
# --- Flight demo ---
46+
drone.enable_api_control()
47+
drone.arm()
48+
49+
await (await drone.takeoff_async())
50+
await (await drone.move_by_velocity_async(v_north=0.0, v_east=0.0, v_down=-1.0, duration=4.0))
51+
await (await drone.move_by_velocity_async(v_north=0.0, v_east=0.0, v_down=1.0, duration=4.0))
52+
await (await drone.land_async())
53+
54+
drone.disarm()
55+
drone.disable_api_control()
56+
57+
except Exception as err:
58+
projectairsim_log().error(f"❌ Exception: {err}", exc_info=True)
59+
finally:
60+
client.disconnect()
61+
image_display.stop()
62+
63+
if __name__ == "__main__":
64+
asyncio.run(main())
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
"""
2+
Creates the world and saves its configuration to shared volume.
3+
"""
4+
5+
import asyncio
6+
import os
7+
from projectairsim import ProjectAirSimClient, World
8+
from projectairsim.utils import projectairsim_log
9+
10+
CONFIG_PATH = "./world_config.json"
11+
12+
async def main():
13+
client = ProjectAirSimClient()
14+
15+
try:
16+
client.connect()
17+
world = World(client, "scene_basic_drone.jsonc", delay_after_load_sec=2)
18+
projectairsim_log().info(f"✅ World created")
19+
except Exception as err:
20+
projectairsim_log().error(f"❌ Exception: {err}", exc_info=True)
21+
finally:
22+
client.disconnect()
23+
24+
if __name__ == "__main__":
25+
asyncio.run(main())

client/python/projectairsim/src/projectairsim/utils.py

Lines changed: 56 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -483,7 +483,7 @@ def load_scene_config_as_dict(
483483
"""
484484
projectairsim_log().info(f"Loading scene config: {config_name}")
485485
total_path = os.path.join(sim_config_path, config_name)
486-
filepaths = [total_path, [], []]
486+
filepaths = [total_path, [], [], []]
487487

488488
robot_config_schema = pkg_resources.resource_string(
489489
__name__, "schema/robot_config_schema.jsonc"
@@ -504,26 +504,35 @@ def load_scene_config_as_dict(
504504
if "actors" in data: # read and write the robot-config param in each actor
505505
for actor in data["actors"]:
506506
if actor["type"] == "robot":
507-
actor_path = actor["robot-config"]
508-
total_actor_path = os.path.join(sim_config_path, actor_path)
509-
filepaths[1].append(total_actor_path)
510-
with open(total_actor_path) as e:
511-
temp = commentjson.load(e)
512-
validate_json(temp, robot_config_schema)
513-
actor["robot-config"] = temp
514-
507+
actor_configs = actor["robot-config"]
508+
combined_config = {}
509+
if not isinstance(actor_configs, list):
510+
actor_configs = [actor_configs]
511+
for actor_path in actor_configs:
512+
total_actor_path = os.path.join(sim_config_path, actor_path)
513+
filepaths[1].append(total_actor_path)
514+
with open(total_actor_path) as e:
515+
temp = commentjson.load(e)
516+
combined_config = merge_dicts(combined_config,temp)
517+
validate_json(combined_config, robot_config_schema)
518+
actor["robot-config"] = combined_config
519+
515520
if "environment-actors" in data:
516-
# read and write the env-actor-config param in each env actor
517521
for env_actor in data["environment-actors"]:
518-
if env_actor["type"] == "env_actor":
519-
env_actor_path = env_actor["env-actor-config"]
520-
total_env_actor_path = os.path.join(sim_config_path, env_actor_path)
521-
filepaths[2].append(env_actor_path)
522-
with open(total_env_actor_path) as e:
523-
temp = commentjson.load(e)
524-
if temp.get("script") != None:
525-
validate_trajectory_json(temp["script"])
526-
env_actor["env-actor-config"] = temp
522+
if env_actor["type"] in ["env_actor"]:
523+
env_actor_configs = env_actor["env-actor-config"]
524+
combined_config = {}
525+
if not isinstance(env_actor_configs, list):
526+
env_actor_configs = [env_actor_configs]
527+
for env_actor_path in env_actor_configs:
528+
total_env_actor_path = os.path.join(sim_config_path, env_actor_path)
529+
filepaths[2].append(env_actor_path)
530+
with open(total_env_actor_path) as e:
531+
temp = commentjson.load(e)
532+
if temp.get("script") is not None:
533+
validate_trajectory_json(temp["script"])
534+
combined_config = merge_dicts(combined_config, temp)
535+
env_actor["env-actor-config"] = combined_config
527536

528537
if "tiles-dir" in data and data.get("tiles-dir-is-client-relative"):
529538
# Convert client-relative path into an absolute path before sending to sim
@@ -535,6 +544,34 @@ def load_scene_config_as_dict(
535544

536545
return (data, filepaths)
537546

547+
def merge_dicts(d1, d2):
548+
"""Recursively merges dict d2 into dict d1"""
549+
for k, v in d2.items():
550+
if isinstance(v, collections.abc.Mapping):
551+
d1[k] = merge_dicts(d1.get(k, {}), v)
552+
elif isinstance(v, list) and isinstance(d1.get(k), list):
553+
d1[k] = merge_lists(d1[k], v)
554+
else:
555+
d1[k] = v
556+
return d1
557+
558+
def merge_lists(l1, l2):
559+
"""Merges two lists of dictionaries, matching elements by the 'name' key if present"""
560+
result = l1[:]
561+
for item2 in l2:
562+
if isinstance(item2, dict) and "name" in item2:
563+
# Try to find the corresponding item in l1 based on 'name'
564+
matching_item = next((item1 for item1 in result if item1.get("name") == item2["name"]), None)
565+
if matching_item:
566+
# Merge the dictionaries
567+
merge_dicts(matching_item, item2)
568+
else:
569+
# If no match, append the item from l2
570+
result.append(item2)
571+
else:
572+
# If it's not a dictionary or doesn't have a 'name', just append it
573+
result.append(item2)
574+
return result
538575

539576
def validate_json(json_data, file_name) -> None:
540577
"""Validates a JSON according to a given schema

client/python/projectairsim/src/projectairsim/world.py

Lines changed: 52 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,15 @@
44
MIT License.
55
Python API class for ProjectAirSim World.
66
"""
7+
from pathlib import Path
78
import commentjson
89
from typing import Dict, List
910
import time
1011
from datetime import datetime
1112
import numpy as np
1213
import math
14+
import json
15+
import os
1316
import random
1417

1518
from projectairsim import ProjectAirSimClient
@@ -33,6 +36,10 @@
3336
)
3437
from projectairsim.planners import AStarPlanner
3538

39+
if os.environ.get("INSIDE_DOCKER") == "1":
40+
DEFAULT_CONFIG_PATH = Path("/home/ue4/airsim_shared/world_config.json")
41+
else:
42+
DEFAULT_CONFIG_PATH = Path.home() / "airsim_shared" / "world_config.json"
3643

3744
class World(object):
3845
def __init__(
@@ -55,21 +62,53 @@ def __init__(
5562
self.client = client
5663
self.sim_config_path = sim_config_path
5764
self.sim_instance_idx = sim_instance_idx
58-
self.parent_topic = "/Sim/SceneBasicDrone" # default-scene's ID
65+
self.parent_topic = "/Sim" # default-scene's ID
66+
self._scene_config_name = scene_config_name
67+
self._delay_after_load_sec = delay_after_load_sec
5968

6069
self.sim_config = None
6170
self.home_geo_point = None
62-
if scene_config_name:
63-
config_loaded, config_paths = load_scene_config_as_dict(
64-
scene_config_name,
65-
sim_config_path,
66-
sim_instance_idx,
67-
)
68-
config_dict = config_loaded
69-
self.scene_config_path = config_paths[0]
70-
self.robot_config_paths = config_paths[1]
71-
self.envactor_config_paths = config_paths[2]
72-
self.load_scene(config_dict, delay_after_load_sec=delay_after_load_sec)
71+
72+
if not scene_config_name:
73+
try:
74+
with open(DEFAULT_CONFIG_PATH, "r") as f:
75+
cfg = json.load(f)
76+
scene_config_name = cfg["scene_config_name"]
77+
delay_after_load_sec = cfg.get("delay_after_load_sec", 0)
78+
sim_config_path = cfg.get("sim_config_path", sim_config_path)
79+
sim_instance_idx = cfg.get("sim_instance_idx", sim_instance_idx)
80+
projectairsim_log().info(f"Loaded world config from {DEFAULT_CONFIG_PATH}")
81+
except Exception as e:
82+
raise RuntimeError(f"Could not load previous world config: {e}")
83+
else:
84+
try:
85+
os.makedirs(DEFAULT_CONFIG_PATH.parent, exist_ok=True)
86+
with open(DEFAULT_CONFIG_PATH, "w") as f:
87+
json.dump(
88+
{
89+
"scene_config_name": scene_config_name,
90+
"delay_after_load_sec": delay_after_load_sec,
91+
"sim_config_path": "./sim_config/",
92+
"sim_instance_idx": sim_instance_idx,
93+
},
94+
f,
95+
)
96+
projectairsim_log().info(f"Saved world config to {DEFAULT_CONFIG_PATH}")
97+
except Exception as e:
98+
projectairsim_log().warning(f"Could not save world config: {e}")
99+
100+
# Load scene config
101+
config_loaded, config_paths = load_scene_config_as_dict(
102+
scene_config_name,
103+
sim_config_path,
104+
sim_instance_idx,
105+
)
106+
config_dict = config_loaded
107+
self.scene_config_path = config_paths[0]
108+
self.robot_config_paths = config_paths[1]
109+
self.envactor_config_paths = config_paths[2]
110+
self.envobject_config_paths = config_paths[3]
111+
self.load_scene(config_dict, delay_after_load_sec=delay_after_load_sec)
73112
random.seed()
74113
self.import_ned_trajectory(
75114
"null_trajectory", [0, 1], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0]
@@ -2391,4 +2430,4 @@ def generate_intercept_trajectory(
23912430
# Import the trajectory
23922431
self.import_ned_trajectory(
23932432
traj_name, traj_times, traj_poses_x, traj_poses_y, traj_poses_z
2394-
)
2433+
)

0 commit comments

Comments
 (0)