Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions client/python/example_user_scripts/world_consumer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
"""
Loads the existing world from saved config and flies a drone.
"""

import asyncio
from projectairsim import ProjectAirSimClient, Drone, World
from projectairsim.utils import projectairsim_log
from projectairsim.image_utils import ImageDisplay

CONFIG_PATH = "./world_config.json"

async def main():
client = ProjectAirSimClient()
image_display = ImageDisplay()

try:
client.connect()
world = World(client) # sin argumentos
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

translate comment

drone = Drone(client, world, "Drone1")

# --- Set up image windows ---
chase_cam_window = "ChaseCam"
image_display.add_chase_cam(chase_cam_window)
client.subscribe(
drone.sensors["Chase"]["scene_camera"],
lambda _, chase: image_display.receive(chase, chase_cam_window),
)

rgb_name = "RGB-Image"
image_display.add_image(rgb_name, subwin_idx=0)
client.subscribe(
drone.sensors["DownCamera"]["scene_camera"],
lambda _, rgb: image_display.receive(rgb, rgb_name),
)

depth_name = "Depth-Image"
image_display.add_image(depth_name, subwin_idx=2)
client.subscribe(
drone.sensors["DownCamera"]["depth_camera"],
lambda _, depth: image_display.receive(depth, depth_name),
)

image_display.start()

# --- Flight demo ---
drone.enable_api_control()
drone.arm()

await (await drone.takeoff_async())
await (await drone.move_by_velocity_async(v_north=0.0, v_east=0.0, v_down=-1.0, duration=4.0))
await (await drone.move_by_velocity_async(v_north=0.0, v_east=0.0, v_down=1.0, duration=4.0))
await (await drone.land_async())

drone.disarm()
drone.disable_api_control()

except Exception as err:
projectairsim_log().error(f"❌ Exception: {err}", exc_info=True)
finally:
client.disconnect()
image_display.stop()

if __name__ == "__main__":
asyncio.run(main())
25 changes: 25 additions & 0 deletions client/python/example_user_scripts/world_creator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"""
Creates the world and saves its configuration to shared volume.
"""

import asyncio
import os
from projectairsim import ProjectAirSimClient, World
from projectairsim.utils import projectairsim_log

CONFIG_PATH = "./world_config.json"

async def main():
client = ProjectAirSimClient()

try:
client.connect()
world = World(client, "scene_basic_drone.jsonc", delay_after_load_sec=2)
projectairsim_log().info(f"✅ World created")
except Exception as err:
projectairsim_log().error(f"❌ Exception: {err}", exc_info=True)
finally:
client.disconnect()

