|
| 1 | +""" |
| 2 | +Copyright (C) 2025 IAMAI CONSULTING CORP |
| 3 | +MIT License. |
| 4 | +
|
| 5 | +Runs a Project AirSim Runtime flight and displays it in the drone position viewer. |
| 6 | +
|
| 7 | +Run from the repository root after starting Project AirSim Runtime. See |
| 8 | +samples/projectairsim_runtime/README.md for Windows and Linux build/run instructions. |
| 9 | +""" |
| 10 | + |
| 11 | +import asyncio |
| 12 | +import threading |
| 13 | +from pathlib import Path |
| 14 | + |
| 15 | +from projectairsim import ProjectAirSimClient, Drone, World |
| 16 | +from projectairsim.drone_position_viewer import DronePositionViewer |
| 17 | +from projectairsim.utils import projectairsim_log |
| 18 | + |
| 19 | +# Resolve the shared sim_config directory even when running from subfolders. |
| 20 | +SIM_CONFIG_PATH = str((Path(__file__).resolve().parent.parent / "sim_config").resolve()) |
| 21 | + |
| 22 | + |
| 23 | +async def drone_sequence(drone): |
| 24 | + api_control_enabled = False |
| 25 | + armed = False |
| 26 | + try: |
| 27 | + # Set the drone to be ready to fly. |
| 28 | + drone.enable_api_control() |
| 29 | + api_control_enabled = True |
| 30 | + drone.arm() |
| 31 | + armed = True |
| 32 | + |
| 33 | + # Command the vehicle to take off and wait until completion. |
| 34 | + projectairsim_log().info("takeoff_async: starting") |
| 35 | + takeoff_task = await drone.takeoff_async() |
| 36 | + await takeoff_task |
| 37 | + projectairsim_log().info("takeoff_async: completed") |
| 38 | + |
| 39 | + # Move up at one meter per second in NED coordinates. |
| 40 | + move_up_task = await drone.move_by_velocity_async( |
| 41 | + v_north=0.0, v_east=0.0, v_down=-1.0, duration=2.0 |
| 42 | + ) |
| 43 | + projectairsim_log().info("Move-Up invoked") |
| 44 | + |
| 45 | + await move_up_task |
| 46 | + projectairsim_log().info("Move-Up completed") |
| 47 | + |
| 48 | + # Move diagonally while maintaining altitude. |
| 49 | + move_diagonal_task = await drone.move_by_velocity_async( |
| 50 | + v_north=-1.0, v_east=1.0, v_down=0.0, duration=3.0 |
| 51 | + ) |
| 52 | + projectairsim_log().info("Move diagonally invoked") |
| 53 | + |
| 54 | + await move_diagonal_task |
| 55 | + projectairsim_log().info("Move diagonally completed") |
| 56 | + |
| 57 | + # Descend and poll task completion to keep this flow explicit. |
| 58 | + move_down_task = await drone.move_by_velocity_async( |
| 59 | + v_north=0.0, v_east=0.0, v_down=1.0, duration=2.0 |
| 60 | + ) |
| 61 | + projectairsim_log().info("Move-Down invoked") |
| 62 | + |
| 63 | + while not move_down_task.done(): |
| 64 | + await asyncio.sleep(0.005) |
| 65 | + projectairsim_log().info("Move-Down completed") |
| 66 | + finally: |
| 67 | + if armed: |
| 68 | + drone.disarm() |
| 69 | + if api_control_enabled: |
| 70 | + drone.disable_api_control() |
| 71 | + |
| 72 | + projectairsim_log().info("Project AirSim Runtime demo completed") |
| 73 | + |
| 74 | + |
| 75 | +def start_drone_thread(drone, completed_event, errors): |
| 76 | + def run_sequence(): |
| 77 | + try: |
| 78 | + asyncio.run(drone_sequence(drone)) |
| 79 | + except Exception as err: |
| 80 | + errors.append(err) |
| 81 | + finally: |
| 82 | + completed_event.set() |
| 83 | + |
| 84 | + thread = threading.Thread( |
| 85 | + target=run_sequence, |
| 86 | + daemon=True, |
| 87 | + ) |
| 88 | + thread.start() |
| 89 | + return thread |
| 90 | + |
| 91 | + |
| 92 | +async def main(): |
| 93 | + # Create a simulation client. |
| 94 | + client = ProjectAirSimClient() |
| 95 | + connected = False |
| 96 | + drone_thread = None |
| 97 | + demo_completed = threading.Event() |
| 98 | + drone_errors = [] |
| 99 | + |
| 100 | + try: |
| 101 | + # Connect to simulation environment. |
| 102 | + client.connect() |
| 103 | + connected = True |
| 104 | + |
| 105 | + # Create a World object to interact with the sim world and load a scene. |
| 106 | + world = World( |
| 107 | + client, |
| 108 | + "scene_basic_drone.jsonc", |
| 109 | + delay_after_load_sec=2, |
| 110 | + sim_config_path=SIM_CONFIG_PATH, |
| 111 | + ) |
| 112 | + |
| 113 | + # Create a Drone object to interact with a drone in the loaded sim world. |
| 114 | + drone = Drone(client, world, "Drone1") |
| 115 | + drone_thread = start_drone_thread(drone, demo_completed, drone_errors) |
| 116 | + # Tk must run on the main thread. The viewer closes when the flight ends. |
| 117 | + DronePositionViewer(drone, close_event=demo_completed) |
| 118 | + |
| 119 | + drone_thread.join() |
| 120 | + if drone_errors: |
| 121 | + raise drone_errors[0] |
| 122 | + |
| 123 | + except Exception as err: |
| 124 | + projectairsim_log().error(f"Exception occurred: {err}") |
| 125 | + raise |
| 126 | + |
| 127 | + finally: |
| 128 | + # Always disconnect to allow clean reconnection in the next run. |
| 129 | + if connected: |
| 130 | + client.disconnect() |
| 131 | + if drone_thread is not None and drone_thread.is_alive(): |
| 132 | + drone_thread.join() |
| 133 | + |
| 134 | + |
| 135 | +if __name__ == "__main__": |
| 136 | + asyncio.run(main()) |
0 commit comments