Hacktoberfest 2026: the issues maintainers tagged for October, open and beginner-friendly. Browse Hacktoberfest issues

[SimpleFlight] takeoff_async() ends early and returns False in stock Blocks

Open
#236 2 comments 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 1 day

Nobody has claimed this yet.

Assessment

Difficulty
4/5
Estimated time
3-5 days
Newbie friendliness
50/100
Issue type
Bug
Clarity
Mostly clear
Activity status
Active
Tech stack
cpp, python, unreal-engine
Domain
backend, game-dev

Research direction

Start with vehicle_apis/multirotor_api/src/multirotor_api_base.cpp, especially MoveOnPathInternal and MoveToPathPosition, then compare their completion behavior with the reported takeoff measurements. Reproduce with the provided Python script against stock Blocks v1.0.1 and inspect the default scene and quadrotor settings if needed. Done means takeoff reaches the requested 2 m height and reports success or a failure reason; the report says latest main was not built or runtime-tested.

Written by the indexing model from the issue text.

Description

Bug report

What's the issue you encountered?

In the official Blocks demo, the drone starts taking off, but the command ends before it reaches its takeoff height. takeoff_async() returns False after about 3 seconds. Increasing the timeout from 20 to 60 seconds does not resolve it. This happens with the default scene and drone configuration.

Expected: the drone completes takeoff and reports success, or reports a failure with a reason.

Include full error message in text form

No exception or error message is returned—only False.

Command Return Elapsed simulation time Actual ascent
Takeoff, 20 s timeout False 2.709 s 1.258 m
Takeoff, 60 s timeout False 3.234 s 1.517 m
Height-setpoint control, 6 s duration True 6.000 s 1.998 m

The stock Simple Flight setting requests a 2 m ascent (takeoff_z=-2.0f). For comparison, move_by_velocity_z_async() reaches that height in the same scene. Its arrival was checked using actual position, not just its return value.

Environment
  • Project AirSim: official Windows UE 5.2 Blocks v1.0.1; server/package commit deff6aa11dd8961ae5de05ea64630995a12bbf67.
  • Autopilot: built-in Simple Flight, quadrotor-x, Fast Physics.
  • OS: Windows 11 x64. Python 3.11.17; projectairsim==1.0.2.
  • Latest main: relevant source inspected at 7b1f4124ca2e275674ab2911da62c531ccc1cd8a; not built or runtime-tested.
Settings

Unmodified v1.0.1 scene_basic_drone.jsonc and robot_quadrotor_fastphysics.jsonc. Default 3 ms steppable clock; Blocks running windowed with a 60 FPS cap.

How can the issue be reproduced?
  1. Start official Blocks v1.0.1 and wait for it to finish loading.

  2. Save the script below as repro_stock_takeoff.py and run it with Python client 1.0.2:

    python repro_stock_takeoff.py --sim-config <release-example-sim_config-directory> --output evidence-new
    

    Set --sim-config to the release's client/python/example_user_scripts/sim_config directory.

  3. The script reloads the scene for each case, waits for the drone to settle, enables control, arms, and records the command result and height. Each case ends with landing and disarming.

Possible cause—please confirm

In MultirotorApiBase:

  • Once the lookahead target reaches the final segment, MoveOnPathInternal can exit when goal_dist <= 0.01. That variable measures movement along the path, not the remaining distance to the endpoint. Could this end takeoff before completion is marked?
  • MoveToPathPosition also commands zero vertical speed within 0.5 m of its local target. The comment describes an oscillation workaround. How should this interact with the takeoff completion check?

These are investigation leads; I have not validated a source fix. I found no matching Issue or pending PR during the 2026-10-23 search.

Reproduction code
Complete script used for the stock Blocks reproduction
"""Reproduce takeoff in stock Blocks using unmodified release JSONC files."""
import argparse
import asyncio
import hashlib
import json
import math
import platform
import time
from datetime import datetime
from importlib.metadata import version
from pathlib import Path

from projectairsim import Drone, ProjectAirSimClient, World


async def resolve(request, timeout=90):
    return await asyncio.wait_for(await request, timeout)


def speed(state):
    return math.sqrt(sum(v*v for v in state["twist"]["linear"].values()))


