简介:面向UR(优遨)机器人二次开发与自动化集成人员的RTDE实时数据交换手册,聚焦如何通过标准TCP/IP连接将外部程序与UR控制器同步,同时不破坏控制器实时属性,适用于现场总线驱动交互、机器人I/O操作与轨迹状态绘制等场景。资源包共1个PDF文件,约226KB,内容围绕RTDE接口展开,涵盖关键特性、机器人控制器输入输出字段名及对应数据类型、数据同步的setup与同步循环两阶段机制,以及协议各报文细节,包括Header、协议版本请求、控制器版本获取、文本消息、数据包与输入输出控制包等。已有929人学习,适合需要精确控制与实时反馈的工业应用开发者,可据此理解recipe配置、字段订阅与二进制序列化数据交换流程,快速搭建定制化同步方案。
1. 从一条 125Hz 的数据流说起:RTDE 到底解决什么问题
很多做机器人集成的朋友第一次接触 RTDE,都是被一个很具体的需求逼过来的:上位机要实时拿到机械臂的关节角、TCP 位姿和数字输入状态,同时还要往控制器里写速度倍率和几个通用寄存器。用 Modbus 轮询吧,延迟和抖动都不太可控;用现场总线吧,又得额外加硬件网关。RTDE(Real-Time Data Exchange)就是在这个缝隙里长出来的东西——它跑在标准 TCP/IP 上,默认端口 30004,控制器一开机就在监听,不需要额外授权或插件,客户端连上去就能按需订阅数据。
这份手册的价值不在于教你机器人运动学,而在于把 RTDE 的协议帧、字段表、数据类型和握手流程一次性摊开。它适合两类人:一类是要给产线写上位机采集程序、需要知道每个字段叫什么类型占几字节的工程师;另一类是已经用现成库跑通了 demo,但遇到“数据对不上”“连上就断”“写进去没反应”想回头查协议细节的人。手册本身是协议级文档,不是教程,所以下面我会把它拆成能直接抄的步骤,顺带把几个容易翻车的地方标出来。
2. 协议帧拆解:Header、控制包与数据包怎么拼
RTDE 的通信模型很朴素:一条 TCP 长连接,客户端和服务端来回发二进制包。所有包都以一个固定的 Header 开头,后面跟 payload。理解这个 Header 是读懂后面所有控制包的前提,也是自己手写客户端时第一个要过的坎。
2.1 Header 的三个字段与字节序陷阱
Header 固定 3 个字段,共 8 字节:RTDE_PROTOCOL_VERSION(UINT16,2 字节)、package_type(UINT8,1 字节)、payload_size(UINT16,2 字节),后面还有 3 字节保留位。手册里明确写了“数据始终以网络字节序发送,即大端”。这句话看着不起眼,但它是新手翻车率最高的地方——你在 x86 机器上用 Python 的struct解包,如果格式串写成小端,读出来的payload_size会是一个离谱的大数,然后你就卡在等数据上,socket 一直阻塞,日志里什么都没有。
常见做法是用struct.unpack(">HB", ...)这种带>的格式串,或者干脆用现成的ur_rtde库把这一层封掉。我一般建议先手写一遍 Header 解析,把字节流打印出来对照,确认自己理解了大端,再去用库,否则出了问题你连从哪查都不知道。
import struct # RTDE Header 固定 8 字节:版本(2) + 包类型(1) + payload长度(2) + 保留(3) HEADER_FMT = ">H B H 3x" # > 表示大端,3x 跳过 3 字节保留位 HEADER_SIZE = struct.calcsize(HEADER_FMT) # 结果应为 8 def parse_header(raw: bytes): if len(raw) < HEADER_SIZE: raise ValueError("header 不完整") version, pkg_type, payload_size = struct.unpack(HEADER_FMT, raw[:HEADER_SIZE]) return version, pkg_type, payload_size这段代码的关键在格式串:>H是 2 字节无符号大端,B是 1 字节,3x吃掉保留位。calcsize算出来必须是 8,如果不是,说明你的格式串写错了。参数上唯一能改的是版本号的处理——协议版本 1 和 2 在RTDE_TEXT_MESSAGE上有差异,后面会提。
2.2 控制包类型与握手顺序
手册列出的包类型构成了完整的握手链路,顺序不能乱:
| 包类型 | 方向 | 作用 |
|---|---|---|
| RTDE_REQUEST_PROTOCOL_VERSION | 客户端→控制器 | 协商协议版本,不协商则默认 v1 |
| RTDE_GET_URCONTROL_VERSION | 客户端→控制器 | 拿控制器软件版本,判断字段可用性 |
| RTDE_CONTROL_PACKAGE_SETUP_OUTPUTS | 客户端→控制器 | 提交输出 recipe,返回 recipe_id |
| RTDE_CONTROL_PACKAGE_SETUP_INPUTS | 客户端→控制器 | 提交输入 recipe,返回 recipe_id |
| RTDE_CONTROL_PACKAGE_START | 客户端→控制器 | 启动同步循环 |
| RTDE_CONTROL_PACKAGE_PAUSE | 客户端→控制器 | 暂停同步循环 |
| RTDE_DATA_PACKAGE | 双向 | 实际数据,无返回 |
这里有个容易被忽略的点:SETUP_OUTPUTS和SETUP_INPUTS的返回里带recipe_id,后续START包要带上这个 id。如果你只配了输出没配输入,输入 recipe_id 传 0 即可。手册里说输入 recipe 配置成功会拿到唯一 id,但没强调“输出 recipe 也必须先配好才能 START”,实际实现里顺序反了会收到一个错误码,而不是崩溃,所以很多人以为是网络问题。
# 构造 SETUP_OUTPUTS 请求:变量名列表用逗号分隔,末尾不加逗号 def build_setup_outputs(field_names): body = ",".join(field_names).encode("ascii") # 包类型 0x4F 对应 SETUP_OUTPUTS,payload 就是字段名串 header = struct.pack(HEADER_FMT, 2, 0x4F, len(body)) return header + body字段名串有 2048 字节上限,超了控制器直接拒绝。实际写的时候别贪多,先订actual_q、actual_TCP_pose、timestamp三个跑通,再往上加。
2.3 数据包的序列化规则
RTDE_DATA_PACKAGE的 payload 是按你 recipe 里字段的声明顺序依次序列化的,没有字段名、没有分隔符,纯靠类型表推算偏移。手册的数据类型表就是干这个用的:BOOL 占 8 位、UINT32 占 32 位、DOUBLE 占 64 位、VECTOR6D 是 6 个 DOUBLE 共 384 位。你订了timestamp(DOUBLE) +actual_q(VECTOR6D),那 payload 就是 8 + 48 = 56 字节,前 8 字节解成 double,后 48 字节解成 6 个 double。
这里有个血泪经验:BOOL 类型在协议里占 8 位而不是 1 位,如果你按位去抠,整个偏移全错。另外VECTOR6INT32是 6 个 INT32,别和VECTOR6D搞混,两者字节数一样但解出来的值天差地别。
# 按 recipe 顺序解析 DATA_PACKAGE,这里假设 recipe = [timestamp, actual_q] def parse_data_package(payload: bytes): ts = struct.unpack(">d", payload[0:8])[0] q = struct.unpack(">6d", payload[8:56]) return {"timestamp": ts, "actual_q": q}参数说明:>d是 8 字节大端 double,>6d是 6 个连续 double。偏移量 8 和 56 是硬编码的,换 recipe 就必须重算,所以生产代码里我一般用一个字段类型表动态累加偏移,而不是手写数字。
3. 字段表怎么用:输入输出寄存器与版本边界
手册最厚的部分就是两张字段表——控制器输入和控制器输出。这两张表不是让你全订,而是让你在写 recipe 时查类型和可用版本。用错版本是第二个高频翻车点。
3.1 输入字段:速度倍率、数字输出与通用寄存器
输入方向是客户端往控制器写。最常用的几个:speed_slider_mask+speed_slider_fraction控制速度倍率,mask 置 1 才生效,fraction 是 0 到 1 的 DOUBLE;standard_digital_output_mask+standard_digital_output控制标准数字输出,mask 是 UINT8 位掩码;通用寄存器分三段,input_bit_register_X的 X 从 64 到 127 是留给外部 RTDE 客户端的,0 到 63 那段被现场总线占了。
这里有个设计上的坑:手册写“同一时刻只允许一个 RTDE 客户端控制某个变量”。如果你产线上已经有一个 PLC 通过现场总线在写input_int_register_0,你的上位机再去写同一个寄存器,行为是未定义的——不是报错,是静默覆盖。我见过一个项目两个程序抢同一个 double 寄存器,调了半天以为是浮点精度问题,最后发现是寄存器冲突。
# 写速度倍率:mask 置 1 表示本次用 fraction 覆盖,fraction 范围 0.0~1.0 def build_speed_input(fraction: float): mask = struct.pack(">I", 1) # speed_slider_mask = 1 frac = struct.pack(">d", fraction) # speed_slider_fraction return mask + frac注意 mask 是 UINT32 不是 UINT8,手册表里写得很清楚,但很多人凭直觉写成 1 字节,结果整个 payload 错位。
3.2 输出字段:位姿、关节与状态位
输出方向是控制器往客户端发。actual_TCP_pose是 VECTOR6D,前三个是 xyz 米,后三个是旋转向量 rx ry rz(不是欧拉角,别直接拿去当 RPU 用);actual_TCP_speed线速度单位 m/s,角速度 rad/s;robot_status_bits和safety_status_bits是 UINT32 位域,手册给了每一位的含义,比如 bit0 是上电、bit1 是程序运行、bit2 是示教按钮按下。
位域解析建议写个小工具函数,别在业务代码里到处& 0x01:
ROBOT_STATUS_BITS = { 0: "power_on", 1: "program_running", 2: "teach_button_pressed", 3: "power_button_pressed", } def decode_bits(value: int, mapping: dict): return {name: bool(value & (1 << bit)) for bit, name in mapping.items()}safety_status_bits有 11 位,含义比 robot 状态更关键,做安全联锁时优先看它而不是自己拼条件。
3.3 版本差异:哪些字段在老控制器上不存在
手册每个字段后面都标了“Introduced in version”,比如input_bit_register_X(64~127)是 3.9.0 / 5.3.0 才有的,script_control_line是 3.14.0 / 5.9.0,ft_raw_wrench是 5.9.0。如果你的控制器是 3.7 的,recipe 里写了ft_raw_wrench,SETUP_OUTPUTS的返回会明确告诉你这个变量没找到,而不是静默忽略。
所以正确姿势是:连上之后先发RTDE_GET_URCONTROL_VERSION,拿到版本号,再根据版本决定 recipe 里放哪些字段。别把新字段硬编码进 recipe 然后指望老控制器兼容,那不叫兼容,那叫碰运气。
4. 避坑与排查:五个真实踩过的坑
4.1 连上就断,日志里只有 EOF
现象:socket connect 成功,发完第一个包,recv 返回空,连接被对端关闭。原因:Header 的payload_size算错了,控制器按错误的长度去读 payload,读越界后直接断链。最常见的是把字段名串的字节数算成了字符数,中文或特殊符号混进去就多算。解决:发之前len(body)打印出来,和payload_size对一遍;字段名只用 ASCII,别带空格。
4.2 数据能收到但数值全是乱的
现象:actual_q解出来是 1e-300 这种离谱值,或者六个关节角完全对不上。原因:字节序搞反了,或者 recipe 顺序和解析顺序不一致。RTDE 是大端,Python 默认struct不带>就是本机序,x86 上就是小端。解决:所有struct.unpack强制加>;解析顺序严格按 recipe 声明顺序,别按字段表顺序。
4.3 写进去的寄存器读回来没变
现象:客户端写了input_double_register_24,但控制器端程序读到的还是旧值。原因:输入 recipe 配了但没 START,或者 START 之后客户端没有在值变化时发送DATA_PACKAGE。手册明确说“输入保持最后收到的值,不需要恒定更新率”,但前提是你至少发过一次。解决:确认START返回成功;写寄存器时构造完整的输入 DATA_PACKAGE,哪怕只改一个字段也要把 recipe 里所有输入字段按顺序填上。
4.4 125Hz 跑不满,数据有跳变
现象:预期每秒 125 帧,实际只有 80 多帧,且时间戳不连续。原因:手册写了,控制器实时循环优先级高于 RTDE,资源紧张时会跳过输出包,且跳过的包不会补发,只发最新数据。这不是 bug,是设计。解决:客户端别假设固定频率,用timestamp字段做时间基准;对连续性要求高的场景,把重计算放到外部 PC,别在控制柜 PC 上跑图像处理。
4.5 两个客户端抢同一个变量
现象:A 程序写的值被 B 程序覆盖,或者读到的值在两个值之间跳。原因:手册的 Note 写得很清楚,同一变量同一时刻只允许一个 RTDE 客户端控制。现场总线和 RTDE 客户端同时写同一寄存器段就会冲突。解决:规划寄存器分配,0~23 段留给现场总线,24~47 段给 RTDE 客户端,别越界;上线前用netstat确认没有多余的 RTDE 连接挂着。
5. 从手册到可用客户端:一个最小验证闭环
把前面几块拼起来,一个能自证正确的验证流程是这样的:先连 30004,发GET_URCONTROL_VERSION确认版本;再发SETUP_OUTPUTS订timestamp+actual_q+actual_TCP_pose,拿到 recipe_id;发START;然后循环收DATA_PACKAGE,用timestamp做单调性检查,用actual_q的六个值做范围检查(关节角一般在 ±2π 内,超出就是解析错了)。这个闭环跑通,再往上加输入 recipe 和写寄存器。
验证时我习惯加一个“回环对照”:订actual_digital_input_bits,同时用示教器手动拨一个数字输入,看客户端解出来的 bit 有没有跟着变。这一步能同时验证字节序、偏移和位域解析三件事,比盯着关节角看半天有效。
| 验证项 | 方法 | 通过标准 |
|---|---|---|
| 版本协商 | 发 REQUEST_PROTOCOL_VERSION 看返回 | 返回版本号与控制器一致 |
| 字段可用性 | SETUP_OUTPUTS 返回 recipe_id | 非 0 且无 not found |
| 字节序 | 解析 timestamp | 单调递增,量级为秒 |
| 偏移正确 | 解析 actual_q | 六值在 ±2π 内且平滑 |
| 位域正确 | 拨数字输入看 bits | 对应 bit 翻转 |
最后说个习惯:从那以后我每次接新控制器,都强制先跑一遍这个最小闭环,把 Header、recipe、DATA_PACKAGE 三层各打印一行原始字节,确认无误再写业务逻辑。RTDE 这东西协议不复杂,复杂的是它太“安静”——错了不报错,只是数据不对,所以验证步骤不能省。希望帮到你。
本文还有配套的精品资源,点击获取