能 ping 通启元 Q1,为什么还读不到电量?

在启元 Q1 的开发文档里,读电量只需要一条命令。

timeout 5 ros2 topic echo /aima/hal/bms/state --once

电脑订阅电池状态话题,收到一条消息,打印出来,然后退出。对于只想知道机器人还剩多少电的人,这个目标很小,甚至用不着先写一个完整程序。

不过,在 Q1 指南里,ping 连通与读到电量分在两处,中间隔着 SDK 编译和环境加载。顺着源码看,机器人回应了地址请求,距离屏幕上出现电量,确实还有一段过程。

设想这样一个场景。Q1 已按指南接入开发网络,电脑也能收到 ping 的回复,可电池消息迟迟没有显示。网线接着,命令也照着输入了,这条消息究竟还停在哪一步?

一、地址回复以后,电量消息还要怎样过来

先看看网线连接的两端。

Q1 的 SDK 架构图里,开发电脑之外还有运控板和大脑板。运控板承担硬件抽象、运动控制及基础交互服务,大脑板负责高阶感知、规划等程序。开发者的 ROS2 程序可以部署在不同设备上,经由局域网通信。

两个板端各有自己的地址。对其中一个地址发起 ping,得到的是那次 ICMP 请求的回复。这台设备能够回应电脑,电池消息能否送到订阅程序,还得看 ROS2 通信系统。

在这里插入图片描述

图 1 开发电脑与板端的局域网通信关系。

开头命令中的 /aima/hal/bms/state 是电池话题名。发布端把 Bms 消息交出去,订阅端按这个名字和类型接收。电脑得找到通信对象,还得具备相应的消息类型支持,才能把数据变成可读字段。

ROS2 用“话题”按名称组织消息交流。IP 帮助数据到达相应网络位置,话题名则参与区分程序交换的内容。记住机器人地址,和能够读出它的电池消息,做的是两件事。

即使同处一个局域网,程序也可能被 ROS_DOMAIN_ID 分在不同的逻辑通信域中。Q1 指南将它列入发现异常时的检查项,并要求通过开发模式开放外部电脑的 SDK 接入。地址已经有回应,话题却还不可见,就可能与这些设置有关。

话题列表能让人看见电脑目前发现了什么。已经知道名称与类型的程序,也可以直接建立订阅,用不着先列一次列表。

而同一台电脑上的两个终端,也可能带着不同的通信设置。

二、同一句 source,两个终端

打开启元 SDK 的环境脚本,开头就是这个条件。

if [ -z "$RMW_IMPLEMENTATION" ]; then
  export RMW_IMPLEMENTATION=rmw_fastrtps_cpp
fi

RMW_IMPLEMENTATION 指定 ROS2 使用的中间件实现。脚本只在变量为空时,为它写入默认值。

假设打开 A、B 两个终端。A 原先没有设置这个变量,加载 SDK 环境时,脚本便把它设成 rmw_fastrtps_cpp。B 已经指定了另一种中间件实现,条件不成立,原来的值便保留下来。

两边输入同一句 source,结果仍可能不同。那条命令保留了 B 原先的选择,也许这恰好符合开发者的需要;加载 SDK 环境,并不意味着每个终端都会改用默认值。

在这里插入图片描述

图 2 同一句 source,在两个假设终端中留下不同配置。

一个窗口用于当前项目,另一个窗口可能早已加载过其他工作空间。两人照着同一段文档操作,或者同一个人在两个窗口输入命令,都可能留下这样的差异。只看 SDK 目录里的默认值,还不知道眼前的程序采用了什么。

Fast DDS XML 配置文件的路径也有类似处理。SDK 脚本检查安装位置的文件,满足条件时设置默认路径;路径变量已有值时,又会沿着已有值处理。“我加载过 SDK 环境”这句话,因此还说不准程序是采用了默认配置,还是保留了此前的选择。

再让 B 往前走一步。假如它已经启动了电量订阅程序,之后你才修改变量、重新 source,窗口中的新值不会自动写回那个既有进程。此后启动的程序可以继承新环境,先前的程序仍保留自己的启动环境。

哪怕两个终端现在显示相同的变量,里面早已运行的订阅程序也可能带着不同设置。程序启动的那一刻,决定了它继承哪份环境;配置路径设置以后,中间件还得成功加载对应文件。读不到电量的那个程序,最终用上了什么,要沿着它自己的启动过程来看。

三、网卡选择也藏在这份配置里

这份配置里,还有电脑上很容易被忽略的另一组地址。

开发电脑可能一边接着机器人网线,一边连着 Wi-Fi,两张网卡各有自己的地址。Q1 SDK 的默认 XML 定义了 UDPv4 传输,网卡白名单部分却放在注释里,没有在这里明确限制本机接口。

注释中的白名单不能替正在运行的程序选定网卡。同时开着 Wi-Fi,也不足以说明电池数据走错了地方,具体还得看生效的配置和实际通信情况。

如果要填写这份白名单,目光就得从机器人转回电脑。Fast DDS 在这里筛选本机接口,想限定电脑使用自己的有线接口,填入的应当是电脑这一端的 IP。

机器人的地址回答“对方在哪”,本机接口地址回答“电脑允许哪个接口参加通信”。目标地址和本机地址各有用途,不能互换。

在这里插入图片描述

图 3 白名单筛选本机接口地址。

查看话题的 ROS2 CLI 背后,还可能有一个早已启动的守护进程。Humble 会把部分图查询交给它,订阅则使用直接节点。终端重新加载环境后,守护进程依然可能保留旧设置,和先前启动的电量程序一样。

