Skip to content
Merged
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
19 changes: 19 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -873,6 +873,24 @@ add_subdirectory(unity)
add_subdirectory(vehicle_apis)

if(BUILD_TESTING)
# Stage shared test runtime dependencies exactly once. Multiple PRE_LINK and
# POST_BUILD copies to SIMLIBS_TEST_DIR can run concurrently under Ninja and
# intermittently fail on Windows when they touch the same destination files.
add_custom_target(simlibs_test_runtime_dependencies
COMMAND ${CMAKE_COMMAND} -E make_directory "${SIMLIBS_TEST_DIR}"
COMMAND ${CMAKE_COMMAND} -E copy_directory
"${UE_PLUGIN_SIMLIBS_DIR}/shared_libs"
"${SIMLIBS_TEST_DIR}"
COMMAND ${CMAKE_COMMAND} -E copy_directory
"${JSBSIM_CORESIM_DIR}/lib/$<IF:$<CONFIG:Release>,Release,Debug>"
"${SIMLIBS_TEST_DIR}"
COMMENT "Staging SimLibs test runtime dependencies"
)
add_dependencies(simlibs_test_runtime_dependencies jsbsim-repo)
if(UNIX)
add_dependencies(simlibs_test_runtime_dependencies zlib-external)
endif()

set(SIMLIBS_UNIT_TEST_TARGETS
core_sim_gtests
physics_gtests
Expand All @@ -888,6 +906,7 @@ if(BUILD_TESTING)
add_custom_target(simlibs_unit_tests)
foreach(TEST_TARGET ${SIMLIBS_UNIT_TEST_TARGETS})
if(TARGET ${TEST_TARGET})
add_dependencies(${TEST_TARGET} simlibs_test_runtime_dependencies)
add_dependencies(simlibs_unit_tests ${TEST_TARGET})
endif()
endforeach()
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,11 @@ See **[Installing system prerequisites](docs/system_specs.md#installing-system-p

## Quick Start: Run from Source

For controller, client, API, and physics iterations that do not need Unreal
rendering or sensors, use **[Project AirSim Runtime](samples/projectairsim_runtime/README.md)**. It
is the lightweight Project AirSim host and includes flat-ground collision
response for Fast Physics vehicles and landing gear.

Follow these steps to set up and run Project AirSim from source:

### 1. Install Unreal Engine versions 5.2 or 5.7
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -233,4 +233,4 @@ async def main(scenefile):
if __name__ == "__main__":
scene_to_run = "scene_quadtiltrotor_dfw_dynamic_city.jsonc"
projectairsim_log().info('Using scene "' + scene_to_run + '"')
asyncio.run(main(scene_to_run)) # Runner for async main function
asyncio.run(main(scene_to_run)) # Runner for async main function
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
"""
Copyright (C) 2025 IAMAI CONSULTING CORP
MIT License.

Runs a Project AirSim Runtime flight and displays it in the drone position viewer.

Run from the repository root after starting Project AirSim Runtime. See
samples/projectairsim_runtime/README.md for Windows and Linux build/run instructions.
"""

import asyncio
import threading
from pathlib import Path

from projectairsim import ProjectAirSimClient, Drone, World
from projectairsim.drone_position_viewer import DronePositionViewer
from projectairsim.utils import projectairsim_log

# Resolve the shared sim_config directory even when running from subfolders.
SIM_CONFIG_PATH = str((Path(__file__).resolve().parent.parent / "sim_config").resolve())


async def drone_sequence(drone):
api_control_enabled = False
armed = False
try:
# Set the drone to be ready to fly.
drone.enable_api_control()
api_control_enabled = True
drone.arm()
armed = True

# Command the vehicle to take off and wait until completion.
projectairsim_log().info("takeoff_async: starting")
takeoff_task = await drone.takeoff_async()
await takeoff_task
projectairsim_log().info("takeoff_async: completed")

# Move up at one meter per second in NED coordinates.
move_up_task = await drone.move_by_velocity_async(
v_north=0.0, v_east=0.0, v_down=-1.0, duration=2.0
)
projectairsim_log().info("Move-Up invoked")

await move_up_task
projectairsim_log().info("Move-Up completed")

# Move diagonally while maintaining altitude.
move_diagonal_task = await drone.move_by_velocity_async(
v_north=-1.0, v_east=1.0, v_down=0.0, duration=3.0
)
projectairsim_log().info("Move diagonally invoked")

await move_diagonal_task
projectairsim_log().info("Move diagonally completed")

# Descend and poll task completion to keep this flow explicit.
move_down_task = await drone.move_by_velocity_async(
v_north=0.0, v_east=0.0, v_down=1.0, duration=2.0
)
projectairsim_log().info("Move-Down invoked")

while not move_down_task.done():
await asyncio.sleep(0.005)
projectairsim_log().info("Move-Down completed")
finally:
if armed:
drone.disarm()
if api_control_enabled:
drone.disable_api_control()

projectairsim_log().info("Project AirSim Runtime demo completed")


def start_drone_thread(drone, completed_event, errors):
def run_sequence():
try:
asyncio.run(drone_sequence(drone))
except Exception as err:
errors.append(err)
finally:
completed_event.set()

thread = threading.Thread(
target=run_sequence,
daemon=True,
)
thread.start()
return thread


async def main():
# Create a simulation client.
client = ProjectAirSimClient()
connected = False
drone_thread = None
demo_completed = threading.Event()
drone_errors = []

try:
# Connect to simulation environment.
client.connect()
connected = True

# Create a World object to interact with the sim world and load a scene.
world = World(
client,
"scene_basic_drone.jsonc",
delay_after_load_sec=2,
sim_config_path=SIM_CONFIG_PATH,
)

# Create a Drone object to interact with a drone in the loaded sim world.
drone = Drone(client, world, "Drone1")
drone_thread = start_drone_thread(drone, demo_completed, drone_errors)
# Tk must run on the main thread. The viewer closes when the flight ends.
DronePositionViewer(drone, close_event=demo_completed)

drone_thread.join()
if drone_errors:
raise drone_errors[0]

except Exception as err:
projectairsim_log().error(f"Exception occurred: {err}")
raise

finally:
# Always disconnect to allow clean reconnection in the next run.
if connected:
client.disconnect()
if drone_thread is not None and drone_thread.is_alive():
drone_thread.join()


if __name__ == "__main__":
asyncio.run(main())
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,6 @@

import tkinter as tk
import threading
import time

from projectairsim import ProjectAirSimClient, Drone, World
from projectairsim.utils import projectairsim_log


# -----------------------------------------------
# Drone Position Viewer
Expand Down Expand Up @@ -158,7 +153,7 @@ def update_view(self, x, y, altitude):
screen_y + self.dot_size
)

# ---------- ALTITUDE BAR UPDATE (SMOOTH + CLEAN LABELS) ----------
# ---------- ALTITUDE BAR UPDATE (SMOOTH + CLEAN LABELS) ----------
ALT_PIXELS = 100 # pixels per meter
TICK_STEP = 0.05 # fine ticks every 0.05 m (smooth scrolling)
VIEW_RANGE = 5.0 # ±5m window
Expand Down Expand Up @@ -226,4 +221,6 @@ def update_view(self, x, y, altitude):

def on_close(self):
self.stop_event.set()
if self.refresh_after_id is not None:
self.root.after_cancel(self.refresh_after_id)
self.root.destroy()
12 changes: 0 additions & 12 deletions core_sim/test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -147,18 +147,6 @@ target_include_directories(${TARGET_NAME} PRIVATE "${ONNXRUNTIME_ROOTDIR}/includ
target_link_directories(${TARGET_NAME} PRIVATE "${ONNX_LIB_DIR}")
target_link_libraries(${TARGET_NAME} PRIVATE onnxruntime jsbsim)

add_custom_command(
TARGET ${TARGET_NAME}
PRE_LINK
COMMAND ${CMAKE_COMMAND} -E copy_directory ${UE_PLUGIN_SIMLIBS_DIR}/shared_libs/ ${SIMLIBS_TEST_DIR}
)

add_custom_command(
TARGET ${TARGET_NAME}
PRE_LINK
COMMAND ${CMAKE_COMMAND} -E copy_directory ${JSBSIM_CORESIM_DIR}/lib/$<IF:$<CONFIG:Release>,Release,Debug>/ ${SIMLIBS_TEST_DIR}
)

# Include CMake's GoogleTest module to use gtest_discover_tests() helper
include(GoogleTest)
gtest_discover_tests(${TARGET_NAME})
2 changes: 1 addition & 1 deletion docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

### Fixed
- Coordinate conversion precision in the Python ROS bridge
- GPU LiDAR behavior and standalone simulator builds on Unreal Engine 5.7
- GPU LiDAR behavior and Project AirSim Runtime builds on Unreal Engine 5.7

## [0.2.0] - 2026-05-29
### Added
Expand Down
1 change: 0 additions & 1 deletion physics/test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,6 @@ add_custom_command(TARGET ${TARGET_NAME}
POST_BUILD
COMMAND ${CMAKE_COMMAND} -E echo "Copying [${TARGET_NAME}] test data to ${SIMLIBS_TEST_DIR}/test_data"
COMMAND ${CMAKE_COMMAND} -E copy_directory ${TARGET_SOURCE_DIR}/test/test_data ${SIMLIBS_TEST_DIR}/test_data
COMMAND ${CMAKE_COMMAND} -E copy_directory ${JSBSIM_CORESIM_DIR}/lib/$<IF:$<CONFIG:Release>,Release,Debug>/ ${SIMLIBS_TEST_DIR}
)

# Include CMake's GoogleTest module to use gtest_discover_tests() helper
Expand Down
3 changes: 1 addition & 2 deletions samples/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,4 @@
#
# ---------------------------------------------------------------------------------------------------------------------

# add_subdirectory(standalone_sim)
add_subdirectory(standalone_sim)
add_subdirectory(projectairsim_runtime)
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,11 @@
#
# ---------------------------------------------------------------------------------------------------------------------

set(TARGET_NAME standalone_sim)
set(TARGET_NAME projectairsim-runtime)

add_executable(
${TARGET_NAME}
ground_collision_host.cpp
main.cpp
)

Expand Down Expand Up @@ -94,3 +95,25 @@ else()
dl # Required by static libcrypto (dlopen/dlsym)
)
endif()

# Keep the standalone runtime self-contained. Both core_sim and physics use
# shared third-party libraries, so launching the executable directly from its
# documented output directory must not depend on a developer shell or PATH.
if(WIN32)
set(RUNTIME_ONNX_SHARED_LIB "${ONNX_LIB_DIR}/onnxruntime.dll")
else()
set(RUNTIME_ONNX_SHARED_LIB "${ONNXRUNTIME_LIBRARY}")
set_target_properties(${TARGET_NAME} PROPERTIES BUILD_RPATH "$ORIGIN")
endif()

add_custom_command(
TARGET ${TARGET_NAME}
POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${JSBSIM_SHARED_LIB}"
"$<TARGET_FILE_DIR:${TARGET_NAME}>"
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${RUNTIME_ONNX_SHARED_LIB}"
"$<TARGET_FILE_DIR:${TARGET_NAME}>"
COMMENT "Copying Project AirSim Runtime shared-library dependencies"
)
74 changes: 74 additions & 0 deletions samples/projectairsim_runtime/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Project AirSim Runtime

`projectairsim-runtime` is a lightweight, engine-independent Project AirSim
host by IAMAI for fast development iterations. It runs `SimServer`,
controllers, and Project AirSim physics while providing a flat ground collision
host without requiring Unreal Engine.

Build the simulation libraries normally:

```powershell
build.cmd simlibs_debug
```

```bash
./build.sh simlibs_debug
```

Replace `simlibs_debug` with `simlibs_release` for a release build.

The debug executable is generated at
`build/win64/Debug/samples/projectairsim_runtime/projectairsim-runtime.exe` on Windows
and `build/linux64/Debug/samples/projectairsim_runtime/projectairsim-runtime` on Linux.
For a release build, use the corresponding `build/win64/Release` or
`build/linux64/Release` path.

Start the Runtime from the repository root:

```powershell
.\build\win64\Debug\samples\projectairsim_runtime\projectairsim-runtime.exe
```

```bash
./build/linux64/Debug/samples/projectairsim_runtime/projectairsim-runtime
```

With the Project AirSim Python client environment activated, run the demo in a
second terminal:

```bash
python client/python/example_user_scripts/projectairsim_runtime/projectairsim_runtime_demo.py
```

The demo loads `scene_basic_drone.jsonc`, executes a short flight, displays the
vehicle position and altitude, and closes the viewer when the flight completes.

The collision lifecycle mirrors the Unreal plugin:

1. Project AirSim physics calculates the next robot kinematics.
2. Project AirSim Runtime evaluates the new pose against the ground plane.
3. It writes `CollisionInfo` back to the robot.
4. Fast Physics applies its existing landing or collision response.

Project AirSim Runtime uses the root link's inertial body box and configured
wheel links as contact geometry. For fixed landing gear that is not represented
by wheel actuators, pass the distance from the robot origin to the gear contact
point:

```powershell
projectairsim-runtime.exe --ground-clearance 1.25
```

On Linux:

```bash
./projectairsim-runtime --ground-clearance 1.25
```

The ground defaults to `z = 0` in the local NED frame. Change it with
`--ground-height`. The existing positional topics and services port arguments
remain supported.

This host intentionally does not render or emulate Unreal sensors, world
meshes, or robot-to-robot collision. It is intended for quick controller,
client, API, and physics iteration with `fast-physics` robots.
Loading
Loading