The current primary PGO implementation is located at:
- Directory:
src/perception/pgo_gps_fusion/ - Colcon package name:
pgo - Launch entry point:
ros2 launch pgo pgo_launch.py
This implementation is no longer a pure loop-closure PGO. It consists of:
- FAST-LIO2 local odometry
- PGO loop-closure graph optimization
- Optional GPS factor absolute position constraints
- Point cloud:
/fastlio2/body_cloud - Odometry:
/fastlio2/lio_odom - GNSS:
/gnss(whengps.enable=true)
- TF:
map -> odom - Odometry:
/pgo/optimized_odom - Visualization:
/pgo/loop_markers
PGO currently handles 5 tasks:
- Collect keyframes from FAST-LIO2
- Perform loop search and ICP confirmation
- Run pose graph optimization with GTSAM
- Periodically add GPS factors when enabled
- Publish
map -> odomand/pgo/optimized_odom
These values come from the current mainline master_params.yaml:
key_pose_delta_deg: 5.0key_pose_delta_trans: 0.1loop_search_radius: 1.0loop_time_tresh: 60.0loop_score_tresh: 0.15loop_submap_half_range: 5submap_resolution: 0.1min_loop_detect_duration: 5.0
When gps.enable=true, PGO uses the following GNSS-related parameters:
gps.topic: /fix(changed from/gnssto/fixin 2026-03-22 corridor v2)gps.noise_xy: 2.5gps.noise_z: 5.0gps.factor_interval: 10gps.quality_hdop_max: 3.0gps.quality_sat_min: 6gps.drift_threshold: 2.0
New additions as of 2026-03-20:
gps.origin_mode: auto | fixedgps.origin_latgps.origin_longps.origin_altgps.topicinnav-gpsmode can be continuously published as scene-calibrated/gnssbygps_anchor_localizer
Behavioral constraints:
auto: Maintains historical compatibility; uses the first valid GPS to initialize LocalCartesianfixed: Uses the configured fixed ENU origin directly at startup- In
fixedmode, the origin must not be overwritten after the first GPS arrives
This allows PGO's map coordinate frame to share the same geographic reference as gps_anchor_localizer, the goal manager, and the scene route graph.
map -> odom -> base_link
- FAST-LIO2 provides
odom -> base_link - PGO provides
map -> odom - Combined, they yield the global pose
If PGO has not entered normal keyframe and optimization flow, map -> odom may disappear, causing RViz to display blank point clouds and costmaps when the fixed frame is set to map.
pgo_nodecontinuously printedReceived out of order messageafter startup- No stable keyframes
- No stable
map -> odom /pgo/optimized_odomunstable or not publishing
last_message_time in the synchronization state was uninitialized, causing the first pair of synchronized messages to be incorrectly flagged as out-of-order.
Initialized last_message_time to 0.0, ensuring the first pair of matched messages is accepted normally.
This fix has been merged to main. The current mainline PGO startup synchronization state uses the stable initialization version.
| Interface | Type | Description |
|---|---|---|
/fastlio2/body_cloud |
sensor_msgs/PointCloud2 |
PGO keyframe point cloud input |
/fastlio2/lio_odom |
nav_msgs/Odometry |
PGO pose input |
/gnss |
sensor_msgs/NavSatFix |
GPS factor input |
/pgo/optimized_odom |
nav_msgs/Odometry |
Optimized pose output |
map -> odom |
TF | Global correction offset |
/pgo/loop_markers |
Marker | Loop closure visualization |
The first round of real scene bundle outdoor testing confirmed that the current pgo has a higher-priority stability issue.
gps_anchor_localizercan successfully transition through:NO_FIX -> UNSTABLE_FIX -> GNSS_READY -> NAV_READY
- Goal manager can successfully reach:
GOAL_REQUESTEDCOMPUTING_ROUTEFOLLOWING_ROUTE
- The vehicle briefly moves but quickly stops
- Goal manager final state:
FAILED; follow_path_status=6
controller_serverlogs:Controller patience exceededTransform data too old when converting from odom to map
- Launch logs:
pgo_node ... process has died ... exit code -11
- On-site TF results:
- Only
odom -> base_linkremains map -> odomdisappeared
- Only
- The current failure is not a problem with the route graph, destination number menu, or
NAV_READYgating - The real failure point is the
pgo_nodesegfault - Once
pgoexits, the Nav2 controller cannot continue transforming the robot pose fromodomtomap - The result is
follow_pathbeing aborted
- Fixed one clear concurrency bug in
syncCB():- Changed anonymous
lock_guardto a named lock
- Changed anonymous
pgosingle package rebuilt- However, upon re-testing on the real vehicle,
pgo_nodestill exits withexit code -11
SimplePGO::addKeyPose()SimplePGO::smoothAndUpdate()- GPS factor insertion path
map -> odomoffset update chain before broadcast
- GPS provides WGS84 latitude/longitude
- PGO uses a fixed ENU origin to convert to ENU (x=East, y=North)
- However, FAST-LIO2's
mapframe yaw is arbitrary (depends on IMU attitude at startup) - Therefore, the ENU->map rotation angle theta and translation t must be estimated
v2 initial version (PGO-side ENU->map estimation):
- The original plan was to have PGO accumulate (enu_xy, map_xy) pairs during operation and publish to
/gps_corridor/enu_to_map - Real-vehicle testing revealed: after PGO took over,
ENU->mapcontinued to drift, and the runner recomputed subgoals leading to backtracking/recovery - This approach has been deprecated
v2 final version (independent Global Aligner):
- Added independent
gps_global_aligner_node.py(commite51a46a), decoupled from PGO - The aligner independently collects GPS->ENU and map->base_link pairs, estimating a smooth ENU->map transform online
- Outputs to
/gps_corridor/enu_to_map(same topic, but source switched from PGO to the independent aligner) - Applies rate limiting and smoothing to estimates to prevent jumps
- In corridor mode, PGO's
gps.enableis turned off; PGO only performs loop closure
- Retained: Graph optimization / loop detection / publishing
map -> odom/optimized_odom - Disabled: GPS factor (
gps.enable: false), no longer directly participates in corridor alignment - Independent aligner takes over: GPS global correction is handled by
gps_global_aligner_node
The runner uses fixed yaw bootstrap at startup to begin navigation immediately:
- Read the yaw0 of
map->base_linkfrom TF - Compute
θ_bootstrap = yaw0 - radians(launch_yaw_deg)using the route YAML'slaunch_yaw_deg - Construct the initial ENU->map transform
- Issue the first goal immediately without waiting for the aligner to converge
This resolves the "stationary startup deadlock" problem from the early v2 planning phase.