即使通信配置已经理清,电池消息也还没变成屏幕上的数字。接收程序得先认得它。

四、发现了名字,还要把消息读出来

假如 /aima/hal/bms/state 已经出现在话题列表里,名字有了,读取它的程序又该怎样理解消息内容?

Q1 的电池话题使用 aimdk_msgs/msg/Bms 类型。图信息能告诉电脑这个名称,接收程序还得在本地找到对应的类型支持,才能建立订阅、解释收到的消息。

在 Python 电池示例中,Bms 先从 aimdk_msgs.msg 导入,再与电池话题名一起传给创建订阅的函数。消息到来以后,程序才把内容交给回调。

仓库里的 Bms.msg 定义了消息,Python 导入的消息类则要由接口构建过程生成。下载目录里能打开那份文件,当前 Python 环境却未必能导入 Bms。编译 SDK、加载环境之后再读电量,也就有了具体的用处。

开头那条命令没有写出 Bms,只给了话题名。Humble 的 echo 会根据图信息确定类型,再尝试加载对应的消息类。Python 示例把类型直接写进代码,命令行工具要多做一步查找。屏幕还没打印出电量时,程序可能连这些准备工作都没有做完。

如果导入 Bms 就报错,再多等几秒也补不出缺少的 Python 模块。等订阅创建起来,发布端有没有消息、双方通信条件能否匹配,才会影响后续接收。同样没有电量显示,程序停下来的位置可能很不一样。

而开头给命令的五秒,已经在流逝了。

timeout 5 管的是受它控制的整个 ROS2 命令。命令启动、初始化通信和准备消息类型,都要花时间;它不会等订阅成功以后再重新计时。即使接口文档标注电池话题频率为 1 Hz,也不能据此把五秒换算成五次完整接收机会。

遇到无法确定类型、无法加载消息类等情况,Humble 的 echo 也可能提前报错退出,根本等不到五秒。电池内容没打印出来时,终端上的错误信息同样值得读,程序还可能把它写进标准错误。

电脑准备好接收,机器人那边也得有消息发出来。电池需要保持连接并正常工作,BMS 服务也得运行。话题列表里有一个名字,可能只是订阅者把它带入了图信息,还不能据此认定机器人正在连续发送电池状态。

Humble 的话题详细信息可以进一步显示发布者数量,以及端点的名称、标识和 QoS。由此能看见谁在参与通信,至于电池消息到了没有,还得看接收程序。

双方找到彼此,还要对数据怎样交付达成匹配,这些要求写在 QoS 里。Q1 为 Bms 列出了 Best Effort、Volatile 等设置,Python 示例使用的则是 qos_profile_sensor_data。

例如,发布者提供 Best Effort,只承诺尽力交付,订阅者却要求 Reliable 的可靠交付,双方的可靠性条件就不兼容。话题名称已经出现,消息仍可能到不了回调。

在这里插入图片描述

图 4 可靠性条件兼容,不等于必然收到消息。

不过,某次 echo 是否碰到了这种组合,还要看实际端点和工具版本。Humble 的实现会根据已发现的发布者选择部分订阅参数,照搬 Python 示例的预设,未必能解决当前的接收问题。

Volatile 则意味着,迟加入的订阅者不能指望发布者替它保留此前的旧样本。电量窗口打开以后,需要等新的状态消息到来。如果五秒大半花在初始化和加载类型上,留给接收的时间也就短了。

五、订阅创建了,电量还在等下一条消息

电量程序启动后,终端里的一句提示很容易让人以为已经读到了消息。

Subscribing bms topic: /aima/hal/bms/state 紧接在订阅创建之后打印,BmsState received 则出现在回调中,后面跟着电量、电压等内容。前一句说明程序已准备订阅,后一句才对应收到的消息。

主函数中的 rclpy.spin(node) 让节点持续处理通信事件,后续消息也由回调接收。

回调拿到 Bms 后,最初想找的电量才有了着落。remaining_capacity_percentage 是剩余百分比,remaining_capacity 则以 mAh 表示剩余容量。电量窗口里要填的那个百分数,来自前一个字段。

旁边的电压、电流也有各自的单位。Bms 中的 voltage 用 mV,current 用 mA,示例把它们除以 1000,再按 V 和 A 输出。少了这一步,消息虽然收到了,屏幕上的读数却会差上一千倍。

电流的正负还表示方向,Bms 将充电记为正、放电记为负。机器人正在耗电时,电流出现负号很正常。

读到一次电量以后,开头那条带 --once 的命令便退出了。Python 示例则继续运行,由回调接收后续消息。想让电量窗口一直更新,需要的是后面这种持续接收。

窗口刚打开时,可以先显示“等待数据”,回调拿到百分比以后,再填入数值。程序正在运行,订阅已经创建,和窗口里已有一条电池读数,分别发生在不同的时候。

如果在窗口里预先填上零,这个差别就容易消失。用户无法一眼分清,机器人确实报告了零电量,还是程序尚未收到数据。等待时留空或显示等待文字,等消息到达后再更新,同一个电量位置才不会混淆这两种状态。

相邻的两条消息可能报告相同百分比,读数没有跳动,接收仍可能在继续。界面可以同时记录上次收到消息的时间,让用户知道眼前电量是刚更新的,还是已经停留了一段时间。

在这里插入图片描述

图 5 电量数值不变,消息仍可能持续更新。

官方 SDK 与 get_bms_state.py 等开发资料,可以在启元机器人开发者社区继续查阅。回到电量窗口,数字没有变化,不代表消息没有到;数字还在,也不代表数据仍然新鲜。

参考资料

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