refactor: remove PLC motor control backend

This commit is contained in:
xtkuang 2026-08-04 10:36:23 +08:00
parent b4ec07c851
commit 60098ee01e
30 changed files with 36 additions and 6331 deletions

View File

@ -145,10 +145,7 @@ sudo script/ethercat/stop_ethercat.sh eno1 --restore-network
- [MotorService gRPC 接口](cmvr-es/service/README.md#motorservice)
- [电机设备模块](cmvr-es/devices/motor/README.md)
- [Modbus TCP PLC runtime](cmvr-es/devices/motor/bus_runtime/modbus_tcp/README.md)
- [MotorService 与 CMVR PLC v1 完整协议](docs/motor_service_modbus_tcp.md)
- [AUBO 控制柜 Standard 数字 IO](cmvr-es/devices/arm/aubo_arm/README.md)
- [配置与部署规则](cmvr-es/config/README.md)
Modbus Quick Stop 只是功能性停止,不能替代硬接线急停或驱动器 STO。AUBO
JSON 接口只访问控制柜 Standard 数字 IO不访问安全 IO。
AUBO JSON 接口只访问控制柜 Standard 数字 IO不访问安全 IO。

View File

@ -1,56 +0,0 @@
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" }
}
}
}

View File

@ -82,14 +82,6 @@ device_manager {
enable: false
}
devices {
id: "plc_motors"
type: DEVICE_TYPE_MOTOR_SYSTEM
config_file: "devices/motor/plc_motors.pb.txt"
# Configure the PLC endpoint and complete the safety checkout before enabling.
enable: false
}
devices {
id: "right_arm"
type: DEVICE_TYPE_ROBOT_ARM

View File

@ -42,7 +42,7 @@ config/cmvr_es.pb.txt
| Microphone | [`microphone/abstract_microphone.h`](microphone/abstract_microphone.h) | [`microphone/microphone_factory.h`](microphone/microphone_factory.h) | FFmpeg |
| Speaker | [`speaker/abstract_speaker.h`](speaker/abstract_speaker.h) | [`speaker/speaker_factory.h`](speaker/speaker_factory.h) | FFmpeg |
| BioHead | [`biohead/abstract_biohead.h`](biohead/abstract_biohead.h) | DeviceFactory 直接创建 | BioHeadRobot |
| MotorSystem | [`motor/`](motor/README.md) | DeviceFactory 直接创建 | CAN、MuJoCo、EtherCAT、Modbus TCP PLC |
| MotorSystem | [`motor/`](motor/README.md) | DeviceFactory 直接创建 | CAN、MuJoCo、EtherCAT |
代码目录存在不等于已经接入配置创建链:

View File

@ -17,52 +17,5 @@ add_subdirectory(drivers/ti5_canopen)
add_subdirectory(drivers/mujoco)
add_subdirectory(bus_runtime)
add_subdirectory(drivers/ethercat_motor)
add_subdirectory(drivers/modbus_plc_motor)
if(BUILD_TESTING)
enable_testing()
set(CMVR_MOTOR_LIBMODBUS_ROOT
${CMAKE_SOURCE_DIR}/dependency/${ARCH}/third_party/modbus/3.1.11)
add_executable(modbus_tcp_motor_bus_runtime_test
bus_runtime/modbus_tcp/tests/modbus_tcp_motor_bus_runtime_test.cpp
)
target_include_directories(modbus_tcp_motor_bus_runtime_test
PRIVATE
${CMAKE_SOURCE_DIR}/cmvr-es
${CMVR_MOTOR_LIBMODBUS_ROOT}/include
)
target_link_directories(modbus_tcp_motor_bus_runtime_test
PRIVATE
${CMVR_MOTOR_LIBMODBUS_ROOT}/lib
)
target_link_libraries(modbus_tcp_motor_bus_runtime_test
PRIVATE
cmvr_es::device::motor_bus_runtime
cmvr_es::device::modbus_plc_motor_driver
cmvr_es::proto
modbus
gtest
gtest_main
pthread
glog
)
target_compile_definitions(modbus_tcp_motor_bus_runtime_test
PRIVATE
CMVR_PLC_MOTOR_SAMPLE_CONFIG_PATH="${CMAKE_SOURCE_DIR}/cmvr-es/config/devices/motor/plc_motors.pb.txt"
)
add_test(NAME modbus_tcp_motor_bus_runtime_test
COMMAND modbus_tcp_motor_bus_runtime_test)
set(_modbus_motor_test_environment
"LD_LIBRARY_PATH=${CMVR_TEST_EXTERNAL_LIBRARY_PATH}"
)
if(CMVR_TEST_SYSTEM_LIBSTDCXX)
list(APPEND _modbus_motor_test_environment
"LD_PRELOAD=${CMVR_TEST_SYSTEM_LIBSTDCXX}")
endif()
set_tests_properties(modbus_tcp_motor_bus_runtime_test PROPERTIES
TIMEOUT 15
ENVIRONMENT "${_modbus_motor_test_environment}"
)
endif()
add_subdirectory(manager)

View File

@ -2,7 +2,7 @@
`devices/motor/` 提供电机管理、协议适配、总线 runtime 和厂商驱动。Service、
RobotArm 和业务 Task 只依赖 `MotorManager`/`AbstractMotor` 的稳定接口,不应
直接访问 libmodbus、CAN、EtherCAT 或厂商 SDK。
直接访问 CAN、EtherCAT、MuJoCo 或厂商 SDK。
返回 [Devices 模块指南](../README.md) 或 [项目总览](../../../README.md)。
@ -11,72 +11,37 @@ RobotArm 和业务 Task 只依赖 `MotorManager`/`AbstractMotor` 的稳定接口
| 目录 | 职责 |
| --- | --- |
| `manager/` | 创建 MotorGroup`motor_id`/`joint_name` 暴露 `AbstractMotor` |
| `bus_runtime/` | 连接、收发、重连、watchdog 和总线生命周期 |
| `drivers/` | CANopen、EtherCAT、MuJoCo、Modbus PLC 等具体后端 |
| `drivers/modbus_plc_motor/` | 关节限位、SI 单位和 CMVR PLC v1 命令映射 |
| `bus_runtime/` | 连接、收发和总线生命周期 |
| `drivers/` | CANopen、EtherCAT 和 MuJoCo 等具体后端 |
PLC 电机链路为:
## 当前后端
```text
gRPC MotorService
|
v
MotorManager -> AbstractMotor
|
v
CmvrPlcMotorProtocol
|
v
ModbusTcpMotorBusRuntime
|
v
libmodbus -> PLC -> 驱动器/电机
```
- CAN + TI5 CANopen
- EtherCAT + EYOU CiA 402
- MuJoCo 仿真电机。
各层边界
配置示例:
- `MotorService` 负责 API 校验、单电机控制权、deadline/cancellation 和
fail-closed Quick Stop
- `MotorManager` 负责电机查找与统一抽象;
- `CmvrPlcMotorProtocol` 负责关节限位、SI 单位和 CMVR PLC v1 命令映射;
- `ModbusTcpMotorBusRuntime` 负责 PLC session、mailbox、ACK、状态和重连
- PLC/驱动器必须独立实现通信 watchdog、周期 watchdog 和硬件安全动作。
- [`ti5_motors.pb.txt`](../../config/devices/motor/ti5_motors.pb.txt)
- [`ethercat_motors.pb.txt`](../../config/devices/motor/ethercat_motors.pb.txt)
- [`mujoco_motors.pb.txt`](../../config/devices/motor/mujoco_motors.pb.txt)
## Modbus TCP PLC
实现、配置、依赖和测试入口见:
- [Modbus TCP runtime README](bus_runtime/modbus_tcp/README.md)
- [完整 MotorService/CMVR PLC v1 协议](../../../docs/motor_service_modbus_tcp.md)
- [MotorService 文档](../../service/README.md#motorservice)
- [`plc_motors.pb.txt`](../../config/devices/motor/plc_motors.pb.txt)
当前 Modbus 后端只提供 x86-64 的 libmodbus 3.1.11。ARM 目录没有对应库,
不能把 x86 ELF 复制到 ARM 设备使用。
对外接口与控制权语义见 [MotorService 文档](../../service/README.md#motorservice)。
## 安全边界
- MotorService 是单轴 API不提供多轴同扫描周期的原子 commit
- PLC/Modbus 的 Profile 或 cyclic 能力不能直接当成毫秒级机械臂组伺服;
- 软件 `emergencyStop`、Quick Stop 和普通 PLC 输出都不是安全急停;
- 真实设备必须具有独立的硬接线急停、安全继电器或 F-CPU/F-I/O以及驱动器
STO 等经风险评估确定的安全链;
- 新硬件配置保持 `enable: false`完成方向、限位、watchdog 和故障注入验证后
才能启用。
- 软件 `emergencyStop` 和 Quick Stop 不具备功能安全等级;
- 真实设备必须具有经风险评估确定的硬接线急停、安全继电器和驱动器安全链;
- 新硬件配置保持 `enable: false`,完成方向、限位和故障注入验证后才能启用。
## 测试
```bash
cmake --build build --target \
modbus_tcp_motor_bus_runtime_test \
grpc_motor_service_test \
grpc_motor_service_modbus_e2e_test \
-j4
cmake --build build --target grpc_motor_service_test -j4
ctest --test-dir build \
-R '^(modbus_tcp_motor_bus_runtime_test|grpc_motor_service_test|grpc_motor_service_modbus_e2e_test)$' \
-R '^grpc_motor_service_test$' \
--output-on-failure
```
端到端 fake PLC 测试需要本地 TCP bind/listen 权限。软件测试不能代替真实
S7-1215C、驱动器、STO 和断网故障台架。
该测试使用 fake motor不替代真实总线、驱动器或安全链验证。

View File

@ -2,29 +2,19 @@ add_library(motor_bus_runtime SHARED
can/src/can_motor_bus_runtime.cpp
mujoco/src/mujoco_motor_bus_runtime.cpp
ethercat/src/ethercat_motor_bus_runtime.cpp
modbus_tcp/src/modbus_tcp_client.cpp
modbus_tcp/src/modbus_tcp_motor_bus_runtime.cpp
)
set(IGH_ETHERCAT_ROOT
${CMAKE_SOURCE_DIR}/dependency/x86/third_party/ethercat/v1.7.0
)
set(CMVR_LIBMODBUS_ROOT
${CMAKE_SOURCE_DIR}/dependency/${ARCH}/third_party/modbus/3.1.11
)
target_include_directories(motor_bus_runtime
PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}
PRIVATE
${IGH_ETHERCAT_ROOT}/include
${CMVR_LIBMODBUS_ROOT}/include
)
target_link_directories(motor_bus_runtime PRIVATE
${IGH_ETHERCAT_ROOT}/lib
${CMVR_LIBMODBUS_ROOT}/lib
)
target_link_directories(motor_bus_runtime PRIVATE ${IGH_ETHERCAT_ROOT}/lib)
target_link_libraries(motor_bus_runtime
PUBLIC
@ -33,7 +23,6 @@ target_link_libraries(motor_bus_runtime
cmvr_es::mujoco_world
PRIVATE
ethercat
modbus
cmvr_es::device::canbus
glog
)

View File

@ -1,93 +0,0 @@
# Modbus TCP PLC Runtime
本目录实现 CMVR PLC v1 的 Modbus TCP 总线 runtime。它负责 PLC 连接、身份
握手、session、心跳、重连、命令 mailbox、ACK 轮询和状态快照,不负责 gRPC
请求解析,也不提供功能安全急停。
返回 [Motor 设备模块](../../README.md) 或 [Devices 模块指南](../../../README.md)。
## 代码与配置
- [`include/modbus_tcp_client.h`](include/modbus_tcp_client.h):有界超时的
libmodbus client
- [`include/modbus_tcp_motor_bus_runtime.h`](include/modbus_tcp_motor_bus_runtime.h)
连接 supervisor、命令和状态 runtime
- [`include/cmvr_plc_register_map.h`](include/cmvr_plc_register_map.h)
CMVR PLC v1 寄存器常量
- [`src/modbus_tcp_client.cpp`](src/modbus_tcp_client.cpp)
- [`src/modbus_tcp_motor_bus_runtime.cpp`](src/modbus_tcp_motor_bus_runtime.cpp)
- [`tests/modbus_tcp_motor_bus_runtime_test.cpp`](tests/modbus_tcp_motor_bus_runtime_test.cpp)
- [`../../../../config/devices/motor/plc_motors.pb.txt`](../../../../config/devices/motor/plc_motors.pb.txt)
完整寄存器表、TIA Portal 数据块、gRPC 语义和台架步骤由
[MotorService 与 CMVR PLC v1 完整协议](../../../../../docs/motor_service_modbus_tcp.md)
统一维护。
## 连接与协议约束
- `host` 必须是 IPv4 字面量,避免 DNS 让建连或停止出现无界等待;
- PLC boot ID 必须非零且每次 PLC 重启变化;
- owner 决策和命令 ACK 必须回显当前 session旧 session 的 mailbox 不得执行;
- runtime `start()` 启动连接 supervisorPLC 可以在进程启动时离线;
- 离线期间状态不可用,运动命令必须在写 mailbox 前失败;
- 重连必须重新完成身份、版本、boot ID 和 session 握手,不得重放旧命令;
- 状态区按 odd/even seqlock 发布CMVR 使用
sequence-before → 64-word block → sequence-after 三段读取;
- payload 必须先完整写入commit sequence 最后写入PLC 只原子消费新的
commit
- Quick Stop、Disable、故障和通信 watchdog 的状态必须通过当前 session
的 ACK/状态确认,不能把本地写成功解释为驱动器已经安全停止。
## Cyclic stream
每次 `OpenCyclicPosition/Velocity` 都创建新的轴级 stream epoch。PLC 必须在
同一个原子状态事务中:
1. 清零 `last_applied_cyclic_sequence`
2. 清理旧样本去重状态;
3. 重置 cyclic watchdog
4. 设置正确的 CSP/CSV mode
5. 发布 `StreamActive=1` 后再 ACK。
重开后的首个样本序列从 `1` 开始,必须重新应用。活动 cyclic 流跨
`connection_epoch` 后不会自动重开;旧流的当前和后续 setpoint 都被拒绝,
Quick Stop 后客户端必须建立新的 gRPC 流。断链前或断链期间 pending 的
setpoint 不得进入新 session。
## 依赖与平台
x86-64 的 libmodbus 3.1.11 位于:
```text
dependency/x86/third_party/modbus/3.1.11
```
`dependency/arm/third_party/` 当前没有对应 libmodbus因此 ARM 构建不支持
该后端。支持 ARM 前必须为目标 ABI 单独编译并验证库,不能复用 x86 二进制。
## 安全边界
标准 S7-1215C DC/DC/DC 不是 failsafe PLC。Modbus Quick Stop、
MotorService `emergencyStop`、普通 OB/FB 和普通数字输出都只是功能性控制,
不能替代:
- 硬接线急停;
- 安全继电器或 F-CPU/F-I/O
- 驱动器双通道 STO
- 接触器、抱闸反馈与必要的 EDM。
PLC 侧通信 watchdog 和 cyclic watchdog 必须在没有 CMVR 进程参与时独立停止
危险运动。真实启用前必须在禁能或脱载轴上验证寄存器、方向、限位、断网、PLC
重启、交换机故障和 Quick Stop 失败。
## 测试
```bash
cmake --build build --target modbus_tcp_motor_bus_runtime_test -j4
ctest --test-dir build \
-R '^modbus_tcp_motor_bus_runtime_test$' \
--output-on-failure
```
fake PLC 测试需要本地 TCP bind/listen 权限。测试通过只证明软件协议和故障注入
路径,不代表真实 PLC、驱动器或硬件安全链已经验收。

View File

@ -1,209 +0,0 @@
#ifndef CMVR_ES_CMVR_PLC_REGISTER_MAP_H
#define CMVR_ES_CMVR_PLC_REGISTER_MAP_H
#include <array>
#include <cstddef>
#include <cstdint>
namespace cmvr::device::cmvr_plc {
constexpr std::uint16_t kMagicCm = 0x434d;
constexpr std::uint16_t kMagicVr = 0x5652;
constexpr std::uint16_t kProtocolMajor = 1;
constexpr std::uint16_t kProtocolMinor = 0;
constexpr double kPositionScale = 1000000.0;
constexpr double kVelocityScale = 1000000.0;
constexpr double kAccelerationScale = 1000000.0;
constexpr double kTorqueScale = 1000.0;
constexpr int kGlobalRegisterCount = 32;
constexpr int kMagicCmOffset = 0;
constexpr int kMagicVrOffset = 1;
constexpr int kProtocolMajorOffset = 2;
constexpr int kProtocolMinorOffset = 3;
constexpr int kAxisCountOffset = 4;
constexpr int kPlcGlobalStateOffset = 5;
constexpr int kPlcBootIdOffset = 6;
constexpr int kCmvrSessionIdOffset = 8;
constexpr int kCmvrHeartbeatOffset = 10;
constexpr int kPlcHeartbeatOffset = 12;
constexpr int kCommunicationWatchdogOffset = 14;
constexpr int kGlobalErrorOffset = 16;
constexpr int kOwnerStateOffset = 17;
constexpr int kOwnerSessionIdOffset = 18;
constexpr int kAxisFirstOffset = 100;
constexpr int kAxisRegisterStride = 128;
constexpr int kAxisControlRegisterCount = 64;
constexpr int kAxisStatusRelativeOffset = 64;
constexpr int kAxisStatusRegisterCount = 64;
constexpr int axisBase(const std::uint32_t axis_index)
{
return kAxisFirstOffset + static_cast<int>(axis_index) * kAxisRegisterStride;
}
enum class CommandCode : std::uint16_t {
Nop = 0,
SetZero = 1,
MoveToZero = 2,
ProfilePosition = 3,
ProfileVelocity = 4,
OpenCyclicPosition = 5,
CyclicPositionSample = 6,
OpenCyclicVelocity = 7,
CyclicVelocitySample = 8,
CloseCyclicStream = 9,
QuickStop = 10,
Enable = 11,
Disable = 12,
};
enum class CommandState : std::uint16_t {
Idle = 0,
Received = 1,
Validating = 2,
Accepted = 3,
Running = 4,
TargetReached = 5,
Completed = 6,
Rejected = 7,
Failed = 8,
TimedOut = 9,
QuickStopped = 10,
CommunicationLost = 11,
};
enum class ResultCode : std::uint16_t {
Ok = 0,
InvalidCommand = 1,
InvalidParameter = 2,
AxisNotReady = 3,
AxisBusy = 4,
NotEnabled = 5,
PositionLimit = 6,
VelocityLimit = 7,
AccelerationLimit = 8,
ZeroNotValid = 9,
DriveFault = 10,
CommandTimeout = 11,
SequenceError = 12,
SessionMismatch = 13,
CommunicationWatchdog = 14,
CyclicWatchdog = 15,
Unsupported = 16,
InternalError = 17,
};
enum StatusFlag : std::uint16_t {
Enabled = 1U << 0U,
Moving = 1U << 1U,
TargetReached = 1U << 2U,
Fault = 1U << 3U,
QuickStopActive = 1U << 4U,
CommunicationWatchdogExpired = 1U << 5U,
CyclicWatchdogExpired = 1U << 6U,
ZeroValid = 1U << 7U,
StreamActive = 1U << 8U,
CommandBusy = 1U << 9U,
};
constexpr std::size_t kCommandPayloadRegisterCount = 62;
constexpr int kCommitSequenceRelativeOffset = 62;
constexpr std::size_t kPayloadSequence = 0;
constexpr std::size_t kCommandCode = 2;
constexpr std::size_t kCommandFlags = 3;
constexpr std::size_t kTargetPosition = 4;
constexpr std::size_t kTargetVelocity = 6;
constexpr std::size_t kAcceleration = 8;
constexpr std::size_t kTargetTorque = 10;
constexpr std::size_t kPositionTolerance = 12;
constexpr std::size_t kVelocityTolerance = 14;
constexpr std::size_t kCommandTimeout = 16;
constexpr std::size_t kStreamWatchdog = 18;
constexpr std::size_t kCyclicSampleSequence = 20;
constexpr std::size_t kClientMonotonicTime = 22;
constexpr std::size_t kExpectedZeroEpoch = 24;
constexpr std::size_t kDisconnectAction = 26;
constexpr std::size_t kCommandSessionId = 27;
constexpr std::size_t kPayloadSequenceMirror = 60;
constexpr std::size_t kAckSequence = 0;
constexpr std::size_t kActiveSequence = 2;
constexpr std::size_t kCommandState = 4;
constexpr std::size_t kResultCode = 5;
constexpr std::size_t kAxisState = 6;
constexpr std::size_t kCurrentMode = 7;
constexpr std::size_t kActualPosition = 8;
constexpr std::size_t kActualVelocity = 10;
constexpr std::size_t kActualTorque = 12;
constexpr std::size_t kTargetPositionStatus = 14;
constexpr std::size_t kTargetVelocityStatus = 16;
constexpr std::size_t kStatusFlags = 18;
constexpr std::size_t kDriveStatusword = 19;
constexpr std::size_t kFaultCode = 20;
constexpr std::size_t kZeroEpoch = 22;
constexpr std::size_t kLastAppliedCyclicSequence = 24;
constexpr std::size_t kStateSequence = 26;
constexpr std::size_t kPlcMonotonicTime = 28;
constexpr std::size_t kHeartbeatAge = 30;
constexpr std::size_t kAckSessionId = 32;
constexpr std::size_t kStateSequenceMirror = 62;
enum class OwnerState : std::uint16_t {
None = 0,
Accepting = 1,
Accepted = 2,
Rejected = 3,
};
inline void encodeUint32(std::uint16_t* registers,
const std::size_t offset,
const std::uint32_t value)
{
registers[offset] = static_cast<std::uint16_t>(value >> 16U);
registers[offset + 1] = static_cast<std::uint16_t>(value & 0xffffU);
}
inline void encodeInt32(std::uint16_t* registers,
const std::size_t offset,
const std::int32_t value)
{
encodeUint32(registers, offset, static_cast<std::uint32_t>(value));
}
inline std::uint32_t decodeUint32(const std::uint16_t* registers,
const std::size_t offset)
{
return (static_cast<std::uint32_t>(registers[offset]) << 16U) |
static_cast<std::uint32_t>(registers[offset + 1]);
}
inline std::int32_t decodeInt32(const std::uint16_t* registers,
const std::size_t offset)
{
return static_cast<std::int32_t>(decodeUint32(registers, offset));
}
inline bool isTerminal(const CommandState state)
{
return state == CommandState::Completed ||
state == CommandState::Rejected ||
state == CommandState::Failed ||
state == CommandState::TimedOut ||
state == CommandState::QuickStopped ||
state == CommandState::CommunicationLost;
}
inline bool isFailure(const CommandState state)
{
return state == CommandState::Rejected ||
state == CommandState::Failed ||
state == CommandState::TimedOut ||
state == CommandState::CommunicationLost;
}
} // namespace cmvr::device::cmvr_plc
#endif // CMVR_ES_CMVR_PLC_REGISTER_MAP_H

View File

@ -1,47 +0,0 @@
#ifndef CMVR_ES_MODBUS_TCP_CLIENT_H
#define CMVR_ES_MODBUS_TCP_CLIENT_H
#include <cstdint>
#include <atomic>
#include <string>
#include <vector>
struct _modbus;
using modbus_t = struct _modbus;
namespace cmvr::device {
class ModbusTcpClient {
public:
ModbusTcpClient() = default;
~ModbusTcpClient();
ModbusTcpClient(const ModbusTcpClient&) = delete;
ModbusTcpClient& operator=(const ModbusTcpClient&) = delete;
bool open(const std::string& host,
std::uint16_t port,
std::uint8_t unit_id,
std::uint32_t connect_timeout_ms,
std::uint32_t response_timeout_ms);
void close();
void shutdown();
bool connected() const { return context_ != nullptr; }
bool readHoldingRegisters(int address, int count, std::vector<std::uint16_t>& values);
bool writeHoldingRegisters(int address, const std::uint16_t* values, int count);
bool writeHoldingRegisters(int address, const std::vector<std::uint16_t>& values);
const std::string& lastError() const { return last_error_; }
private:
void setLastErrnoError_(const char* operation);
modbus_t* context_{nullptr};
std::atomic<int> socket_fd_{-1};
std::string last_error_;
};
} // namespace cmvr::device
#endif // CMVR_ES_MODBUS_TCP_CLIENT_H

View File

@ -1,173 +0,0 @@
#ifndef CMVR_ES_MODBUS_TCP_MOTOR_BUS_RUNTIME_H
#define CMVR_ES_MODBUS_TCP_MOTOR_BUS_RUNTIME_H
#include <array>
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <cstdint>
#include <memory>
#include <mutex>
#include <optional>
#include <string>
#include <thread>
#include <unordered_map>
#include <vector>
#include "devices/motor/bus_runtime/abstract_motor_bus_runtime.h"
#include "devices/motor/bus_runtime/modbus_tcp/include/cmvr_plc_register_map.h"
#include "devices/motor/bus_runtime/modbus_tcp/include/modbus_tcp_client.h"
namespace cmvr::device {
struct ModbusTcpMotorBusRuntimeTestAccess;
struct CmvrPlcAxisCommand {
cmvr_plc::CommandCode code{cmvr_plc::CommandCode::Nop};
std::uint16_t flags{0};
std::int32_t target_position{0};
std::int32_t target_velocity{0};
std::int32_t acceleration{0};
std::int32_t target_torque{0};
std::int32_t position_tolerance{0};
std::int32_t velocity_tolerance{0};
std::uint32_t command_timeout_ms{0};
std::uint32_t stream_watchdog_ms{0};
std::uint32_t cyclic_sample_sequence{0};
std::uint32_t client_monotonic_time_ms{0};
std::uint32_t expected_zero_epoch{0};
std::uint16_t disconnect_action{0};
};
struct CmvrPlcAxisStatus {
std::uint32_t ack_sequence{0};
std::uint32_t active_sequence{0};
cmvr_plc::CommandState command_state{cmvr_plc::CommandState::Idle};
cmvr_plc::ResultCode result_code{cmvr_plc::ResultCode::Ok};
std::uint16_t axis_state{0};
std::uint16_t current_mode{0};
std::int32_t actual_position{0};
std::int32_t actual_velocity{0};
std::int32_t actual_torque{0};
std::int32_t target_position{0};
std::int32_t target_velocity{0};
std::uint16_t status_flags{0};
std::uint16_t drive_statusword{0};
std::uint32_t fault_code{0};
std::uint32_t zero_epoch{0};
std::uint32_t last_applied_cyclic_sequence{0};
std::uint32_t state_sequence{0};
std::uint32_t plc_monotonic_time_ms{0};
std::uint32_t heartbeat_age_ms{0};
std::uint32_t ack_session_id{0};
};
class ModbusTcpMotorBusRuntime final : public AbstractMotorBusRuntime {
public:
ModbusTcpMotorBusRuntime();
~ModbusTcpMotorBusRuntime() override;
bool init(const config::MotorGroupConfig& group_cfg) override;
bool start() override;
void stop() override;
config::MotorBusType busType() const override { return config::MOTOR_BUS_MODBUS_TCP; }
bool hasMotor(std::uint8_t motor_id) const;
bool axisForMotor(std::uint8_t motor_id, std::uint32_t& axis_index) const;
bool connected() const { return connected_.load(); }
std::uint32_t sessionId() const { return session_id_.load(); }
std::uint32_t plcBootId() const { return plc_boot_id_.load(); }
std::uint64_t connectionEpoch() const { return connection_epoch_.load(); }
std::uint32_t streamWatchdogMs() const { return stream_watchdog_ms_; }
std::uint32_t commandAckTimeoutMs() const { return command_ack_timeout_ms_; }
const std::string& id() const { return id_; }
std::string lastError() const;
bool readAxisStatus(std::uint8_t motor_id, CmvrPlcAxisStatus& status);
bool submitAxisCommand(std::uint8_t motor_id,
const CmvrPlcAxisCommand& command,
bool wait_for_terminal_state = false,
std::optional<std::uint64_t>
expected_connection_epoch = std::nullopt);
bool submitAxisSafetyCommand(std::uint8_t motor_id,
const CmvrPlcAxisCommand& command);
private:
friend struct ModbusTcpMotorBusRuntimeTestAccess;
bool connectAndHandshakeLocked_();
void markDisconnectedLocked_(const std::string& error);
bool readRegistersLocked_(int address, int count, std::vector<std::uint16_t>& values);
bool writeRegistersLocked_(int address, const std::uint16_t* values, int count);
bool writeHeartbeatLocked_();
bool readAxisStatusByIndex_(std::uint32_t axis_index, CmvrPlcAxisStatus& status);
bool waitForCommand_(std::uint32_t axis_index,
std::uint32_t command_sequence,
std::uint32_t command_session_id,
std::uint64_t connection_epoch,
std::uint64_t cancel_generation,
bool cancel_on_safety_preemption,
const CmvrPlcAxisCommand& command,
bool wait_for_terminal_state,
const CmvrPlcAxisStatus& initial_status);
bool submitAxisCommandImpl_(std::uint8_t motor_id,
const CmvrPlcAxisCommand& command,
bool wait_for_terminal_state,
bool safety_priority,
std::optional<std::uint64_t>
expected_connection_epoch);
bool writeClientHeartbeatLocked_();
std::shared_ptr<std::mutex> axisMutex_(std::uint8_t motor_id) const;
std::shared_ptr<std::mutex> safetyMutex_(std::uint8_t motor_id) const;
void workerLoop_();
static std::uint32_t randomNonZeroSessionId_();
static std::uint32_t monotonicMilliseconds_();
std::string id_;
config::ModbusTcpConfig config_;
std::unordered_map<std::uint8_t, std::uint32_t> motor_axes_;
mutable std::unordered_map<std::uint8_t, std::shared_ptr<std::mutex>> axis_mutexes_;
mutable std::unordered_map<std::uint8_t, std::shared_ptr<std::mutex>> safety_mutexes_;
std::unordered_map<std::uint8_t, std::uint32_t> command_sequences_;
std::unordered_map<std::uint8_t, std::uint64_t> cancel_generations_;
// Monotonic admission counter protected by state_mutex_. Besides being
// useful when diagnosing queueing, it gives concurrency tests an exact
// synchronization point after an invocation has captured its safety
// generation and connection epoch, but before it waits on a command
// serialization mutex.
std::uint64_t command_admission_count_{0};
mutable std::mutex io_mutex_;
mutable std::mutex state_mutex_;
mutable std::mutex lifecycle_mutex_;
ModbusTcpClient client_;
std::string last_error_;
std::thread worker_;
std::mutex worker_wait_mutex_;
std::condition_variable worker_wait_cv_;
std::atomic<bool> running_{false};
std::atomic<bool> connected_{false};
std::atomic<std::uint32_t> plc_boot_id_{0};
std::atomic<std::uint64_t> connection_epoch_{0};
std::atomic<std::uint32_t> session_id_{0};
std::uint32_t last_session_id_{0};
std::uint32_t heartbeat_counter_{0};
std::chrono::steady_clock::time_point last_client_heartbeat_write_at_{};
std::uint32_t plc_heartbeat_counter_{0};
std::chrono::steady_clock::time_point plc_heartbeat_changed_at_{};
std::uint32_t connect_timeout_ms_{500};
std::uint32_t io_timeout_ms_{100};
std::uint32_t heartbeat_period_ms_{100};
std::uint32_t communication_watchdog_ms_{500};
std::uint32_t status_poll_period_ms_{20};
std::uint32_t reconnect_min_ms_{100};
std::uint32_t reconnect_max_ms_{2000};
std::uint32_t command_ack_timeout_ms_{500};
std::uint32_t stream_watchdog_ms_{500};
std::uint16_t protocol_major_{cmvr_plc::kProtocolMajor};
std::uint16_t protocol_minor_{cmvr_plc::kProtocolMinor};
};
} // namespace cmvr::device
#endif // CMVR_ES_MODBUS_TCP_MOTOR_BUS_RUNTIME_H

View File

@ -1,188 +0,0 @@
#include "devices/motor/bus_runtime/modbus_tcp/include/modbus_tcp_client.h"
#include <cerrno>
#include <limits>
#include <fcntl.h>
#include <netdb.h>
#include <poll.h>
#include <sys/socket.h>
#include <unistd.h>
#include <modbus/modbus.h>
namespace cmvr::device {
ModbusTcpClient::~ModbusTcpClient()
{
close();
}
bool ModbusTcpClient::open(const std::string& host,
const std::uint16_t port,
const std::uint8_t unit_id,
const std::uint32_t connect_timeout_ms,
const std::uint32_t response_timeout_ms)
{
close();
context_ = modbus_new_tcp(host.c_str(), static_cast<int>(port));
if (!context_) {
last_error_ = "modbus_new_tcp failed";
return false;
}
if (modbus_set_slave(context_, static_cast<int>(unit_id)) == -1) {
setLastErrnoError_("modbus_set_slave");
close();
return false;
}
const auto timeout = response_timeout_ms == 0 ? 200U : response_timeout_ms;
if (modbus_set_response_timeout(context_, timeout / 1000U,
(timeout % 1000U) * 1000U) == -1 ||
modbus_set_byte_timeout(context_, timeout / 1000U,
(timeout % 1000U) * 1000U) == -1) {
setLastErrnoError_("modbus_set_timeout");
close();
return false;
}
addrinfo hints{};
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_STREAM;
hints.ai_flags = AI_NUMERICHOST;
addrinfo* addresses = nullptr;
const auto service = std::to_string(port);
const auto resolve_result =
getaddrinfo(host.c_str(), service.c_str(), &hints, &addresses);
if (resolve_result != 0) {
last_error_ = std::string("host must be a numeric IPv4 address: ") +
gai_strerror(resolve_result);
close();
return false;
}
int connected_fd = -1;
for (auto* address = addresses; address; address = address->ai_next) {
const int fd = ::socket(address->ai_family, address->ai_socktype, address->ai_protocol);
if (fd < 0) {
continue;
}
const int old_flags = fcntl(fd, F_GETFL, 0);
if (old_flags < 0 || fcntl(fd, F_SETFL, old_flags | O_NONBLOCK) < 0) {
::close(fd);
continue;
}
socket_fd_.store(fd);
const int rc = ::connect(fd, address->ai_addr, address->ai_addrlen);
if (rc == 0 || errno == EINPROGRESS) {
pollfd descriptor{fd, POLLOUT, 0};
const auto timeout = static_cast<int>(
connect_timeout_ms == 0 ? 1000U : connect_timeout_ms);
if (rc == 0 || poll(&descriptor, 1, timeout) > 0) {
int socket_error = 0;
socklen_t error_size = sizeof(socket_error);
if (getsockopt(fd, SOL_SOCKET, SO_ERROR, &socket_error, &error_size) == 0 &&
socket_error == 0) {
if (fcntl(fd, F_SETFL, old_flags) == 0) {
connected_fd = fd;
break;
}
}
}
}
socket_fd_.store(-1);
::close(fd);
}
freeaddrinfo(addresses);
if (connected_fd < 0 || modbus_set_socket(context_, connected_fd) == -1) {
if (connected_fd >= 0) {
::close(connected_fd);
}
last_error_ = "Modbus TCP connect timed out or failed";
close();
return false;
}
socket_fd_.store(connected_fd);
last_error_.clear();
return true;
}
void ModbusTcpClient::close()
{
socket_fd_.store(-1);
if (context_) {
modbus_close(context_);
modbus_free(context_);
context_ = nullptr;
}
}
void ModbusTcpClient::shutdown()
{
const auto fd = socket_fd_.load();
if (fd >= 0) {
::shutdown(fd, SHUT_RDWR);
}
}
bool ModbusTcpClient::readHoldingRegisters(
const int address,
const int count,
std::vector<std::uint16_t>& values)
{
if (!context_) {
last_error_ = "Modbus TCP connection is closed";
return false;
}
if (address < 0 || count <= 0 || count > MODBUS_MAX_READ_REGISTERS) {
last_error_ = "invalid holding-register read range";
return false;
}
values.assign(static_cast<std::size_t>(count), 0);
const auto rc = modbus_read_registers(context_, address, count, values.data());
if (rc != count) {
setLastErrnoError_("modbus_read_registers");
return false;
}
last_error_.clear();
return true;
}
bool ModbusTcpClient::writeHoldingRegisters(
const int address,
const std::uint16_t* values,
const int count)
{
if (!context_) {
last_error_ = "Modbus TCP connection is closed";
return false;
}
if (address < 0 || !values || count <= 0 || count > MODBUS_MAX_WRITE_REGISTERS) {
last_error_ = "invalid holding-register write range";
return false;
}
const auto rc = modbus_write_registers(context_, address, count, values);
if (rc != count) {
setLastErrnoError_("modbus_write_registers");
return false;
}
last_error_.clear();
return true;
}
bool ModbusTcpClient::writeHoldingRegisters(
const int address,
const std::vector<std::uint16_t>& values)
{
if (values.size() > static_cast<std::size_t>(std::numeric_limits<int>::max())) {
last_error_ = "holding-register write is too large";
return false;
}
return writeHoldingRegisters(address, values.data(), static_cast<int>(values.size()));
}
void ModbusTcpClient::setLastErrnoError_(const char* operation)
{
last_error_ = std::string(operation) + ": " + modbus_strerror(errno);
}
} // namespace cmvr::device

View File

@ -1,21 +0,0 @@
add_library(modbus_plc_motor_driver SHARED
src/cmvr_plc_motor_protocol.cpp
src/modbus_plc_motor.cpp
)
target_include_directories(modbus_plc_motor_driver
PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/include
)
target_link_libraries(modbus_plc_motor_driver
PUBLIC
cmvr_es::device::motor_core
cmvr_es::device::motor_bus_runtime
PRIVATE
cmvr_es::proto
glog
)
add_library(cmvr_es::device::modbus_plc_motor_driver ALIAS modbus_plc_motor_driver)
install(TARGETS modbus_plc_motor_driver LIBRARY DESTINATION lib)

View File

@ -1,89 +0,0 @@
#ifndef CMVR_ES_CMVR_PLC_MOTOR_PROTOCOL_H
#define CMVR_ES_CMVR_PLC_MOTOR_PROTOCOL_H
#include <cstdint>
#include <memory>
#include <mutex>
#include <optional>
#include <unordered_map>
#include "devices/motor/bus_runtime/modbus_tcp/include/modbus_tcp_motor_bus_runtime.h"
#include "devices/motor/motor_protocol_interface.h"
namespace cmvr::device {
class CmvrPlcMotorProtocol final : public MotorProtocolInterface {
public:
explicit CmvrPlcMotorProtocol(std::shared_ptr<ModbusTcpMotorBusRuntime> bus_runtime);
~CmvrPlcMotorProtocol() override = default;
bool initNode(std::uint8_t node_id) override;
void setMode(std::uint8_t node_id, msgs::RunMode mode) override;
msgs::RunMode getMode(std::uint8_t node_id) override;
void setLimitQdd(std::uint8_t node_id, double u_qdd, double l_qdd) override;
void setLimitQd(std::uint8_t node_id, double qd) override;
void setLimitQ(std::uint8_t node_id, double ub, double lb) override;
bool calibrateZeroQ(std::uint8_t node_id) override;
bool reachedTargetQ(std::uint8_t node_id) override;
bool commandProfilePosition(std::uint8_t node_id,
double target_q,
double max_qd,
double max_qdd) override;
bool commandProfileVelocity(std::uint8_t node_id,
double target_qd,
double max_qdd) override;
bool commandCyclicPosition(std::uint8_t node_id,
double target_q,
double target_qd) override;
bool commandCyclicVelocity(std::uint8_t node_id, double target_qd) override;
bool commandCyclicTorque(std::uint8_t node_id, double target_tau) override;
void setMotorConversion(std::uint8_t node_id,
double encoder_counts_per_rev,
double gear_ratio) override;
bool torqueOn(std::uint8_t node_id) override;
bool torqueOff(std::uint8_t node_id) override;
bool brakeRelease(std::uint8_t node_id) override;
bool quickStop(std::uint8_t node_id) override;
double getQ(std::uint8_t node_id) override;
double getQd(std::uint8_t node_id) override;
private:
struct NodeState {
msgs::RunMode requested_mode{msgs::RUN_MODE_UNSPECIFIED};
double limit_q_lb{0.0};
double limit_q_ub{0.0};
double limit_qd{0.0};
double limit_qdd{0.0};
bool cyclic_stream_open{false};
bool cyclic_reconnect_latched{false};
std::uint32_t cyclic_sequence{0};
std::uint64_t cyclic_generation{0};
std::uint64_t cyclic_connection_epoch{0};
bool profile_feedback_bound{false};
std::uint64_t profile_connection_epoch{0};
};
bool submitSimple_(std::uint8_t node_id,
cmvr_plc::CommandCode code,
bool wait_for_terminal,
std::uint32_t timeout_ms);
bool ensureCyclicOpen_(std::uint8_t node_id,
NodeState& state,
msgs::RunMode mode);
static std::optional<std::int32_t> toMicroUnits_(double value);
static double fromMicroUnits_(std::int32_t value);
static bool isSupportedMode_(msgs::RunMode mode);
bool validatePosition_(const NodeState& state, double position) const;
bool validateVelocity_(const NodeState& state, double velocity) const;
bool validateAcceleration_(const NodeState& state, double acceleration) const;
bool readMotionStatus_(std::uint8_t node_id, CmvrPlcAxisStatus& status);
NodeState& nodeStateLocked_(std::uint8_t node_id);
std::shared_ptr<ModbusTcpMotorBusRuntime> bus_runtime_;
std::mutex nodes_mutex_;
std::unordered_map<std::uint8_t, NodeState> nodes_;
};
} // namespace cmvr::device
#endif // CMVR_ES_CMVR_PLC_MOTOR_PROTOCOL_H

View File

@ -1,21 +0,0 @@
#ifndef CMVR_ES_MODBUS_PLC_MOTOR_H
#define CMVR_ES_MODBUS_PLC_MOTOR_H
#include "cmvr/config/motor_config/motor_config.pb.h"
#include "devices/motor/abstract_motor.h"
namespace cmvr::device {
class ModbusPlcMotor final : public AbstractMotor {
public:
explicit ModbusPlcMotor(const config::MotorConfigItem& config);
std::string typeName() const override { return "ModbusPlcMotor"; }
bool init() override;
bool torqueOff() override;
bool quickStop() override;
};
} // namespace cmvr::device
#endif // CMVR_ES_MODBUS_PLC_MOTOR_H

View File

@ -1,582 +0,0 @@
#include "devices/motor/drivers/modbus_plc_motor/include/cmvr_plc_motor_protocol.h"
#include <algorithm>
#include <cmath>
#include <limits>
#include "common/base/logging/logger.h"
namespace cmvr::device {
namespace {
// MotorService accepts waits up to 10 minutes. The PLC timeout is a ceiling;
// shorter RPC timeouts still actively QuickStop from the service layer.
constexpr std::uint32_t kProfileCommandTimeoutCeilingMs = 600000;
constexpr std::uint32_t kSafetyCommandTimeoutMs = 5000;
bool statusAllowsMotionFeedback(const CmvrPlcAxisStatus& status)
{
constexpr std::uint16_t kFatalFlags =
cmvr_plc::StatusFlag::Fault |
cmvr_plc::StatusFlag::CommunicationWatchdogExpired |
cmvr_plc::StatusFlag::CyclicWatchdogExpired;
return (status.status_flags & kFatalFlags) == 0U &&
status.result_code == cmvr_plc::ResultCode::Ok &&
static_cast<std::uint16_t>(status.command_state) <=
static_cast<std::uint16_t>(
cmvr_plc::CommandState::CommunicationLost) &&
!cmvr_plc::isFailure(status.command_state);
}
} // namespace
CmvrPlcMotorProtocol::CmvrPlcMotorProtocol(
std::shared_ptr<ModbusTcpMotorBusRuntime> bus_runtime)
: bus_runtime_(std::move(bus_runtime))
{
comm_proto = CommProto::CUSTOM;
}
bool CmvrPlcMotorProtocol::initNode(const std::uint8_t node_id)
{
if (!bus_runtime_ || !bus_runtime_->hasMotor(node_id)) {
CMVR_LOG(ERROR) << "[CmvrPlcMotorProtocol] missing PLC axis mapping for motor "
<< static_cast<int>(node_id);
return false;
}
std::lock_guard<std::mutex> lock(nodes_mutex_);
auto& state = nodeStateLocked_(node_id);
state.cyclic_connection_epoch = bus_runtime_->connectionEpoch();
state.cyclic_stream_open = false;
state.cyclic_reconnect_latched = false;
state.cyclic_sequence = 0;
state.cyclic_generation = 0;
state.profile_feedback_bound = false;
state.profile_connection_epoch = 0;
return true;
}
void CmvrPlcMotorProtocol::setMode(
const std::uint8_t node_id,
const msgs::RunMode mode)
{
if (!isSupportedMode_(mode)) {
CMVR_LOG(ERROR) << "[CmvrPlcMotorProtocol] unsupported mode "
<< msgs::RunMode_Name(mode);
return;
}
std::lock_guard<std::mutex> lock(nodes_mutex_);
auto& state = nodeStateLocked_(node_id);
const bool cyclic_mode =
mode == msgs::RUN_MODE_CYCLIC_SYNC_POSITION ||
mode == msgs::RUN_MODE_CYCLIC_SYNC_VELOCITY;
if (cyclic_mode) {
// Every explicit cyclic setMode call is a new upper-layer stream
// generation, even when the mode value itself is unchanged. This is
// the only transition allowed to clear a reconnect latch.
state.requested_mode = mode;
state.cyclic_connection_epoch =
bus_runtime_ ? bus_runtime_->connectionEpoch() : 0U;
state.cyclic_stream_open = false;
state.cyclic_reconnect_latched = false;
state.cyclic_sequence = 0;
if (++state.cyclic_generation == 0U) {
++state.cyclic_generation;
}
return;
}
if (state.requested_mode != mode) {
state.cyclic_stream_open = false;
}
state.requested_mode = mode;
}
msgs::RunMode CmvrPlcMotorProtocol::getMode(const std::uint8_t node_id)
{
CmvrPlcAxisStatus status;
if (bus_runtime_ && bus_runtime_->readAxisStatus(node_id, status)) {
const auto mode = static_cast<msgs::RunMode>(status.current_mode);
if (isSupportedMode_(mode)) {
return mode;
}
}
std::lock_guard<std::mutex> lock(nodes_mutex_);
return nodeStateLocked_(node_id).requested_mode;
}
void CmvrPlcMotorProtocol::setLimitQdd(
const std::uint8_t node_id,
const double u_qdd,
const double l_qdd)
{
std::lock_guard<std::mutex> lock(nodes_mutex_);
nodeStateLocked_(node_id).limit_qdd = std::max(std::abs(u_qdd), std::abs(l_qdd));
}
void CmvrPlcMotorProtocol::setLimitQd(
const std::uint8_t node_id,
const double qd)
{
std::lock_guard<std::mutex> lock(nodes_mutex_);
nodeStateLocked_(node_id).limit_qd = std::abs(qd);
}
void CmvrPlcMotorProtocol::setLimitQ(
const std::uint8_t node_id,
const double ub,
const double lb)
{
std::lock_guard<std::mutex> lock(nodes_mutex_);
auto& state = nodeStateLocked_(node_id);
state.limit_q_ub = ub;
state.limit_q_lb = lb;
}
bool CmvrPlcMotorProtocol::calibrateZeroQ(const std::uint8_t node_id)
{
return submitSimple_(node_id, cmvr_plc::CommandCode::SetZero, true,
kSafetyCommandTimeoutMs);
}
bool CmvrPlcMotorProtocol::reachedTargetQ(const std::uint8_t node_id)
{
CmvrPlcAxisStatus status;
return readMotionStatus_(node_id, status) &&
statusAllowsMotionFeedback(status) &&
(status.status_flags & cmvr_plc::StatusFlag::TargetReached) != 0U;
}
bool CmvrPlcMotorProtocol::commandProfilePosition(
const std::uint8_t node_id,
const double target_q,
const double max_qd,
const double max_qdd)
{
NodeState state;
{
std::lock_guard<std::mutex> lock(nodes_mutex_);
auto& stored = nodeStateLocked_(node_id);
stored.requested_mode = msgs::RUN_MODE_PROFILE_POSITION;
stored.cyclic_stream_open = false;
state = stored;
}
if (!validatePosition_(state, target_q) ||
!validateVelocity_(state, max_qd) ||
!validateAcceleration_(state, max_qdd)) {
CMVR_LOG(ERROR) << "[CmvrPlcMotorProtocol] invalid profile-position command";
return false;
}
const auto position = toMicroUnits_(target_q);
const auto velocity = toMicroUnits_(std::abs(max_qd));
const auto acceleration = toMicroUnits_(std::abs(max_qdd));
if (!position || !velocity || !acceleration) {
return false;
}
CmvrPlcAxisCommand command;
command.code = cmvr_plc::CommandCode::ProfilePosition;
command.target_position = *position;
command.target_velocity = *velocity;
command.acceleration = *acceleration;
command.command_timeout_ms = kProfileCommandTimeoutCeilingMs;
if (!bus_runtime_) {
return false;
}
const auto submission_epoch = bus_runtime_->connectionEpoch();
const auto submitted =
bus_runtime_->submitAxisCommand(
node_id, command, false, submission_epoch);
const auto completed_epoch = bus_runtime_->connectionEpoch();
if (!submitted && completed_epoch == submission_epoch) {
return false;
}
{
std::lock_guard<std::mutex> lock(nodes_mutex_);
auto& stored = nodeStateLocked_(node_id);
// Bind on every cross-epoch outcome, including an ambiguous failed
// submit, so feedback remains invalid until safety cleanup.
stored.profile_feedback_bound = true;
stored.profile_connection_epoch = submission_epoch;
}
return submitted && completed_epoch == submission_epoch;
}
bool CmvrPlcMotorProtocol::commandProfileVelocity(
const std::uint8_t node_id,
const double target_qd,
const double max_qdd)
{
NodeState state;
{
std::lock_guard<std::mutex> lock(nodes_mutex_);
auto& stored = nodeStateLocked_(node_id);
stored.requested_mode = msgs::RUN_MODE_PROFILE_VELOCITY;
stored.cyclic_stream_open = false;
state = stored;
}
if (!validateVelocity_(state, target_qd) ||
!validateAcceleration_(state, max_qdd)) {
CMVR_LOG(ERROR) << "[CmvrPlcMotorProtocol] invalid profile-velocity command";
return false;
}
const auto velocity = toMicroUnits_(target_qd);
const auto acceleration = toMicroUnits_(std::abs(max_qdd));
if (!velocity || !acceleration) {
return false;
}
CmvrPlcAxisCommand command;
command.code = cmvr_plc::CommandCode::ProfileVelocity;
command.target_velocity = *velocity;
command.acceleration = *acceleration;
command.command_timeout_ms = kProfileCommandTimeoutCeilingMs;
if (!bus_runtime_) {
return false;
}
const auto submission_epoch = bus_runtime_->connectionEpoch();
const auto submitted =
bus_runtime_->submitAxisCommand(
node_id, command, false, submission_epoch);
const auto completed_epoch = bus_runtime_->connectionEpoch();
if (!submitted && completed_epoch == submission_epoch) {
return false;
}
{
std::lock_guard<std::mutex> lock(nodes_mutex_);
auto& stored = nodeStateLocked_(node_id);
stored.profile_feedback_bound = true;
stored.profile_connection_epoch = submission_epoch;
}
return submitted && completed_epoch == submission_epoch;
}
bool CmvrPlcMotorProtocol::commandCyclicPosition(
const std::uint8_t node_id,
const double target_q,
const double target_qd)
{
std::lock_guard<std::mutex> lock(nodes_mutex_);
auto& state = nodeStateLocked_(node_id);
if (!validatePosition_(state, target_q) ||
!validateVelocity_(state, target_qd) ||
!ensureCyclicOpen_(node_id, state, msgs::RUN_MODE_CYCLIC_SYNC_POSITION)) {
return false;
}
const auto position = toMicroUnits_(target_q);
const auto velocity = toMicroUnits_(target_qd);
if (!position || !velocity) {
return false;
}
CmvrPlcAxisCommand command;
command.code = cmvr_plc::CommandCode::CyclicPositionSample;
command.target_position = *position;
command.target_velocity = *velocity;
command.stream_watchdog_ms = bus_runtime_->streamWatchdogMs();
if (++state.cyclic_sequence == 0) {
++state.cyclic_sequence;
}
command.cyclic_sample_sequence = state.cyclic_sequence;
const auto submitted =
bus_runtime_->submitAxisCommand(
node_id, command, false, state.cyclic_connection_epoch);
if (submitted) {
state.profile_feedback_bound = false;
state.profile_connection_epoch = 0;
} else if (
bus_runtime_->connectionEpoch() != state.cyclic_connection_epoch) {
state.cyclic_reconnect_latched = true;
state.cyclic_stream_open = false;
state.cyclic_sequence = 0;
state.cyclic_connection_epoch = bus_runtime_->connectionEpoch();
}
return submitted;
}
bool CmvrPlcMotorProtocol::commandCyclicVelocity(
const std::uint8_t node_id,
const double target_qd)
{
std::lock_guard<std::mutex> lock(nodes_mutex_);
auto& state = nodeStateLocked_(node_id);
if (!validateVelocity_(state, target_qd) ||
!ensureCyclicOpen_(node_id, state, msgs::RUN_MODE_CYCLIC_SYNC_VELOCITY)) {
return false;
}
const auto velocity = toMicroUnits_(target_qd);
if (!velocity) {
return false;
}
CmvrPlcAxisCommand command;
command.code = cmvr_plc::CommandCode::CyclicVelocitySample;
command.target_velocity = *velocity;
command.stream_watchdog_ms = bus_runtime_->streamWatchdogMs();
if (++state.cyclic_sequence == 0) {
++state.cyclic_sequence;
}
command.cyclic_sample_sequence = state.cyclic_sequence;
const auto submitted =
bus_runtime_->submitAxisCommand(
node_id, command, false, state.cyclic_connection_epoch);
if (submitted) {
state.profile_feedback_bound = false;
state.profile_connection_epoch = 0;
} else if (
bus_runtime_->connectionEpoch() != state.cyclic_connection_epoch) {
state.cyclic_reconnect_latched = true;
state.cyclic_stream_open = false;
state.cyclic_sequence = 0;
state.cyclic_connection_epoch = bus_runtime_->connectionEpoch();
}
return submitted;
}
bool CmvrPlcMotorProtocol::commandCyclicTorque(
const std::uint8_t node_id,
const double target_tau)
{
(void)target_tau;
CMVR_LOG(ERROR) << "[CmvrPlcMotorProtocol] cyclic torque is unsupported, motor="
<< static_cast<int>(node_id);
return false;
}
void CmvrPlcMotorProtocol::setMotorConversion(
const std::uint8_t node_id,
const double encoder_counts_per_rev,
const double gear_ratio)
{
(void)node_id;
(void)encoder_counts_per_rev;
(void)gear_ratio;
// CMVR PLC v1 exchanges SI quantities in fixed-point micro-units.
}
bool CmvrPlcMotorProtocol::torqueOn(const std::uint8_t node_id)
{
return submitSimple_(node_id, cmvr_plc::CommandCode::Enable, true,
kSafetyCommandTimeoutMs);
}
bool CmvrPlcMotorProtocol::torqueOff(const std::uint8_t node_id)
{
CmvrPlcAxisCommand command;
command.code = cmvr_plc::CommandCode::Disable;
command.command_timeout_ms = kSafetyCommandTimeoutMs;
const auto result =
bus_runtime_ && bus_runtime_->submitAxisSafetyCommand(node_id, command);
std::lock_guard<std::mutex> lock(nodes_mutex_);
auto& state = nodeStateLocked_(node_id);
state.cyclic_stream_open = false;
if (result) {
state.profile_feedback_bound = false;
state.profile_connection_epoch = 0;
}
return result;
}
bool CmvrPlcMotorProtocol::brakeRelease(const std::uint8_t node_id)
{
CMVR_LOG(ERROR) << "[CmvrPlcMotorProtocol] brake release is unsupported, motor="
<< static_cast<int>(node_id);
return false;
}
bool CmvrPlcMotorProtocol::quickStop(const std::uint8_t node_id)
{
CmvrPlcAxisCommand command;
command.code = cmvr_plc::CommandCode::QuickStop;
command.command_timeout_ms = kSafetyCommandTimeoutMs;
const auto result =
bus_runtime_ && bus_runtime_->submitAxisSafetyCommand(node_id, command);
std::lock_guard<std::mutex> lock(nodes_mutex_);
auto& state = nodeStateLocked_(node_id);
state.cyclic_stream_open = false;
if (result) {
state.profile_feedback_bound = false;
state.profile_connection_epoch = 0;
}
return result;
}
double CmvrPlcMotorProtocol::getQ(const std::uint8_t node_id)
{
CmvrPlcAxisStatus status;
if (!readMotionStatus_(node_id, status) ||
!statusAllowsMotionFeedback(status)) {
return std::numeric_limits<double>::quiet_NaN();
}
return fromMicroUnits_(status.actual_position);
}
double CmvrPlcMotorProtocol::getQd(const std::uint8_t node_id)
{
CmvrPlcAxisStatus status;
if (!readMotionStatus_(node_id, status) ||
!statusAllowsMotionFeedback(status)) {
return std::numeric_limits<double>::quiet_NaN();
}
return fromMicroUnits_(status.actual_velocity);
}
bool CmvrPlcMotorProtocol::readMotionStatus_(
const std::uint8_t node_id,
CmvrPlcAxisStatus& status)
{
if (!bus_runtime_) {
return false;
}
std::lock_guard<std::mutex> lock(nodes_mutex_);
const auto& state = nodeStateLocked_(node_id);
const auto read_epoch = bus_runtime_->connectionEpoch();
if (state.profile_feedback_bound &&
state.profile_connection_epoch != read_epoch) {
return false;
}
if (!bus_runtime_->readAxisStatus(node_id, status)) {
return false;
}
// Reject a status transaction that straddled a disconnect/reconnect even
// when no profile binding was active at the first check.
if (bus_runtime_->connectionEpoch() != read_epoch) {
return false;
}
return !state.profile_feedback_bound ||
state.profile_connection_epoch == read_epoch;
}
bool CmvrPlcMotorProtocol::submitSimple_(
const std::uint8_t node_id,
const cmvr_plc::CommandCode code,
const bool wait_for_terminal,
const std::uint32_t timeout_ms)
{
CmvrPlcAxisCommand command;
command.code = code;
command.command_timeout_ms = timeout_ms;
return bus_runtime_ &&
bus_runtime_->submitAxisCommand(node_id, command, wait_for_terminal);
}
bool CmvrPlcMotorProtocol::ensureCyclicOpen_(
const std::uint8_t node_id,
NodeState& state,
const msgs::RunMode mode)
{
if (!bus_runtime_) {
return false;
}
const auto connection_epoch = bus_runtime_->connectionEpoch();
if (state.cyclic_connection_epoch != connection_epoch) {
// A cyclic generation is bound to the PLC ownership epoch in which it
// was created. Never reinterpret a setpoint from that generation as
// the first setpoint of a freshly reconnected PLC stream.
if (state.cyclic_generation != 0U) {
state.cyclic_reconnect_latched = true;
}
state.cyclic_connection_epoch = connection_epoch;
state.cyclic_stream_open = false;
state.cyclic_sequence = 0;
}
if (state.cyclic_reconnect_latched) {
CMVR_LOG(WARNING)
<< "[CmvrPlcMotorProtocol] cyclic stream crossed a PLC session; "
"explicit setMode from a new upper-layer stream is required, motor="
<< static_cast<int>(node_id);
return false;
}
if (state.cyclic_stream_open && state.requested_mode == mode) {
return true;
}
// Preserve direct protocol use that does not call setMode explicitly:
// its first successful Open still establishes a generation. Once that
// generation has observed a reconnect, only explicit setMode can recover.
if (state.cyclic_generation == 0U) {
state.cyclic_generation = 1U;
state.cyclic_connection_epoch = connection_epoch;
}
CmvrPlcAxisCommand command;
command.code = mode == msgs::RUN_MODE_CYCLIC_SYNC_POSITION
? cmvr_plc::CommandCode::OpenCyclicPosition
: cmvr_plc::CommandCode::OpenCyclicVelocity;
command.stream_watchdog_ms = bus_runtime_->streamWatchdogMs();
command.command_timeout_ms = kSafetyCommandTimeoutMs;
if (!bus_runtime_->submitAxisCommand(
node_id, command, false, state.cyclic_connection_epoch)) {
if (bus_runtime_->connectionEpoch() !=
state.cyclic_connection_epoch) {
state.cyclic_reconnect_latched = true;
state.cyclic_stream_open = false;
state.cyclic_sequence = 0;
state.cyclic_connection_epoch =
bus_runtime_->connectionEpoch();
}
return false;
}
state.requested_mode = mode;
state.cyclic_stream_open = true;
state.cyclic_sequence = 0;
return true;
}
std::optional<std::int32_t> CmvrPlcMotorProtocol::toMicroUnits_(const double value)
{
if (!std::isfinite(value)) {
return std::nullopt;
}
const auto scaled = std::round(value * cmvr_plc::kPositionScale);
if (scaled < static_cast<double>(std::numeric_limits<std::int32_t>::min()) ||
scaled > static_cast<double>(std::numeric_limits<std::int32_t>::max())) {
return std::nullopt;
}
return static_cast<std::int32_t>(scaled);
}
double CmvrPlcMotorProtocol::fromMicroUnits_(const std::int32_t value)
{
return static_cast<double>(value) / cmvr_plc::kPositionScale;
}
bool CmvrPlcMotorProtocol::isSupportedMode_(const msgs::RunMode mode)
{
return mode == msgs::RUN_MODE_PROFILE_POSITION ||
mode == msgs::RUN_MODE_PROFILE_VELOCITY ||
mode == msgs::RUN_MODE_CYCLIC_SYNC_POSITION ||
mode == msgs::RUN_MODE_CYCLIC_SYNC_VELOCITY;
}
bool CmvrPlcMotorProtocol::validatePosition_(
const NodeState& state,
const double position) const
{
return std::isfinite(position) &&
(state.limit_q_ub <= state.limit_q_lb ||
(position >= state.limit_q_lb && position <= state.limit_q_ub));
}
bool CmvrPlcMotorProtocol::validateVelocity_(
const NodeState& state,
const double velocity) const
{
return std::isfinite(velocity) &&
(state.limit_qd <= 0.0 || std::abs(velocity) <= state.limit_qd);
}
bool CmvrPlcMotorProtocol::validateAcceleration_(
const NodeState& state,
const double acceleration) const
{
return std::isfinite(acceleration) && acceleration >= 0.0 &&
(state.limit_qdd <= 0.0 || acceleration <= state.limit_qdd);
}
CmvrPlcMotorProtocol::NodeState& CmvrPlcMotorProtocol::nodeStateLocked_(
const std::uint8_t node_id)
{
return nodes_[node_id];
}
} // namespace cmvr::device

View File

@ -1,55 +0,0 @@
#include "devices/motor/drivers/modbus_plc_motor/include/modbus_plc_motor.h"
#include "common/base/logging/logger.h"
#include "devices/motor/drivers/modbus_plc_motor/include/cmvr_plc_motor_protocol.h"
#include <cmath>
namespace cmvr::device {
ModbusPlcMotor::ModbusPlcMotor(const config::MotorConfigItem& config)
{
info_.id = config.id();
info_.joint_name = config.joint_name();
info_.limit_q_lb = config.limit_q_lb();
info_.limit_q_ub = config.limit_q_ub();
info_.limit_qd = config.limit_qd();
info_.limit_qdd = config.limit_qdd();
node_id_ = static_cast<std::uint8_t>(config.id());
}
bool ModbusPlcMotor::init()
{
if (!protocol_ ||
!std::dynamic_pointer_cast<CmvrPlcMotorProtocol>(protocol_)) {
CMVR_LOG(ERROR) << "[ModbusPlcMotor] invalid CMVR PLC protocol for "
<< info_.joint_name;
return false;
}
if (!std::isfinite(info_.limit_q_lb) || !std::isfinite(info_.limit_q_ub) ||
!std::isfinite(info_.limit_qd) || !std::isfinite(info_.limit_qdd) ||
info_.limit_q_ub <= info_.limit_q_lb ||
info_.limit_qd <= 0.0 || info_.limit_qdd <= 0.0) {
CMVR_LOG(ERROR) << "[ModbusPlcMotor] finite position/velocity/acceleration "
"limits are required for "
<< info_.joint_name;
return false;
}
setLimitQ(info_.limit_q_ub, info_.limit_q_lb);
setLimitQd(info_.limit_qd);
setLimitQdd(info_.limit_qdd, -info_.limit_qdd);
return true;
}
bool ModbusPlcMotor::torqueOff()
{
auto protocol = std::dynamic_pointer_cast<CmvrPlcMotorProtocol>(protocol_);
return protocol && protocol->torqueOff(node_id_);
}
bool ModbusPlcMotor::quickStop()
{
auto protocol = std::dynamic_pointer_cast<CmvrPlcMotorProtocol>(protocol_);
return protocol && protocol->quickStop(node_id_);
}
} // namespace cmvr::device

View File

@ -13,7 +13,6 @@ target_link_libraries(motor_manager
cmvr_es::device::ti5_canopen_motor_driver
cmvr_es::device::mujoco_motor_driver
cmvr_es::device::ethercat_motor_driver
cmvr_es::device::modbus_plc_motor_driver
cmvr_es::ik_solver
glog
)

View File

@ -78,10 +78,6 @@ private:
const config::MotorGroupConfig& group_cfg,
const std::vector<config::MotorConfigItem>& motor_cfgs,
const std::shared_ptr<AbstractMotorBusRuntime>& bus_runtime) const;
std::vector<std::shared_ptr<AbstractMotor>> createModbusTcpMotors_(
const config::MotorGroupConfig& group_cfg,
const std::vector<config::MotorConfigItem>& motor_cfgs,
const std::shared_ptr<AbstractMotorBusRuntime>& bus_runtime) const;
private:
config::MotorConfig cfg_;

View File

@ -15,14 +15,11 @@
#include "devices/motor/bus_runtime/can/include/can_motor_bus_runtime.h"
#include "devices/motor/bus_runtime/ethercat/include/ethercat_motor_bus_runtime.h"
#include "devices/motor/bus_runtime/mujoco/include/mujoco_motor_bus_runtime.h"
#include "devices/motor/bus_runtime/modbus_tcp/include/modbus_tcp_motor_bus_runtime.h"
#include "devices/motor/drivers/ethercat_motor/include/cia402/cia402_protocol.h"
#include "devices/motor/drivers/ethercat_motor/include/vendor/eyou/eyou_cia402_pdo_mapping.h"
#include "devices/motor/drivers/ethercat_motor/include/vendor/eyou/eyou_motor.h"
#include "devices/motor/drivers/ethercat_motor/include/vendor/eyou/eyou_motor_adapter.h"
#include "devices/motor/drivers/mujoco/include/mujoco_motor.h"
#include "devices/motor/drivers/modbus_plc_motor/include/cmvr_plc_motor_protocol.h"
#include "devices/motor/drivers/modbus_plc_motor/include/modbus_plc_motor.h"
#include "devices/motor/drivers/ti5_canopen/include/ti5_motor.h"
#include "devices/motor/drivers/ti5_canopen/include/ti5_motor_canopen_protocol.h"
@ -418,16 +415,6 @@ std::shared_ptr<AbstractMotorBusRuntime> MotorManager::createBusRuntime_(
return std::make_shared<CanMotorBusRuntime>();
case config::MOTOR_BUS_MUJOCO:
return std::make_shared<MujocoMotorBusRuntime>();
case config::MOTOR_BUS_MODBUS_TCP:
if (group_cfg.vendor() == config::MOTOR_VENDOR_PLC_GENERIC &&
group_cfg.protocol() == config::MOTOR_PROTOCOL_CMVR_PLC_V1) {
return std::make_shared<ModbusTcpMotorBusRuntime>();
}
CMVR_LOG(ERROR) << "[MotorManager] unsupported Modbus TCP motor: vendor="
<< config::MotorVendor_Name(group_cfg.vendor())
<< ", protocol=" << config::MotorProtocol_Name(group_cfg.protocol())
<< ", group=" << group_cfg.id();
return nullptr;
case config::MOTOR_BUS_ETHERCAT: {
auto runtime = std::make_shared<EthercatMotorBusRuntime>();
if (group_cfg.vendor() == config::MOTOR_VENDOR_EYOU &&
@ -467,8 +454,6 @@ std::vector<std::shared_ptr<AbstractMotor>> MotorManager::createMotors_(
return createMujocoMotors_(group_cfg, motor_cfgs, bus_runtime);
case config::MOTOR_BUS_ETHERCAT:
return createEthercatMotors_(group_cfg, motor_cfgs, bus_runtime);
case config::MOTOR_BUS_MODBUS_TCP:
return createModbusTcpMotors_(group_cfg, motor_cfgs, bus_runtime);
default:
CMVR_LOG(ERROR) << "[MotorManager] unsupported motor bus type: "
<< config::MotorBusType_Name(group_cfg.bus_type())
@ -611,50 +596,4 @@ std::vector<std::shared_ptr<AbstractMotor>> MotorManager::createEthercatMotors_(
return motors;
}
std::vector<std::shared_ptr<AbstractMotor>> MotorManager::createModbusTcpMotors_(
const config::MotorGroupConfig& group_cfg,
const std::vector<config::MotorConfigItem>& motor_cfgs,
const std::shared_ptr<AbstractMotorBusRuntime>& bus_runtime) const
{
auto modbus_runtime =
std::dynamic_pointer_cast<ModbusTcpMotorBusRuntime>(bus_runtime);
if (!modbus_runtime || !group_cfg.has_modbus_tcp()) {
CMVR_LOG(ERROR) << "[MotorManager] missing Modbus TCP runtime/config: "
<< group_cfg.id();
return {};
}
if (group_cfg.vendor() != config::MOTOR_VENDOR_PLC_GENERIC ||
group_cfg.protocol() != config::MOTOR_PROTOCOL_CMVR_PLC_V1) {
CMVR_LOG(ERROR) << "[MotorManager] unsupported Modbus TCP PLC motor: vendor="
<< config::MotorVendor_Name(group_cfg.vendor())
<< ", protocol=" << config::MotorProtocol_Name(group_cfg.protocol());
return {};
}
for (const auto& motor_cfg : motor_cfgs) {
if (motor_cfg.id() < 0 || motor_cfg.id() > 255 ||
!modbus_runtime->hasMotor(static_cast<std::uint8_t>(motor_cfg.id()))) {
CMVR_LOG(ERROR) << "[MotorManager] missing PLC axis mapping for motor id "
<< motor_cfg.id() << " in group: " << group_cfg.id();
return {};
}
}
auto protocol = std::make_shared<CmvrPlcMotorProtocol>(modbus_runtime);
std::vector<std::shared_ptr<AbstractMotor>> motors;
motors.reserve(motor_cfgs.size());
for (const auto& cfg : motor_cfgs) {
auto motor = std::make_shared<ModbusPlcMotor>(cfg);
motor->setProtocol(protocol);
// initNode() and ModbusPlcMotor::init() only establish local mappings and
// limits. The shared TCP connection starts after all motors are created.
if (!motor->init()) {
CMVR_LOG(ERROR) << "[MotorManager] failed to init Modbus PLC motor: "
<< cfg.joint_name();
return {};
}
motors.push_back(std::move(motor));
}
return motors;
}
} // namespace cmvr::device

View File

@ -221,44 +221,6 @@ if(BUILD_TESTING)
ENVIRONMENT "${_grpc_agv_test_environment}"
)
set(_grpc_motor_modbus_e2e_libmodbus_root
"${CMAKE_SOURCE_DIR}/dependency/${ARCH}/third_party/modbus/3.1.11")
add_executable(grpc_motor_service_modbus_e2e_test
grpc/tests/grpc_motor_service_modbus_e2e_test.cpp
)
target_include_directories(grpc_motor_service_modbus_e2e_test
PRIVATE
${CMAKE_SOURCE_DIR}/cmvr-es
${CMAKE_SOURCE_DIR}/cmvr-es/manager/device_manager
${_grpc_motor_modbus_e2e_libmodbus_root}/include
)
target_link_directories(grpc_motor_service_modbus_e2e_test
PRIVATE
${_grpc_motor_modbus_e2e_libmodbus_root}/lib
)
target_link_libraries(grpc_motor_service_modbus_e2e_test
PRIVATE
service
modbus
gtest
gtest_main
pthread
)
add_test(
NAME grpc_motor_service_modbus_e2e_test
COMMAND grpc_motor_service_modbus_e2e_test
)
set(_grpc_motor_modbus_e2e_environment
"LD_LIBRARY_PATH=${CMVR_TEST_EXTERNAL_LIBRARY_PATH}"
)
if(CMVR_TEST_SYSTEM_LIBSTDCXX)
list(APPEND _grpc_motor_modbus_e2e_environment
"LD_PRELOAD=${CMVR_TEST_SYSTEM_LIBSTDCXX}")
endif()
set_tests_properties(grpc_motor_service_modbus_e2e_test PROPERTIES
TIMEOUT 20
ENVIRONMENT "${_grpc_motor_modbus_e2e_environment}"
)
endif()
# --------------------------------------------------------

View File

@ -42,7 +42,7 @@ grpcurl -plaintext \
## MotorService
`MotorService` 将 gRPC 电机命令适配到已经由 `DeviceManager` 创建的
`MotorManager``AbstractMotor`,不直接持有 PLC、现场总线或厂商驱动。
`MotorManager``AbstractMotor`,不直接持有现场总线或厂商驱动。
关键文件:
@ -52,8 +52,6 @@ grpcurl -plaintext \
和 [`grpc/src/grpc_motor_service.cpp`](grpc/src/grpc_motor_service.cpp)
- 注册:[`../task/grpc_server_task/src/grpc_server_task.cpp`](../task/grpc_server_task/src/grpc_server_task.cpp)
- 单元测试:[`grpc/tests/grpc_motor_service_test.cpp`](grpc/tests/grpc_motor_service_test.cpp)
- gRPCModbus 端到端测试:
[`grpc/tests/grpc_motor_service_modbus_e2e_test.cpp`](grpc/tests/grpc_motor_service_modbus_e2e_test.cpp)
服务按单电机仲裁。同步 Profile 命令、Cyclic Position/Velocity 双向流、
`setEnabled`、状态读取和软件 `emergencyStop` 共用同一控制权状态:
@ -66,11 +64,6 @@ grpcurl -plaintext \
- 只有成功执行 `setEnabled(true)` 才解除服务内软件急停锁存;
- 服务层 Quick Stop 和 `emergencyStop` 都不具备功能安全等级。
PLC 后端的连接 epoch、stream epoch、寄存器、ACK 和 TIA Portal 要求见:
- [Modbus TCP PLC runtime](../devices/motor/bus_runtime/modbus_tcp/README.md)
- [MotorService 与 CMVR PLC v1 完整协议](../../docs/motor_service_modbus_tcp.md)
AUBO 控制柜 IO 不经过 `MotorService`,由
`ArmService/ExecuteJsonCommand` 转发到目标 `RobotArm`。厂商命令和安全约束见
[AUBO 控制柜 IO](../devices/arm/aubo_arm/README.md)。

View File

@ -966,7 +966,7 @@ grpc::Status gRPCMotorServiceImpl::setZeroImpl(
}
if (!calibrated) {
const std::string error =
"zero calibration outcome unknown; inspect PLC zero_epoch/session before retry";
"zero calibration outcome unknown; inspect device state before retry";
setLastError(resolved.control, error);
lease.reset();
fillFeedback(response->mutable_header(), false, error);
@ -1984,7 +1984,7 @@ grpc::Status gRPCMotorServiceImpl::setEnabledImpl(
}
}
// A false acknowledgement may still mean the PLC committed the
// A false acknowledgement may still mean the backend committed the
// request. A successful enable also needs rollback if cancellation or
// E-stop won while torqueOn was in flight.
const bool cleanup_required =

View File

@ -1,907 +0,0 @@
#include "service/grpc/include/grpc_motor_service.h"
#include <arpa/inet.h>
#include <array>
#include <atomic>
#include <cerrno>
#include <chrono>
#include <cstdio>
#include <cstring>
#include <fstream>
#include <memory>
#include <sstream>
#include <string>
#include <thread>
#include <grpcpp/grpcpp.h>
#include <gtest/gtest.h>
#include <modbus/modbus.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <unistd.h>
#include "cmvr/config/device_manager_config/device_manager_config.pb.h"
#include "cmvr/config/motor_config/motor_config.pb.h"
#include "common/config/config_files.h"
#include "devices/motor/bus_runtime/modbus_tcp/include/cmvr_plc_register_map.h"
#include "devices/motor/manager/include/motor_manager.h"
#include "manager/device_manager/include/device_manager.h"
namespace cmvr::service {
namespace {
using namespace std::chrono_literals;
namespace plc = device::cmvr_plc;
constexpr char kManagerId[] = "e2e_plc_motors";
constexpr char kMotorGroupId[] = "e2e_plc_axis_group";
constexpr char kJointName[] = "E2E_PLC_AXIS";
std::uint16_t reserveLoopbackPort()
{
const int socket_fd = ::socket(AF_INET, SOCK_STREAM, 0);
if (socket_fd < 0) {
return 0;
}
sockaddr_in address{};
address.sin_family = AF_INET;
address.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
address.sin_port = 0;
if (::bind(socket_fd, reinterpret_cast<sockaddr*>(&address),
sizeof(address)) != 0) {
::close(socket_fd);
return 0;
}
socklen_t length = sizeof(address);
if (::getsockname(socket_fd, reinterpret_cast<sockaddr*>(&address),
&length) != 0) {
::close(socket_fd);
return 0;
}
const auto port = ntohs(address.sin_port);
::close(socket_fd);
return port;
}
template <typename Predicate>
bool waitUntil(Predicate&& predicate, const std::chrono::milliseconds timeout)
{
const auto deadline = std::chrono::steady_clock::now() + timeout;
do {
if (predicate()) {
return true;
}
std::this_thread::sleep_for(5ms);
} while (std::chrono::steady_clock::now() < deadline);
return predicate();
}
class FakeCmvrPlc final {
public:
~FakeCmvrPlc() { stop(); }
bool start()
{
port_ = reserveLoopbackPort();
if (port_ == 0) {
std::fprintf(stderr, "reserveLoopbackPort failed: %s\n",
std::strerror(errno));
return false;
}
context_ = modbus_new_tcp("127.0.0.1", port_);
mapping_ = modbus_mapping_new(
1, 1, plc::kAxisFirstOffset + plc::kAxisRegisterStride, 1);
if (!context_ || !mapping_) {
std::fprintf(stderr, "fake PLC allocation failed: %s\n",
modbus_strerror(errno));
stop();
return false;
}
auto* registers = mapping_->tab_registers;
registers[plc::kMagicCmOffset] = plc::kMagicCm;
registers[plc::kMagicVrOffset] = plc::kMagicVr;
registers[plc::kProtocolMajorOffset] = plc::kProtocolMajor;
registers[plc::kProtocolMinorOffset] = plc::kProtocolMinor;
registers[plc::kAxisCountOffset] = 1;
registers[plc::kOwnerStateOffset] =
static_cast<std::uint16_t>(plc::OwnerState::None);
plc::encodeUint32(registers, plc::kPlcBootIdOffset, 0x45453245U);
plc::encodeUint32(registers, plc::kPlcHeartbeatOffset,
plc_heartbeat_);
const auto status_base =
plc::axisBase(0) + plc::kAxisStatusRelativeOffset;
registers[status_base + plc::kCommandState] =
static_cast<std::uint16_t>(plc::CommandState::Idle);
registers[status_base + plc::kResultCode] =
static_cast<std::uint16_t>(plc::ResultCode::Ok);
publishSnapshot_(registers + status_base);
listen_socket_.store(modbus_tcp_listen(context_, 2));
if (listen_socket_.load() < 0) {
std::fprintf(stderr, "modbus_tcp_listen failed on %u: %s\n",
port_, modbus_strerror(errno));
stop();
return false;
}
running_.store(true);
worker_ = std::thread(&FakeCmvrPlc::loop_, this);
return true;
}
void stop()
{
running_.store(false);
const auto client = client_socket_.exchange(-1);
if (client >= 0) {
::shutdown(client, SHUT_RDWR);
::close(client);
}
const auto listener = listen_socket_.exchange(-1);
if (listener >= 0) {
::shutdown(listener, SHUT_RDWR);
::close(listener);
}
if (worker_.joinable()) {
worker_.join();
}
if (mapping_) {
modbus_mapping_free(mapping_);
mapping_ = nullptr;
}
if (context_) {
modbus_free(context_);
context_ = nullptr;
}
}
void disconnectClient()
{
const auto client = client_socket_.load();
if (client >= 0) {
::shutdown(client, SHUT_RDWR);
}
}
std::uint16_t port() const { return port_; }
std::uint32_t commandCount() const { return command_count_.load(); }
std::uint32_t acceptedSessionCount() const
{
return accepted_session_count_.load();
}
std::uint32_t enableCount() const { return enable_count_.load(); }
std::uint32_t profilePositionCount() const
{
return profile_position_count_.load();
}
std::uint32_t profileVelocityCount() const
{
return profile_velocity_count_.load();
}
std::uint32_t cyclicOpenCount() const
{
return cyclic_open_count_.load();
}
std::uint32_t cyclicSampleCount() const
{
return cyclic_sample_count_.load();
}
std::uint32_t quickStopCount() const
{
return quick_stop_count_.load();
}
private:
void loop_()
{
std::array<std::uint8_t, MODBUS_TCP_MAX_ADU_LENGTH> request{};
while (running_.load()) {
int listener = listen_socket_.load();
const int accepted =
listener < 0 ? -1 : modbus_tcp_accept(context_, &listener);
if (accepted < 0) {
if (running_.load()) {
std::this_thread::sleep_for(5ms);
}
continue;
}
client_socket_.store(accepted);
while (running_.load()) {
const auto request_length =
modbus_receive(context_, request.data());
if (request_length <= 0) {
break;
}
if (modbus_reply(context_, request.data(), request_length,
mapping_) < 0) {
break;
}
// Treat the mapping as the PLC's published process image.
// Read-only FC3 requests must not themselves advance the
// seqlock; otherwise the runtime's guard/full/guard snapshot
// validation could never observe one stable scan.
if (request_length > 7 && request[7] == 0x10U) {
processMailbox_();
}
}
const auto client = client_socket_.exchange(-1);
if (client >= 0) {
::close(client);
}
}
}
void processMailbox_()
{
auto* registers = mapping_->tab_registers;
const auto base = plc::axisBase(0);
const auto status_base = base + plc::kAxisStatusRelativeOffset;
beginSnapshot_(registers + status_base);
plc::encodeUint32(registers, plc::kPlcHeartbeatOffset,
++plc_heartbeat_);
const auto session =
plc::decodeUint32(registers, plc::kCmvrSessionIdOffset);
if (session != observed_session_) {
// A new session is not observable as Accepted until the old
// stream/mailbox state has been made safe.
registers[plc::kOwnerStateOffset] =
static_cast<std::uint16_t>(plc::OwnerState::Accepting);
observed_session_ = session;
active_session_ = session;
last_sequence_ = 0;
registers[status_base + plc::kStatusFlags] &=
static_cast<std::uint16_t>(~plc::StatusFlag::StreamActive);
plc::encodeUint32(registers + base, plc::kPayloadSequence, 0);
plc::encodeUint32(registers + base,
plc::kPayloadSequenceMirror, 0);
plc::encodeUint32(registers + base,
plc::kCommitSequenceRelativeOffset, 0);
plc::encodeUint32(registers, plc::kOwnerSessionIdOffset,
active_session_);
registers[plc::kOwnerStateOffset] =
static_cast<std::uint16_t>(
active_session_ == 0 ? plc::OwnerState::None
: plc::OwnerState::Accepted);
if (active_session_ != 0) {
accepted_session_count_.fetch_add(1);
}
}
if (active_session_ == 0 || active_session_ != session) {
publishSnapshot_(registers + status_base);
return;
}
const auto sequence =
plc::decodeUint32(registers + base, plc::kPayloadSequence);
const auto sequence_mirror =
plc::decodeUint32(registers + base,
plc::kPayloadSequenceMirror);
const auto commit =
plc::decodeUint32(registers + base,
plc::kCommitSequenceRelativeOffset);
const auto command_session =
plc::decodeUint32(registers + base, plc::kCommandSessionId);
if (sequence == 0 || sequence == last_sequence_ ||
sequence != sequence_mirror || sequence != commit ||
command_session != active_session_) {
publishSnapshot_(registers + status_base);
return;
}
last_sequence_ = sequence;
command_count_.fetch_add(1);
plc::encodeUint32(registers + status_base, plc::kAckSequence,
sequence);
plc::encodeUint32(registers + status_base, plc::kActiveSequence,
sequence);
plc::encodeUint32(registers + status_base, plc::kAckSessionId,
active_session_);
registers[status_base + plc::kResultCode] =
static_cast<std::uint16_t>(plc::ResultCode::Ok);
const auto code = static_cast<plc::CommandCode>(
registers[base + plc::kCommandCode]);
const auto target_position =
plc::decodeInt32(registers + base, plc::kTargetPosition);
const auto target_velocity =
plc::decodeInt32(registers + base, plc::kTargetVelocity);
auto& flags = registers[status_base + plc::kStatusFlags];
auto state = plc::CommandState::Completed;
switch (code) {
case plc::CommandCode::ProfilePosition:
profile_position_count_.fetch_add(1);
registers[status_base + plc::kCurrentMode] =
msgs::RUN_MODE_PROFILE_POSITION;
plc::encodeInt32(registers + status_base,
plc::kActualPosition, target_position);
plc::encodeInt32(registers + status_base,
plc::kActualVelocity, 0);
plc::encodeInt32(registers + status_base,
plc::kTargetPositionStatus, target_position);
flags |= plc::StatusFlag::TargetReached;
flags &= static_cast<std::uint16_t>(
~plc::StatusFlag::QuickStopActive);
state = plc::CommandState::TargetReached;
break;
case plc::CommandCode::ProfileVelocity:
profile_velocity_count_.fetch_add(1);
registers[status_base + plc::kCurrentMode] =
msgs::RUN_MODE_PROFILE_VELOCITY;
plc::encodeInt32(registers + status_base,
plc::kActualVelocity, target_velocity);
plc::encodeInt32(registers + status_base,
plc::kTargetVelocityStatus, target_velocity);
flags &= static_cast<std::uint16_t>(
~plc::StatusFlag::QuickStopActive);
state = plc::CommandState::Accepted;
break;
case plc::CommandCode::OpenCyclicPosition:
cyclic_open_count_.fetch_add(1);
registers[status_base + plc::kCurrentMode] =
msgs::RUN_MODE_CYCLIC_SYNC_POSITION;
plc::encodeUint32(
registers + status_base,
plc::kLastAppliedCyclicSequence, 0);
flags |= plc::StatusFlag::StreamActive;
flags &= static_cast<std::uint16_t>(
~plc::StatusFlag::QuickStopActive);
state = plc::CommandState::Accepted;
break;
case plc::CommandCode::CyclicPositionSample:
cyclic_sample_count_.fetch_add(1);
plc::encodeInt32(registers + status_base,
plc::kActualPosition, target_position);
plc::encodeInt32(registers + status_base,
plc::kActualVelocity, target_velocity);
plc::encodeUint32(
registers + status_base,
plc::kLastAppliedCyclicSequence,
plc::decodeUint32(registers + base,
plc::kCyclicSampleSequence));
state = plc::CommandState::Accepted;
break;
case plc::CommandCode::QuickStop:
quick_stop_count_.fetch_add(1);
plc::encodeInt32(registers + status_base,
plc::kActualVelocity, 0);
flags |= plc::StatusFlag::QuickStopActive;
flags &= static_cast<std::uint16_t>(
~plc::StatusFlag::StreamActive);
state = plc::CommandState::QuickStopped;
break;
case plc::CommandCode::Enable:
enable_count_.fetch_add(1);
flags |= plc::StatusFlag::Enabled;
flags &= static_cast<std::uint16_t>(
~plc::StatusFlag::QuickStopActive);
break;
case plc::CommandCode::Disable:
flags &= static_cast<std::uint16_t>(
~plc::StatusFlag::Enabled);
flags &= static_cast<std::uint16_t>(
~plc::StatusFlag::StreamActive);
break;
default:
registers[status_base + plc::kResultCode] =
static_cast<std::uint16_t>(
plc::ResultCode::Unsupported);
state = plc::CommandState::Rejected;
break;
}
registers[status_base + plc::kCommandState] =
static_cast<std::uint16_t>(state);
publishSnapshot_(registers + status_base);
}
void publishSnapshot_(std::uint16_t* status)
{
if (pending_state_sequence_ == 0U) {
beginSnapshot_(status);
}
plc::encodeUint32(status, plc::kHeartbeatAge, 0);
plc::encodeUint32(status, plc::kStateSequenceMirror,
pending_state_sequence_);
plc::encodeUint32(status, plc::kStateSequence,
pending_state_sequence_);
state_sequence_ = pending_state_sequence_;
pending_state_sequence_ = 0U;
}
void beginSnapshot_(std::uint16_t* status)
{
auto next = state_sequence_ + 2U;
if (next == 0U) {
next = 2U;
}
pending_state_sequence_ = next;
plc::encodeUint32(status, plc::kStateSequence, next - 1U);
}
modbus_t* context_{nullptr};
modbus_mapping_t* mapping_{nullptr};
std::thread worker_;
std::atomic<bool> running_{false};
std::atomic<int> listen_socket_{-1};
std::atomic<int> client_socket_{-1};
std::atomic<std::uint32_t> command_count_{0};
std::atomic<std::uint32_t> accepted_session_count_{0};
std::atomic<std::uint32_t> enable_count_{0};
std::atomic<std::uint32_t> profile_position_count_{0};
std::atomic<std::uint32_t> profile_velocity_count_{0};
std::atomic<std::uint32_t> cyclic_open_count_{0};
std::atomic<std::uint32_t> cyclic_sample_count_{0};
std::atomic<std::uint32_t> quick_stop_count_{0};
std::uint32_t observed_session_{0};
std::uint32_t active_session_{0};
std::uint32_t plc_heartbeat_{1};
std::uint32_t state_sequence_{0};
std::uint32_t pending_state_sequence_{0};
std::uint32_t last_sequence_{0};
std::uint16_t port_{0};
};
std::string writeTemporaryMotorConfig(const std::uint16_t port)
{
char path[] = "/tmp/cmvr_motor_service_modbus_e2e_XXXXXX.pb.txt";
const int descriptor = ::mkstemps(path, 7);
if (descriptor < 0) {
return {};
}
::close(descriptor);
std::ofstream output(path, std::ios::out | std::ios::trunc);
if (!output.is_open()) {
std::remove(path);
return {};
}
output << "motor {\n"
<< " id: \"" << kManagerId << "\"\n"
<< " motor_groups {\n"
<< " id: \"" << kMotorGroupId << "\"\n"
<< " bus_type: MOTOR_BUS_MODBUS_TCP\n"
<< " vendor: MOTOR_VENDOR_PLC_GENERIC\n"
<< " protocol: MOTOR_PROTOCOL_CMVR_PLC_V1\n"
<< " modbus_tcp {\n"
<< " host: \"127.0.0.1\"\n"
<< " port: " << port << "\n"
<< " unit_id: 1\n"
<< " connect_timeout_ms: 200\n"
<< " io_timeout_ms: 50\n"
<< " heartbeat_period_ms: 20\n"
<< " communication_watchdog_ms: 300\n"
<< " status_poll_period_ms: 2\n"
<< " reconnect_min_ms: 10\n"
<< " reconnect_max_ms: 50\n"
<< " command_ack_timeout_ms: 300\n"
<< " cyclic_watchdog_ms: 300\n"
<< " protocol_major: 1\n"
<< " protocol_minor: 0\n"
<< " axes { motor_id: 1 axis_index: 0 }\n"
<< " }\n"
<< " joint_limits {\n"
<< " enable: true\n"
<< " source: JOINT_LIMIT_SOURCE_CUSTOM\n"
<< " joints {\n"
<< " joint_name: \"" << kJointName << "\"\n"
<< " q_lb: -2.0\n"
<< " q_ub: 2.0\n"
<< " qd: 2.0\n"
<< " qdd: 4.0\n"
<< " }\n"
<< " }\n"
<< " motors {\n"
<< " motors { id: 1 joint_name: \"" << kJointName << "\" }\n"
<< " }\n"
<< " }\n"
<< "}\n";
output.close();
if (!output) {
std::remove(path);
return {};
}
return path;
}
class MotorServiceModbusE2eTest : public ::testing::Test {
protected:
void SetUp() override
{
device::DeviceManager::destroyInstance();
ASSERT_TRUE(plc_.start());
config_path_ = writeTemporaryMotorConfig(plc_.port());
ASSERT_FALSE(config_path_.empty());
config::MotorRootConfig parsed;
ASSERT_TRUE(ConfigHelper::loadConfigFileSilent(config_path_, parsed));
ASSERT_EQ(parsed.motor().id(), kManagerId);
ASSERT_EQ(parsed.motor().motor_groups_size(), 1);
config::DeviceManagerConfig device_config;
device_config.set_init_all_motors_when_no_active_joints(true);
auto* entry = device_config.add_devices();
entry->set_id(kManagerId);
entry->set_type(
config::DeviceConfigEntry::DEVICE_TYPE_MOTOR_SYSTEM);
entry->set_config_file(config_path_);
entry->set_enable(true);
auto& device_manager =
device::DeviceManager::getInstance(device_config);
manager_ =
device_manager.getDevice<device::MotorManager>(kManagerId);
ASSERT_NE(manager_, nullptr);
ASSERT_NE(manager_->getMotor(1), nullptr);
ASSERT_EQ(manager_->getMotor(1)->jointName(), kJointName);
service_ = std::make_unique<gRPCMotorServiceImpl>();
ASSERT_TRUE(startGrpcServer_());
}
void TearDown() override
{
if (grpc_server_) {
grpc_server_->Shutdown();
grpc_server_->Wait();
grpc_server_.reset();
}
stub_.reset();
service_.reset();
if (manager_) {
manager_->stop();
manager_.reset();
}
device::DeviceManager::destroyInstance();
plc_.stop();
if (!grpc_socket_path_.empty()) {
std::remove(grpc_socket_path_.c_str());
grpc_socket_path_.clear();
}
if (!config_path_.empty()) {
std::remove(config_path_.c_str());
config_path_.clear();
}
}
static api::MotorTarget makeTarget_()
{
api::MotorTarget target;
target.mutable_header()->set_device_id(kManagerId);
target.set_motor_id(1);
return target;
}
bool startGrpcServer_()
{
grpc_socket_path_ =
"/tmp/cmvr_motor_service_modbus_e2e_" +
std::to_string(static_cast<long long>(::getpid())) + ".sock";
std::remove(grpc_socket_path_.c_str());
const std::string server_address = "unix:" + grpc_socket_path_;
grpc::ServerBuilder builder;
builder.AddListeningPort(
server_address, grpc::InsecureServerCredentials());
builder.RegisterService(service_.get());
grpc_server_ = builder.BuildAndStart();
if (!grpc_server_) {
return false;
}
stub_ = api::MotorService::NewStub(
grpc::CreateChannel(
server_address,
grpc::InsecureChannelCredentials()));
return stub_ != nullptr;
}
FakeCmvrPlc plc_;
std::shared_ptr<device::MotorManager> manager_;
std::unique_ptr<gRPCMotorServiceImpl> service_;
std::unique_ptr<grpc::Server> grpc_server_;
std::unique_ptr<api::MotorService::Stub> stub_;
std::string config_path_;
std::string grpc_socket_path_;
};
TEST_F(MotorServiceModbusE2eTest,
StubTraversesManagerAndModbusRuntimeWithSafetyAndReconnect)
{
api::SetMotorEnabledRequest enable_request;
*enable_request.mutable_target() = makeTarget_();
enable_request.set_enabled(true);
grpc::ClientContext enable_context;
enable_context.set_deadline(
std::chrono::system_clock::now() + 3s);
api::MotorCommandResponse enable_response;
const auto enable_status =
stub_->setEnabled(&enable_context, enable_request, &enable_response);
ASSERT_TRUE(enable_status.ok()) << enable_status.error_message();
ASSERT_TRUE(enable_response.header().success())
<< enable_response.header().error_message();
EXPECT_EQ(plc_.enableCount(), 1U);
api::ProfilePositionRequest position_request;
*position_request.mutable_target() = makeTarget_();
position_request.set_target_position_rad(0.75);
position_request.set_max_velocity_rad_s(0.5);
position_request.set_acceleration_rad_s2(1.0);
position_request.mutable_wait()->set_timeout_ms(1000);
position_request.mutable_wait()->set_poll_period_ms(2);
position_request.mutable_wait()->set_settle_sample_count(1);
grpc::ClientContext position_context;
position_context.set_deadline(
std::chrono::system_clock::now() + 3s);
api::MotorCommandResponse position_response;
const auto position_status = stub_->profilePosition(
&position_context, position_request, &position_response);
ASSERT_TRUE(position_status.ok()) << position_status.error_message();
ASSERT_TRUE(position_response.header().success())
<< position_response.header().error_message();
EXPECT_NEAR(position_response.status().position_rad(), 0.75, 1e-6);
EXPECT_TRUE(position_response.status().target_reached());
EXPECT_EQ(plc_.profilePositionCount(), 1U);
api::GetMotorStatusRequest get_status_request;
*get_status_request.mutable_target() = makeTarget_();
grpc::ClientContext get_status_context;
get_status_context.set_deadline(
std::chrono::system_clock::now() + 3s);
api::GetMotorStatusResponse get_status_response;
const auto get_status = stub_->getStatus(
&get_status_context, get_status_request, &get_status_response);
ASSERT_TRUE(get_status.ok()) << get_status.error_message();
ASSERT_TRUE(get_status_response.header().success())
<< get_status_response.header().error_message();
EXPECT_EQ(get_status_response.status().motor_id(), 1U);
EXPECT_EQ(get_status_response.status().joint_name(), kJointName);
EXPECT_EQ(get_status_response.status().run_mode(),
msgs::RUN_MODE_PROFILE_POSITION);
EXPECT_NEAR(get_status_response.status().position_rad(), 0.75, 1e-6);
api::ProfileVelocityRequest velocity_request;
*velocity_request.mutable_target() = makeTarget_();
velocity_request.set_target_velocity_rad_s(-0.2);
velocity_request.set_acceleration_rad_s2(0.5);
velocity_request.mutable_wait()->set_timeout_ms(1000);
velocity_request.mutable_wait()->set_poll_period_ms(2);
velocity_request.mutable_wait()->set_settle_sample_count(1);
grpc::ClientContext velocity_context;
velocity_context.set_deadline(
std::chrono::system_clock::now() + 3s);
api::MotorCommandResponse velocity_response;
const auto velocity_status = stub_->profileVelocity(
&velocity_context, velocity_request, &velocity_response);
ASSERT_TRUE(velocity_status.ok()) << velocity_status.error_message();
ASSERT_TRUE(velocity_response.header().success())
<< velocity_response.header().error_message();
EXPECT_NEAR(velocity_response.status().velocity_rad_s(), -0.2, 1e-6);
EXPECT_EQ(plc_.profileVelocityCount(), 1U);
const auto quick_stops_before_stream = plc_.quickStopCount();
grpc::ClientContext stream_context;
stream_context.set_deadline(
std::chrono::system_clock::now() + 3s);
auto stream = stub_->streamCyclicPosition(&stream_context);
ASSERT_NE(stream, nullptr);
api::CyclicPositionRequest open_request;
*open_request.mutable_open()->mutable_target() = makeTarget_();
open_request.mutable_open()->set_watchdog_timeout_ms(500);
ASSERT_TRUE(stream->Write(open_request));
api::CyclicControlResponse stream_response;
ASSERT_TRUE(stream->Read(&stream_response));
ASSERT_TRUE(stream_response.header().success())
<< stream_response.header().error_message();
EXPECT_EQ(stream_response.phase(), api::CYCLIC_STREAM_OPENED);
api::CyclicPositionRequest setpoint_request;
auto* setpoint = setpoint_request.mutable_setpoint();
setpoint->set_sequence(1);
setpoint->set_target_position_rad(0.25);
setpoint->set_target_velocity_rad_s(0.1);
ASSERT_TRUE(stream->Write(setpoint_request));
stream_response.Clear();
ASSERT_TRUE(stream->Read(&stream_response));
ASSERT_TRUE(stream_response.header().success())
<< stream_response.header().error_message();
EXPECT_EQ(stream_response.phase(), api::CYCLIC_STREAM_APPLIED);
EXPECT_EQ(stream_response.sequence(), 1U);
EXPECT_FALSE(stream_response.has_status());
EXPECT_EQ(plc_.cyclicOpenCount(), 1U);
EXPECT_EQ(plc_.cyclicSampleCount(), 1U);
ASSERT_TRUE(stream->WritesDone());
stream_response.Clear();
ASSERT_TRUE(stream->Read(&stream_response));
EXPECT_TRUE(stream_response.header().success())
<< stream_response.header().error_message();
EXPECT_EQ(stream_response.phase(), api::CYCLIC_STREAM_STOPPED);
EXPECT_FALSE(stream->Read(&stream_response));
const auto stream_finish = stream->Finish();
ASSERT_TRUE(stream_finish.ok()) << stream_finish.error_message();
EXPECT_GT(plc_.quickStopCount(), quick_stops_before_stream);
const auto quick_stops_before_emergency = plc_.quickStopCount();
api::EmergencyStopRequest emergency_request;
*emergency_request.mutable_target() = makeTarget_();
grpc::ClientContext emergency_context;
emergency_context.set_deadline(
std::chrono::system_clock::now() + 3s);
api::MotorCommandResponse emergency_response;
const auto emergency_status = stub_->emergencyStop(
&emergency_context, emergency_request, &emergency_response);
ASSERT_TRUE(emergency_status.ok()) << emergency_status.error_message();
ASSERT_TRUE(emergency_response.header().success())
<< emergency_response.header().error_message();
EXPECT_TRUE(emergency_response.status().emergency_stopped());
EXPECT_GT(plc_.quickStopCount(), quick_stops_before_emergency);
const auto commands_before_rejected_motion = plc_.commandCount();
api::ProfilePositionRequest rejected_request;
*rejected_request.mutable_target() = makeTarget_();
rejected_request.set_target_position_rad(0.5);
rejected_request.set_max_velocity_rad_s(0.5);
rejected_request.set_acceleration_rad_s2(1.0);
grpc::ClientContext rejected_context;
rejected_context.set_deadline(
std::chrono::system_clock::now() + 3s);
api::MotorCommandResponse rejected_response;
const auto rejected_status = stub_->profilePosition(
&rejected_context, rejected_request, &rejected_response);
EXPECT_EQ(rejected_status.error_code(),
grpc::StatusCode::FAILED_PRECONDITION);
EXPECT_FALSE(rejected_response.header().success());
EXPECT_EQ(plc_.commandCount(), commands_before_rejected_motion);
const auto sessions_before_disconnect =
plc_.acceptedSessionCount();
const auto commands_before_disconnect = plc_.commandCount();
plc_.disconnectClient();
ASSERT_TRUE(waitUntil(
[&] {
return plc_.acceptedSessionCount() >
sessions_before_disconnect;
},
3s));
std::this_thread::sleep_for(100ms);
EXPECT_EQ(plc_.commandCount(), commands_before_disconnect);
api::SetMotorEnabledRequest reenable_request;
*reenable_request.mutable_target() = makeTarget_();
reenable_request.set_enabled(true);
grpc::ClientContext reenable_context;
reenable_context.set_deadline(
std::chrono::system_clock::now() + 3s);
api::MotorCommandResponse reenable_response;
const auto reenable_status = stub_->setEnabled(
&reenable_context, reenable_request, &reenable_response);
ASSERT_TRUE(reenable_status.ok()) << reenable_status.error_message();
EXPECT_TRUE(reenable_response.header().success())
<< reenable_response.header().error_message();
EXPECT_EQ(plc_.commandCount(), commands_before_disconnect + 1U);
EXPECT_FALSE(reenable_response.status().emergency_stopped());
}
TEST_F(MotorServiceModbusE2eTest,
ActiveCyclicStreamFailsClosedAcrossReconnectAndNewStreamRecovers)
{
grpc::ClientContext old_context;
old_context.set_deadline(std::chrono::system_clock::now() + 8s);
auto old_stream = stub_->streamCyclicPosition(&old_context);
ASSERT_NE(old_stream, nullptr);
api::CyclicPositionRequest open_request;
*open_request.mutable_open()->mutable_target() = makeTarget_();
open_request.mutable_open()->set_watchdog_timeout_ms(1000);
ASSERT_TRUE(old_stream->Write(open_request));
api::CyclicControlResponse response;
ASSERT_TRUE(old_stream->Read(&response));
ASSERT_TRUE(response.header().success())
<< response.header().error_message();
ASSERT_EQ(response.phase(), api::CYCLIC_STREAM_OPENED);
api::CyclicPositionRequest first_sample;
first_sample.mutable_setpoint()->set_sequence(1);
first_sample.mutable_setpoint()->set_target_position_rad(0.1);
first_sample.mutable_setpoint()->set_target_velocity_rad_s(0.0);
ASSERT_TRUE(old_stream->Write(first_sample));
response.Clear();
ASSERT_TRUE(old_stream->Read(&response));
ASSERT_TRUE(response.header().success())
<< response.header().error_message();
ASSERT_EQ(response.phase(), api::CYCLIC_STREAM_APPLIED);
ASSERT_EQ(plc_.cyclicOpenCount(), 1U);
ASSERT_EQ(plc_.cyclicSampleCount(), 1U);
const auto accepted_sessions_before = plc_.acceptedSessionCount();
plc_.disconnectClient();
ASSERT_TRUE(waitUntil(
[&] {
return plc_.acceptedSessionCount() >
accepted_sessions_before;
},
3s));
const auto opens_after_reconnect = plc_.cyclicOpenCount();
const auto samples_after_reconnect = plc_.cyclicSampleCount();
const auto stops_after_reconnect = plc_.quickStopCount();
api::CyclicPositionRequest stale_sample;
stale_sample.mutable_setpoint()->set_sequence(2);
stale_sample.mutable_setpoint()->set_target_position_rad(0.2);
stale_sample.mutable_setpoint()->set_target_velocity_rad_s(0.0);
ASSERT_TRUE(old_stream->Write(stale_sample));
response.Clear();
ASSERT_TRUE(old_stream->Read(&response));
EXPECT_FALSE(response.header().success());
EXPECT_EQ(response.phase(), api::CYCLIC_STREAM_FAILED);
EXPECT_NE(response.header().error_message().find(
"rejected cyclic position setpoint"),
std::string::npos);
EXPECT_FALSE(old_stream->Read(&response));
const auto old_finish = old_stream->Finish();
EXPECT_TRUE(
old_finish.error_code() == grpc::StatusCode::FAILED_PRECONDITION ||
old_finish.error_code() == grpc::StatusCode::CANCELLED)
<< old_finish.error_message();
// Stream cleanup may commit a safety QuickStop, but the stale generation
// must never commit an Open or sample into the new PLC session.
EXPECT_EQ(plc_.cyclicOpenCount(), opens_after_reconnect);
EXPECT_EQ(plc_.cyclicSampleCount(), samples_after_reconnect);
EXPECT_GT(plc_.quickStopCount(), stops_after_reconnect);
grpc::ClientContext new_context;
new_context.set_deadline(std::chrono::system_clock::now() + 5s);
auto new_stream = stub_->streamCyclicPosition(&new_context);
ASSERT_NE(new_stream, nullptr);
ASSERT_TRUE(new_stream->Write(open_request));
response.Clear();
ASSERT_TRUE(new_stream->Read(&response));
ASSERT_TRUE(response.header().success())
<< response.header().error_message();
ASSERT_EQ(response.phase(), api::CYCLIC_STREAM_OPENED);
api::CyclicPositionRequest fresh_sample;
fresh_sample.mutable_setpoint()->set_sequence(1);
fresh_sample.mutable_setpoint()->set_target_position_rad(0.3);
fresh_sample.mutable_setpoint()->set_target_velocity_rad_s(0.0);
ASSERT_TRUE(new_stream->Write(fresh_sample));
response.Clear();
ASSERT_TRUE(new_stream->Read(&response));
ASSERT_TRUE(response.header().success())
<< response.header().error_message();
EXPECT_EQ(response.phase(), api::CYCLIC_STREAM_APPLIED);
EXPECT_EQ(plc_.cyclicOpenCount(), opens_after_reconnect + 1U);
EXPECT_EQ(plc_.cyclicSampleCount(), samples_after_reconnect + 1U);
ASSERT_TRUE(new_stream->WritesDone());
response.Clear();
ASSERT_TRUE(new_stream->Read(&response));
EXPECT_TRUE(response.header().success())
<< response.header().error_message();
EXPECT_EQ(response.phase(), api::CYCLIC_STREAM_STOPPED);
EXPECT_FALSE(new_stream->Read(&response));
const auto new_finish = new_stream->Finish();
EXPECT_TRUE(new_finish.ok()) << new_finish.error_message();
}
} // namespace
} // namespace cmvr::service

View File

@ -309,7 +309,7 @@ protected:
std::string grpc_socket_path_;
};
TEST_F(MotorServiceTest, SetZeroAckLossReportsUnknownOutcome)
TEST_F(MotorServiceTest, SetZeroBackendFailureReportsUnknownOutcome)
{
protocol_->calibrate_success_ = false;
@ -323,7 +323,7 @@ TEST_F(MotorServiceTest, SetZeroAckLossReportsUnknownOutcome)
EXPECT_FALSE(response.header().success());
EXPECT_NE(response.header().error_message().find("outcome unknown"),
std::string::npos);
EXPECT_NE(response.header().error_message().find("zero_epoch"),
EXPECT_NE(response.header().error_message().find("device state"),
std::string::npos);
EXPECT_EQ(protocol_->quick_stop_count_.load(), 0);
}

File diff suppressed because it is too large Load Diff

View File

@ -101,7 +101,7 @@ message SetMotorEnabledRequest {
message CyclicStreamOpen {
MotorTarget target = 1;
// The PLC/driver watchdog is authoritative. This service watchdog prevents a
// The device/driver watchdog is authoritative. This service watchdog prevents a
// stalled gRPC client from retaining control indefinitely.
uint32 watchdog_timeout_ms = 2;
}

View File

@ -76,35 +76,13 @@ message MujocoMotorGroupConfig {
string world_id = 1;
}
message ModbusTcpAxisConfig {
int32 motor_id = 1;
uint32 axis_index = 2;
}
message ModbusTcpConfig {
string host = 1;
uint32 port = 2;
uint32 unit_id = 3;
uint32 connect_timeout_ms = 4;
uint32 io_timeout_ms = 5;
uint32 heartbeat_period_ms = 6;
uint32 communication_watchdog_ms = 7;
uint32 status_poll_period_ms = 8;
uint32 reconnect_min_ms = 9;
uint32 reconnect_max_ms = 10;
uint32 command_ack_timeout_ms = 11;
uint32 protocol_major = 12;
uint32 protocol_minor = 13;
uint32 cyclic_watchdog_ms = 14;
repeated ModbusTcpAxisConfig axes = 20;
}
enum MotorBusType {
MOTOR_BUS_UNKNOWN = 0;
MOTOR_BUS_CAN = 1;
MOTOR_BUS_ETHERCAT = 2;
MOTOR_BUS_MUJOCO = 3;
MOTOR_BUS_MODBUS_TCP = 4;
reserved 4;
reserved "MOTOR_BUS_MODBUS_TCP";
}
enum MotorVendor {
@ -112,7 +90,8 @@ enum MotorVendor {
MOTOR_VENDOR_TI5 = 1;
MOTOR_VENDOR_MUJOCO = 2;
MOTOR_VENDOR_EYOU = 3;
MOTOR_VENDOR_PLC_GENERIC = 4;
reserved 4;
reserved "MOTOR_VENDOR_PLC_GENERIC";
}
enum MotorProtocol {
@ -120,10 +99,14 @@ enum MotorProtocol {
MOTOR_PROTOCOL_CANOPEN = 1;
MOTOR_PROTOCOL_ETHERCAT_CIA402 = 2;
MOTOR_PROTOCOL_MUJOCO = 3;
MOTOR_PROTOCOL_CMVR_PLC_V1 = 4;
reserved 4;
reserved "MOTOR_PROTOCOL_CMVR_PLC_V1";
}
message MotorGroupConfig {
reserved 13;
reserved "modbus_tcp";
string id = 1;
MotorBusType bus_type = 2;
MotorVendor vendor = 3;
@ -133,7 +116,6 @@ message MotorGroupConfig {
SocketCanConfig can = 10;
EtherCATConfig ethercat = 11;
MujocoMotorGroupConfig mujoco = 12;
ModbusTcpConfig modbus_tcp = 13;
}
JointLimitsConfig joint_limits = 30;