有段时间,实验室的哥们儿跑来找我,说他在MoveIt里把一条轨迹规划得漂漂亮亮,但老板要求他在ABB工业臂上验证一下。真机在产线上排期,想碰一下得等生产结束,时间窗口短得可怜。他的解决办法是用ABB官方仿真软件RobotStudio先做验证,可问题随之而来:MoveIt跑在Ubuntu的ROS环境里,RobotStudio跑在Windows10上,两个系统之间怎么打通?这就引出了这篇的主题——在虚拟机Ubuntu中搭建ROS环境,与Windows10中的ABB Robot Studio建立稳定的通信连接。
这类需求在机器人算法验证、离线编程、数字孪生场景里越来越常见。把虚拟机里的ROS和宿主机上的RobotStudio连起来,本质上就是打通两个世界:一边是ROS生态里丰富的运动规划、感知、仿真工具,另一边是ABB工业机器人的官方虚拟控制器。本文会从网络规划、RobotStudio侧配置、ROS侧节点实现到故障排查,完整走一遍这条路。适合正在做ABB机器人仿真联动、想在低成本环境里验证ROS控制算法的人参考,也适合刚接触RWS接口的开发者当作入门笔记。
1. 为什么要把虚拟机里的ROS和RobotStudio连起来:一个被反复问到的需求
1.1 仿真验证场景正在成为刚需
先别急着看技术细节,想清楚“为什么要连”比“怎么连”更重要。工业机器人项目里最贵的资源不是软件,而是产线上的真机时间。真机一旦投入生产,用来做算法验证的时间窗口非常有限,有时候一周能给你两个小时就不错了。而且直接拿真机调试运动规划算法,风险也不小,万一轨迹有问题,轻则报警停机,重则撞工具撞夹具。
RobotStudio的价值就在这里:它里面跑的虚拟控制器和真机控制器使用同源代码,RAPID程序、运动学模型、IO逻辑在虚拟环境里跑,跟在真机上的行为高度一致。把ROS里的规划结果发到虚拟控制器上执行,可以提前暴露大量问题,等真机时间窗口来临时,直接切换就能上手。
所以“虚拟机Ubuntu中ROS与Windows10中ABB Robot Studio通信连接”不是炫技,是真实工程里的省钱方案。用一个虚拟机加一套仿真软件,就能把原本只能在产线旁做的实验搬到办公桌上。
1.2 三层架构:控制算法层、通信链路层、仿真执行层
这套系统的整体结构并不复杂,拆开看就三层:
| 层级 | 作用 | 运行位置 | 关键组件 |
|---|---|---|---|
| 控制算法层 | 运动规划、状态监听 | Ubuntu虚拟机 | ROS、MoveIt、通信节点 |
| 通信链路层 | 数据交换与指令下发 | Windows宿主机80端口 | RWS HTTP接口 |
| 仿真执行层 | 运动学执行与状态反馈 | RobotStudio工作站 | 虚拟控制器、RAPID任务 |
ROS节点在虚拟机里运行,通过HTTP请求访问Windows宿主机上的RobotStudio RWS服务,RWS再与虚拟控制器通信。虚拟控制器执行运动后,状态数据原路返回,ROS节点拿到数据再发布到话题上。整个过程就像你在浏览器里访问一个网站:浏览器是ROS节点,网站服务器是RWS,网站背后干活的系统是虚拟控制器。
第一版实现里,我用一个Python写的ROS节点轮询RWS接口拿关节角,同时监听目标话题,收到目标值就调用RWS写接口把关节角发给虚拟控制器。这套架构简洁直接,后续加MoveIt联动也只是在算法层多接一个节点。
1.3 技术选型:为什么是RWS而不是PC SDK或Ethernet/IP
ABB RobotStudio对外提供好几种通信方式,最容易混淆的是RWS、PC SDK和Ethernet/IP。我最终选了RWS,理由很现实。
PC SDK功能很强大,能操作RAPID程序、文件系统、IO信号,但它是一套.NET库,原生跑在Windows上。ROS跑在Linux虚拟机里,要跨进程调.NET库,得在Windows侧写一个中间服务,把PC SDK封装成网络接口,相当于自己造一个RWS。既然ABB已经给了现成的RWS,没必要重复造轮子。
Ethernet/IP是工业现场总线协议,主要用来和PLC通信,走的是隐式报文和显式报文那套机制。ROS里想直接发Ethernet/IP包不是不行,但需要额外引入协议栈,而且配置IO和EDS文件相当繁琐,属于把简单问题复杂化。
RWS是ABB基于HTTP的RESTful接口,跨平台、协议直观、调试方便。用Python的requests库就能直接调,返回XML或JSON都好解析。打个比方,PC SDK像是给你一把能开所有门的钥匙,但你得自己走到门前;RWS则像是给每扇门装了一个门铃,你按一下门铃,对方把门打开,简单直接。
2. 网络是一切通信的生命线:虚拟机的联网模式选择与IP规划
2.1 三种网络模式对比
通信连接,网络是地基。虚拟机如果只有一个孤立的网络环境,后面所有HTTP请求都发不出去。VMware里常见的虚拟机网络模式有三种,我一开始在这个问题上吃了不少亏,先把对比列出来。
| 网络模式 | 虚拟机是否有独立IP | 宿主机访问虚拟机 | 虚拟机访问宿主机 | 适用场景 |
|---|---|---|---|---|
| 桥接模式 | 有,与宿主机同网段 | 直接访问 | 直接访问 | 本次通信首选 |
| NAT模式 | 有,但和宿主机不同网段 | 直接访问 | 默认不行,需配置端口转发 | 虚拟机用来上外网 |
| Host-Only | 有,私有网段 | 能访问,不能出外网 | 能访问宿主机,不能出外网 | 本地隔离测试 |
NAT模式最大的坑在于虚拟机访问宿主机需要额外做端口转发,而且转发的地址和端口管理起来很不直观。Host-Only又太封闭,没法访问外部网络。RobotStudio的RWS服务跑在Windows宿主机上,虚拟机里的ROS要主动去访问它,双方还得能双向通讯,所以桥接模式是最自然的选择。
2.2 我给这套环境的IP规划
通信之前,先把IP规划写清楚。我的实验环境里,Windows宿主机用的是有线网卡,网段在192.168.1.0/24。规划如下:
| 设备 | 操作系统 | 固定IP | 说明 |
|---|---|---|---|
| Windows宿主机 | Windows10 | 192.168.1.100 | 运行RobotStudio与RWS服务 |
| Ubuntu虚拟机 | Ubuntu 20.04 | 192.168.1.101 | 运行ROS与通信节点 |
| 网关 | 路由器 | 192.168.1.1 | 用于验证网络链路 |
为什么不直接用DHCP自动分配?因为RWS服务是常驻的,ROS节点每次都通过IP去找它,虚拟机的IP如果频繁变动,通信节点就得频繁改配置。固定IP写死之后,两边都省心。
2.3 桥接模式下Ubuntu静态IP配置步骤
VMware里把虚拟机的网络适配器切换到桥接模式,具体路径是“虚拟机”菜单 -> “设置” -> “网络适配器” -> 选中“桥接模式”。这一步做完之后,虚拟机还需要配置静态IP。Ubuntu 20.04用的是netplan,配置文件在/etc/netplan/下,通常是01-network-manager-all.yaml,内容改成这样:
network: version: 2 renderer: NetworkManager ethernets: ens33: dhcp4: no addresses: - 192.168.1.101/24 routes: - to: default via: 192.168.1.1 nameservers: addresses: [114.114.114.114]保存后执行:
sudo netplan apply然后确认网络已经生效:
ip addr show ens33注意网卡名字不一定是ens33,如果你的机器上是eth0或者ens160,就把配置文件里的名字改掉。这一步如果写错网卡名,apply的时候会直接报错,顺着报错提示调整即可。
有个小细节值得单独提出来:如果用无线网卡做桥接,连接可能不稳定,桥接模式在无线网卡上的表现偶尔会抽风。遇到这种情况,打开VMware的“虚拟网络编辑器”,把VMnet0桥接到你的无线网卡型号,通常能缓解。
2.4 自测清单:从互Ping到curl
网络配完不要急着写代码,先跑一遍自测清单,确认链路是通的:
- 在Ubuntu虚拟机里ping网关192.168.1.1,通了说明虚拟机出得去。
- 在Ubuntu虚拟机里ping宿主机192.168.1.100,通了说明虚拟机到宿主机没问题。
- 在Windows里ping虚拟机192.168.1.101,通了说明反向也OK。
- 在Ubuntu里检查80端口通不通:
nc -vz 192.168.1.100 80,通了说明RWS端口可达。 - 在Ubuntu里用curl访问RWS接口,能返回数据说明跨系统通信已建立。
第5步值得展开说一下。RobotStudio的RWS服务跑起来后,在Ubuntu虚拟机里执行:
curl -u admin:robotics http://192.168.1.100/rw/motion/robjoints如果网络和RWS都正常,会返回一段XML,里面是机器人当前的关节角数据。当你看到这段XML的时候,意味着虚拟机里的ROS和宿主机上的RobotStudio在物理链路上已经打通,剩下的就是写代码解析和处理数据了。
3. RobotStudio侧的“对外开放”:虚拟控制器与RWS接口准备
3.1 创建虚拟控制器:两分钟搞定一个ABB工作站
RobotStudio的安装这里不展开,默认你已经装好了。安装版本我以RobotStudio 2021.1搭配RobotWare 6.08为例,其他版本流程大同小异。
打开RobotStudio,新建“空工作站”,然后在“控制器”菜单里选择“虚拟控制器” -> “创建新控制器”。这一步会弹出向导,让你选机器人型号和RobotWare版本。我实验用的是ABB IRB 1200-5/0.9,这个型号小巧、虚拟控制器启动快,做通信测试足够。RobotWare版本用默认的就行,不用刻意选最新。
创建过程中RobotStudio会自动下载依赖的系统文件,首次创建可能需要几分钟,耐心等一下。完成后左侧控制器树里会出现一个控制器节点,展开能看到“任务”下的T_ROB1。T_ROB1是ABB机器人系统的默认任务名,RWS访问机器人运动接口时,很多地方都要用到这个任务名,后面写代码时会遇到。
3.2 RWS服务到底需不需要手动打开
我第一次搭这套环境时,最困惑的就是RWS服务需不需要额外开启。经验是:RobotStudio 2021.1这种新版本,虚拟控制器启动后RWS服务默认随控制器一起运行,不需要手动勾选。老版本可能会在控制器属性里有个Web Services选项,需要手动启用。新版如果访问http://localhost/rw能弹出认证框,就说明服务已经起来了。
在Windows宿主机上可以先自测一下。浏览器访问:
http://localhost/rw会弹出一个HTTP Basic认证对话框,输入用户名admin、密码robotics,能进到RWS的欢迎页或者返回一段XML,就说明RWS服务正常。
如果你在浏览器里访问时发现打了账号密码还是401,先确认一下你用的RobotWare版本。RobotWare不同版本默认密码不一样,有的版本是admin/admin,有的是admin/robotics。可以在RobotStudio的控制器属性里找到用户管理,确认当前系统用户的账号密码。这个信息直接关系到后面ROS节点能不能通过认证,先确认清楚再往下走。
3.3 Windows防火墙与RWS默认端口
RWS默认跑在80端口。如果ROS节点从虚拟机访问宿主机IP时,发现TCP连接被拒或者直接卡住,多半是Windows防火墙把80端口拦了。
处理方式是在Windows防火墙高级设置里添加入站规则,放行TCP 80端口。具体路径:控制面板 -> Windows Defender防火墙 -> 高级设置 -> 入站规则 -> 新建规则 -> 端口 -> TCP -> 特定本地端口填80 -> 允许连接。这一步做完后,从Ubuntu虚拟机再执行nc -vz 192.168.1.100 80,应该能连上。
这里有个安全提醒:80端口只建议在专用网络里放行,如果你所在的网络环境是公共WiFi,别为了省事把防火墙全关掉,只要放行必要端口就好。RWS的认证虽然走的是HTTP Basic,但它毕竟是明文传输,在不可信网络里裸奔风险很大。
3.4 在Windows上先自测接口
回到Windows宿主机上,打开命令行,先验证一下RWS接口:
curl -u admin:robotics http://localhost/rw/motion/robjoints返回的XML里会有一堆<joint j="1">-0.025115</joint>这样的字段,j="1"到j="6"对应机器人六个关节轴,数据单位是弧度。看到这段XML,就说明虚拟控制器已经把当前关节角暴露出来了。
这里有个关键技术点:RWS返回的关节数据中除了机器人本体六个关节,还可能有外部轴(extax)的数据。外部轴是ABB系统里用于导轨、变位机这类附加轴的表示,如果你们的设备只有机器人本体没有外部轴,extax字段会一直是0。ROS节点解析数据时要注意区分,别把外部轴的值当成机器人关节用。
4. ROS侧通信节点的设计与实现:从轮询到指令下发
4.1 节点任务划分与话题设计
网络通了,RWS接口也能访问了,剩下的核心工作就是在ROS侧写一个通信节点,把这套链路串起来。
我的通信节点设计比较简单,就干三件事:
- 按固定频率轮询RWS的robjoints接口,拿到机器人当前关节角。
- 把关节角发布到/joint_states话题,这样ROS里的MoveIt、RViz都能订阅使用。
- 订阅/joint_cmd话题,收到目标关节角后,通过RWS写接口把目标值发给虚拟控制器。
话题设计如下:
| 话题名 | 消息类型 | 方向 | 说明 |
|---|---|---|---|
| /joint_states | sensor_msgs/JointState | 节点发布 | 机器人当前关节状态 |
| /joint_cmd | std_msgs/Float64MultiArray | 节点订阅 | 目标关节角,顺序为joint1~joint6 |
之所以用Float64MultiArray而不是JointState作为指令消息,是因为规划端往往只关心6个关节的目标值,用一个双精度数组表达最直接。发布指令时只要按顺序填6个弧度值就好。
4.2 完整Python节点代码
我的实现用Python的requests库发HTTP请求,用xml.etree.ElementTree解析返回数据。完整代码如下:
#!/usr/bin/env python3 import rospy import requests import xml.etree.ElementTree as ET from sensor_msgs.msg import JointState from std_msgs.msg import Float64MultiArray RWS_HOST = "192.168.1.100" RWS_USER = "admin" RWS_PWD = "robotics" TASK = "T_ROB1" RWS_NS = {"rw": "http://www.abb.com/RWS"} session = requests.Session() session.auth = (RWS_USER, RWS_PWD) session.headers.update({"Accept": "application/xml"}) def read_robot_joints(): url = "http://{}/rw/motion/robjoints".format(RWS_HOST) resp = session.get(url, timeout=2.0) resp.raise_for_status() root = ET.fromstring(resp.text) joints = {} for item in root.findall(".//rw:joint", RWS_NS): jname = item.attrib.get("j") joints[jname] = float(item.text) return [joints[str(i)] for i in range(1, 7)] def write_robot_joints(q): url = "http://{}/rw/motion/tasks/{}/robjoints".format(RWS_HOST, TASK) data = {} for i, val in enumerate(q, start=1): data["joint-{}".format(i)] = str(val) resp = session.post(url, data=data, timeout=2.0) resp.raise_for_status() def main(): rospy.init_node("abb_rws_bridge") pub = rospy.Publisher("/joint_states", JointState, queue_size=1) rate = rospy.Rate(10) def on_cmd(msg): rospy.loginfo("收到目标关节角,下发至虚拟控制器") write_robot_joints(list(msg.data)) rospy.Subscriber("/joint_cmd", Float64MultiArray, on_cmd) joint_names = ["joint1", "joint2", "joint3", "joint4", "joint5", "joint6"] while not rospy.is_shutdown(): try: q = read_robot_joints() msg = JointState() msg.header = rospy.Time.now().to_msg() msg.name = joint_names msg.position = q pub.publish(msg) except Exception as e: rospy.logwarn("通信异常: {}".format(e)) rate.sleep() if __name__ == "__main__": main()这段代码有几个细节值得注意。第一,用requests.Session而不是裸的requests.get,是因为Session会复用TCP连接,多次轮询时性能好很多,不容易出现连接被反复拆除再建立的情况。第二,解析XML时必须带上命名空间,直接root.findall(".//joint")是找不到节点的,需要把{"rw": "http://www.abb.com/RWS"}传进findall的命名空间参数。第三,发布JointState时header里的时间戳用rospy.Time.now()而不是Python的time.time(),这样ROS的TF和轨迹回放功能才能正确识别消息时序。
4.3 指令下发:手动模式下写关节角
write_robot_joints函数是核心下行通路。首次调试时,RobotStudio里的虚拟控制器如果处于自动模式,写robjoints接口可能不会产生预期的移动效果,因为自动模式下运动指令由RAPID程序控制。所以在RobotStudio的操作界面上,把控制模式切到手动(Manual)模式。手动模式下,通过RWS写入关节角会让虚拟控制器尝试驱动机械臂达到目标位置。
我在RobotStudio 2021.1 + RobotWare 6.08上测试时,写接口的字段名用的是joint-1到joint-6。如果你的RobotWare版本较新或较旧,字段命名可能有差异。如果发现POST后没有反应,可以用RobotStudio自带的抓包工具或者WireShark看一下RWS请求的实际字段格式,多半是joint-1和j1的区别。这个版本差异我踩过,印象太深了。
4.4 跑起来:rostopic echo验证与控制实验
节点写完,放到ROS环境里跑起来。假设你已经建好了catkin工作区,把文件放到某个包的src目录下,然后:
chmod +x abb_rws_bridge.py source ~/catkin_ws/devel/setup.bash rosrun your_package abb_rws_bridge.py另开一个终端,查看关节状态:
rostopic echo /joint_states这时在RobotStudio里用手动操纵功能拖动虚拟机械臂,RViz里如果订阅了/joint_states,能看到虚拟机械臂跟着同步运动。ROS和RobotStudio之间已经实现了实时状态同步。
再验证下行控制链路。发布一组目标关节角:
rostopic pub -1 /joint_cmd std_msgs/Float64MultiArray "data: [0.1, -0.2, 0.3, 0.0, 0.0, 0.0]"如果RobotStudio里的虚拟控制器处于手动模式,机械臂会动起来,移动到目标角度附近。到了这一步,双向通信就已经完全打通了。
5. 三次翻车与完整排查链路:网络、认证、数据解析
5.1 故障一:虚拟机ping不通宿主机
第一次搭建时,我掉进了最简单的坑里。配完静态IP后,虚拟机里ping 192.168.1.100,一直request timeout。当时我的第一反应是Windows防火墙把ICMP拦了,但仔细想想不对劲,防火墙一般只拦入站ICMP,不会让宿主机彻底不可达。
排查链路应该是这样的:先ping网关192.168.1.1,发现也是timeout。这个结果说明问题不在Windows防火墙,而在虚拟机自身的网络路径上。跑去检查VMware的虚拟网络适配器,发现还挂在NAT模式上,根本不在桥接的网段里。切到桥接模式后,网关通了,宿主机也通通了。
如果发现网关通、宿主机不通,那才轮到Windows防火墙的ICMP回显问题。Windows默认会拦ICMP回显请求,需要在“高级安全Windows Defender防火墙”里放行“文件和打印机共享(回显请求 - ICMPv4-In)”规则。这一步做完,双向ping就通了。
5.2 故障二:401 Unauthorized连续报错
网络通了之后,我在Ubuntu虚拟机里执行curl访问RWS接口,返回401 Unauthorized。这是HTTP Basic认证失败的标准响应,说明账号密码不对。
排查过程是这样的:先试了admin/admin,401。再试admin/robotics,还是401。当时一度怀疑RWS服务挂了,但浏览器访问Windows本机的http://localhost/rw时明明能弹出认证框。问题可能出在账号密码上,而不是服务本身。
后来我在RobotStudio的控制器属性里翻了半天,确认虚拟控制器的用户管理里,当前激活的密码是安装系统时设置的自定义密码。把这个密码在curl里试了一下,返回正常XML。这个坑提醒我:网上很多教程写的默认密码不一定适用你的版本,最可靠的方法是去RobotStudio的控制器属性里查用户信息。
5.3 故障三:数据握手成功但角度永远对不上
链路通了、认证过了、数据也能读了,但把读到的数据发布到/joint_states后,RViz里的模型角度和RobotStudio里的实际角度对不上,看起来像“抽筋”一样乱跳。
这个问题分两部分。第一部分是解析时没有正确处理命名空间,导致读出来的数据全是0或者缺值。在ElementTree里,findall(".//joint")找不到带默认命名空间的节点,必须用findall(".//rw:joint", {"rw": "http://www.abb.com/RWS"})。我第一次写代码时偷懒没理命名空间,解析结果为空列表,程序却也不报错,只是发布出去的数据全是默认值,场面一度十分迷惑。
第二部分是顺序问题。RWS返回的robjoints里,可能同时包含外部轴和机器人本体数据。如果不加筛选,直接把所有数据塞进JointState,顺序和数量都对不上。正确做法是只取j="1"到j="6"这六个机器人本体关节。如果你的URDF里关节名不叫joint1~joint6,发布时name列表也要跟着改,否则TF树匹配不上。
5.4 给排查过程做个减法
回头复盘这几个坑,我发现它们都有一个共同特征:问题都出在“看似理所当然”的环节。网络模式想当然觉得是桥接,结果还挂在NAT上;账号密码想当然用网上的默认值,结果被自定义密码挡在门外;XML解析想当然找节点,结果忘了命名空间这回事。
我的经验是,碰到跨系统通信问题时,先砍掉所有中间环节,做最小系统验证。网络不通就先测网关,网关通了再测宿主机,宿主机通了再测端口,端口通了再测接口认证,一步一步缩小范围。不要一上来就在ROS节点里写满日志,那样反而把问题淹没在信息流里。
6. 从“能通信”到“能干活”:数据对齐、频率控制与后续扩展
6.1 单位、坐标系、关节名字:数据对齐的三座大山
通信链路通了只是第一步,真正要让这套系统干实事,还得过三关:单位、坐标系、关节名字。
单位方面,ABB RWS返回的关节角单位是弧度,不是度。如果你在MoveIt里处理的角速度、加速度是别的方式换算的,发布给虚拟控制器之前必须统一。很多人第一次接手时想当然认为是度,结果机器人猛转一下,吓一跳。
坐标系方面,ABB机器人的基坐标系定义和ROS里URDF的基坐标系定义可能存在差异,尤其是关节零位和正方向。如果发现RViz模型和RobotStudio里角度一致但姿态看起来不对,多半要检查关节正方向和零位偏移。
关节名字方面,URDF里的关节名、MoveIt规划组的关节名、以及RWS返回的j1~j6,这三者要一一对应。我的做法是在通信节点里用一个映射表把RWS的索引翻译成URDF关节名,这样上层完全感知不到底层接口的差异。
6.2 轮询频率与HTTP连接池
我上面给的节点用的轮询频率是10Hz,实测在虚拟控制器上完全够用。如果你觉得状态刷新太慢,可以试着调到20Hz,但要留意几个问题。
一个是HTTP请求的响应时间。RWS接口正常情况下几毫秒就能返回,但如果RobotStudio里虚拟控制器负载较高,或者虚拟机CPU资源紧张,响应时间可能飙升。轮询频率太高但接口响应不过来,反而会导致请求堆积,占满连接池。
另一个问题是requests.Session的连接复用。Session会自动维护HTTP连接池,但如果服务器主动断开连接,Session可能不知道,下次请求会报ConnectionResetError。我的经验是给get和post请求都加上timeout参数,并且在外层做异常捕获。出现ConnectionResetError时重新建立Session,基本就能解决。
6.3 下一步:MoveIt轨迹联动与真机切换
通信打通后,这套系统的扩展空间很大。
最常见的扩展方向是MoveIt联动。MoveIt规划出来的轨迹是一串带时间戳的关节角序列,你可以让MoveIt而不是人肉去发布/joint_cmd。这样RViz里规划的轨迹就能同步到RobotStudio的虚拟控制器里执行。如果后续要换成真机,只要把RWS_HOST从宿主机IP换成真机控制器的IP,通信节点基本不用改,RWS接口在真机虚拟控制器上同样可用。
另一个扩展方向是状态闭环。目前我的实现是单向读取关节角加单向下发目标值,如果要做轨迹跟踪或者力控这类闭环算法,可以在节点里引入PID反馈,把读取到的关节角和目标值做误差计算,再通过RWS写接口调整指令值。这样做的前提是控制周期要足够短,RWS接口的响应速度会成为瓶颈,需要做性能测试。
说起这套系统,我个人最大的体会是:跨系统通信的难点不在写代码,而在于把网络、协议、认证这些基础问题钉死在纸面上。只要基础链路稳固,后续的业务逻辑都是水到渠成的事。