1212# See the License for the specific language governing permissions and
1313# limitations under the License.
1414"""
15- Nav2 + AMCL on an existing map, for the cleaning-with-map workflow.
15+ Nav2 on an existing map, for the cleaning-with-map workflow.
1616
1717Localization and navigation ONLY -- no Gazebo, no RViz -- so it composes with a
1818separately-launched robot source (`oomwoo_gazebo world.launch.py` in sim, or
1919`oomwoo_bringup physical.launch.py` on a real robot) and a separate viewer
2020(`oomwoo_clean_ui cleaning_debug.launch.py`). Uses the selected robot's own
2121navigation.yaml, so the same command follows `kaia use <robot>`.
2222
23- auto_localize:=true (default) seeds AMCL at the spawn pose so the sim localizes
24- itself -- no manual RViz 2D Pose Estimate. Set it false on a real robot (you do
25- not know the pose there) and seed with the 2D Pose Estimate instead.
26-
27- coverage:=true adds the ground-truth coverage meter that marks the floor clean
28- as the robot drives. That meter is ground-truth based, so it is SIM ONLY; on a
29- real robot leave coverage off (a belief-based estimator does not exist yet). Both
30- auto_localize and coverage use the spawn pose, which must match the sim's
31- (world.launch.py defaults), so the robot localizes and the covered cells line up
32- with the map.
23+ localization:=
24+ amcl (default) -- Nav2 AMCL scan-matches against the map. Real, but only as
25+ good as the map: if the loaded map does not match the robot's LiDAR,
26+ AMCL's estimate wanders. auto_localize:=true seeds it at the spawn pose
27+ (no manual RViz 2D Pose Estimate); set false on a real robot.
28+ truth -- DEBUG, SIM ONLY. No AMCL: publish a static map->odom at the spawn
29+ pose. The sim's odometry is noise-free, so map->base then tracks the
30+ true pose exactly, forever. Use it to take localization error out of the
31+ picture and debug navigation / the map / tuning on their own.
32+
33+ coverage:=true adds the ground-truth coverage meter (sim only). auto_localize,
34+ coverage and the truth transform all use the spawn pose, which must match the
35+ sim's (world.launch.py defaults) so everything lines up with the map.
3336"""
3437
3538import os
4750from launch_ros .parameter_descriptions import ParameterValue
4851
4952
50- def _nav (context , robot_model , map_yaml , use_sim_time ):
53+ def _seed_node (xv , yv , yawv ):
54+ # (Re)publish /initialpose until AMCL localizes, then exit. The robot's AMCL
55+ # does not self-seed, so this is what puts map->odom on the tree in sim.
56+ return Node (
57+ package = 'oomwoo_sim_support' , executable = 'initialpose_pub' ,
58+ name = 'initialpose_pub' , output = 'screen' ,
59+ parameters = [{'use_sim_time' : True , 'reseed_after_sec' : 1.0 ,
60+ 'x' : xv , 'y' : yv , 'yaw' : yawv }])
61+
62+
63+ def _truth_localization (mapf , use_sim ):
64+ # Perfect debug localization: a static IDENTITY map->odom. The sim's
65+ # odometry is noise-free AND world-referenced (odom->base already reports
66+ # the true world pose), and the map is world-aligned, so map == odom == world
67+ # -- map->base then equals the true pose for all time, no AMCL, no
68+ # scan-vs-map fitting.
69+ map_server = Node (
70+ package = 'nav2_map_server' , executable = 'map_server' , name = 'map_server' ,
71+ output = 'screen' ,
72+ parameters = [{'yaml_filename' : mapf , 'use_sim_time' : use_sim ,
73+ 'topic_name' : 'map' , 'frame_id' : 'map' }])
74+ lifecycle = Node (
75+ package = 'nav2_lifecycle_manager' , executable = 'lifecycle_manager' ,
76+ name = 'lifecycle_manager_localization' , output = 'screen' ,
77+ parameters = [{'use_sim_time' : use_sim , 'autostart' : True ,
78+ 'node_names' : ['map_server' ]}])
79+ map_odom = Node (
80+ package = 'tf2_ros' , executable = 'static_transform_publisher' ,
81+ name = 'map_odom_truth' , output = 'screen' ,
82+ arguments = ['--frame-id' , 'map' , '--child-frame-id' , 'odom' ])
83+ return [map_server , lifecycle , map_odom ]
84+
85+
86+ def _nav (context , robot_model , map_yaml , use_sim_time , localization ,
87+ auto_localize , x0 , y0 , yaw0 ):
5188 model = context .perform_substitution (robot_model )
5289 if not model :
5390 try :
@@ -57,16 +94,32 @@ def _nav(context, robot_model, map_yaml, use_sim_time):
5794 model = 'oomwoo_one'
5895 params = os .path .join (
5996 get_package_share_directory (model ), 'config' , 'navigation.yaml' )
60- bringup = os .path .join (
61- get_package_share_directory ('nav2_bringup' ),
62- 'launch' , 'bringup_launch.py' )
63- return [IncludeLaunchDescription (
64- PythonLaunchDescriptionSource (bringup ),
65- launch_arguments = {
66- 'map' : context .perform_substitution (map_yaml ),
67- 'use_sim_time' : context .perform_substitution (use_sim_time ),
68- 'params_file' : params ,
69- 'slam' : 'False' }.items ())]
97+ launch_dir = os .path .join (
98+ get_package_share_directory ('nav2_bringup' ), 'launch' )
99+ mapf = context .perform_substitution (map_yaml )
100+ sim = context .perform_substitution (use_sim_time )
101+ xv = float (context .perform_substitution (x0 ))
102+ yv = float (context .perform_substitution (y0 ))
103+ yawv = float (context .perform_substitution (yaw0 ))
104+
105+ if context .perform_substitution (localization ) == 'truth' :
106+ nav = IncludeLaunchDescription (
107+ PythonLaunchDescriptionSource (
108+ os .path .join (launch_dir , 'navigation_launch.py' )),
109+ launch_arguments = {'use_sim_time' : sim ,
110+ 'params_file' : params }.items ())
111+ return _truth_localization (mapf , sim .lower () == 'true' ) + [nav ]
112+
113+ # amcl (default): the full Nav2 bringup (map_server + AMCL + navigation)
114+ bringup = IncludeLaunchDescription (
115+ PythonLaunchDescriptionSource (
116+ os .path .join (launch_dir , 'bringup_launch.py' )),
117+ launch_arguments = {'map' : mapf , 'use_sim_time' : sim ,
118+ 'params_file' : params , 'slam' : 'False' }.items ())
119+ actions = [bringup ]
120+ if context .perform_substitution (auto_localize ).lower () in ('true' , '1' ):
121+ actions .append (_seed_node (xv , yv , yawv ))
122+ return actions
70123
71124
72125def generate_launch_description () -> LaunchDescription :
@@ -90,12 +143,14 @@ def generate_launch_description() -> LaunchDescription:
90143 DeclareLaunchArgument ('robot_model' , default_value = '' ),
91144 DeclareLaunchArgument ('map' , default_value = default_map ),
92145 DeclareLaunchArgument ('use_sim_time' , default_value = 'true' ),
93- # auto-seed AMCL at the spawn pose (sim); false on a real robot
146+ # 'amcl' (real) or 'truth' (perfect static map->odom, sim debug)
147+ DeclareLaunchArgument ('localization' , default_value = 'amcl' ,
148+ choices = ['amcl' , 'truth' ]),
149+ # auto-seed AMCL at the spawn pose (amcl mode, sim); false on a robot
94150 DeclareLaunchArgument ('auto_localize' , default_value = 'true' ),
95151 # ground-truth coverage marking: sim only, off by default
96152 DeclareLaunchArgument ('coverage' , default_value = 'false' ),
97- # must match the sim spawn (world.launch.py defaults) so the robot
98- # localizes and coverage aligns
153+ # must match the sim spawn (world.launch.py defaults)
99154 DeclareLaunchArgument ('x_pose' , default_value = '-2.0' ),
100155 DeclareLaunchArgument ('y_pose' , default_value = '-0.5' ),
101156 DeclareLaunchArgument ('yaw' , default_value = '0.0' ),
@@ -104,7 +159,10 @@ def generate_launch_description() -> LaunchDescription:
104159 nav = OpaqueFunction (function = _nav , args = [
105160 LaunchConfiguration ('robot_model' ),
106161 LaunchConfiguration ('map' ),
107- LaunchConfiguration ('use_sim_time' )])
162+ LaunchConfiguration ('use_sim_time' ),
163+ LaunchConfiguration ('localization' ),
164+ LaunchConfiguration ('auto_localize' ),
165+ x0 , y0 , yaw0 ])
108166
109167 with_coverage = IfCondition (LaunchConfiguration ('coverage' ))
110168 ground_truth = Node (
@@ -125,18 +183,4 @@ def generate_launch_description() -> LaunchDescription:
125183 ('ground_truth/pose' , '/ground_truth/pose' ),
126184 ('cleaning_active' , '/coverage_planner/cleaning_active' )])
127185
128- # Seed AMCL at the spawn pose so the sim localizes without a manual RViz
129- # 2D Pose Estimate. initialpose_pub (re)publishes /initialpose until AMCL
130- # localizes, then exits; the robot's AMCL does not self-seed, so this is
131- # what puts map->odom on the tree.
132- seed = Node (
133- package = 'oomwoo_sim_support' , executable = 'initialpose_pub' ,
134- name = 'initialpose_pub' , output = 'screen' ,
135- condition = IfCondition (LaunchConfiguration ('auto_localize' )),
136- parameters = [{'use_sim_time' : True , 'reseed_after_sec' : 1.0 ,
137- 'x' : ParameterValue (x0 , value_type = float ),
138- 'y' : ParameterValue (y0 , value_type = float ),
139- 'yaw' : ParameterValue (yaw0 , value_type = float )}])
140-
141- return LaunchDescription (
142- args + [nav , ground_truth , coverage_meter , seed ])
186+ return LaunchDescription (args + [nav , ground_truth , coverage_meter ])
0 commit comments