async def run_case(config, output, mode, timeout):
    path = output / f"{mode}-{timeout}.json"
    if path.exists():
        raise FileExistsError(path)
    record = {"mode": mode, "timeout_sec": timeout,
              "started_at": datetime.now().astimezone().isoformat(),
              "python": platform.python_version(), "client_version": version("projectairsim"),
              "scene": "scene_basic_drone.jsonc", "config_hashes": {}}
    for name in ("scene_basic_drone.jsonc", "robot_quadrotor_fastphysics.jsonc"):
        record["config_hashes"][name] = hashlib.sha256((config/name).read_bytes()).hexdigest()
    client = ProjectAirSimClient(address="127.0.0.1")
    drone = None
    armed = False
    try:
        client.connect()
        client.socket_services.recv_timeout = 90000
        record["server_commit"] = client.get_build_commit_hash()
        world = World(client, "scene_basic_drone.jsonc", sim_config_path=str(config),
                      delay_after_load_sec=3)
        drone = Drone(client, world, "Drone1")
        initial = drone.get_ground_truth_kinematics()
        if speed(initial) > 0.05 or int(drone.get_landed_state()) != 0:
            raise RuntimeError("Virtual drone has not settled on the ground")
        record["initial"] = initial
        if not drone.enable_api_control() or not drone.arm():
            raise RuntimeError("Virtual drone API control/arming rejected")
        armed = True
        start = time.perf_counter()
        sim_start = world.get_sim_time()
        if mode == "native":
            record["api_result"] = await resolve(drone.takeoff_async(timeout_sec=timeout))
        else:
            target_z = initial["pose"]["position"]["z"] - 2.0
            record["api_result"] = await resolve(
                drone.move_by_velocity_z_async(0, 0, target_z, duration=6))
        record["wall_seconds"] = time.perf_counter()-start
        record["sim_seconds"] = (world.get_sim_time()-sim_start)/1e9
        record["final"] = drone.get_ground_truth_kinematics()
        record["ascent_m"] = (initial["pose"]["position"]["z"]
                              - record["final"]["pose"]["position"]["z"])
        record["height_error_m"] = abs(record["ascent_m"]-2.0)
        record["arrival_verified"] = record["height_error_m"] <= 0.2
    except Exception as exc:  # noqa: BLE001 - preserve evidence, fail after cleanup
        record["error"] = repr(exc)
    finally:
        if drone is not None and armed and client.state:
            try:
                record["land_result"] = await resolve(drone.land_async(timeout_sec=25), 35)
                ground = drone.get_ground_truth_kinematics()
                record["ground"] = ground
                if abs(ground["pose"]["position"]["z"]-initial["pose"]["position"]["z"]) > 0.1 or speed(ground) > 0.1:
                    raise RuntimeError("Touchdown not verified")
                record["disarm_result"] = drone.disarm()
                await asyncio.sleep(0.5)
                record["final_landed_state"] = int(drone.get_landed_state())
                drone.disable_api_control()
            except Exception as exc:  # noqa: BLE001 - retain cleanup failure
                record["cleanup_error"] = repr(exc)
        if client.state:
            client.disconnect()
        path.write_text(json.dumps(record, indent=2), encoding="utf-8")
        print(json.dumps({k: v for k, v in record.items()
                          if k not in {"initial", "final", "ground"}}), flush=True)
    if record.get("error") or record.get("cleanup_error"):
        raise RuntimeError(f"Diagnostic infrastructure failed; inspect {path}")


async def main(args):
    args.output.mkdir(parents=True, exist_ok=True)
    for mode, timeout in (("native", 20), ("native", 60), ("altitude", 20)):
        await run_case(args.sim_config, args.output, mode, timeout)


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--sim-config", type=Path, required=True)
    parser.add_argument("--output", type=Path, required=True)
    asyncio.run(main(parser.parse_args()))

Dominant language
C++
Stars
890
Forks
170
Avg merge
17h 15m
Merged PRs (30d)
8

Getting set up

  • No Dockerfile or Docker Compose file
  • Has a pull request template
  • No contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from iamaisim/ProjectAirSim

All issues in iamaisim/ProjectAirSim

Similar issues

More C++ issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.