OptiTrack (1/3): robot-side NatNet client + PX4 external-vision fusion - #374
Draft
JohnYanxinLiu wants to merge 10 commits into
Draft
OptiTrack (1/3): robot-side NatNet client + PX4 external-vision fusion#374JohnYanxinLiu wants to merge 10 commits into
JohnYanxinLiu wants to merge 10 commits into
Conversation
…ion baseline Take the natnet_ros2 package from #367 onto the reworked base: the C++ NatNet client (natnet_ros2_node + client adapter + natnet_logic seam), the base mavros_gp_origin and vision_pose_converter nodes, per-robot natnet_config profiles, launch files, and the co-located C++/Python unit tests. natnet_ros2 is already listed in tests/colcon_unit_test_packages.yaml, so the base's YAML-driven collection picks up the updated unit tests directly — no proxy files. Real-robot PX4 external-vision fusion (px4_param_setter, geoid-corrected origin, EV-pose bounds) is layered on next. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Layer the Hummingbird real-robot fusion pipeline onto natnet_ros2 so an OptiTrack-only drone (no GNSS/mag/baro) fuses mocap pose into PX4 EKF2: - mavros_gp_origin_node: publishes a guarded synthetic GPS origin. On real HW, use_geoid_altitude feeds the egm96-5 geoid undulation (N ≈ 54 m at Lisbon) so mavros's ellipsoidal→AMSL conversion cancels and local z == OptiTrack z (fixes the ~36 m = 90 − 54 boot offset; see docs). Auto-skipped in sim. - vision_pose_converter_node: rate-limited mocap → MAVROS vision_pose bridge. - px4_params.yaml: the external-vision EKF2 param set. - natnet_ros2.launch.py wires the bridges when a robot's vision_pose block is on. px4_param_setter reworked into a **checker** (R3): auto_set=false by default — it reads and *flags* FCU params that differ from the desired set instead of writing them; on_mismatch=warn|halt (default warn). Set the params in QGroundControl; the node is the pre-flight safety net. auto_set=true restores the legacy enforce path. Excludes the duplicate vendored NatNet SDK (sensors/natnet_ros2) and deployment override .envs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Move the PX4 external-vision setup guide into docs/ (was a repo-root markdown) and wire it into the mkdocs nav under Perception. Adapt it to the reworked param checker (auto_set default off; check-and-flag, not enforce), and add a "height datum" section explaining the ~36 m local_z offset: AirStack's 90.0 ellipsoidal world datum minus the egm96-5 geoid undulation (N ≈ 54 m at Lisbon) = 36 m; fixed by publishing the geoid-corrected origin altitude so mavros's conversion cancels. Documents why it's invisible in sim and why the shared 90.0 datum must not be changed globally. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…hema Refine the perception bringup comment on the LAUNCH_NATNET include so it points at the per-robot natnet_config.yaml schema parsed by natnet_ros2.launch.py. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…uning Three defects that together meant the OptiTrack client could never connect to anything, in sim or on a real robot. 1. NATNET_SERVER_IP was unreachable config. natnet_config.yaml resolves it via $(env ...), but docker compose only injects variables named in a service's `environment:` block and no service declared it — not the compose files, not .env, not tests/system/test_optitrack_e2e.py. The client therefore always fell back to its hardcoded default (192.168.123.199), which is neither the in-sim emulator (172.31.0.200) nor any Motive host. Forwarded in robot-base-docker-compose.yaml, defaulting to the emulator so the sim path works unconfigured. 2. The tracked rigid body could never match. robot_1 pinned "Hummingbird" id 1146 while the emulator streams "Drone" id 1, and the NatNet client filters incoming frames by NUMERIC id — a mismatch yields a connected client that silently never publishes. Body name/id now accept $(env ...) (expanded in _build_node_params, with the id still coerced to int) and default to the emulator's body; sites override via NATNET_BODY_NAME / NATNET_BODY_ID. 3. EV tuning was not the deployment-validated set. EKF2_EV_DELAY 8.0 -> 7.0 and EKF2_EVP_NOISE 0.01 -> 0.05. EKF2_EVP_NOISE is not marker precision: it also sets the innovation gate at EKF2_EVP_GATE (default 5) sigma, so 0.01 gave a 5 cm gate that rejected legitimate mocap updates and refused to arm. 0.05 is a 25 cm gate, still far tighter than PX4's 0.1 default. px4_params.yaml keeps the evidence inline, including two results that are expensive to rediscover: raising EKF2_EV_DELAY to 50.0 measurably degrades tracking (the negative best-fit time shift shows the estimate running ahead of truth), and the drift-and-snap excursions were a 90 deg body-yaw offset in the Motive rigid-body definition, not a gate problem — so the fix belongs in Motive, never as yaw compensation in code. Adds two unit tests covering body-field env expansion and the emulator-matching defaults (natnet_ros2: 14 -> 16 passing). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Mocap counterpart to l4t-px4-realrobot.env: same Jetson stack, plus the NatNet server/body settings and LAUNCH_NATNET. Carries the two things that are easy to get wrong and produce no error. The body id must match Motive's streaming id, since the client filters frames numerically and a mismatch just never publishes. And nothing writes the EKF2 external-vision parameters to a real FCU — px4_param_setter only reads them back and warns — so they have to be set once in QGroundControl. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
JohnYanxinLiu
force-pushed
the
johnliu/optitrack-autonomy
branch
from
August 13, 2026 15:29
eaf4b13 to
a1287ca
Compare
JohnYanxinLiu
changed the base branch from
johnliu/test-infra-rework
to
develop
August 13, 2026 15:33
This was referenced Aug 13, 2026
The rigid body a robot tracks is now set only in its natnet_config.yaml profile, keyed by ROBOT_NAME. NATNET_BODY_NAME / NATNET_BODY_ID are gone: a single global env var cannot express per-robot values, so it blocked the multi-robot case the profiles already handle. NATNET_SERVER_IP stays in the environment — one Motive host serves every robot. Comments across the package are cut back to what is not evident from the code. The EKF2 tuning results that were buried in px4_params.yaml move into docs/robot/px4_external_vision.md, which also had stale values (EV_DELAY 15.0, EVP_NOISE 0.01) contradicting the config: that raising EV_DELAY measurably hurts tracking, and that drift-and-snap was a Motive rigid-body yaw offset rather than a gate problem. Kept: the license header, and the note on why the SDK needs a reachability pre-check before Connect(). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
JohnYanxinLiu
force-pushed
the
johnliu/optitrack-autonomy
branch
from
August 13, 2026 16:04
bfa9b5c to
2c467ef
Compare
desired_floor_amsl 0.0 -> 36.0, the world datum (90 m ellipsoidal) expressed in AMSL, so a mocap robot's reported global altitude agrees with sim and the GCS instead of sitting at sea level. The published ellipsoidal origin works out to ~90 m, the datum itself. local_position.z equals the OptiTrack height for any value of this parameter — it only moves the global altitude. Reasoning lives in the external-vision doc, which also now records that GeoPoint.altitude is ellipsoidal by contract, so AMSL must not be sent here. Not yet confirmed on hardware. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
MAVROS constructs the egm96-5 geoid in its UAS core, before any plugin loads, and throws std::invalid_argument if the dataset is absent — mavros_node terminates at startup, so there is no MAVROS at all, GPS or mocap. The image could ship without it. mavros' install_geographiclib_datasets.sh sends the downloader's output to /dev/null and, on failure, prints "Error while installing" and returns without a non-zero exit, so the RUN layer succeeded regardless. The tool it calls, geographiclib-get-geoids, was also only a transitive dependency of ros-mavros rather than something we pinned. Now pins geographiclib-tools and asserts the file landed, so a failed download fails the build. Verified against the shipped image: with the downloader broken the script still exits 0, and the new test -f returns non-zero. This is the dependency the OptiTrack external-vision path needs — mavros_gp_origin resolves the geoid undulation with the same egm96-5 model — hence landing it here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What features did you add and/or bugs did you address?
None — this is the robot-side half of running AirStack on OptiTrack motion capture, brought up
against a real Jetson + Cube Orange in a mocap room.
Lets PX4 fly on mocap instead of GPS:
natnet_ros2connects to a Motive server, publishesthe tracked rigid body, and a bridge feeds it to PX4 EKF2 as external vision.
Three pieces:
natnet_ros2client brought up to theoptitrack_emulationbaseline — anatnet:config schema describing multiple robots and multiple rigid bodies per robot, plus
per-message latency reporting.
mavros_gp_originpublishes a geoid-corrected syntheticGPS origin so
local_position.zequals OptiTrack z (fixing a ~36 m boot offset),vision_pose_converterbridges the mocap pose to MAVROS, andpx4_param_setterchecksthe FCU's EKF2 parameters (
auto_set: falseby default;on_mismatchwarn/halt).overrides/l4t-optitrack-realrobot.env, the mocap counterpart tol4t-px4-realrobot.env.Also fixes three defects found while deploying:
NATNET_SERVER_IPcould never resolve.natnet_config.yamlreads it via$(env ...),but no compose service declared it, so the client always fell back to a hardcoded default and
could reach neither the in-sim emulator nor a real Motive host. Now forwarded in
robot-base-docker-compose.yaml.The tracked rigid body could never match.
robot_1's profile pinned a site-specific body(id 1146) that no emulator streams. The client filters frames by numeric id, so a mismatch
produces a connected client that silently never publishes. It now defaults to the emulator's
body (
Drone, id 1).EV tuning corrected from flight bags —
EKF2_EV_DELAY8.0 → 7.0 andEKF2_EVP_NOISE0.01 → 0.05.
EKF2_EVP_NOISEis not marker precision: it also sets the innovation gate atEKF2_EVP_GATE(default 5) sigma, so 0.01 gave a 5 cm gate that rejected valid mocap updatesand blocked arming. 0.05 is a 25 cm gate, still far tighter than PX4's 0.1 default.
Please add videos and images
N/A — no GUI component. Setup guide:
docs/robot/px4_external_vision.md.How did you implement it?
Per-robot bodies live in
natnet_config.yaml, not the environment. Eachrobots.<robot_name>profile names the bodies that robot tracks, and the launch file selectsthe profile by
ROBOT_NAME.px4_param_setteris a checker, not a writer. Nothing here writes parameters to a real FCU;they are set once in QGroundControl and the node warns on mismatch, so a missed parameter shows
up in the logs rather than in the air.
Tuning rationale lives in the docs, not in comments.
docs/robot/px4_external_vision.mdgained a "Two tuning results worth not rediscovering" section, and its parameter table — which
was stale, still showing
EKF2_EV_DELAY 15.0/EKF2_EVP_NOISE 0.01— was corrected tomatch the config:
EKF2_EV_DELAYis measurably worse, not better. 50.0 vs 7.0: median |odom −mocap| while moving 0.037 → 0.060 m. The negative best-fit time shift is the tell — over-
declaring delay makes EKF2 attribute the measurement to too old a state, so the estimate runs
ahead of truth. It can never compensate for apparent lag.
the Motive rigid-body definition. Fixing the rigid body cut moving error 0.25 → 0.04 m.
Do not add yaw compensation in code;
natnet_ros2andvision_pose_converteraredeliberate identity pass-throughs and a code-side correction would double-compensate.
Comments across the package were cut back to what is not evident from the code. Two kept
deliberately: the license header, and the note on why the SDK needs a reachability pre-check
before
Connect()(it canSIGABRTinstead of returning an error, so the check looks redundantand is not).
How do you run and use it?
Real robot (Jetson):
Verify the chain in order — each stage failing points at a different cause:
Watch the robot container's startup logs for
px4_param_settermismatch warnings — that is thecheck that the FCU is actually configured for external vision.
Testing with PyTest
natnet_ros2unit tests (unitmark, co-locatedtest/) covering the config-flatteninghelpers:
$(env ...)expansion for the server block, per-robot profile selection, body-listflattening, covariance defaults, topic namespacing, and the quaternion canonicalization in
vision_pose_converter.airstack test -m unitAll pass;
natnet_ros2contributes 14. They are hermetic — no Docker, sim or GPU.The hardware path (a real Motive server and FCU) is not covered by automated tests. #375
adds host integration tests that drive this client from an emulated server, and #376 adds a
sim e2e flight.
Documentation
y — adds
docs/robot/px4_external_vision.mdto the nav.Yes. The guide covers the three things that must all be right (EKF2 parameters, frame
conventions, height datum), a parameter table with the reasoning per entry, the two tuning
results above, and a symptom → cause → where-to-look troubleshooting table.
Diagrams of the data path and the height-datum explainer are in the guide.
Versioning
Yes —
0.19.0-alpha.13.🤖 Generated with Claude Code