|
2 | 2 |
|
3 | 3 | # Layered Global Map Observations |
4 | 4 |
|
5 | | -This post describes the observation interface for contributing temporary map |
6 | | -information to the next generation Open-RMF prototype. |
| 5 | +This post describes the implemented observation interface and demos for contributing temporary map information to the next generation Open-RMF prototype. |
7 | 6 |
|
8 | | -The goal is to let robots and perception systems publish what they currently |
9 | | -observe without tying them to a specific central map implementation. The first |
10 | | -prototype consumes sparse 2D occupancy regions because that is enough for the |
11 | | -current global planning grid, but the interface is intentionally isolated in |
12 | | -`rmf_layered_map_msgs` so richer representations, such as height-aware or voxel |
13 | | -observations, can be added later. |
| 7 | +The goal is to let robots and perception systems publish what they currently observe without tying them to a specific central map implementation. The first prototype consumes sparse 2D occupancy regions because that is enough for the current global planning grid, but the interface is isolated in `rmf_layered_map_msgs` so richer representations, such as height-aware or voxel observations, can be added later. |
14 | 8 |
|
15 | 9 | # Quick Summary |
16 | 10 |
|
17 | 11 | * Observation sources publish sparse temporary map patches |
18 | | -* One message can contain both clear-space and occupied-space patches from the |
19 | | - same sensor snapshot |
| 12 | +* One message can contain both clear-space and occupied-space patches from the same sensor snapshot |
20 | 13 | * Clear-space patches have TTLs, just like obstacle patches |
21 | 14 | * A source can reset its previous observations without using a TTL |
22 | | -* Robot-mounted sources can include the robot pose at observation time |
23 | | -* The observation messages live in `rmf_layered_map_msgs`, leaving |
24 | | - `rmf_prototype_msgs` unchanged |
| 15 | +* Robot-mounted sources include the observation-frame pose at observation time |
| 16 | +* The Rust map server composes a static occupancy grid and active observations into `/map` |
| 17 | +* The Nav2 demo converts scans from three robots into point regions and displays the combined map |
| 18 | +* The observation messages live in `rmf_layered_map_msgs`, leaving `rmf_prototype_msgs` unchanged |
25 | 19 |
|
26 | | -# Observation Topic |
| 20 | +# Observation Topics |
27 | 21 |
|
28 | | -Observation sources publish dynamic map observations on: |
| 22 | +The layered map server uses these topics: |
29 | 23 |
|
| 24 | +* `/map/static` - static `nav_msgs/OccupancyGrid` |
30 | 25 | * `/map/region_updates` - [`MapRegionUpdate.msg`](../rmf_layered_map_msgs/msg/MapRegionUpdate.msg) |
| 26 | +* `/map` - composed `nav_msgs/OccupancyGrid` |
31 | 27 |
|
32 | | -The topic is an event stream. Updates are not latched because expired |
33 | | -observations should not be replayed to a restarted map service as if they were |
34 | | -current. |
| 28 | +The observation topic is an event stream. Updates are not latched because expired observations should not be replayed to a restarted map service as if they were current. |
35 | 29 |
|
36 | | -Each observation stream should use a stable `source_id`, such as |
37 | | -`robot_1/local_costmap`, `robot_1/front_lidar`, or `door_sensor/west_lobby`. |
38 | | -The map service tracks each source independently so one robot can update or |
39 | | -reset its own temporary observations without disturbing observations from other |
40 | | -sources. |
| 30 | +Each observation stream uses a stable `source_id`, such as `robot_1/local_costmap`, `robot_1/front_lidar`, or `door_sensor/west_lobby`. The map service tracks each source independently so one robot can update or reset its temporary observations without disturbing observations from other sources. |
41 | 31 |
|
42 | 32 | # Messages |
43 | 33 |
|
44 | 34 | ## `MapObservationSource` |
45 | 35 |
|
46 | | -[`MapObservationSource.msg`](../rmf_layered_map_msgs/msg/MapObservationSource.msg) |
47 | | -describes where an observation came from. |
| 36 | +[`MapObservationSource.msg`](../rmf_layered_map_msgs/msg/MapObservationSource.msg) describes where an observation came from. |
48 | 37 |
|
49 | 38 | Important fields: |
50 | 39 |
|
51 | | -* `header`: global frame for the observation and its required, non-zero |
52 | | - timestamp |
53 | | -* `source_id`: stable source identifier, usually the robot namespace plus the |
54 | | - local source name |
55 | | -* `robot_name`: robot that produced this observation, if the source is mounted |
56 | | - on a robot. This can be empty for fixed sensors or synthetic map layers |
| 40 | +* `header`: global frame for the observation and its required, non-zero timestamp |
| 41 | +* `source_id`: stable source identifier, usually the robot namespace plus the local source name |
| 42 | +* `robot_name`: robot that produced this observation, if the source is mounted on a robot; this can be empty for fixed sensors or synthetic map layers |
57 | 43 | * `map_name`: map or level that the observations belong to |
58 | | -* `robot_pose`: pose of the robot-local observation frame in `header.frame_id` |
59 | | - when the observation was produced, so consumers do not need to reconstruct |
60 | | - it from a separate TF-like lookup |
| 44 | +* `robot_pose`: pose of the robot-local observation frame in `header.frame_id` when the observation was produced, so consumers do not need to reconstruct it from a separate TF-like lookup |
61 | 45 | * `default_ttl_sec`: fallback TTL in seconds for patches from this source |
62 | 46 |
|
63 | 47 | ## `MapRegionUpdate` |
64 | 48 |
|
65 | | -[`MapRegionUpdate.msg`](../rmf_layered_map_msgs/msg/MapRegionUpdate.msg) |
66 | | -describes one observation snapshot from a source. |
| 49 | +[`MapRegionUpdate.msg`](../rmf_layered_map_msgs/msg/MapRegionUpdate.msg) describes one observation snapshot from a source. |
67 | 50 |
|
68 | 51 | Important fields: |
69 | 52 |
|
70 | 53 | * `source`: metadata for the observation source |
71 | | -* `reset_source`: remove active observations from the same `source_id` and |
72 | | - `map_name` before applying the new patches |
| 54 | +* `reset_source`: remove active observations from the same `source_id` and `map_name` before applying the new patches |
73 | 55 | * `patches`: clear-space and occupied-space patches from this snapshot |
74 | 56 |
|
75 | | -`reset_source` is useful when a source publishes replacement snapshots, changes |
76 | | -maps, shuts down, or knows its previous observation state is no longer valid. It |
77 | | -does not have a TTL because it is a bookkeeping operation, not an active map |
78 | | -observation. |
| 57 | +`reset_source` is useful when a source publishes replacement snapshots, changes maps, shuts down, or knows its previous observation state is no longer valid. It does not have a TTL because it is a bookkeeping operation, not an active map observation. |
79 | 58 |
|
80 | 59 | ## `MapRegionPatch` |
81 | 60 |
|
82 | | -[`MapRegionPatch.msg`](../rmf_layered_map_msgs/msg/MapRegionPatch.msg) |
83 | | -describes one group of regions with the same action and TTL. |
| 61 | +[`MapRegionPatch.msg`](../rmf_layered_map_msgs/msg/MapRegionPatch.msg) describes one group of regions with the same action and TTL. |
84 | 62 |
|
85 | 63 | Patch types: |
86 | 64 |
|
87 | 65 | * `UPDATE_CLEAR`: regions are temporary free space |
88 | 66 | * `UPDATE_OBSTACLE`: regions are temporary occupied space |
89 | 67 |
|
90 | | -Clear patches and obstacle patches both require a TTL because both describe |
91 | | -temporary observations. If a patch TTL is zero or negative, the source |
92 | | -`default_ttl_sec` is used. If that is also zero or negative, the map service's |
93 | | -configured default TTL is used. |
| 68 | +Clear patches and obstacle patches both require a TTL because both describe temporary observations. If a patch TTL is zero or negative, the source `default_ttl_sec` is used. If that is also zero or negative, the map server's configured default TTL is used. |
94 | 69 |
|
95 | | -When a sensor snapshot includes both clear and occupied regions, publish them in |
96 | | -one `MapRegionUpdate` message. The map service applies clear patches before |
97 | | -obstacle patches, which gives deterministic "clear then mark" behavior without |
98 | | -relying on the arrival order of separate ROS messages. |
| 70 | +When a sensor snapshot includes both clear and occupied regions, publish them in one `MapRegionUpdate` message. The map server applies clear patches before obstacle patches, which gives deterministic "clear then mark" behavior without relying on the arrival order of separate ROS messages. |
99 | 71 |
|
100 | | -The first prototype uses `rmf_prototype_msgs/Region` for sparse 2D geometry so |
101 | | -robots can publish compact patches. Region coordinates are robot-local. The map |
102 | | -service transforms them through `source.robot_pose` into |
103 | | -`source.header.frame_id` before rasterizing them. Fixed sensors can publish |
104 | | -sensor-local regions with their sensor pose, while synthetic sources whose |
105 | | -regions are already global can use the identity pose. The first implementation |
106 | | -accepts point and axis-aligned rectangle regions. |
| 72 | +The first prototype uses `rmf_prototype_msgs/Region` for sparse 2D geometry. Region coordinates are robot-local, and the map server transforms them through `source.robot_pose` into `source.header.frame_id` before rasterizing them. Fixed sensors can publish sensor-local regions with their sensor pose, while synthetic sources whose regions are already global can use the identity pose. The current implementation accepts point and axis-aligned rectangle regions. |
| 73 | + |
| 74 | +# Layered Map Server |
| 75 | + |
| 76 | +`rmf_layered_map_server` keeps the static occupancy grid separate from dynamic observations and publishes their composition on `/map`. It validates source timestamps and frames, ignores updates that are older than the latest accepted update from the same source, supports source resets, and removes observations after their TTL expires. |
| 77 | + |
| 78 | +The server applies clear patches before obstacle patches from the same update. During composition, obstacle observations win over clear observations so occupied space is not accidentally erased by another active source. |
| 79 | + |
| 80 | +# Three-Robot Nav2 Demo |
| 81 | + |
| 82 | +The committed Nav2 demo can be launched with: |
| 83 | + |
| 84 | +```bash |
| 85 | +ros2 launch rmf_layered_map_server_demo nav2_observations.launch.py |
| 86 | +``` |
| 87 | + |
| 88 | +The launch file starts three stationary robots in different free corners of the warehouse and spawns one deterministic Gazebo box near each robot. Each observation node subscribes to its robot's local `sensor_msgs/LaserScan`, filters invalid or out-of-range returns, and publishes the remaining scan endpoints as point regions. |
| 89 | + |
| 90 | +Each scan is a replacement snapshot: `reset_source` removes the source's preceding points before the new points are added, and a short TTL removes the source if it stops publishing. The observation-frame pose is recorded in the shared `map` frame so the map server can transform the scan-local regions before rasterizing them. |
| 91 | + |
| 92 | +The demo launches Nav2 localization but does not launch Nav2 planning or control components, RMF planning, or navigation goals. Three robot-local RViz windows are enabled by default, and another RViz window displays the combined global `/map`. |
| 93 | + |
| 94 | +The scan-to-region conversion is deliberately simple so its information loss and publication cost are visible. `beam_stride` reduces the number of point regions, `publish_period_sec` throttles replacement snapshots, and `max_observation_range` limits represented returns. The current demo publishes occupied endpoints only; it does not convert raycast clearing into free-space regions or compress adjacent points into larger regions. |
107 | 95 |
|
108 | 96 | # Example Flow |
109 | 97 |
|
110 | | -A local costmap or LiDAR observation node can publish a replacement snapshot by: |
| 98 | +A local costmap, LiDAR, or other observation node can publish a replacement snapshot by: |
111 | 99 |
|
112 | | -1. Stamping the observation source with the global frame and observation time, |
113 | | - and including the pose of the robot-local observation frame. |
114 | | -2. Setting `reset_source` if the new snapshot replaces the source's previous |
115 | | - temporary observations. |
| 100 | +1. Stamping the observation source with the global frame and observation time, and including the pose of the robot-local observation frame. |
| 101 | +2. Setting `reset_source` if the new snapshot replaces the source's previous temporary observations. |
116 | 102 | 3. Adding one or more clear patches for free space seen by the sensor. |
117 | 103 | 4. Adding one or more obstacle patches for occupied space seen by the sensor. |
118 | | -5. Giving each patch a TTL long enough to survive normal publication jitter but |
119 | | - short enough to decay when the observation is no longer refreshed. |
| 104 | +5. Giving each patch a TTL long enough to survive normal publication jitter but short enough to decay when the observation is no longer refreshed. |
120 | 105 |
|
121 | | -The map service should ignore snapshots from a source if their timestamp is |
122 | | -older than a newer snapshot that has already been accepted. This prevents a late |
123 | | -clear/reset message from removing obstacle information that came from a newer |
124 | | -observation. |
| 106 | +The map server ignores snapshots from a source if their timestamp is older than a newer snapshot that has already been accepted. This prevents a late clear or reset message from removing obstacle information that came from a newer observation. |
125 | 107 |
|
126 | 108 | # Implemented Test Coverage |
127 | 109 |
|
128 | | -The first server tests cover: |
| 110 | +The committed server and demo tests cover: |
129 | 111 |
|
130 | 112 | * composing obstacle regions over a static planning grid |
131 | 113 | * point regions with non-zero map origins and non-1.0 resolutions |
132 | 114 | * transforming robot-local regions into the global map frame |
133 | 115 | * out-of-bounds regions, malformed point arrays, and unsupported region types |
134 | | -* rejecting updates without a timestamp |
| 116 | +* rejecting updates without a timestamp or in a different global frame |
135 | 117 | * pruning expired observations by TTL |
136 | 118 | * clear and obstacle patches in the same update |
137 | 119 | * late older snapshots being ignored |
138 | 120 | * reset updates removing observations from the same source and map |
139 | 121 | * multiple robot sources being stitched into one composed grid |
| 122 | +* filtering invalid and out-of-range laser returns |
| 123 | +* preserving original beam angles when scan points are sampled |
0 commit comments