Skip to content

Commit 6a89c2c

Browse files
committed
demo: combine Nav2 scan observations
1 parent 2f8c0c7 commit 6a89c2c

15 files changed

Lines changed: 873 additions & 10 deletions

File tree

map_server/README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,18 @@ ros2 launch rmf_layered_map_server_demo demo.launch.py
1111
```
1212

1313
Use `use_rviz:=False` for a headless run.
14+
15+
## Nav2 Observations
16+
17+
The three-robot demo converts local laser observations into point regions and
18+
combines them into the global `/map`. It launches three robots in warehouse
19+
corners, deterministic clutter for each robot to observe, the layered map
20+
server, and a separate RViz window for the combined map. It does not launch any
21+
RMF planning components.
22+
23+
```bash
24+
ros2 launch rmf_layered_map_server_demo nav2_observations.launch.py
25+
```
26+
27+
See [`rmf_layered_map_server_demo/README.md`](rmf_layered_map_server_demo/README.md)
28+
for launch arguments and the current scan-to-region limitations.

map_server/rmf_layered_map_server/README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,16 @@ ros2 launch rmf_layered_map_server_demo demo.launch.py
4343
```
4444

4545
The launch file starts RViz with a `Map` display on `/map` and the fixed frame set to `map`. The demo publisher sends a bordered static map and a temporary obstacle region with a 5 second TTL. The obstacle should appear in the composed map and then disappear when the TTL expires.
46+
47+
## Nav2 Observation Demo
48+
49+
To combine observations from three Nav2 robots without starting RMF planning,
50+
launch:
51+
52+
```bash
53+
ros2 launch rmf_layered_map_server_demo nav2_observations.launch.py
54+
```
55+
56+
Each robot converts valid laser endpoints into point regions. A separate RViz
57+
window displays the composed `/map`; see the demo package README for the
58+
fidelity and publication-rate tuning arguments.
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# Layered Map Server Demos
2+
3+
This package contains two demonstrations of the layered map server.
4+
5+
## TTL smoke test
6+
7+
The small smoke test publishes a synthetic static grid and a temporary
8+
rectangle with a five-second TTL:
9+
10+
```bash
11+
ros2 launch rmf_layered_map_server_demo demo.launch.py
12+
```
13+
14+
## Three-robot Nav2 observation demo
15+
16+
The Nav2 demo starts three stationary robots in free corners of the warehouse
17+
map and spawns one deterministic box near each robot. Each robot's observation
18+
node subscribes to its `sensor_msgs/LaserScan`, represents valid scan endpoints
19+
as point `Region` messages, and publishes a replacing `MapRegionUpdate`
20+
snapshot. The layered map server transforms and combines all three sources over
21+
the static map.
22+
23+
No RMF planning components or navigation goals are required. By default the
24+
Nav2 launch opens three robot-local RViz windows, and the demo opens another
25+
RViz window showing the combined `/map`:
26+
27+
```bash
28+
ros2 launch rmf_layered_map_server_demo nav2_observations.launch.py
29+
```
30+
31+
Use `use_nav2_rviz:=False` to open only the combined-map view. Use
32+
`spawn_clutter:=False` to run against the unmodified warehouse world.
33+
34+
The initial implementation deliberately keeps the scan-to-region conversion
35+
simple so its cost and information loss are visible. `beam_stride` samples
36+
fewer endpoints, while `publish_period_sec` throttles snapshots. For example:
37+
38+
```bash
39+
ros2 launch rmf_layered_map_server_demo nav2_observations.launch.py \
40+
beam_stride:=4 publish_period_sec:=1.0
41+
```
42+
43+
Each publisher logs the number of input beams and output regions. This makes it
44+
possible to compare message density and global-map update rate against the
45+
visual fidelity in RViz.
46+
47+
The prototype publishes occupied endpoints only. It does not yet turn clearing
48+
rays into free-space regions or compress adjacent endpoints into larger region
49+
primitives. `reset_source` removes the previous snapshot before each new scan,
50+
and TTL expiry removes observations if a robot stops publishing.
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
# Copyright 2026 Open Source Robotics Foundation
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import os
16+
17+
from ament_index_python.packages import get_package_share_directory
18+
from launch import LaunchDescription
19+
from launch.actions import (
20+
DeclareLaunchArgument,
21+
EmitEvent,
22+
ExecuteProcess,
23+
RegisterEventHandler,
24+
TimerAction,
25+
)
26+
from launch.conditions import IfCondition
27+
from launch.event_handlers import OnProcessExit
28+
from launch.events import Shutdown
29+
from launch.substitutions import LaunchConfiguration
30+
from launch_ros.actions import Node
31+
from launch_ros.parameter_descriptions import ParameterValue
32+
33+
34+
ROBOTS = (
35+
('robot0', -12.0, -21.0, 0.7854),
36+
('robot1', 10.0, -21.0, 2.3562),
37+
('robot2', -12.0, 21.0, -0.7854),
38+
)
39+
40+
41+
def robots_argument():
42+
"""Format the three warehouse-corner robot poses for Nav2 bringup."""
43+
return '; '.join(
44+
f'{name}={{x: {x}, y: {y}, yaw: {yaw}}}'
45+
for name, x, y, yaw in ROBOTS
46+
)
47+
48+
49+
def generate_launch_description():
50+
demo_share = get_package_share_directory('rmf_layered_map_server_demo')
51+
nav2_share = get_package_share_directory('sp_demo_nav2_bringup')
52+
53+
map_file = LaunchConfiguration('map')
54+
params_file = LaunchConfiguration('params_file')
55+
use_nav2_rviz = LaunchConfiguration('use_nav2_rviz')
56+
use_global_rviz = LaunchConfiguration('use_global_rviz')
57+
spawn_clutter = LaunchConfiguration('spawn_clutter')
58+
beam_stride = LaunchConfiguration('beam_stride')
59+
publish_period_sec = LaunchConfiguration('publish_period_sec')
60+
ttl_sec = LaunchConfiguration('ttl_sec')
61+
max_observation_range = LaunchConfiguration('max_observation_range')
62+
63+
declarations = [
64+
DeclareLaunchArgument(
65+
'map',
66+
default_value=os.path.join(nav2_share, 'maps', 'warehouse.yaml'),
67+
description='Static map used by each Nav2 robot.',
68+
),
69+
DeclareLaunchArgument(
70+
'params_file',
71+
default_value=os.path.join(
72+
nav2_share, 'params', 'nav2_multirobot_params_all.yaml'
73+
),
74+
description='Nav2 parameters shared by the three robots.',
75+
),
76+
DeclareLaunchArgument(
77+
'use_nav2_rviz',
78+
default_value='True',
79+
description='Whether to open one local Nav2 RViz window per robot.',
80+
),
81+
DeclareLaunchArgument(
82+
'use_global_rviz',
83+
default_value='True',
84+
description='Whether to open the combined global-map RViz window.',
85+
),
86+
DeclareLaunchArgument(
87+
'spawn_clutter',
88+
default_value='True',
89+
description='Whether to spawn one deterministic obstacle near each robot.',
90+
),
91+
DeclareLaunchArgument(
92+
'beam_stride',
93+
default_value='1',
94+
description='Publish every Nth valid laser return as a point region.',
95+
),
96+
DeclareLaunchArgument(
97+
'publish_period_sec',
98+
default_value='0.5',
99+
description='Minimum period between region snapshots from each robot.',
100+
),
101+
DeclareLaunchArgument(
102+
'ttl_sec',
103+
default_value='1.0',
104+
description='Lifetime of each robot observation snapshot.',
105+
),
106+
DeclareLaunchArgument(
107+
'max_observation_range',
108+
default_value='2.5',
109+
description='Maximum laser return distance represented globally.',
110+
),
111+
]
112+
113+
# ParseMultiRobotPose in the Nav2 demo reads sys.argv directly. Launch it as
114+
# a managed child process so the robot list reaches that parser.
115+
nav2_simulation = ExecuteProcess(
116+
cmd=[
117+
'ros2',
118+
'launch',
119+
'sp_demo_nav2_bringup',
120+
'cloned_multi_tb3_simulation_launch.py',
121+
f'robots:={robots_argument()}',
122+
['map:=', map_file],
123+
['params_file:=', params_file],
124+
['use_rviz:=', use_nav2_rviz],
125+
'use_navigation:=False',
126+
],
127+
output='screen',
128+
)
129+
130+
layered_map_server = Node(
131+
package='rmf_layered_map_server',
132+
executable='rmf_layered_map_server',
133+
output='screen',
134+
remappings=[('/map/static', '/robot0/inner/map')],
135+
)
136+
137+
observation_nodes = []
138+
for robot_name, _, _, _ in ROBOTS:
139+
observation_nodes.append(Node(
140+
package='rmf_layered_map_server_demo',
141+
executable='scan_region_publisher',
142+
namespace=f'{robot_name}/inner',
143+
output='screen',
144+
parameters=[{
145+
'robot_name': robot_name,
146+
'map_frame': 'map',
147+
'map_name': 'warehouse',
148+
'scan_topic': f'/{robot_name}/inner/scan',
149+
'beam_stride': ParameterValue(beam_stride, value_type=int),
150+
'publish_period_sec': ParameterValue(
151+
publish_period_sec, value_type=float
152+
),
153+
'ttl_sec': ParameterValue(ttl_sec, value_type=float),
154+
'max_observation_range': ParameterValue(
155+
max_observation_range, value_type=float
156+
),
157+
}],
158+
remappings=[('/tf', 'tf'), ('/tf_static', 'tf_static')],
159+
))
160+
161+
clutter_spawner = TimerAction(
162+
period=5.0,
163+
actions=[Node(
164+
condition=IfCondition(spawn_clutter),
165+
package='rmf_layered_map_server_demo',
166+
executable='layered_map_demo_clutter_spawner',
167+
output='screen',
168+
)],
169+
)
170+
171+
global_rviz = Node(
172+
condition=IfCondition(use_global_rviz),
173+
package='rviz2',
174+
executable='rviz2',
175+
name='layered_global_map_rviz',
176+
arguments=[
177+
'-d',
178+
os.path.join(demo_share, 'rviz', 'nav2_observations.rviz'),
179+
],
180+
parameters=[{'use_sim_time': True}],
181+
output='screen',
182+
)
183+
global_rviz_exit_handler = RegisterEventHandler(
184+
condition=IfCondition(use_global_rviz),
185+
event_handler=OnProcessExit(
186+
target_action=global_rviz,
187+
on_exit=EmitEvent(event=Shutdown(reason='global RViz exited')),
188+
),
189+
)
190+
191+
return LaunchDescription([
192+
*declarations,
193+
nav2_simulation,
194+
layered_map_server,
195+
*observation_nodes,
196+
clutter_spawner,
197+
global_rviz,
198+
global_rviz_exit_handler,
199+
])

map_server/rmf_layered_map_server_demo/package.xml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,19 @@
1212
<exec_depend>rclpy</exec_depend>
1313
<exec_depend>ament_index_python</exec_depend>
1414
<exec_depend>geometry_msgs</exec_depend>
15+
<exec_depend>gz_tools_vendor</exec_depend>
16+
<exec_depend>launch</exec_depend>
17+
<exec_depend>launch_ros</exec_depend>
1518
<exec_depend>nav_msgs</exec_depend>
1619
<exec_depend>rmf_layered_map_msgs</exec_depend>
1720
<exec_depend>rmf_layered_map_server</exec_depend>
1821
<exec_depend>rmf_prototype_msgs</exec_depend>
1922
<exec_depend>rviz2</exec_depend>
23+
<exec_depend>sensor_msgs</exec_depend>
24+
<exec_depend>sp_demo_nav2_bringup</exec_depend>
25+
<exec_depend>tf2_ros_py</exec_depend>
26+
27+
<test_depend>python3-pytest</test_depend>
2028

2129
<export>
2230
<build_type>ament_python</build_type>

0 commit comments

Comments
 (0)