AirStack 0.19.0
Added
-
Intent flags on
airstack up—--sim isaac|airsim,--robots N,--headless,--play/--no-play,--no-autolaunch,--wait,--dry-run— deriving the coordinated env-var sets (compose profiles, URDF, single/multi Isaac launch script) as exported leaf values, with a resolved-config banner and a per-run.airstack/runs/<ts>/effective_config.envdump; contract-tested intests/meta/test_launch_intent_contract.py(unit mark) -
airstack ready(andairstack up --wait): staged flight-readiness gates mirroring the system-test budgets — containers → sim/clock→ per-robot sentinel nodes → PX4 MAVROS-connected +local_position/odomstreaming (the armable signal) — with per-gate diagnostics and--jsonfor scripts -
Preflight validation in
airstack upon resolved configuration (env >--env-file>.env): one-simulator guard no longer bypassed by--env-file;NUM_ROBOTS>1with the single-drone Isaac script is a named hard error; missing images are listed with animage-pullhint before compose starts an implicit build; missingomni_pass.env/ empty Pegasus submodule / Docker < 29 surfaced on the host (AIRSTACK_SKIP_PREFLIGHT=1downgrades errors to warnings) -
tmux pane output is mirrored to container stdout via shared
.tmux.confhooks, sodocker logs/airstack logsnow show colcon builds,ros2 launchoutput, sim loading, and crashes -
Automatic
unit-tests.ymlPR gate onubuntu-latest, plusrun_meta.jsonoutcome metadata so reports distinguish completed simulation campaigns from collection errors, empty selections, timeouts, and cancellations -
overrides/isaac-optitrack-simulation.env— brings up Isaac Sim with the NatNet emulator and PX4 flying on mocap EKF2 external vision (GPS/baro/range aiding off), i.e. the configurationtests/system/test_optitrack_e2e.pyruns, reproducible by hand -
overrides/l4t-optitrack-realrobot.env— deployment override for a real Jetson robot flying on OptiTrack mocap (PX4 EKF2 external vision instead of GPS): the NatNet server/body settings, plus the multi-NIC and FCU-parameter notes that path needs -
Feature notebook workflow (
use-feature-notebookskill): every agent-implemented feature gets a local, gitignorednotebook/NNN-feature-slug/entry with a status-trackeddesign_spec.md(written before coding) andresults/artifacts + self-containedresults_summary.mdthat populate the feature's PR description -
Battery and telemetry display in GCS RQT control panel (voltage and percentage per robot when MAVROS battery topic is bridged)
-
TARGET_ARCHbuild arg (defaultx86_64) inDockerfile.robotto arch-parametrizeLD_LIBRARY_PATH;docker-compose.yamlpassesTARGET_ARCH: aarch64to thevoxlandl4treal-robot image builds -
ros-${ROS_DISTRO}-mavros-extrasin the robot image (provides the vision_pose plugin used for external-pose deployments) -
overrides/l4t-px4-realrobot.env— site-agnostic deployment override for a single real PX4 robot on a Jetson (aarch64/l4t) -
integrationtest tier (tests/integration/,integrationmark) with a sharedrobot_autonomy_stackfixture (robot container, no sim/GPU) -
waypoint_flightsystem test (tests/system/test_waypoint_flight.py): takeoff → ordered waypoint route viaNavigateTask(dispatched as a dense plan) → land, judged on the odometry track by the standalone stdlib-onlytests/waypoint_checker.py(in-order corridor arrival within--waypoint-tolerance, final goal within--goal-tolerance, per-waypoint--waypoint-timeout); validated end-to-end in Isaac Sim; serves as the standard acceptance check after integrating or swapping a planner module -
Real-robot PX4 external-vision fusion in
natnet_ros2(OptiTrack mocap → EKF2):mavros_gp_origin(geoid-corrected synthetic GPS origin solocal_position.z== OptiTrack z, fixing the ~36 m boot offset),vision_pose_converter, and a PX4 param checker (px4_param_setter,auto_setoff by default;on_mismatchwarn/halt) — setup guide atdocs/robot/px4_external_vision.md -
NatNet server emulator (
optitrack.natnet.emulator, protocol core) — pure-Python OptiTrack Motive server emulation sonatnet_ros2can be driven without hardware; host integration tests (tests/integration/natnet/) wire it to the robot client -
Isaac wrapper for the NatNet emulator (USD scene → server) + natnet Pegasus launch scripts, and a dedicated OptiTrack sim e2e test (
optitrackmark,tests/system/test_optitrack_e2e.py) that flies a Circle trajectory on mocap EKF2 fusion — GPS, baro and range aiding are disabled for the run, so the OptiTrack stream is the vehicle's only position source and cross-track error scores the whole chain
Changed
-
Isaac launch scripts deduplicated onto a shared
pegasus_app.PegasusAppbase (simulation/isaac-sim/launch_scripts/pegasus_app.py): the six scripts become scenario declarations (~40–170 lines each, net −438 lines) with hooks for NatNet/scene-import extras; behavior verified by full system-test parity (liveliness, sensors, takeoff/hover/land on Isaac).ISAAC_SIM_HEADLESSandISAAC_SIM_LIVESTREAMnow work uniformly in every launch script (previously each was honored by only half of them) -
Launch-workflow docs corrected against actual behavior:
ISAAC_SIM_SCENE(nonexistent) replaced byISAAC_SIM_SCRIPT_NAME/ISAAC_SIM_GUI, getting-started reflects the paused-by-default sim and Foxglove UI, isaac docker.md defaults table matches.env, ms-airsim MAVROS ports/FOV/vehicle naming fixed, AGENTS.md uses the realdown/image-buildcommand names -
Unit-test documentation now matches the co-located layout: the
add-unit-testsandrun-system-testsskills and the testing docs record which runner each language uses (C++ gtests viacolcon testunder thebuild_packagesmark; Python via the root harness, pluscolcon testforament_pythonpackages), and stop instructing authors to write@pytest.mark.unitby hand —conftest.pyapplies it by file location -
Ephemeral CI GPU runners spawn via NVIDIA OSMO (not OpenStack);
system-tests.yml/docker-build.ymlstill useairstack-ephemeral -
Default system-test
--simisisaacsim; pass--sim msairsimto opt in to Microsoft AirSim -
-m build_packagesCI runs pullcache_*images instead of baking sim images -
docker-build.ymlretags unchanged images on VERSION bumps (content fingerprint) instead of always rebuilding; floatingcache_*tags still seed PR layer cache -
Automatic OSMO validation runs the pull-only
build_packagesgate whenever a PR is opened, updated, or reopened; GPU-intensive simulation campaigns (including OptiTrack) are selected through/pytestorworkflow_dispatch -
robot-l4tcompose service knobs are now env-overridable (AUTONOMY_ROLE,FCU_URL, and the rosbag path viaBAG_STORAGE_PATH);FCU_URLunquoted so the literal serial path reaches MAVROS -
zed-l4timage: ZED SDK 4.2 → 5.2 with the coupled ROS deps (zed_msgs5.2.1,point_cloud_transport(_plugins)4.x, addbackward_ros) -
Unit tests are defined by
tests/colcon_unit_test_packages.yaml:conftest.pycollects each listed package's co-locatedtest/dir under--import-mode=importliband marks itunit(ament lint files are skipped and run undercolcon test)
Removed
- Pre-co-location unit-test scaffolding: the six per-layer stub READMEs under
tests/robot/(which instructed authors to add tests in directories tests no longer live in) andtests/sim/motive_emulator/README.md(superseded bysimulation/isaac-sim/extensions/optitrack.natnet.emulator/andtests/integration/natnet/)
Fixed
barebones_pegasus_launch.py(the documented template script) crashed withNameError: oson constructionisaac-sim-livestreamcompose service silently produced a black stream whenISAAC_SIM_SCRIPT_NAMEwas a multi-drone script (livestream setup existed only in the single-drone scripts)NATNET_BODY_NAME/NATNET_TARGET_NAMEenv overrides documented by the single-drone NatNet script now actually workairstack upguards (one-simulator, URDF pairing) validated.envonly and were bypassed by--env-file overrides/...; they now check the resolved configurationpytest tests/now collects the co-located unit tests before mark filtering. The old guard skipped injection whenever any path was on the command line, andtests/is a path — CI collected 97 of 252 items and the Python unit tests ran nowhere. Narrowing (pytest tests/system/test_x.py) still skips injection; repository-root and empty-path collection are rejected- Empty CI pytest arguments no longer become
pytest tests/ ""and recurse through the repository; collection/import, setup/teardown, partial, and interrupted artifacts are reported as non-comparable instead of false 0% simulation-policy results, and metric regression runs only for an identical simulation campaign fingerprint - Isaac Sim image: PX4
ubuntu.shno longer fails dpkg configure on the NVIDIA base (ca-certificates/software-properties-common); use--no-nuttx --no-sim-toolslike ms-airsim - Robot image: pin
pytest<8.1and disablelaunch_testingfor colcon unit tests so ROS Jazzy's outdated pytest hook does not abortcolcon test - Robot name resolution now honors a pre-set
ROBOT_NAME(e.g. injected via docker compose) instead of always overriding it from the container/hostname mapping (robot/docker/.bashrc) - Robot name-map catch-all fallback now maps to
unknown_robot(valid ROS namespace token) instead ofunknown-robot(default_robot_name_map.yaml) - l4t robot image: replace dustynv's
/ros_entrypoint.shwith a passthrough so its prebuilt source-ROS libs (olderfastcdr) no longer shadow the apt Jazzy runtime and crash apt-built nodes like MAVROS RECORD_BAGS=truenever brought the bag recorder up on a robot:logging.launch.xmlhardcodedrecord_bag=falseandonboard_autonomy_all.launch.xmlincludes it with no arguments, so the variable was forwarded into the container and read by nobody (onlygcs.launch.xmlconsumed it). With nobag_recordnode running, the GCS control panel'sset_recording_statustoggle had nothing to reach despite being bridged indomain_bridge.yaml/dds_router.yaml. It now readsRECORD_BAGSand selects its topic set viaLOG_CONFIG- Falling back to
unknown_robot/ domain 0 now logs a warning naming both fixes (rename the devicerobot-<n>on the host, or supply aROBOT_NAME_MAP_CONFIG_FILEmatching your hostnames). The fallback itself is unchanged — it deliberately keeps an unidentified robot out of every real robot's namespace — but it used to resolve silently, so the symptoms surfaced far from the cause - Dropped
ROBOT_NAME/ROS_DOMAIN_IDfromoverrides/l4t-px4-realrobot.env: no compose service declares either, so an env file could never set them and the lines were inert bag_record/bag_recording_statuswas bridged GCS -> robot indomain_bridge.yaml, the same direction as the command it answers, so recorder status never reached the GCS and every recording indicator stayed blankbag_record_nodepassed--excludetoros2 bag record, which Jazzy renamed to--exclude-regex. It is now an ambiguous prefix of four options, so argparse rejected the command and any section usingexclude:(includinglog.yaml'sairstacksection, i.e. everything but the cameras) recorded nothing — surfacing only as a usage dump in the node's stdout. Multipleexclude:entries are now alternated into one regex instead of repeating a single-valued flag, which had silently kept only the lastnatnet_config.yaml's$(env NATNET_SERVER_IP ...)could never resolve: no compose service declared the variable, so the NatNet client always fell back to its hardcoded default and could reach neither the in-sim emulator nor a real Motive host. It is now forwarded inrobot-base-docker-compose.yaml, defaulting to the in-sim emulator- The NatNet rigid body tracked by
robot_1defaulted to a site-specific body (id 1146) that no emulator streams; since the client filters frames by numeric id, that produced a connected client that never published. It now defaults to the emulator's body (Drone, id 1). Per-robot bodies are configured in each robot's profile innatnet_config.yaml, selected byROBOT_NAME - OptiTrack external-vision tuning corrected from real-flight bags:
EKF2_EV_DELAY8.0 → 7.0 andEKF2_EVP_NOISE0.01 → 0.05. The old 0.01 gave a 5 cm innovation gate (EKF2_EVP_GATE× 5σ) that rejected valid mocap updates and blocked arming;px4_params.yamlnow records the supporting measurements and the drift-and-snap misdiagnosis so neither is repeated - The synthetic GPS origin now places the mocap floor at the shared world datum (
desired_floor_amsl: 36.0, i.e. 90 m ellipsoidal in AMSL) rather than at sea level, so a mocap robot's reported global altitude agrees with sim and the GCS.local_position.zstill equals the OptiTrack height either way - The robot image could ship without the GeographicLib
egm96-5geoid: mavros'install_geographiclib_datasets.shswallows a failed download and still exits 0, so theRUNlayer succeeded either way, andgeographiclib-toolswas only ever a transitive dependency. MAVROS builds that geoid in its UAS core before any plugin loads and throws if it is missing, somavros_nodedied at startup on affected images.Dockerfile.robotnow pins the tool and asserts the file exists, failing the build instead - An unrecognised
connection_typeinnatnet_config.yamlsilently fell back tounicast, so a typo produced a client that connected on the wrong transport and never received frames.validate_connection_typenow throws andnatnet_ros2_nodefails at startup naming the offending value natnet_ros2_nodeonrobot_1now compares the NatNet server's MODELDEF drone-body count (Drone/Drone1…DroneN, excludingTargetand skeleton bones) againstNUM_ROBOTSafter the handshake and logs an error on mismatch, so a sim launch script andnatnet_config.yamlthat disagree about how many drones exist is caught at startup rather than as a robot that silently never receives frames.NUM_ROBOTSis forwarded into the robot container for it- Isaac Sim PX4 never fused the mocap stream:
EKF2_EV_CTRLdefaults to 0 and the isaac compose set no PX4 parameters, so the emulator could stream perfectly while PX4 flew on sim GPS. The compose now passes the EKF2 external-vision set asPX4_PARAM_*(applied by PX4 SITL'srcSat boot), each defaulting to PX4's own default so non-mocap sims are unaffected; the mocap path opts in - The NatNet emulator hardcoded the drone's streaming id to 1 while the client reads
NATNET_BODY_ID, so a real Motive id desynced the two into a connected client that never published (example_one_px4_pegasus_natnet_launch_script.py) test_optitrack_e2e.py::test_px4_fuses_visionasserted only thatlocal_position/posepublishes, which it does off GPS — the check passed with external vision disabled. It is now the pre-flight gate (an estimate exists) and the Circle flight is the actual proof of fusion- The NatNet emulator is now installed as a Kit extension:
Dockerfile.isaac-rospip-installs it editable into the Isaac python and bind-mounts the repo copy over it (the same pattern aspegasus.simulator), and the natnet launch scriptsenable_extensionit before importing. Being on a Kit--ext-foldersearch path only makes Kit aware of an extension — it does not put the package onsys.path— so the scripts previously died withModuleNotFoundError: No module named 'optitrack'