SLAM

SLAM建图话题数据流

一、全景图

═══════════ Pi(车上,碰硬件的唯一进程:backend)═══════════
 雷达 A1M8                     编码器×4(pigpio)        IMU(CH340串口)
   │串口帧                       │GPIO边沿               │7E23帧
   ▼                            ▼                       ▼
 lidar.py                 encoder.py                imu.py
 720点/圈·物理量           tick增量(带符号)           gz(rad/s)
   │                            └────────┬──────────────┘
   ▼                                     ▼
 ScanBroadcaster        odom_exporter._step() 每1/30s
 TCP:5001 NDJSON        调 integrate() → 速度组(v,w) + 位姿组(x,y,θ)
 每行一圈JSON                 │TCP:5003 NDJSON
   │                          │ 每拍三条: wheel / imu / odom
═══ 容器 ros_env(两座桥:JSON → ROS 话题的翻译官)═══
   ▼                          ▼
 lidar_bridge.py            odom_bridge.py --mode raw
   │/scan (LaserScan)          │/wheel_odom(只twist) + /imu/data_raw(只vyaw)
   │BEST_EFFORT                │TF-free(位姿决定权上交)
   └────────── WiFi DDS(domain 0 + 静态对端)──────────┐
                                                        ▼
═══════════ 笔记本(ROS 世界,全部计算)═══════════════════
 EKF(robot_localization)                    slam_toolbox
 /wheel_odom取vx+vyaw、/imu取vyaw            ①取/scan + TF放置扫描
 ──积分+加权──→ (x,y,θ) ──→ TF odom→base_link ②匹配/建节点(阈值0.5)/回环
          │                                ──→ /map + TF map→odom(改正数表)
          └──────────────┬────────────────────────┘
                         ▼
              slam_service.py :5002 订阅/map
                         │HTTP 栅格(gzip ~6KB) 每3s
                         ▼
              浏览器「地图」画布  ←—— 点云走的是另一条路:Socket.IO :5000 直连,不过 ROS

这张图本质上是三段地域(Pi / 容器 / 笔记本)、两座桥、一次跨海(WiFi DDS)。

二、逐块讲解

第一块:Pi 硬件层(顶部三个并排的源头)

雷达 A1M8          编码器×4(pigpio)      IMU(CH340串口)
  │串口帧            │GPIO边沿             │7E23帧

三个传感器,三种完全不同的原始信号:雷达从串口吐字节;编码器是GPIO 引脚上的电平跳变(每秒近万个边沿,pigpio 守护进程捕获);IMU 从 USB 串口吐 7E 23 开头的协议帧。

第二块:三个驱动模块(物理量层)

lidar.py            encoder.py           imu.py
720点/圈·物理量      tick增量(带符号)      gz(rad/s)

各把各的原始信号翻译成有物理意义的数:

  • lidar.py:字节重组成一圈 720 个 0.5° 方向的距离值——“我看见了什么”;
  • encoder.py:边沿喂进转移表状态机 → 带符号 tick——“轮子转了多少”;
  • imu.py:帧解析 → gz(rad/s,正=逆时针左转)——“车体转多快”。

第三块:两个出口(TCP 广播层)

ScanBroadcaster          odom_exporter._step() 每1/30s
TCP:5001 NDJSON          调 integrate() → 速度组(v,w) + 位姿组(x,y,θ)
每行一圈JSON                │ 每拍三条: wheel / imu / odom

左边:雷达每转完一圈,打包成一行 JSON 广播到 :5001(每个客户端独立 2 格缓冲,慢消费者丢帧不阻塞采集)。

右边:每 1/30 秒一拍,odom_exporter 去编码器和 IMU 手里取当拍数据,喂给纯函数 integrate()——轮子管走多快、陀螺管转多快,合成速度组 (v, w),顺带积出车上底账 (x, y, θ)。然后广播三条 JSON:wheel(四轮 tick + vL/vR)、imu(gz)、odom(底账位姿)——同一拍三份视图,原始层留着分诊,融合层给遥测。

这一块是国境线:TCP + 每行一个 JSON,nc 就能看。过了这两个端口,数据就不再属于”车”,而属于”任何来取的人”。

:5001 和 :5003 这两个端口是裸 TCP 服务——客户端连上后,服务端就不停地把数据字节流灌过来,仅此而已。它不是网页(没有 HTTP 的请求/响应、没有头部),也不是 Socket.IO 那种带协议握手的通道。任何能建立 TCP 连接的工具都能收,不挑客户端。