if __name__ == "__main__":
asyncio.run(main())
51 changes: 42 additions & 9 deletions client/python/projectairsim/src/projectairsim/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -483,7 +483,7 @@ def load_scene_config_as_dict(
"""
projectairsim_log().info(f"Loading scene config: {config_name}")
total_path = os.path.join(sim_config_path, config_name)
filepaths = [total_path, [], []]
filepaths = [total_path, [], [], []]

robot_config_schema = pkg_resources.resource_string(
__name__, "schema/robot_config_schema.jsonc"
Expand All @@ -504,14 +504,19 @@ def load_scene_config_as_dict(
if "actors" in data: # read and write the robot-config param in each actor
for actor in data["actors"]:
if actor["type"] == "robot":
actor_path = actor["robot-config"]
total_actor_path = os.path.join(sim_config_path, actor_path)
filepaths[1].append(total_actor_path)
with open(total_actor_path) as e:
temp = commentjson.load(e)
validate_json(temp, robot_config_schema)
actor["robot-config"] = temp

actor_configs = actor["robot-config"]
combined_config = {}
if not isinstance(actor_configs, list):
actor_configs = [actor_configs]
for actor_path in actor_configs:
total_actor_path = os.path.join(sim_config_path, actor_path)
filepaths[1].append(total_actor_path)
with open(total_actor_path) as e:
temp = commentjson.load(e)
combined_config = merge_dicts(combined_config,temp)
validate_json(combined_config, robot_config_schema)
actor["robot-config"] = combined_config

if "environment-actors" in data:
# read and write the env-actor-config param in each env actor
for env_actor in data["environment-actors"]:
Expand All @@ -535,6 +540,34 @@ def load_scene_config_as_dict(

return (data, filepaths)

def merge_dicts(d1, d2):
"""Recursively merges dict d2 into dict d1"""
for k, v in d2.items():
if isinstance(v, collections.abc.Mapping):
d1[k] = merge_dicts(d1.get(k, {}), v)
elif isinstance(v, list) and isinstance(d1.get(k), list):
d1[k] = merge_lists(d1[k], v)
else:
d1[k] = v
return d1

def merge_lists(l1, l2):
"""Merges two lists of dictionaries, matching elements by the 'name' key if present"""
result = l1[:]
for item2 in l2:
if isinstance(item2, dict) and "name" in item2:
# Try to find the corresponding item in l1 based on 'name'
matching_item = next((item1 for item1 in result if item1.get("name") == item2["name"]), None)
if matching_item:
# Merge the dictionaries
merge_dicts(matching_item, item2)
else:
# If no match, append the item from l2
result.append(item2)
else:
# If it's not a dictionary or doesn't have a 'name', just append it
result.append(item2)
return result

def validate_json(json_data, file_name) -> None:
"""Validates a JSON according to a given schema
Expand Down
65 changes: 52 additions & 13 deletions client/python/projectairsim/src/projectairsim/world.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@
MIT License.
Python API class for ProjectAirSim World.
"""
from pathlib import Path
import commentjson
from typing import Dict, List
import time
from datetime import datetime
import numpy as np
import math
import json
import os
import random

from projectairsim import ProjectAirSimClient
Expand All @@ -33,6 +36,10 @@
)
from projectairsim.planners import AStarPlanner

if os.environ.get("INSIDE_DOCKER") == "1":
DEFAULT_CONFIG_PATH = Path("/home/ue4/airsim_shared/world_config.json")
else:
DEFAULT_CONFIG_PATH = Path.home() / "airsim_shared" / "world_config.json"

class World(object):
def __init__(
Expand All @@ -55,21 +62,53 @@ def __init__(
self.client = client
self.sim_config_path = sim_config_path
self.sim_instance_idx = sim_instance_idx
self.parent_topic = "/Sim/SceneBasicDrone" # default-scene's ID
self.parent_topic = "/Sim" # default-scene's ID
self._scene_config_name = scene_config_name
self._delay_after_load_sec = delay_after_load_sec

self.sim_config = None
self.home_geo_point = None
if scene_config_name:
config_loaded, config_paths = load_scene_config_as_dict(
scene_config_name,
sim_config_path,
sim_instance_idx,
)
config_dict = config_loaded
self.scene_config_path = config_paths[0]
self.robot_config_paths = config_paths[1]
self.envactor_config_paths = config_paths[2]
self.load_scene(config_dict, delay_after_load_sec=delay_after_load_sec)

if not scene_config_name:
try:
with open(DEFAULT_CONFIG_PATH, "r") as f:
cfg = json.load(f)
scene_config_name = cfg["scene_config_name"]
delay_after_load_sec = cfg.get("delay_after_load_sec", 0)
sim_config_path = cfg.get("sim_config_path", sim_config_path)
sim_instance_idx = cfg.get("sim_instance_idx", sim_instance_idx)
projectairsim_log().info(f"Loaded world config from {DEFAULT_CONFIG_PATH}")
except Exception as e:
raise RuntimeError(f"Could not load previous world config: {e}")
else:
try:
os.makedirs(DEFAULT_CONFIG_PATH.parent, exist_ok=True)
with open(DEFAULT_CONFIG_PATH, "w") as f:
json.dump(
{
"scene_config_name": scene_config_name,
"delay_after_load_sec": delay_after_load_sec,
"sim_config_path": "./sim_config/",
"sim_instance_idx": sim_instance_idx,
},
f,
)
projectairsim_log().info(f"Saved world config to {DEFAULT_CONFIG_PATH}")
except Exception as e:
projectairsim_log().warning(f"Could not save world config: {e}")

# Load scene config
config_loaded, config_paths = load_scene_config_as_dict(
scene_config_name,
sim_config_path,
sim_instance_idx,
)
config_dict = config_loaded
self.scene_config_path = config_paths[0]
self.robot_config_paths = config_paths[1]
self.envactor_config_paths = config_paths[2]
self.envobject_config_paths = config_paths[3]
self.load_scene(config_dict, delay_after_load_sec=delay_after_load_sec)
random.seed()
self.import_ned_trajectory(
"null_trajectory", [0, 1], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0]
Expand Down Expand Up @@ -2391,4 +2430,4 @@ def generate_intercept_trajectory(
# Import the trajectory
self.import_ned_trajectory(
traj_name, traj_times, traj_poses_x, traj_poses_y, traj_poses_z
)
)