nav2_map_server 实际作用与用法图解
配套文档:32-nav2_map_server解读.md(源码级分析)
本文目标:用大白话讲清楚 map_server 到底干嘛、怎么用、跟 costmap 怎么配合
1. 一句话理解
map_server 就是 Nav2 的"地图文件阅读器"------它读一个 YAML+图片文件,变成 ROS 里的 OccupancyGrid 消息发到 topic 上,让下游(AMCL、costmap)能拿到地图数据。
它不管"图层叠加",那是 costmap_2d 的事。它只管"读文件→发消息"这一件事。
2. 地图在 Nav2 里的完整流转
先搞清楚一张地图从文件到被机器人使用,经过哪些环节:
你的磁盘 ROS2 网络 Nav2 下游
──────── ────────── ──────────
map.yaml ─┐ ┌→ AMCL(定位)
│ │
map.pgm ──┤ │
│ ┌──────────┐ /map topic ┌──────────────┐ │
└──→│map_server│────────────────→│ StaticLayer │───┘
│ (读文件 │ OccupancyGrid │ (costmap_2d) │
│ 转消息) │ └──────────────┘
└──────────┘
↑
│ 还有两个服务:
│ /map_server/map ← GetMap(问它要当前地图)
│ /map_server/load_map← LoadMap(运行时换地图)
就这一条链路。 map_server 读文件 → 发 topic → StaticLayer 订阅 → 写入 costmap。
3. map_server 只负责"一张静态图"
很多人以为 map_server 管所有地图,其实不是。Nav2 的地图分三类,各管各的:
| 地图类型 | 谁负责 | 数据来源 | 给谁用 |
|---|---|---|---|
| SLAM 静态地图 | map_server | YAML+PGM 文件 | StaticLayer |
| 动态障碍物 | ObstacleLayer(在costmap里) | 激光雷达/摄像头实时数据 | ObstacleLayer 自己 |
| 语义掩码(禁行区/限速区) | 另一个 map_server 实例或自定义节点 | YAML+PGM 文件 | CostmapFilter |
map_server 不管动态障碍物,那玩意是 ObstacleLayer 直接订阅激光雷达 topic 拿到的,跟 map_server 毫无关系。
4. 如果只有一张 SLAM 地图(最常见场景)
90% 的场景只需要一张地图。启动命令:
bash
ros2 run nav2_map_server map_server \
--ros-args -p yaml_filename:=/maps/map.yaml
就这么简单。此时:
节点名: map_server(默认)
发布topic: /map(默认,latched)
服务: /map_server/map (GetMap)
/map_server/load_map (LoadMap)
下游怎么拿到地图?
- StaticLayer 在
nav2_params.yaml里配map_topic: /map - AMCL 默认订阅
/maptopic - 因为用了
transient_localQoS(latched),后启动的节点也能收到地图
5. 如果要同时有 SLAM 地图 + 禁行区掩码
比如扫地机有"不能进的房间"(禁行区),需要两张地图:
磁盘文件:
├── map.yaml + map.pgm ← SLAM建的地图(墙、空地)
└── keepout.yaml + keepout.png ← 禁行区(黑=禁行,白=可通行)
5.1 要跑三个进程
进程1: map_server (SLAM地图) 进程2: map_server (禁行mask)
┌──────────────────────┐ ┌──────────────────────┐
│ 包名: nav2_map_server │ │ 包名: nav2_map_server │ ← 同一个包
│ 可执行: map_server │ │ 可执行: map_server │ ← 同一个exe
│ │ │ │
│ 改了什么: │ │ 改了什么: │
│ yaml_filename=map │ │ yaml_filename=keepout │
│ topic_name=map(默认) │ │ topic_name=keepout_ │ ← 关键!
│ 节点名=map_server(默认)│ │ mask │
│ │ │ 节点名=map_server_ │ ← 关键!
│ │ │ keepout │
│ │ │ │
│ 发布: /map │ │ 发布: /keepout_mask │
│ 服务: /map_server/map │ │ 服务: /map_server_ │
│ /map_server/ │ │ keepout/map │
│ load_map │ │ /map_server_ │
│ │ │ keepout/load_map │
└──────────────────────┘ └──────────────────────┘
进程3: costmap_filter_info_server (接线员)
┌──────────────────────────────┐
│ 包名: nav2_map_server │ ← 同一个包!
│ 可执行: costmap_filter_info │
│ _server │
│ │
│ 参数: │
│ type: 0 │ ← 0=禁行, 1=限速, 2=二值
│ mask_topic: "keepout_mask" │ ← 告诉filter去订阅这个topic
│ base: 0.0 │
│ multiplier: 1.0 │
│ │
│ 发布: /costmap_filter_info │ ← 只发一次(latched)
└──────────────────────────────┘
5.2 三个名字为什么是独立的?
看源码就明白了:
map_server.cpp:66 LifecycleNode("map_server", ...) ← 节点名(可改)
map_server.cpp:72 declare_parameter("topic_name", "map") ← topic名(可改)
map_server.cpp:108 service_prefix = get_name() + "/" ← 服务名=节点名+/
- 节点名 → 决定服务名(
/节点名/map,/节点名/load_map) - topic_name → 决定发布topic名(跟节点名无关!)
- 两个独立,所以可以改 topic 不改节点名,也可以改节点名不改 topic
5.3 启动命令
bash
# 进程1: SLAM地图(什么都不改,用默认值)
ros2 run nav2_map_server map_server \
--ros-args -p yaml_filename:=/maps/map.yaml
# → 节点=map_server, topic=/map
# 进程2: 禁行mask(改节点名+改topic名)
ros2 run nav2_map_server map_server \
--ros-args -p yaml_filename:=/maps/keepout.yaml \
-p topic_name:=keepout_mask \
-r __node:=map_server_keepout
# → 节点=map_server_keepout, topic=/keepout_mask
# 进程3: 接线员
ros2 run nav2_map_server costmap_filter_info_server \
--ros-args -p type:=0 \
-p mask_topic:=keepout_mask \
-p base:=0.0 \
-p multiplier:=1.0
# → 发布 /costmap_filter_info 消息
5.4 下游怎么订阅
/map topic
│
└──→ StaticLayer 订阅(在 nav2_params.yaml 里配 map_topic: /map)
/keepout_mask topic
│
└──→ KeepoutFilter 订阅(不是直接配的!是动态订阅的!)
│
│ 订阅流程:
│ 1. KeepoutFilter 启动时订阅 /costmap_filter_info
│ 2. 收到 CostmapFilterInfo 消息:
│ {
│ type: 0, ← 0=禁行
│ filter_mask_topic: "keepout_mask",
│ base: 0.0,
│ multiplier: 1.0
│ }
│ 3. KeepoutFilter 看到 mask_topic="keepout_mask"
│ → 动态创建订阅 /keepout_mask
│ 4. 收到 OccupancyGrid mask
│ → 把黑色区域对应的costmap值设为LETHAL(253)
│ → 规划器就不会规划路径穿过禁行区了
KeepoutFilter 订阅哪个 topic 不是在配置文件里写的,是 CostmapFilterInfoServer 通过消息告诉它的。 这就是 CostmapFilterInfoServer 这个"接线员"存在的意义。
6. nav2_params.yaml 里怎么配
yaml
# nav2_params.yaml (截取 costmap 部分)
global_costmap:
ros__parameters:
# 声明要加载哪些 layer 插件
plugins: ["static_layer", "obstacle_layer", "inflation_layer"]
filters: ["keepout_filter"] # ← 声明要加载哪个 filter
static_layer:
plugin: "nav2_costmap_2d::StaticLayer"
map_topic: /map # ← 直接指定订阅哪个topic
obstacle_layer:
plugin: "nav2_costmap_2d::ObstacleLayer"
# 不需要 map_topic,它订阅的是 /scan 等传感器topic
inflation_layer:
plugin: "nav2_costmap_2d::InflationLayer"
# 纯计算层,不订阅任何外部topic
keepout_filter:
plugin: "nav2_costmap_2d::KeepoutFilter"
filter_info_topic: /costmap_filter_info # ← 订阅接线员的topic
# 注意:这里没有 mask_topic 参数!
# 因为 mask_topic 是从 CostmapFilterInfo 消息里动态获取的
7. 完整接线图(一张图看全)
┌────────────────┐ /map ┌──────────────┐
│ map_server │──────────────────→│ StaticLayer │
│ yaml:=map.yaml │ └──────┬───────┘
│ topic:=map │ │
│ node:=map_serv │ ▼
└────────────────┘ ┌──────────────┐
│primary_costmap│
┌────────────────┐ /keepout_mask │ (基础层) │
│ map_server │──────────────────→└──────┬───────┘
│ yaml:=keepout │ ┌────────────────────┘
│ topic:=keepout │ │ ┌──────────────┐
│ _mask │ │ │combined_costmap│
│ node:=map_serv │ │ │ (最终层) │
│ _keepout │ │ └──────┬───────┘
└────────────────┘ │ │
│ │
┌────────────────┐ │ │
│costmap_filter │ │ │
│_info_server │ │ │
│ type:=0 │ /costmap_filter_info │
│ mask_topic:= │──────────────────────┐ │
│ keepout_mask │ │ │
│ base:=0.0 │ ┌───────▼──────┴───┐
│ multiplier:=1.0│ │ KeepoutFilter │
└────────────────┘ │ 1.收info→知道 │
│ 要订阅 │
│ keepout_mask │
│ 2.订阅mask→ │
│ 把禁行区写入 │
│ combined_costmap│
└───────────────────┘
8. 常见误区纠正
误区1:"map_server 管所有地图图层"
错。 map_server 只管读文件→发topic。图层叠加是 costmap_2d 的 LayeredCostmap 干的。
误区2:"多地图需要多yaml文件传给一个map_server"
错。 一个 map_server 实例只加载一个 yaml。要多张地图,就跑多个 map_server 实例(或自己写节点发布 OccupancyGrid)。
误区3:"KeepoutFilter 的 mask topic 在 nav2_params.yaml 里配"
错。 KeepoutFilter 的 mask_topic 是从 CostmapFilterInfo 消息里动态获取的,不是配置文件里写的。配置文件里只配 filter_info_topic(默认 /costmap_filter_info)。
误区4:"topic_name 跟着节点名走"
错。 它们是两个独立参数。源码里 topic_name 直接作为 publisher 的 topic 名,不加节点名前缀。但 service 名会加节点名前缀。
误区5:"README说的multifloor maps是已有功能"
错。 README第31行的 "multifloor maps" 是设计愿景,代码里完全没实现。目前只支持 OccupancyGrid 单层地图。
9. 速查表:两个实例的命名对照
| SLAM地图实例 | 禁行mask实例 | |
|---|---|---|
| ROS包名 | nav2_map_server |
nav2_map_server |
| 可执行文件 | map_server |
map_server |
| 节点名 | map_server |
map_server_keepout |
| yaml_filename参数 | map.yaml |
keepout.yaml |
| topic_name参数 | map(默认) |
keepout_mask |
| 发布的topic | /map |
/keepout_mask |
| GetMap服务 | /map_server/map |
/map_server_keepout/map |
| LoadMap服务 | /map_server/load_map |
/map_server_keepout/load_map |
| 谁订阅这个topic | StaticLayer | KeepoutFilter(动态订阅) |
| 怎么告诉下游订阅 | nav2_params.yaml直接配 map_topic |
CostmapFilterInfoServer发消息告诉 |