补充:nc(netcat)是 Linux 自带的“TCP 直连接工具”:给它一个地址和端口,它就建立连接,把收到的一切原样打印到终端。

第四块:容器里的两座桥(翻译官)

lidar_bridge.py            odom_bridge.py --mode raw
 │/scan (LaserScan)          │/wheel_odom(只twist) + /imu/data_raw(只vyaw)
 │BEST_EFFORT                │TF-free(位姿决定权上交)

Pi 上没有 ROS 节点直接读硬件——翻译发生在 ros_env 容器里的两个 Python 脚本上。它们读 TCP、把 JSON 行组装成 ROS 消息发出去:

  • 左桥:一行圈 JSON → 一个 LaserScan 话题消息,发到 /scan。BEST_EFFORT = 传感器的正确取舍(要最新,不要重传旧的)——这也是”测频率必须加 --qos-reliability best_effort“的根源。
  • 右桥(raw 模式):wheel 行 → /wheel_odom(只填速度半边 twist:vx、vyaw;位姿留零);imu 行 → /imu/data_raw(只填 gz)。TF-free = 它不发任何”车在哪”的声明——寄生观测不寄结论,位姿的决定权上交给笔记本的 EKF。(为什么:Pi 的融合是二选一硬开关,EKF 的是按质量加权——生数据交上去,让更好的算法做菜。)

跨海:WiFi DDS

└────────── WiFi DDS(domain 0 + 静态对端)──────────┐

ROS 话题是分布式的:桥发到话题上,谁订谁收,不指定收件人。发现机制靠 domain 0 + 静态对端(两端方向相反:笔记本填 Pi 的 IP,Pi 容器填笔记本的 IP)+ SUBNET 范围。桥和消费者隔着一整段 WiFi,但代码里看不见”发送到笔记本”这种字样——这就是 ROS 的解耦:发布者根本不知道笔记本存在。

第五块:笔记本,两个计算节点

左边,EKF——速度变位姿:

/wheel_odom取vx+vyaw、/imu取vyaw
──积分+加权──→ (x,y,θ) ──→ TF odom→base_link

订两路生数据,各取一项(15 位配置里其余 13 位全关)。加权:陀螺 vyaw 置信 0.01、编码器 vyaw 置信 0.09,按 9:1 混合,某路挂了自动塌向另一路;积分:vx、vyaw 攒成每一时刻的 (x, y, θ)。然后以 TF odom→base_link 的形式发布——数值就是位姿 (x, y, θ),意义是”车此刻在里程计坐标系里在哪朝哪”。建图用的那本账,在这站落笔。

右边,slam_toolbox——位姿+扫描变地图:

①取/scan + TF放置扫描
②匹配/建节点(阈值0.5)/回环
──→ /map + TF map→odom(改正数表)

每帧 /scan 到了:① 查 TF 链(map→odom→base_link→laser_frame,GIS 式分步基准转换)知道”这帧时刻雷达在地图里的大概位置朝向”,把点放进地图;② 扫描匹配微调 → 挪够 0.5m 或转够 0.5rad(≈28.6°)设一个新驿站(节点)→ 转回老地方时认出来(回环)→ 位姿图优化平差,误差分摊回整条轨迹。产出两样:/map(占据栅格地图)和 TF map→odom(改正数表——odom 账照漂不改,修正全挂在这条变换上)。

至此 TF 树闭合,三条各司其主:

map ──TF①(slam_toolbox发)── odom ──TF②(EKF发)── base_link ──TF③(静态)── laser_frame
      平差改正                   死推位姿            车体              雷达安装
TF谁发布数据来源消费者性质
odom→base_linkEKFvx+vyaw 积分slam_toolbox(放扫描)实时,30Hz
map→odomslam_toolbox回环平差整棵树的修正慢速跳变(回环时)
base_link→laser_frame静态发布器安装关系(本项目全零)所有要用扫描的人永不变

第六块:最后一跳,进浏览器

slam_service.py :5002 订阅/map
        │HTTP 栅格(gzip ~6KB) 每3s
        ▼
浏览器「地图」画布 ←—— 点云走的是另一条路:Socket.IO :5000 直连,不过 ROS

slam_service 订阅 /map,攒成 HTTP 服务;浏览器每 3 秒轮询栅格(gzip 后约 6KB,没变不重发),画进”地图”画布。注意最后的分岔:点云画面根本不进 ROS——后端 Socket.IO :5000 直推浏览器。

三、附录

个人理解笔记(10.4 10.5):

发表评论