Add synchronous and streaming MotorService APIs backed by the PLC Modbus TCP runtime and protocol driver. Extend AUBO JSON commands and isolate vendor libstdc++ paths while keeping build-tree tests runnable.
46 KiB
MotorService 与 Modbus TCP PLC 电机控制
本文说明当前 MotorService、CMVR PLC v1 寄存器协议和 Siemens
S7-1215C DC/DC/DC 侧的对接方法。本文只描述当前源码已经存在的接口和
协议;寄存器表中标为“预留”或“当前未发送”的内容,不代表上层已经开放。
1. 当前范围
当前实现是一条刻意保持简单的单轴控制链:
gRPC MotorService
|
v
MotorManager
|
v
AbstractMotor
|
v
CmvrPlcMotorProtocol
|
v
ModbusTcpMotorBusRuntime
|
v
libmodbus -> PLC MB_SERVER -> 驱动器/电机
各层职责如下:
MotorService:解析MotorManagerID 和电机选择器,执行单电机互斥、 同步等待、gRPC 取消检测、流式 latest-wins 和软件急停抢占。MotorManager:按motor_id或joint_name返回AbstractMotor。AbstractMotor:提供统一的置零、Profile、Cyclic、使能和 Quick Stop 接口。CmvrPlcMotorProtocol:执行关节限位检查,把 SI 单位转换为定点整数, 并映射成 CMVR PLC v1 命令。ModbusTcpMotorBusRuntime:管理一个 PLC 连接、握手、心跳、重连、每轴 命令序列、payload/commit 写入和 ACK 轮询。- PLC:必须原子接收命令、执行驱动器控制、发布状态,并独立执行通信和 周期指令 watchdog。
当前服务按“一个 RPC 或一个流独占一个电机”仲裁,不是多轴同步控制器。
同一个 MotorManager 中不同电机可以分别被不同调用者控制。需要多轴同一扫描
周期锁存时,应在后续版本增加 manager/runtime 级批量 commit,不应依赖客户端
逐轴调用。
runtime 的 start() 启动连接 supervisor,而不是要求 PLC 当时已经在线。首次
同步握手失败时 MotorManager 仍完成注册,runtime 保持
connected=false,并从 reconnect_min_ms 起按上限退避持续重试。离线期间
状态 RPC 返回 UNAVAILABLE,运动命令在写 mailbox 前安全失败;PLC 后续上线
并完成完整身份/session 握手后无需重启 cmvr_es。
2. 实际 gRPC 接口
服务定义位于:
protos/cmvr/api/motor_service.protoprotos/cmvr/api/motor_command.proto
MotorTarget.header.device_id 是 MotorManager 的设备 ID,不是单个电机
的 ID。MotorTarget.selector 必须且只能设置一个:
motor_id uint32,当前服务要求不大于 255
joint_name 非空字符串
2.1 Unary RPC
| RPC | 当前语义 | 是否阻塞 |
|---|---|---|
setZero |
调用 calibrateZeroQ()。PLC 后端发送 SetZero,等待 PLC 进入成功终态后返回 |
是,PLC 后端安全命令上限当前为 5 s |
moveToZero |
复用 Profile Position,把目标位置设为 0 rad;不是 setZero,也不发送寄存器图中的 MoveToZero opcode |
是,直到到位、取消或超时 |
profilePosition |
设置 Profile Position 的目标位置、最大速度和加速度 | 是,直到到位、取消或超时 |
profileVelocity |
设置 Profile Velocity 的目标速度和加速度 | 是,直到实际速度稳定进入容差 |
emergencyStop |
抢占当前阻塞命令/流,设置服务内软件急停锁存并调用 quickStop() |
是,等待 PLC Quick Stop 成功终态 |
getStatus |
读取模式、位置、速度、到位标志和 MotorService 仲裁状态 | 同步快照读取 |
setEnabled |
enabled=true 调用 torqueOn(),成功后解除服务内软件急停锁存;false 调用 torqueOff() |
是,等待 PLC 成功终态 |
moveToZero、profilePosition 和 profileVelocity 使用
MotorWaitOptions:
| 字段 | 0 时默认值 |
服务端范围/语义 |
|---|---|---|
timeout_ms |
30000 ms | 1 ms 至 600000 ms |
poll_period_ms |
10 ms | 1 ms 至 1000 ms |
position_tolerance_rad |
0.001 rad | 仅正值覆盖默认值 |
velocity_tolerance_rad_s |
0.01 rad/s | 仅正值覆盖默认值 |
settle_sample_count |
3 | 1 至 1000 个连续样本 |
setZero 只有在 PLC 返回 Completed + ZeroValid 且 zero_epoch 相比命令前
变化时才成功。如果 RPC 返回失败、取消、断链或超时,而 commit 可能已经写出,
session/zero epoch 的最终结果是不确定的;禁止直接盲重试,应先通过 PLC/HMI
或维护诊断确认当前 session、zero_epoch 和实际零位状态。
Profile Position 只有在 PLC/驱动器报告 target_reached,同时实际位置和
速度连续满足容差时才返回成功。Profile Velocity 在实际速度连续满足目标速度
容差时返回成功;RPC 返回后速度命令仍然有效。正常停车应再发送目标速度为
0 rad/s 的 profileVelocity,不能把 RPC 返回理解为“速度控制已经结束”。
Profile Position/Velocity 的 PLC payload 使用 600000 ms
command_timeout_ms 上限,与 MotorService 允许的最长等待一致。RPC 使用更短
超时或被取消时,服务端仍会主动发送 Quick Stop;PLC 通信 watchdog 仍是上位机
失联时的权威保护。该 600000 ms 是 PLC 执行上限,不会把 RPC 默认等待从
30000 ms 延长。
成功 ACK 的 Profile Position/Velocity 会绑定到提交时的
connection_epoch。只要 PLC 断线重连使 epoch 改变,该 Profile 的位置、速度
和到位反馈就全部失效:getQ/getQd 返回 NaN,reachedTargetQ 返回 false。
即使新 session 的安全停止速度恰好也是目标 0 rad/s,也不能把数值相等误认为
旧命令成功。成功的新 Profile 会重新绑定当前 epoch;成功的首个 Cyclic
sample、Quick Stop 或 Disable 会清除该绑定。Enable/SetZero 本身不恢复旧
Profile 反馈的可信性。
当前 getStatus 会依次调用模式、位置、速度和到位读取。对于 Modbus TCP 后端,
这些读取不是一次原子寄存器快照,字段可能来自相邻的 PLC 扫描周期。
它只暴露 AbstractMotor 的通用状态和 MotorService 仲裁状态,不包含
plc_boot_id、session、fault_code、zero_epoch 或 PLC result code。
结果不确定或需要故障恢复时,应从 PLC/HMI 或维护诊断层读取这些原始字段,
不能只依赖本 RPC。
2.2 双向流 RPC
当前有两个双向流:
rpc streamCyclicPosition(stream CyclicPositionRequest)
returns (stream CyclicControlResponse);
rpc streamCyclicVelocity(stream CyclicVelocityRequest)
returns (stream CyclicControlResponse);
首帧必须是 open:
open.target 选择一个 MotorManager 中的单个电机
open.watchdog_timeout_ms gRPC 层输入 watchdog
watchdog 为 0 时默认 500 ms,非零值被限制到 20 ms 至 60000 ms。打开
成功后,服务端先返回:
phase = CYCLIC_STREAM_OPENED
sequence = 0
status = 当前电机状态
CYCLIC_STREAM_OPENED 只表示 MotorService 已解析目标、取得该电机的独占
控制权并选择了 Cyclic 模式。CMVR PLC 后端采用惰性打开:收到第一个合法
setpoint 时才依次发送 OpenCyclic* 和对应的 Cyclic*Sample。因此客户端
必须等到该 setpoint 的 CYCLIC_STREAM_APPLIED,才能确认 PLC 已 ACK 打开
命令并锁存了首个样本。
每次 OpenCyclicPosition/Velocity 都创建新的轴级 stream epoch。PLC 必须在
Open 的同一个原子状态事务中清零 last_applied_cyclic_sequence、旧样本去重
状态和 cyclic watchdog,再设置正确的 CSP/CSV mode 与 StreamActive=1。
runtime 只有确认这些 Open 后置条件才接受 ACK,Protocol 随后才从样本序列 1
重新开始。Quick Stop、Disable 或断链关闭旧 epoch;重开后的首个 1 必须
重新应用,不能被旧流的样本 1 去重。
每次 gRPC cyclic 流首帧触发的 setMode 都建立一个新的上层
cyclic_generation,即使模式值与上一条流相同。该 generation 绑定当时的
connection_epoch。活动流遇到重连后会永久锁存失败:同一流的当前和后续
setpoint 都返回失败,不发送新的 OpenCyclic* 或 Cyclic*Sample;服务端
终止并 Quick Stop 该流。客户端必须创建新的 gRPC 流,由新首帧显式建立新的
generation 后才允许重新 Open。禁止把断链前或断链期间 pending 的 latest
setpoint 自动应用到新 PLC session。
后续每帧只能是 setpoint:
- 周期位置:严格递增且非零的
sequence、target_position_rad,以及可选target_velocity_rad_s。 - 周期速度:严格递增且非零的
sequence、target_velocity_rad_s。
序列号允许跳号,但不能为 0、重复或倒退。服务端 reader 只保留一个尚未
处理的最新帧;新帧覆盖旧帧时,dropped_setpoints 累计增加。这是低延迟
latest-wins 语义,不适合必须逐点无损执行的离线轨迹。
每次 AbstractMotor 接受 setpoint 后返回:
phase = CYCLIC_STREAM_APPLIED
sequence = 已应用的客户端序列
dropped_setpoints = 本流累计覆盖数
对于 CMVR PLC 后端,AbstractMotor 返回成功前,runtime 已经看到相同
command sequence 的 PLC ACK;周期样本还要求
last_applied_cyclic_sequence 严格等于本次样本序列。对于其他电机后端,
APPLIED 只保证对应后端的同步调用返回 true。
为避免每个周期样本额外触发多次现场总线状态读取,APPLIED 响应刻意不携带
status。OPENED 和终止响应仍携带完整状态;需要连续遥测的客户端应以独立、
较低频率调用 getStatus,不能把 APPLIED 当作状态采样接口。
客户端正常结束请求流后,服务端先同步调用 quickStop();确认后返回
CYCLIC_STREAM_STOPPED 并释放电机所有权。当前协议没有显式 close
请求帧;客户端 half-close 就是结束信号。若 Quick Stop 未被后端确认,
服务端改发 CYCLIC_STREAM_FAILED 并以 INTERNAL 结束 RPC,不会把失败
误报成 STOPPED。
出现以下情况时,服务端会 Quick Stop:
- gRPC context 被取消或 deadline 到期;
- 输入在 watchdog 窗口内没有新帧;
- 序列号或 payload 非法;
- PLC/后端拒绝 setpoint;
- 客户端停止读取响应;
emergencyStop增加抢占 generation。
服务端流使用同步 Write()。慢客户端可能阻塞反馈写入,reader 此时仍会
覆盖 pending setpoint,但上层 watchdog 检查也可能被延迟。因此:
- 客户端必须并发、持续读取反馈;
- gRPC watchdog 只是第一层保护;
- PLC 侧通信 watchdog 和周期样本 watchdog 才是断网、进程卡死时的 权威保护。
2.3 当前没有开放的能力
当前 MotorService 没有以下 RPC:
- Profile Torque;
- Cyclic Torque 流;
- Clear Fault;
- 普通 Stop/Halt;
- 多轴原子控制。
寄存器块预留了 target_torque,但 CmvrPlcMotorProtocol 当前明确拒绝
周期力矩。不要因为寄存器存在就让 PLC 项目把该功能标记为已经可用。
2.4 仲裁和 gRPC 状态
同一电机已有阻塞命令或流时,新控制调用返回
RESOURCE_EXHAUSTED。软件急停锁存期间,运动命令返回
FAILED_PRECONDITION;只有成功执行 setEnabled(enabled=true) 才清除
该服务内锁存。
任何取消、超时、异常或周期流结束后的 Quick Stop 如果未被后端确认成功,
MotorService 也会按 fail-closed 原则进入同一软件急停锁存。此时 RPC 的
INTERNAL 表示“安全状态尚未确认”,不能继续发送运动命令;应先排查 PLC/
驱动器状态,再通过成功的 setEnabled(enabled=true) 显式恢复。
显式 emergencyStop 会在调用后端前先锁存;若最终 Quick Stop 未确认,它
返回 FAILED_PRECONDITION,但仍保持锁存。两种返回码都不能解释为已经安全
停机。
常见 gRPC 状态包括:
INVALID_ARGUMENT:选择器、数值、首帧或序列号非法;NOT_FOUND:MotorManager 或电机不存在;RESOURCE_EXHAUSTED:同一电机已有 owner;FAILED_PRECONDITION:软件急停锁存或后端拒绝命令;DEADLINE_EXCEEDED:同步等待或流 watchdog 超时;ABORTED:被emergencyStop抢占;CANCELLED:客户端取消或停止读取。UNAVAILABLE:后端断链,或状态因故障/watchdog 返回非 finite;INTERNAL:服务异常,或命令失败后的安全清理/Quick Stop 也失败。
3. MotorManager 配置
仓库已提供单 PLC、两轴配置示例
cmvr-es/config/devices/motor/plc_motors.pb.txt,内容如下:
motor {
id: "plc_motors"
motor_groups {
id: "plc_axis_group"
bus_type: MOTOR_BUS_MODBUS_TCP
vendor: MOTOR_VENDOR_PLC_GENERIC
protocol: MOTOR_PROTOCOL_CMVR_PLC_V1
modbus_tcp {
host: "192.168.0.10"
port: 502
unit_id: 1
connect_timeout_ms: 500
io_timeout_ms: 100
heartbeat_period_ms: 100
communication_watchdog_ms: 1000
cyclic_watchdog_ms: 500
status_poll_period_ms: 20
reconnect_min_ms: 100
reconnect_max_ms: 2000
command_ack_timeout_ms: 500
protocol_major: 1
protocol_minor: 0
axes { motor_id: 1 axis_index: 0 }
axes { motor_id: 2 axis_index: 1 }
}
joint_limits {
enable: true
source: JOINT_LIMIT_SOURCE_CUSTOM
joints {
joint_name: "PLC_AXIS_1"
q_lb: -3.141592653589793
q_ub: 3.141592653589793
qd: 1.0
qdd: 2.0
}
joints {
joint_name: "PLC_AXIS_2"
q_lb: -1.5707963267948966
q_ub: 1.5707963267948966
qd: 0.5
qdd: 1.0
}
}
motors {
motors { id: 1 joint_name: "PLC_AXIS_1" }
motors { id: 2 joint_name: "PLC_AXIS_2" }
}
}
}
字段行为:
host必须是 IPv4 字面量(例如192.168.0.10),不做 DNS 解析;这样 TCP 建连和stop()不会被无界的名称解析阻塞。port=0时 runtime 使用 502;unit_id=0时使用 1。connect_timeout_ms=0时使用 500 ms;该值为 TCP 建连的独立硬超时。io_timeout_ms=0时使用 100 ms,并同时用于 libmodbus response timeout 和 byte timeout。- 心跳、通信 watchdog、周期 watchdog、状态轮询、重连和 ACK timeout
为
0时,当前 runtime 分别使用 100、500、500、20、100、2000、500 ms;上面的实体样例将通信 watchdog 显式放宽为 1000 ms。 communication_watchdog_ms必须同时不少于2 * heartbeat_period_ms和3 * io_timeout_ms + 2 * heartbeat_period_ms;command_ack_timeout_ms、cyclic_watchdog_ms必须不少于3 * io_timeout_ms + status_poll_period_ms,因为一次可靠状态读取包含 sequence-before、完整状态块、sequence-after 三次 FC3;cyclic_watchdog_ms必须在 20 ms 至 60000 ms 内;reconnect_max_ms不得小于reconnect_min_ms,否则初始化失败。protocol_major=0时使用当前 major1。握手严格检查 major; PLC 的 minor 版本不得低于配置要求。axis_index是 PLC 轴块索引,必须小于 PLC 发布的axis_count,且同一 group 内不能重复。encoder_counts_per_rev和gear_ratio对 CMVR PLC v1 不生效,因为 PLC 协议交换的是 SI 定点值,而不是编码器 count。
还需要在 DeviceManager 配置中注册该 MotorSystem:
device_manager {
# 只有未被 RobotArm 选择且仍希望初始化全部电机时才需要 true。
init_all_motors_when_no_active_joints: true
devices {
id: "plc_motors"
type: DEVICE_TYPE_MOTOR_SYSTEM
config_file: "devices/motor/plc_motors.pb.txt"
enable: true
}
}
id 必须与 motor.id 以及 gRPC
MotorTarget.header.device_id 一致。安装产物读取
output/bin/config/;修改源码树配置后需再次执行 cmake --install build。
4. CMVR PLC v1 Holding Register
4.1 地址和 32 位编码
本文和 C++ runtime 中的地址都是 零基 Modbus PDU Holding Register
地址。地址 0 是第一个 Holding Register,PLC/HMI 文档若使用 40001
风格显示,则通常对应本文地址 0。不要把 40001 直接传给
modbus_read_registers()。
每个寄存器是 16 位。所有 32 位有符号或无符号值均使用:
register[offset] = bits 31..16(高 WORD)
register[offset + 1] = bits 15..0 (低 WORD)
libmodbus 负责单个 16 位寄存器在线路上的字节序;PLC 应显式按“高 WORD
在前、低 WORD 在后”组合 32 位值。TIA 中建议用移位和 OR 的辅助 FC,
不要依赖 AT 视图、BLKMOV 或 CPU 内部字节布局偶然得到相同结果。
4.2 全局区
全局区从地址 0 开始,共 32 个寄存器:
| 地址 | 长度 | 类型 | 名称 | 方向与说明 |
|---|---|---|---|---|
| 0 | 1 | UINT16 |
magic_cm |
PLC -> CMVR,固定 16#434D |
| 1 | 1 | UINT16 |
magic_vr |
PLC -> CMVR,固定 16#5652 |
| 2 | 1 | UINT16 |
protocol_major |
PLC -> CMVR,当前为 1 |
| 3 | 1 | UINT16 |
protocol_minor |
PLC -> CMVR,当前为 0 |
| 4 | 1 | UINT16 |
axis_count |
PLC -> CMVR,可用轴块数量 |
| 5 | 1 | UINT16 |
plc_global_state |
PLC -> CMVR,全局状态 |
| 6..7 | 2 | UINT32 |
plc_boot_id |
PLC -> CMVR,每次 PLC 程序运行实例重启必须改变 |
| 8..9 | 2 | UINT32 |
cmvr_session_id |
CMVR -> PLC,每次成功握手/重连生成新的非零随机会话 |
| 10..11 | 2 | UINT32 |
cmvr_heartbeat |
CMVR -> PLC,周期递增 |
| 12..13 | 2 | UINT32 |
plc_heartbeat |
PLC -> CMVR,PLC 自己的周期计数 |
| 14..15 | 2 | UINT32 |
communication_watchdog_ms |
CMVR -> PLC,PLC 侧通信 watchdog |
| 16 | 1 | UINT16 |
global_error |
PLC -> CMVR,全局错误 |
| 17 | 1 | UINT16 |
owner_state |
PLC -> CMVR,当前 owner/握手状态 |
| 18..19 | 2 | UINT32 |
owner_session_id |
PLC -> CMVR,当前 Accepted/Rejected 决策对应的候选 session |
| 20..31 | 12 | - | reserved | 写 0,PLC 忽略 |
当前 runtime 在握手初始快照中检查 magic、版本、axis_count 和非零
plc_boot_id,先写通信 watchdog,最后以新 session + 首个 heartbeat 作为
候选 owner 的发布动作。握手的
每次轮询以及 owner 接受后的最终完整快照都必须保持相同的 magic、版本、
axis_count 和 boot ID;其中任一变化或 boot ID 变为 0 都会立即断线重连。
只有
owner_session_id 等于本次 session,且 owner_state=Accepted,连接才
进入可发命令状态。后续心跳周期同时检查 plc_boot_id、owner/session 以及
plc_heartbeat 是否持续推进;PLC 心跳在通信 watchdog 窗口内不变化会主动
断线并重连。owner_state=Rejected 也只有在 owner_session_id 回显本次
session 时才表示本次握手被拒;旧 session 遗留的 Rejected 状态会被忽略并继续
轮询。
PLC 检测到新 session 后,发布顺序必须是:先把 owner_state 改成
Accepting(此时不能先改 owner_session_id),然后安全停止旧 owner、清除
mailbox/旧 stream,再写候选 owner_session_id,最后发布 Accepted。拒绝时
同样先处于 Accepting/None,写完对应 session 后最后发布 Rejected。
否则新的 session ID 可能与残留的旧 Accepted 短暂组合,令 CMVR 过早发命令。
4.3 每轴布局
轴 i 的基地址:
B(i) = 100 + 128 * i
每轴占 128 个 Holding Registers:
B + 0 .. B + 63 控制区
B + 64 .. B + 127 状态区
控制区
| 相对地址 | 长度 | 类型 | 名称 | 当前说明 |
|---|---|---|---|---|
| 0..1 | 2 | UINT32 |
payload_sequence |
本次轴命令序列 |
| 2 | 1 | UINT16 |
command_code |
见命令码表 |
| 3 | 1 | UINT16 |
command_flags |
当前写 0 |
| 4..5 | 2 | INT32 |
target_position |
micro-rad |
| 6..7 | 2 | INT32 |
target_velocity |
micro-rad/s |
| 8..9 | 2 | INT32 |
acceleration |
micro-rad/s^2 |
| 10..11 | 2 | INT32 |
target_torque |
预留,mN·m |
| 12..13 | 2 | INT32 |
position_tolerance |
预留,micro-rad |
| 14..15 | 2 | INT32 |
velocity_tolerance |
预留,micro-rad/s |
| 16..17 | 2 | UINT32 |
command_timeout_ms |
命令完成时限;Profile 当前写 600000 ms |
| 18..19 | 2 | UINT32 |
stream_watchdog_ms |
PLC 侧周期样本 watchdog |
| 20..21 | 2 | UINT32 |
cyclic_sample_sequence |
周期样本序列 |
| 22..23 | 2 | UINT32 |
client_monotonic_time_ms |
CMVR steady-clock 低 32 位 |
| 24..25 | 2 | UINT32 |
expected_zero_epoch |
SetZero 写入命令前读到的 zero epoch |
| 26 | 1 | UINT16 |
disconnect_action |
预留;当前驱动写 0 |
| 27..28 | 2 | UINT32 |
command_session_id |
必须等于当前已接受 session |
| 29..59 | 31 | - | reserved | 写 0 |
| 60..61 | 2 | UINT32 |
payload_sequence_mirror |
必须等于 payload sequence |
| 62..63 | 2 | UINT32 |
commit_sequence |
单独、最后写入 |
状态区
下表地址相对于 S = B + 64:
| 相对地址 | 长度 | 类型 | 名称 | 说明 |
|---|---|---|---|---|
| 0..1 | 2 | UINT32 |
ack_sequence |
PLC 已解析的 command sequence |
| 2..3 | 2 | UINT32 |
active_sequence |
当前执行中的 command sequence |
| 4 | 1 | UINT16 |
command_state |
命令状态 |
| 5 | 1 | UINT16 |
result_code |
结果码 |
| 6 | 1 | UINT16 |
axis_state |
PLC/驱动器轴状态 |
| 7 | 1 | UINT16 |
current_mode |
cmvr.msgs.RunMode 数值 |
| 8..9 | 2 | INT32 |
actual_position |
micro-rad |
| 10..11 | 2 | INT32 |
actual_velocity |
micro-rad/s |
| 12..13 | 2 | INT32 |
actual_torque |
mN·m |
| 14..15 | 2 | INT32 |
target_position |
PLC 当前目标,micro-rad |
| 16..17 | 2 | INT32 |
target_velocity |
PLC 当前目标,micro-rad/s |
| 18 | 1 | bit field | status_flags |
见状态位表 |
| 19 | 1 | UINT16 |
drive_statusword |
原始驱动器状态字 |
| 20..21 | 2 | UINT32 |
fault_code |
PLC/驱动器故障码 |
| 22..23 | 2 | UINT32 |
zero_epoch |
置零版本 |
| 24..25 | 2 | UINT32 |
last_applied_cyclic_sequence |
最后实际锁存的周期样本 |
| 26..27 | 2 | UINT32 |
state_sequence |
状态 seqlock;非零偶数才是稳定版本 |
| 28..29 | 2 | UINT32 |
plc_monotonic_time_ms |
PLC 单调时间低 32 位 |
| 30..31 | 2 | UINT32 |
heartbeat_age_ms |
PLC 计算的 CMVR 心跳年龄 |
| 32..33 | 2 | UINT32 |
ack_session_id |
ack_sequence 所属 session |
| 34..61 | 28 | - | reserved | PLC 写 0 |
| 62..63 | 2 | UINT32 |
state_sequence_mirror |
稳定版本镜像 |
PLC 状态区使用 seqlock 发布。稳定版本从非零偶数 2 开始,每次完整更新增加
2;版本回绕到 0 时跳到 2。发布顺序必须是:
- 更新任何状态字段前,先把
state_sequence写成下一奇数,state_sequence_mirror保持上一个稳定偶数; - 写完 ACK、状态、实际值、flags、heartbeat age 等所有字段;
- 先把
state_sequence_mirror写成新的非零偶数; - 最后把
state_sequence写成相同偶数。
runtime 在同一 socket 互斥区内执行三段读取:先单独读取
state_sequence,再读取完整 64-word 状态块,最后再次单独读取
state_sequence。只有 before、after、块内 sequence 和 mirror 四者相等,
且为非零偶数时才接受完整块;失败最多重试 3 次。这样不要求 Siemens
MB_SERVER 对 64-word FC3 做原子内存快照,也允许不同的完整状态读取之间版本
持续推进。
为保证活性,PLC 不得在每个高速控制扫描都无条件翻转 Holding 状态版本。建议 驱动控制状态先写内部 shadow,再由较低频的对外发布任务在字段有意义变化时复制 到 Holding 状态区,并让每个稳定偶数版本至少覆盖三次连续 FC3 的时间窗口; 也可以使用双缓冲后按上述 seqlock 顺序发布。否则 PLC 每次 FC3 之间都推进版本, runtime 的 3 次有限重试会按设计失败,而不是返回可能撕裂的状态。
4.4 命令码
| 值 | 名称 | 当前上层使用 |
|---|---|---|
| 0 | Nop |
否 |
| 1 | SetZero |
setZero |
| 2 | MoveToZero |
预留;当前 moveToZero 发送 ProfilePosition(target=0) |
| 3 | ProfilePosition |
moveToZero、profilePosition |
| 4 | ProfileVelocity |
profileVelocity |
| 5 | OpenCyclicPosition |
第一个周期位置样本前自动发送 |
| 6 | CyclicPositionSample |
周期位置样本 |
| 7 | OpenCyclicVelocity |
第一个周期速度样本前自动发送 |
| 8 | CyclicVelocitySample |
周期速度样本 |
| 9 | CloseCyclicStream |
预留;当前流结束使用 QuickStop |
| 10 | QuickStop |
emergencyStop、流结束和上层超时 |
| 11 | Enable |
setEnabled(true) |
| 12 | Disable |
setEnabled(false) |
PLC 必须区分:
SetZero:按已确认的项目语义设置当前位置基准,不得擅自解释为运动回原点;ProfilePosition(target=0):运动到已经建立的零位;- Homing/寻找原点:当前 gRPC 和寄存器协议没有独立开放。
4.5 命令状态、结果和状态位
command_state 定义:
0 Idle 1 Received 2 Validating
3 Accepted 4 Running 5 TargetReached
6 Completed 7 Rejected 8 Failed
9 TimedOut 10 QuickStopped 11 CommunicationLost
runtime 会先拒绝未知状态,再按命令检查成功后置条件:
SetZero:Completed、ZeroValid=1,且zero_epoch相比命令前变化;Enable:Completed且Enabled=1;Disable:Completed且Enabled=0;QuickStop:QuickStopped或Completed,且实际速度已接近 0;- Profile:
Completed或TargetReached; - 非终态 ACK 只接受
Accepted、Running、TargetReached、Completed。
Rejected、Failed、TimedOut、CommunicationLost 始终视为失败。
result_code 定义:
0 Ok 1 InvalidCommand
2 InvalidParameter 3 AxisNotReady
4 AxisBusy 5 NotEnabled
6 PositionLimit 7 VelocityLimit
8 AccelerationLimit 9 ZeroNotValid
10 DriveFault 11 CommandTimeout
12 SequenceError 13 SessionMismatch
14 CommunicationWatchdog
15 CyclicWatchdog 16 Unsupported
17 InternalError
status_flags:
| bit | 名称 |
|---|---|
| 0 | Enabled |
| 1 | Moving |
| 2 | TargetReached |
| 3 | Fault |
| 4 | QuickStopActive |
| 5 | CommunicationWatchdogExpired |
| 6 | CyclicWatchdogExpired |
| 7 | ZeroValid |
| 8 | StreamActive |
| 9 | CommandBusy |
CmvrPlcMotorProtocol 把 Fault、CommunicationWatchdogExpired 和
CyclicWatchdogExpired 都作为致命反馈状态;失败/未知 command_state 或非
Ok result_code 同样无效。此时 getQ/getQd 返回 NaN,reachedTargetQ
返回 false,MotorService 不得把残留的有限位置/速度误判为到位或成功状态。
4.6 缩放和范围
CMVR PLC v1 使用固定缩放:
| 量 | gRPC/C++ 单位 | 寄存器值 |
|---|---|---|
| 位置 | rad | round(rad * 1,000,000),micro-rad |
| 速度 | rad/s | round(rad/s * 1,000,000),micro-rad/s |
| 加速度 | rad/s^2 | round(rad/s^2 * 1,000,000),micro-rad/s^2 |
| 力矩 | N·m | 预留为 round(N·m * 1,000),mN·m |
前三者当前由 CmvrPlcMotorProtocol 实际使用,并在转换前检查 finite 和
INT32 范围。缩放为 1,000,000 时,理论可表示范围约为
[-2147.483648, 2147.483647] 个对应 SI 单位。PLC 仍必须再次执行软件限位、
驱动器限位和状态检查,不能只依赖上位机检查。
Modbus PLC 电机初始化强制要求每个轴都有有限、有效的 q_lb、q_ub、qd
和 qdd:q_ub > q_lb,且 qd、qdd 均大于 0。缺失、NaN、无穷或非法
限位会令电机初始化失败,不能以“未配置限位”的方式继续带轴运行。
5. payload-first、commit-last 和 ACK
5.1 CMVR 写入顺序
每轴的 command_sequence 独立递增并跳过 0。一次命令严格执行:
- 仅
SetZero先可靠读取 freshzero_epoch;其他命令不做冗余 baseline 状态读取; - 在本地构造 62 个寄存器的完整 payload;
- 同时写入
payload_sequence和payload_sequence_mirror; - 使用一次 Holding Register 批量写,把
B+0 .. B+61写入 PLC; - 再使用第二次写,把同一序列写入
B+62 .. B+63; - 轮询状态区,直到
ack_sequence等于本次 command sequence; - 检查
command_state和result_code; - 周期样本还要检查
last_applied_cyclic_sequence。
PLC 只允许在以下条件全部满足时消费 payload:
commit_sequence != last_processed_commit
payload_sequence == payload_sequence_mirror
payload_sequence == commit_sequence
command_session_id == owner_session_id
当前 cmvr_session_id 是已取得 owner 的有效 session
对于 SetZero,PLC 还必须要求 expected_zero_epoch 等于执行前的当前
zero_epoch;不匹配时以 Rejected + SequenceError ACK,避免陈旧或重复的
置零事务改变新的零位基准。
PLC 应先把完整 payload 复制到内部命令快照,再更新
last_processed_commit。不要一边读取 Holding Register,一边执行驱动器动作。
无论接受还是拒绝,PLC 都应把 ack_sequence 和 ack_session_id 更新为
本次命令的序列和 session,同时填写 command_state 和 result_code。
runtime 只有在两者都精确匹配时才接受 ACK,旧连接残留的相同序列不会被误认。
否则 CMVR 只能得到模糊的 ACK timeout。
如果 commit 已写成功但 ACK 读取失败,电机是否已经执行是不确定的。
CMVR runtime 不会自动重放该运动命令。特别是 SetZero、非零速度和使能命令,
调用方不得在未知结果下盲目重试;应先读取 PLC 状态、boot/session、zero epoch
和实际轴状态。
5.2 boot/session 防重放
PLC 必须把全局 session 和每轴 command sequence 共同作为命令命名空间:
cmvr_session_id在每次成功握手/重连时重新生成非零随机值。检测到新 session 时,PLC 必须先把owner_state置为Accepting,并且这一步必须 早于改写owner_session_id;随后安全停止旧 owner 的轴、清除旧的 stream-active 状态,并清空各轴旧 mailbox 的 commit/payload 接收状态。 清理全部完成后再写入新的owner_session_id,最后一步才把owner_state发布为Accepted。这样 runtime 不会把“新 session ID + 旧 Accepted”误认为新 owner 已就绪。拒绝候选 session 时也必须先保持Accepting、写入对应owner_session_id,最后一步发布Rejected。 CMVR 只有看到该 握手确认后才把连接标记为可用。CMVR 会把每轴 command sequence 从1重新开始,PLC 必须在新 session 命名空间内接受该序列。runtime 至少保证相邻两次连接的 session 不相同, 避免紧邻重连立即复用旧 mailbox/ACK 命名空间。- 每个轴应保存“当前 session 下最后处理的 commit sequence”。相同 commit 只返回原 ACK,不能再次执行。
- PLC 启动时必须生成新的非零
plc_boot_id,清除 owner 和旧 commit 接受 状态,并要求看到新的有效 session/heartbeat 后才接受命令。若 Holding DB 设置为 retentive,也不能让上次启动遗留的 commit 自动执行。 - CMVR 周期检查
plc_boot_id。boot ID 变化会断开连接;重连成功后connection_epoch改变。Protocol 会锁存并拒绝旧 cyclic generation, 不会自动重新OpenCyclic*。只有客户端新建 gRPC 流并通过首帧建立新 generation 后才能恢复。 - Profile 和 Cyclic 命令把 Protocol 已绑定的
connection_epoch作为expected_connection_epoch传给 runtime。runtime 在调用入口、等待每轴 队列之后,以及持有 I/O 锁准备分配 sequence/写 commit 前都要求 invocation/current/expected 三者严格一致。因此即使重连恰好发生在 Protocol 读取 epoch 与 runtime 写 mailbox 之间,旧命令也只会失败,不会 写入新 session。Quick Stop/Disable 不绑定旧 expected epoch,它们作为新调用 只清理当前 session。 - command sequence 是 32 位并会回绕。PLC 应使用 session 加序列的状态机, 明确处理回绕;不能简单把“任何不相等的值”永远视为新命令。
TCP 自身有序可靠,但不能替代上述应用层规则:PLC DB 可能保留旧值,PLC 和 CMVR 也可能独立重启。
6. S7-1215C DC/DC/DC 与 TIA Portal
6.1 MB_SERVER 数据块
建议创建一个专用、非 retentive 的协议数据块,例如:
HoldingRegister : ARRAY[0 .. 100 + 128 * AXIS_COUNT - 1] OF WORD;
数组下标与本文零基 PDU 地址一致。根据使用的 TIA Portal 和 CPU firmware,
MB_HOLD_REG 对数据块访问方式可能有要求;若编译器不允许优化 DB 的
VARIANT/指针映射,应关闭该协议 DB 的 optimized block access。不要把命令
状态机、驱动器实例 DB 与外部可写 Holding Register 直接重叠。
在 OB1 或固定周期 OB 中每个扫描周期调用一个 MB_SERVER 实例。典型参数
包括:
DISCONNECT = FALSE
CONNECT_ID = 项目内唯一连接 ID
IP_PORT = 502
MB_HOLD_REG = 协议 HoldingRegister 数组
NDR/DR/ERROR/STATUS = 诊断输出
不同 TIA Portal 版本的块接口和 VARIANT 写法可能略有差异,应以当前工程中
插入的 MB_SERVER 指令帮助为准。一个 server 实例使用自己的 instance DB;
连接 ID 和 TCP 端口不得与其他 OUC/Modbus 实例冲突。
MB_SERVER 只负责 Modbus TCP 搬运。另建 PLC FB 完成:
- 初始化 magic、协议版本、axis count 和 boot ID;
- 监视 session 与 heartbeat;
- 对每轴执行 payload/commit 原子接收;
- 做范围、状态、使能、零位和 command timeout 校验;
- 调用 Technology Object、PROFINET 驱动器 telegram 或项目已有驱动器 FB;
- 更新 ACK、命令状态、结果码、实际值和状态位;
- 执行通信/周期 watchdog 和安全降级。
6.2 32 位辅助函数
PLC 侧应显式实现以下等价逻辑:
DecodeUDInt(high, low) =
SHL(WORD_TO_DWORD(high), 16) OR WORD_TO_DWORD(low)
EncodeHigh(value) = DWORD_TO_WORD(SHR(value, 16))
EncodeLow(value) = DWORD_TO_WORD(value AND 16#0000_FFFF)
有符号值先按 DWORD 原样组合,再解释为 DINT。负值使用二进制补码;不要分别 对高、低 WORD 做有符号运算。
6.3 驱动器动作映射
映射必须由实际驱动器/Technology Object 语义决定:
SetZero只执行已确认的“当前位置建立零位”动作,不得自动替换成会运动的 Homing。- “回 0 位”当前收到的是
ProfilePosition(target=0)。 - Profile Position/Velocity 的轨迹生成在 PLC/驱动器侧完成;Modbus TCP 不是驱动器位置环或电流环。
- Cyclic Position/Velocity 是 CMVR 到 PLC 的软实时 setpoint 更新。PLC 在本地扫描周期锁存最新样本,再由 PLC/驱动器的确定性周期执行。
- 稳态周期样本通常需要 5 次 Modbus 事务(payload、commit,以及 guard/full/guard 三次 ACK 快照读取);恰逢心跳到期时增加 1 次。首个样本 还需要先完成一次惰性的 Open。这个事务模型不承诺固定控制频率,也不是硬 实时链路。
- Quick Stop 的减速度、抱闸时序和重力轴保持策略必须在 PLC/驱动器中配置。
- Enable/Disable 必须检查故障、STO、抱闸和轴 ready 状态;不能只翻转一个 普通布尔位。
PLC 必须在执行前再次检查位置、速度、加速度、驱动器状态和项目级互锁,并把
拒绝原因写入 result_code。
当前测试只在 x86 loopback fake PLC 上验证功能和协议一致性,尚未给出
S7-1215C 实机可持续频率。投产前必须在目标 TIA Portal 程序、真实 PLC 扫描
周期和现场交换网络下阶梯增加 CSP/CSV 发送频率,记录 ACK 延迟的
P50/P99/最大值、dropped_setpoints、Modbus 异常与 watchdog 触发次数。
cyclic_watchdog_ms 应依据实测最坏延迟并保留工程余量设置;在完成这项台架
测试前,不能宣称支持某个固定 Hz。
CMVR 中的 QuickStop 和 Disable 走 safety-priority 通道:它们会增加该轴
的取消 generation,令正在等待的普通命令失败,并绕过普通轴命令互斥锁。安全
命令不先读取状态,也不在 mailbox 前插入心跳写;拿到 socket 后首先发送安全
payload/commit。它仍与单个 Modbus socket 的一次事务互斥,不会把两条报文
交错写入;普通命令在 commit 前会再次检查 generation,避免急停完成后补发
旧运动命令。
同一轴的 safety 命令使用独立 safety mutex 串行。每个 safety 调用在等待该锁 之前就提升普通命令的取消 generation,因此抢占不会被前一个 Quick Stop 阻塞; 但后来的 safety 调用不会取消前一个 safety 调用的 ACK 等待,多个并发 Quick Stop/Disable 都能得到各自确定的执行结果。
6.4 心跳、watchdog 和断链
CMVR 默认每 100 ms 写一次 cmvr_session_id + cmvr_heartbeat。本仓库实体
样例要求 PLC 在 1000 ms 通信 watchdog 内看到 heartbeat 发生变化
(字段为 0 时 runtime 默认 500 ms)。PLC 应使用自己的
单调时间测量“最后一次变化”的年龄,不能只检查 TCP socket 仍连接,也不能把
重复读到同一个 counter 当作有效心跳。
命令提交不会为每个周期样本强制写 heartbeat;runtime 记录最后一次成功写入
时刻,仅在 heartbeat_period_ms 已到期时由当前命令顺带补写。supervisor
worker 仍按周期写心跳并检查 PLC boot/session/heartbeat,因此高频 setpoint
既不会产生一倍额外 Modbus 写流量,也不会饿死通信 watchdog。
推荐 PLC 状态机:
无 owner
-> 收到有效非零 session 且 heartbeat 开始变化
-> owner active
-> 接受该 session 的 commit
owner active
-> heartbeat age 超过 communication_watchdog_ms
-> 对所有 owner 轴执行受控停止/Quick Stop
-> 设置 CommunicationWatchdogExpired
-> command_state = CommunicationLost
-> 释放 owner
周期流还需要每轴独立 watchdog。只有新的
Cyclic*Sample.cyclic_sample_sequence 才刷新它;普通 CMVR 心跳不能让旧的
非零速度无限保持。周期 watchdog 超时后应停止该轴、清除 StreamActive,
设置 CyclicWatchdogExpired,并要求重新 OpenCyclic*。网络恢复后不得
自动恢复断链前的速度或 setpoint;旧 gRPC 流必须失败关闭,由客户端新建流。
CMVR runtime 遇到 Modbus 读写错误会关闭 socket,并按
reconnect_min_ms 到 reconnect_max_ms 指数退避重连。它不会自动重放上一
条运动命令。PLC 侧安全动作必须在没有 CMVR 参与的情况下独立完成。
7. 安全边界
标准 S7-1215C DC/DC/DC 不是 failsafe PLC。MB_SERVER、普通 OB/FB、
普通数字输出以及本服务的 emergencyStop 都只是功能性控制,不能提供
安全等级的急停、STO 或防护门联锁。
实际设备至少应按风险评估使用:
- 硬接线急停回路;
- 合规的安全继电器,或 F-CPU + F-I/O;
- 驱动器 STO 双通道或经认证的安全功能;
- 接触器/抱闸反馈和必要的 EDM;
- 与机械负载、重力轴和制动距离匹配的安全设计。
软件 emergencyStop 和 Modbus Quick Stop 可以作为操作层的快速停止,但
不能替代硬接线安全回路。标准 CPU 程序卡死、以太网交换机故障、普通输出粘连
或软件错误时,硬件安全链仍必须独立切断危险能量。
8. 构建、依赖和测试
8.1 x86-64
仓库已有 x86-64 的 libmodbus 3.1.11:
dependency/x86/third_party/modbus/3.1.11/include/modbus
dependency/x86/third_party/modbus/3.1.11/lib/libmodbus.so
dependency/x86/third_party/modbus/3.1.11/lib/libmodbus.so.5
request.txt 已包含 third_party/modbus/3.1.11。标准构建流程:
cmake -S . -B build -DBUILD_TESTING=ON
cmake --build build -j"$(nproc)"
ctest --test-dir build --output-on-failure
cmake --install build
ldd -r output/bin/cmvr_es | grep -E 'modbus|not found'
只验证本次 MotorService/PLC 电机链路时,可执行:
cmake --build build --target \
modbus_tcp_motor_bus_runtime_test \
grpc_motor_service_test \
grpc_motor_service_modbus_e2e_test \
-j4
ctest --test-dir build \
-R '^(modbus_tcp_motor_bus_runtime_test|grpc_motor_service_test|grpc_motor_service_modbus_e2e_test)$' \
--output-on-failure
当前这 3 个 CTest 目标共包含 63 个 GoogleTest 用例:Modbus runtime 25 个、 MotorService 36 个、gRPC–Modbus 端到端 2 个。
测试分别覆盖:
- Modbus runtime 的命令、离线启动后上线、重连/stop-start session 隔离、 stale owner 决策、握手身份漂移、三段 seqlock 撕裂重试、旧 cyclic generation 跨 epoch 的 fail-closed 锁存、Protocol-to-runtime epoch TOCTOU 拒绝、Profile 反馈 session 绑定、CSP/CSV 样本 ACK、冗余 状态/心跳事务抑制、并发 safety 串行、故障反馈拒绝、非法轴/样本,以及 仓库真实样例配置的解析和 runtime 初始化;
- MotorService 的同步 Profile Position/Velocity 等待、取消/超时、急停竞争、 异常边界、CSP/CSV 流、流背压和停止失败;
- 单进程真实链路
gRPC stub → DeviceManager → MotorManager →
AbstractMotor→ CMVR PLC protocol → Modbus TCP fake PLC,包括使能、Profile Position/Velocity、CSP、half-close Quick Stop、急停锁存、活动 cyclic 流 跨重连失败关闭、新流恢复,以及断线重连不重放。
fake PLC 用例会在 loopback 地址启动本地 server,运行环境必须允许本地 TCP bind/listen。
运行安装产物:
./output/bin/cmvr_es
默认 gRPC 端口由
cmvr-es/config/tasks/grpc_server_task/grpc_server_task.pb.txt 配置,当前为
50052。启动前应先用禁能或脱载轴验证 PLC 寄存器和方向。
8.2 启用配置和 gRPC 调用
首次联调前:
- 把
cmvr-es/config/devices/motor/plc_motors.pb.txt中的host、轴映射 和关节限位改成现场值; - 完成 PLC watchdog、驱动器 Quick Stop 和硬件安全链检查;
- 将
cmvr-es/config/manager/device_manager.pb.txt中plc_motors的enable改为true; - 重新执行
cmake --install build,再启动./output/bin/cmvr_es。
启用 reflection 后,可先确认服务和状态:
grpcurl -plaintext 127.0.0.1:50052 list cmvr.api.MotorService
grpcurl -plaintext \
-d '{"target":{"header":{"deviceId":"plc_motors"},"jointName":"PLC_AXIS_1"}}' \
127.0.0.1:50052 cmvr.api.MotorService/getStatus
在轴已安全脱载、PLC/驱动器允许使能后,显式使能并执行一个同步位置命令:
grpcurl -plaintext \
-d '{"target":{"header":{"deviceId":"plc_motors"},"jointName":"PLC_AXIS_1"},"enabled":true}' \
127.0.0.1:50052 cmvr.api.MotorService/setEnabled
grpcurl -plaintext \
-d '{"target":{"header":{"deviceId":"plc_motors"},"jointName":"PLC_AXIS_1"},"targetPositionRad":0.1,"maxVelocityRadS":0.2,"accelerationRadS2":0.5,"wait":{"timeoutMs":30000}}' \
127.0.0.1:50052 cmvr.api.MotorService/profilePosition
软件 Quick Stop:
grpcurl -plaintext \
-d '{"target":{"header":{"deviceId":"plc_motors"},"jointName":"PLC_AXIS_1"}}' \
127.0.0.1:50052 cmvr.api.MotorService/emergencyStop
双向周期流应使用生成的 gRPC client stub,并发写 setpoint、持续读反馈;不要
用只发送一次 JSON 的 unary 调用方式模拟。客户端必须先收到 OPENED,发送
首个样本,再等待相同 sequence 的 APPLIED。正常退出时 half-close 写端并
继续读取,直到收到 STOPPED 和最终 OK status。
当前服务默认监听 0.0.0.0:50052,使用 insecure gRPC,任何可达客户端都能
发控制命令。现场至少应绑定可信控制网接口或回环地址并配置防火墙;不得直接
暴露到办公网或公网。若需要跨不可信网络访问,应在进入设备前增加认证、TLS
和工业安全网关。
8.3 ARM 当前缺失
dependency/arm/third_party/ 当前没有 libmodbus。现有
libmodbus.so.5.1.0 是 x86-64 ELF,不能复制到 ARM 设备使用。
ARM 支持前需要:
- 为目标 ARM ABI 编译 libmodbus 3.1.11;
- 按相同布局放入
dependency/arm/third_party/modbus/3.1.11/{include,lib}; - 确认 ARM toolchain/顶层 CMake 选择
dependency/arm。当前顶层CMakeLists.txt仍把ARCH设为x86; - 在目标设备执行
file、readelf -h和ldd -r验证架构、SONAME 和 运行时依赖; - 重新执行无硬件测试和 PLC 台架测试。
在这些步骤完成前,ARM 构建应视为不支持 Modbus PLC 电机后端。
8.4 PLC 台架检查
建议按以下顺序验证:
- PLC 上电后检查 magic、版本、axis count、boot ID。
- 只连接 Modbus,确认 session、CMVR heartbeat 和 PLC heartbeat。
- 禁能状态验证错误参数、重复 commit、旧 session 和 ACK/result。
- 验证
SetZero的项目语义,确认没有意外运动。 - 低速、低加速度验证 Profile Position 和 Profile Velocity。
- 验证周期流正常结束、gRPC watchdog 和 PLC cyclic watchdog。
- 分别拔网线、停止
cmvr_es、重启交换机、重启 PLC,确认不会恢复旧速度。 - 验证
emergencyStop后必须显式 enable 才能再次运动。 - 在硬件安全回路测试合格后,才允许带载运行。
9. 当前已知约束
- MotorService 是单轴 API,没有多轴同扫描周期 commit。
DeviceManager/MotorManager拓扑在 gRPC 服务运行期间必须保持不变; 当前管理器只支持启动期注册,不支持在仍有 RPC 或流持有电机时热移除、 热替换同 ID 的 MotorManager。需要换配置时,应先停止 gRPC 服务和设备, 再重建运行时。- Profile Torque、Cyclic Torque、Clear Fault 和独立 Homing 未开放。
- gRPC 流的同步
Write()可能受慢客户端背压;PLC watchdog 必须独立。 getStatus不是一次原子 Modbus 快照。- Modbus TCP 不提供认证、加密或安全完整性。控制网络应隔离,并在需要时通过 防火墙/VPN/工业安全网关限制访问。
- Modbus TCP 的普通软件停止不具备功能安全等级。