Compare commits

..

7 Commits

77 changed files with 20476 additions and 561 deletions

View File

@ -18,7 +18,9 @@ set(CMAKE_INSTALL_PREFIX "${CMAKE_SOURCE_DIR}/output" CACHE PATH "" FORCE)
set(CMAKE_BUILD_RPATH "\$ORIGIN:\$ORIGIN/../lib")
set(CMAKE_INSTALL_RPATH "\$ORIGIN:\$ORIGIN/../lib")
# Use RUNPATH (new dtags) generally preferable
# Use transitive RPATH so CLion can run build-tree test executables without
# manually setting LD_LIBRARY_PATH for indirect third-party dependencies.
add_link_options(-Wl,--disable-new-dtags)
set(CMAKE_BUILD_WITH_INSTALL_RPATH OFF)
set(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE)

103
README.md
View File

@ -1,23 +1,25 @@
# CMVR-ES
## Overview
## 简介
## Installation
CMVR-ES 工程。
### 1. Git submodules install
## 安装
### 1. 拉取 Git 子模块
```
git submodule update --init --recursive
```
### 2. Dependency install
### 2. 安装系统依赖
```shell
# basic
# 基础工具
sudo apt-get update
sudo apt install -y build-essential cmake git pkg-config patchelf libboost-all-dev libssl-dev
# opencv
# OpenCV
sudo apt install -y \
libjpeg-dev libpng-dev libtiff-dev \
libavcodec-dev libavformat-dev libswscale-dev \
@ -39,7 +41,7 @@ sudo apt-get install libassimp-dev
# visp
sudo apt-get install -y libx11-dev liblapack-dev libzbar-dev libpthread-stubs0-dev libdc1394-dev nlohmann-json3-dev
# realsense
# RealSense
sudo apt-get install -y \
libusb-1.0-0-dev libudev-dev \
libglu1-mesa-dev
@ -48,4 +50,91 @@ sudo apt-get install -y \
sudo apt install gnuplot-qt
```
### 3. 配置 IgH EtherCAT
工程已内置 IgH EtherCAT 1.7.0 的 userspace 文件:
```text
dependency/x86/third_party/ethercat/v1.7.0
```
先设置本机路径:
```shell
export CMVR_ES_ROOT=/path/to/cmvr-es
export IGH_ETHERCAT_ROOT=$CMVR_ES_ROOT/dependency/x86/third_party/ethercat/v1.7.0
```
安装当前内核的 header
```shell
sudo apt-get update
sudo apt-get install -y linux-headers-$(uname -r)
```
如果内置目录里已经有当前内核版本的 EtherCAT 内核模块,直接安装到系统:
```shell
sudo mkdir -p /lib/modules/$(uname -r)/ethercat
sudo cp -r $IGH_ETHERCAT_ROOT/lib/modules/$(uname -r)/ethercat/* /lib/modules/$(uname -r)/ethercat/
sudo depmod
```
内置内核模块只适用于相同内核版本。如果 `$IGH_ETHERCAT_ROOT/lib/modules/$(uname -r)` 不存在,说明这台机器的内核版本不匹配,需要在这台机器上重新编译安装 IgH EtherCAT
```shell
sudo apt-get update
sudo apt-get install -y \
build-essential autoconf automake libtool pkg-config git \
linux-headers-$(uname -r)
cd /tmp
git clone --branch stable-1.7 --depth 1 https://gitlab.com/etherlab.org/ethercat.git ethercat-stable-1.7
cd ethercat-stable-1.7
./bootstrap
./configure \
--prefix=$IGH_ETHERCAT_ROOT \
--libdir=$IGH_ETHERCAT_ROOT/lib \
--includedir=$IGH_ETHERCAT_ROOT/include \
--sysconfdir=$IGH_ETHERCAT_ROOT/etc \
--with-systemdsystemunitdir=$IGH_ETHERCAT_ROOT/lib/systemd/system \
--enable-generic \
--with-linux-dir=/lib/modules/$(uname -r)/build
make -j$(nproc) all modules
make install
sudo make modules_install
sudo depmod
```
启动 EtherCAT。`eno1` 换成实际连接 EtherCAT 从站的网卡:
```shell
sudo script/ethercat/start_ethercat.sh eno1
```
脚本会写入内置 IgH 配置文件:
```text
$IGH_ETHERCAT_ROOT/etc/ethercat.conf
```
脚本会把该网卡的 MAC 写到 `MASTER0_DEVICE`,使用 `DEVICE_MODULES="generic"`,把网卡从 NetworkManager 断开,并通过 `ethercatctl -c` 启动 IgH master。
查看状态:
```shell
script/ethercat/status_ethercat.sh
$IGH_ETHERCAT_ROOT/bin/ethercat master
$IGH_ETHERCAT_ROOT/bin/ethercat slaves
$IGH_ETHERCAT_ROOT/bin/ethercat pdos
```
停止 EtherCAT
```shell
sudo script/ethercat/stop_ethercat.sh eno1
sudo script/ethercat/stop_ethercat.sh eno1 --restore-network
```

View File

@ -0,0 +1,65 @@
motor {
id: "ethercat_motors"
motor_groups {
id: "right_arm_ethercat"
bus_type: MOTOR_BUS_ETHERCAT
vendor: MOTOR_VENDOR_EYOU
protocol: MOTOR_PROTOCOL_ETHERCAT_CIA402
ethercat {
master_index: 0
cycle_us: 1000
slave_op_timeout_ms: 12000
slave_state_poll_period_ms: 10
cia402 {
profile_position_trigger_delay_ms: 2
state_transition_timeout_ms: 1200
velocity_stop_timeout_ms: 2000
status_poll_period_ms: 10
stopped_velocity_tolerance_rad_s: 0.001
}
dc {
enable: true
reference_motor_id: 1
sync0_cycle_us: 1000
sync0_shift_us: 0
sync_reference_clock_period: 1
assign_activate: 768
sync_monitor_period_ms: 1000
}
slaves { motor_id: 1 alias: 0 position: 0 }
slaves { motor_id: 2 alias: 0 position: 1 }
slaves { motor_id: 3 alias: 0 position: 2 }
slaves { motor_id: 4 alias: 0 position: 3 }
slaves { motor_id: 5 alias: 0 position: 4 }
slaves { motor_id: 6 alias: 0 position: 5 }
slaves { motor_id: 7 alias: 0 position: 6 }
}
joint_limits {
enable: true
source: JOINT_LIMIT_SOURCE_CUSTOM
joints { joint_name: "R_SHOULDER_P" q_lb: -3.14 q_ub: 3.14 qd: 5.0 qdd: 10.0 }
joints { joint_name: "R_SHOULDER_R" q_lb: -0.78 q_ub: 1.57 qd: 5.0 qdd: 10.0 }
joints { joint_name: "R_SHOULDER_Y" q_lb: -3.14 q_ub: 3.14 qd: 5.0 qdd: 10.0 }
joints { joint_name: "R_ELBOW_R" q_lb: 0 q_ub: 2.05 qd: 5.0 qdd: 10.0 }
joints { joint_name: "R_WRIST_P" q_lb: -3.14 q_ub: 3.14 qd: 5.0 qdd: 10.0 }
joints { joint_name: "R_WRIST_Y" q_lb: -0.78 q_ub: 0.78 qd: 5.0 qdd: 10.0 }
joints { joint_name: "R_WRIST_R" q_lb: -0.57 q_ub: 1.57 qd: 5.0 qdd: 10.0 }
}
motors {
motors { id: 1 joint_name: "R_SHOULDER_P" encoder_counts_per_rev: 65536 gear_ratio: 101.0 }
motors { id: 2 joint_name: "R_SHOULDER_R" encoder_counts_per_rev: 65536 gear_ratio: 101.0 }
motors { id: 3 joint_name: "R_SHOULDER_Y" encoder_counts_per_rev: 65536 gear_ratio: 101.0 }
motors { id: 4 joint_name: "R_ELBOW_R" encoder_counts_per_rev: 65536 gear_ratio: 101.0 }
motors { id: 5 joint_name: "R_WRIST_P" encoder_counts_per_rev: 65536 gear_ratio: 101.0 }
motors { id: 6 joint_name: "R_WRIST_Y" encoder_counts_per_rev: 65536 gear_ratio: 101.0 }
motors { id: 7 joint_name: "R_WRIST_R" encoder_counts_per_rev: 65536 gear_ratio: 101.0 }
}
}
}

View File

@ -0,0 +1,56 @@
motor {
id: "ethercat_motors"
motor_groups {
id: "right_arm_ethercat"
bus_type: MOTOR_BUS_ETHERCAT
vendor: MOTOR_VENDOR_EYOU
protocol: MOTOR_PROTOCOL_ETHERCAT_CIA402
ethercat {
master_index: 0
cycle_us: 1000
slave_op_timeout_ms: 15000
slave_state_poll_period_ms: 10
cia402 {
profile_position_trigger_delay_ms: 2
state_transition_timeout_ms: 1200
velocity_stop_timeout_ms: 2000
status_poll_period_ms: 10
stopped_velocity_tolerance_rad_s: 0.001
}
dc {
enable: false
reference_motor_id: 1
sync0_cycle_us: 1000
sync0_shift_us: 0
sync_reference_clock_period: 1
assign_activate: 768
sync_monitor_period_ms: 1000
}
slaves { motor_id: 1 alias: 0 position: 0 }
slaves { motor_id: 2 alias: 0 position: 1 }
slaves { motor_id: 3 alias: 0 position: 2 }
slaves { motor_id: 4 alias: 0 position: 3 }
}
joint_limits {
enable: true
source: JOINT_LIMIT_SOURCE_CUSTOM
joints { joint_name: "R_SHOULDER_P" q_lb: -3.14 q_ub: 3.14 qd: 5.0 qdd: 10.0 }
joints { joint_name: "R_SHOULDER_R" q_lb: -0.78 q_ub: 1.57 qd: 5.0 qdd: 10.0 }
joints { joint_name: "R_SHOULDER_Y" q_lb: -3.14 q_ub: 3.14 qd: 5.0 qdd: 10.0 }
joints { joint_name: "R_ELBOW_R" q_lb: 0 q_ub: 2.05 qd: 5.0 qdd: 10.0 }
}
motors {
motors { id: 1 joint_name: "R_SHOULDER_P" encoder_counts_per_rev: 65536 gear_ratio: 101.0 }
motors { id: 2 joint_name: "R_SHOULDER_R" encoder_counts_per_rev: 65536 gear_ratio: 101.0 }
motors { id: 3 joint_name: "R_SHOULDER_Y" encoder_counts_per_rev: 65536 gear_ratio: 101.0 }
motors { id: 4 joint_name: "R_ELBOW_R" encoder_counts_per_rev: 65536 gear_ratio: 101.0 }
}
}
}

View File

@ -6,7 +6,6 @@ motor {
bus_type: MOTOR_BUS_MUJOCO
vendor: MOTOR_VENDOR_MUJOCO
protocol: MOTOR_PROTOCOL_MUJOCO
tool_frame: "R_FINGER_TIP"
mujoco {
world_id: "mujoco_world"
}

View File

@ -6,7 +6,6 @@ motor {
bus_type: MOTOR_BUS_CAN
vendor: MOTOR_VENDOR_TI5
protocol: MOTOR_PROTOCOL_CANOPEN
tool_frame: "L_FINGER_TIP"
can {
channel_id: 0
}
@ -16,13 +15,13 @@ motor {
urdf_path: "model/xiaoyan_description/dual_arm.urdf"
}
motors {
motors { id: 23 joint_name: "L_SHOULDER_P" }
motors { id: 24 joint_name: "L_SHOULDER_R" }
motors { id: 25 joint_name: "L_SHOULDER_Y" }
motors { id: 26 joint_name: "L_ELBOW_R" }
motors { id: 27 joint_name: "L_WRIST_P" }
motors { id: 28 joint_name: "L_WRIST_Y" }
motors { id: 29 joint_name: "L_WRIST_R" }
motors { id: 23 joint_name: "L_SHOULDER_P" encoder_counts_per_rev: 65536 gear_ratio: 101.0 }
motors { id: 24 joint_name: "L_SHOULDER_R" encoder_counts_per_rev: 65536 gear_ratio: 101.0 }
motors { id: 25 joint_name: "L_SHOULDER_Y" encoder_counts_per_rev: 65536 gear_ratio: 101.0 }
motors { id: 26 joint_name: "L_ELBOW_R" encoder_counts_per_rev: 65536 gear_ratio: 101.0 }
motors { id: 27 joint_name: "L_WRIST_P" encoder_counts_per_rev: 65536 gear_ratio: 101.0 }
motors { id: 28 joint_name: "L_WRIST_Y" encoder_counts_per_rev: 65536 gear_ratio: 101.0 }
motors { id: 29 joint_name: "L_WRIST_R" encoder_counts_per_rev: 65536 gear_ratio: 101.0 }
}
}
@ -31,7 +30,6 @@ motor {
bus_type: MOTOR_BUS_CAN
vendor: MOTOR_VENDOR_TI5
protocol: MOTOR_PROTOCOL_CANOPEN
tool_frame: "R_FINGER_TIP"
can {
channel_id: 1
}
@ -47,13 +45,13 @@ motor {
joints { joint_name: "R_WRIST_R" q_lb: -0.57 q_ub: 1.57 qd: 5.0 qdd: 10.0 }
}
motors {
motors { id: 16 joint_name: "R_SHOULDER_P" }
motors { id: 17 joint_name: "R_SHOULDER_R" }
motors { id: 18 joint_name: "R_SHOULDER_Y" }
motors { id: 19 joint_name: "R_ELBOW_R" }
motors { id: 20 joint_name: "R_WRIST_P" }
motors { id: 21 joint_name: "R_WRIST_Y" }
motors { id: 22 joint_name: "R_WRIST_R" }
motors { id: 16 joint_name: "R_SHOULDER_P" encoder_counts_per_rev: 65536 gear_ratio: 101.0 }
motors { id: 17 joint_name: "R_SHOULDER_R" encoder_counts_per_rev: 65536 gear_ratio: 101.0 }
motors { id: 18 joint_name: "R_SHOULDER_Y" encoder_counts_per_rev: 65536 gear_ratio: 101.0 }
motors { id: 19 joint_name: "R_ELBOW_R" encoder_counts_per_rev: 65536 gear_ratio: 101.0 }
motors { id: 20 joint_name: "R_WRIST_P" encoder_counts_per_rev: 65536 gear_ratio: 101.0 }
motors { id: 21 joint_name: "R_WRIST_Y" encoder_counts_per_rev: 65536 gear_ratio: 101.0 }
motors { id: 22 joint_name: "R_WRIST_R" encoder_counts_per_rev: 65536 gear_ratio: 101.0 }
}
}
@ -73,9 +71,9 @@ motor {
joints { joint_name: "HEAD_R" q_lb: -3.14 q_ub: 3.14 qd: 3.0 }
}
motors {
motors { id: 32 joint_name: "HEAD_Y" }
motors { id: 30 joint_name: "HEAD_P" }
motors { id: 31 joint_name: "HEAD_R" }
motors { id: 32 joint_name: "HEAD_Y" encoder_counts_per_rev: 65536 gear_ratio: 101.0 }
motors { id: 30 joint_name: "HEAD_P" encoder_counts_per_rev: 65536 gear_ratio: 101.0 }
motors { id: 31 joint_name: "HEAD_R" encoder_counts_per_rev: 65536 gear_ratio: 101.0 }
}
}
@ -94,8 +92,8 @@ motor {
joints { joint_name: "WAIST_P" q_lb: -3.14 q_ub: 3.14 qd: 3.0 }
}
motors {
motors { id: 4 joint_name: "WAIST_Y" }
motors { id: 15 joint_name: "WAIST_P" }
motors { id: 4 joint_name: "WAIST_Y" encoder_counts_per_rev: 65536 gear_ratio: 101.0 }
motors { id: 15 joint_name: "WAIST_P" encoder_counts_per_rev: 65536 gear_ratio: 101.0 }
}
}
}

View File

@ -2,6 +2,7 @@ device_manager {
name: "cmvr_es"
version: "0.1"
description: "cmvr edge system version 0.1"
init_all_motors_when_no_active_joints: true
devices {
id: "mujoco_world"
@ -74,6 +75,13 @@ device_manager {
enable: false
}
devices {
id: "ethercat_motors"
type: DEVICE_TYPE_MOTOR_SYSTEM
config_file: "devices/motor/ethercat_motors.pb.txt"
enable: false
}
devices {
id: "right_arm"
type: DEVICE_TYPE_ROBOT_ARM

View File

@ -73,7 +73,7 @@ public:
bool isConnected() const override { return motor_manager_ != nullptr; }
Result powerOn() override { return torqueOn(); }
Result powerOff() override { return torqueOff(); }
Result brakeRelease() override { return torqueOn(); }
Result brakeRelease() override;
Result shutdown() override;
Result clearFault() override { return Result::success(); }
Result unlockProtectiveStop() override { return Result::success(); }

View File

@ -196,7 +196,10 @@ Result MotorRobotArm::torqueOn()
if (!motor) {
return Result::failure(ArmErrorCode::RobotNotReady, "motor not found for joint: " + joint_name);
}
motor->brake();
if (!motor->torqueOn()) {
return Result::failure(ArmErrorCode::CommandFailed,
"failed to torque on motor for joint: " + joint_name);
}
}
emergency_stopped_ = false;
return Result::success();
@ -209,7 +212,26 @@ Result MotorRobotArm::torqueOff()
if (!motor) {
return Result::failure(ArmErrorCode::RobotNotReady, "motor not found for joint: " + joint_name);
}
motor->torqueOff();
if (!motor->torqueOff() || !motor->brakeRelease()) {
return Result::failure(ArmErrorCode::CommandFailed,
"failed to torque off motor for joint: " + joint_name);
}
}
return Result::success();
}
Result MotorRobotArm::brakeRelease()
{
for (const auto& joint_name : joint_names_) {
auto motor = getMotor_(joint_name);
if (!motor) {
return Result::failure(ArmErrorCode::RobotNotReady,
"motor not found for joint: " + joint_name);
}
if (!motor->brakeRelease()) {
return Result::failure(ArmErrorCode::CommandFailed,
"failed to release brake for joint: " + joint_name);
}
}
return Result::success();
}
@ -238,7 +260,10 @@ Result MotorRobotArm::emergencyStop()
if (!motor) {
return Result::failure(ArmErrorCode::RobotNotReady, "motor not found for joint: " + joint_name);
}
motor->brake();
if (!motor->quickStop()) {
return Result::failure(ArmErrorCode::CommandFailed,
"failed to quick stop motor for joint: " + joint_name);
}
}
emergency_stopped_ = true;
return Result::success();
@ -298,7 +323,11 @@ Result MotorRobotArm::moveJ(const JointPositionCommand& target, const MotionOpti
}
for (std::size_t i = 0; i < motors.size(); ++i) {
const double qd = i < sample.velocity.size() ? sample.velocity[i] : 0.0;
motors[i]->setTarget(sample.position[i], qd);
if (!motors[i]->commandCyclicPosition(sample.position[i], qd)) {
return Result::failure(ArmErrorCode::CommandFailed,
"failed to command cyclic position for joint: " +
motors[i]->jointName());
}
}
if (k + 1 < samples.size()) {
const double next_t = samples[k + 1].t > 0.0 ? samples[k + 1].t
@ -331,7 +360,11 @@ Result MotorRobotArm::speedJ(const JointVelocityCommand& velocity,
if (motor->getMode() != msgs::RUN_MODE_CYCLIC_SYNC_VELOCITY) {
motor->setMode(msgs::RUN_MODE_CYCLIC_SYNC_VELOCITY);
}
motor->setTarget(velocity.velocity[i] * speed_scaling_);
if (!motor->commandCyclicVelocity(velocity.velocity[i] * speed_scaling_)) {
return Result::failure(ArmErrorCode::CommandFailed,
"failed to command cyclic velocity for joint: " +
joint_names_[i]);
}
}
}
@ -457,7 +490,11 @@ Result MotorRobotArm::servoJ(const JointPositionCommand& target)
if (motor->getMode() != msgs::RUN_MODE_CYCLIC_SYNC_POSITION) {
motor->setMode(msgs::RUN_MODE_CYCLIC_SYNC_POSITION);
}
motor->setTarget(target.position[i], 0.0);
if (!motor->commandCyclicPosition(target.position[i], 0.0)) {
return Result::failure(ArmErrorCode::CommandFailed,
"failed to command cyclic position for joint: " +
joint_names_[i]);
}
}
return Result::success();
}
@ -740,7 +777,9 @@ bool MotorRobotArm::executeMoveLTrajectory_(const CartesianJointTrajectory& traj
return false;
}
for (std::size_t j = 0; j < motors.size(); ++j) {
motors[j]->setTarget(position[j], velocity[j]);
if (!motors[j]->commandCyclicPosition(position[j], velocity[j])) {
return false;
}
}
next_deadline += std::chrono::duration_cast<std::chrono::steady_clock::duration>(
std::chrono::duration<double>(dt_segment));

View File

@ -30,7 +30,7 @@ namespace cmvr {
return BASE_ID + sdo_frame_.node_id();
}
void SetFrameData(msgs::CommandSpecifier cs, msgs::ObIndex index,msgs::ObSubIndex sub_index, uint32_t data) {
void SetFrameData(msgs::CommandSpecifier cs, uint32_t index, uint32_t sub_index, uint32_t data) {
std::lock_guard<std::mutex> lock(mutex_);
sdo_frame_.set_cs(cs);
sdo_frame_.set_index(index);

View File

@ -49,10 +49,10 @@ namespace cmvr {
auto command = static_cast<msgs::CommandSpecifier>(bytes[0]);
// 解析 index字节1和字节2低字节优先
auto index = static_cast<msgs::ObIndex>(bytes[1] + (bytes[2] << 8));
const uint32_t index = bytes[1] + (bytes[2] << 8);
// 解析 subindex字节3
auto subindex = static_cast<msgs::ObSubIndex>(bytes[3]);
const uint32_t subindex = bytes[3];
// 根据 command 解析 data字节4~7
uint32_t data = 0;
@ -94,4 +94,4 @@ namespace cmvr {
ParseSdoData(sdo_response_, sensor_data);
}
}
}
}

View File

@ -1,6 +1,10 @@
add_library(motor_core INTERFACE)
target_include_directories(motor_core INTERFACE ${CMAKE_SOURCE_DIR}/cmvr-es/devices)
target_include_directories(motor_core
INTERFACE
${CMAKE_SOURCE_DIR}/cmvr-es
${CMAKE_SOURCE_DIR}/cmvr-es/devices
)
target_link_libraries(motor_core
INTERFACE
@ -12,4 +16,5 @@ add_library(cmvr_es::device::motor_core ALIAS motor_core)
add_subdirectory(drivers/ti5_canopen)
add_subdirectory(drivers/mujoco)
add_subdirectory(bus_runtime)
add_subdirectory(drivers/ethercat_motor)
add_subdirectory(manager)

View File

@ -66,13 +66,22 @@ namespace cmvr::device{
return protocol_->getMode(node_id_);
}
virtual void torqueOff() {
virtual bool torqueOn() {
std::scoped_lock lock(mtx_);
if (!protocol_) {
CMVR_LOG(ERROR) << "Protocol not set for motor";
return;
return false;
}
protocol_->torqueOff(node_id_);
return protocol_->torqueOn(node_id_);
}
virtual bool torqueOff() {
std::scoped_lock lock(mtx_);
if (!protocol_) {
CMVR_LOG(ERROR) << "Protocol not set for motor";
return false;
}
return protocol_->torqueOff(node_id_);
}
virtual void setLimitQ(double ub, double lb) {
@ -101,43 +110,69 @@ namespace cmvr::device{
}
// virtual void setLimitTau(double tau) = 0;
// virtual void setLimitCurrent(double tau) = 0;
virtual void brake() {
virtual bool brakeRelease() {
std::scoped_lock lock(mtx_);
if (!protocol_) {
CMVR_LOG(ERROR) << "Protocol not set for motor";
return;
return false;
}
protocol_->brake(node_id_);
}
/**
*
* @param q unit : rad
*/
virtual void setQ(double q) {
std::scoped_lock lock(mtx_);
if (!protocol_) {
CMVR_LOG(ERROR) << "Protocol not set for motor";
return;
}
protocol_->setQ(node_id_, q);
return protocol_->brakeRelease(node_id_);
}
virtual void setTarget(double q,double qd) {
virtual bool quickStop() {
std::scoped_lock lock(mtx_);
if (!protocol_) {
CMVR_LOG(ERROR) << "Protocol not set for motor";
return;
return false;
}
protocol_->setTarget(node_id_,q, qd);
return protocol_->quickStop(node_id_);
}
virtual bool commandProfilePosition(double target_q,
double max_qd = 0.0,
double max_qdd = 0.0) {
std::scoped_lock lock(mtx_);
if (!protocol_) {
CMVR_LOG(ERROR) << "Protocol not set for motor";
return false;
}
return protocol_->commandProfilePosition(node_id_, target_q, max_qd, max_qdd);
}
virtual void setTarget(double qd) {
virtual bool commandProfileVelocity(double target_qd, double max_qdd = 0.0) {
std::scoped_lock lock(mtx_);
if (!protocol_) {
CMVR_LOG(ERROR) << "Protocol not set for motor";
return;
return false;
}
protocol_->setTarget(node_id_, qd);
return protocol_->commandProfileVelocity(node_id_, target_qd, max_qdd);
}
virtual bool commandCyclicPosition(double target_q,
double target_qd = 0.0) {
std::scoped_lock lock(mtx_);
if (!protocol_) {
CMVR_LOG(ERROR) << "Protocol not set for motor";
return false;
}
return protocol_->commandCyclicPosition(node_id_, target_q, target_qd);
}
virtual bool commandCyclicVelocity(double target_qd) {
std::scoped_lock lock(mtx_);
if (!protocol_) {
CMVR_LOG(ERROR) << "Protocol not set for motor";
return false;
}
return protocol_->commandCyclicVelocity(node_id_, target_qd);
}
virtual bool commandCyclicTorque(double target_tau) {
std::scoped_lock lock(mtx_);
if (!protocol_) {
CMVR_LOG(ERROR) << "Protocol not set for motor";
return false;
}
return protocol_->commandCyclicTorque(node_id_, target_tau);
}
virtual bool calibrateZeroQ() {
@ -156,16 +191,6 @@ namespace cmvr::device{
}
return protocol_->reachedTargetQ(node_id_);
}
// rad /s
virtual void setQd(double qd) {
std::scoped_lock lock(mtx_);
if (!protocol_) {
CMVR_LOG(ERROR) << "Protocol not set for motor";
return;
}
return protocol_->setQd(node_id_,qd);
}
// virtual void setQdd(double qdd) = 0; // rad /s^2
// virtual void setTau(double tau) = 0; // N m
// virtual void clear_err() = 0;
// virtual void getStatus() = 0;

View File

@ -4,7 +4,18 @@ add_library(motor_bus_runtime SHARED
ethercat/src/ethercat_motor_bus_runtime.cpp
)
target_include_directories(motor_bus_runtime PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
set(IGH_ETHERCAT_ROOT
${CMAKE_SOURCE_DIR}/dependency/x86/third_party/ethercat/v1.7.0
)
target_include_directories(motor_bus_runtime
PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}
PRIVATE
${IGH_ETHERCAT_ROOT}/include
)
target_link_directories(motor_bus_runtime PRIVATE ${IGH_ETHERCAT_ROOT}/lib)
target_link_libraries(motor_bus_runtime
PUBLIC
@ -12,9 +23,28 @@ target_link_libraries(motor_bus_runtime
cmvr_es::device::motor_core
cmvr_es::mujoco_world
PRIVATE
ethercat
cmvr_es::device::canbus
glog
)
add_library(cmvr_es::device::motor_bus_runtime ALIAS motor_bus_runtime)
install(TARGETS motor_bus_runtime LIBRARY DESTINATION lib)
add_executable(ethercat_motor_bus_runtime_real_test
ethercat/src/ethercat_motor_bus_runtime_real_test.cpp
)
target_include_directories(ethercat_motor_bus_runtime_real_test
PRIVATE
${CMAKE_SOURCE_DIR}/cmvr-es
)
target_link_libraries(ethercat_motor_bus_runtime_real_test
PRIVATE
cmvr_es::device::motor_bus_runtime
gtest
gtest_main
pthread
glog
)

View File

@ -1,10 +1,23 @@
#ifndef CMVR_ES_ETHERCAT_MOTOR_BUS_RUNTIME_H
#define CMVR_ES_ETHERCAT_MOTOR_BUS_RUNTIME_H
#include <atomic>
#include <chrono>
#include <cstdint>
#include <cstring>
#include <mutex>
#include <string>
#include <thread>
#include <type_traits>
#include <unordered_map>
#include <vector>
#include "../../abstract_motor_bus_runtime.h"
#include "motor/bus_runtime/ethercat/include/ethercat_pdo_mapping.h"
typedef struct ec_domain ec_domain_t;
typedef struct ec_master ec_master_t;
typedef struct ec_slave_config ec_slave_config_t;
namespace cmvr::device {
@ -17,12 +30,136 @@ public:
const std::string& id() const { return id_; }
const config::EtherCATConfig& config() const { return config_; }
void setPdoMapping(EthercatPdoMapping mapping);
const EthercatPdoMapping& pdoMapping() const { return pdo_mapping_; }
const config::EthercatSlaveConfig* slaveForMotor(int motor_id) const;
bool hasMotor(int motor_id) const;
bool hasPdoEntry(int motor_id, std::uint16_t index, std::uint8_t subindex) const;
template <typename T>
bool writePdo(int motor_id, std::uint16_t index, std::uint8_t subindex, T value)
{
return writePdoRaw_(motor_id, index, subindex, valueBitLength_<T>(),
toRawValue_(value));
}
template <typename T>
bool readPdo(int motor_id, std::uint16_t index, std::uint8_t subindex, T& value) const
{
std::uint64_t raw = 0;
if (!readPdoRaw_(motor_id, index, subindex, valueBitLength_<T>(), raw)) {
return false;
}
value = fromRawValue_<T>(raw);
return true;
}
template <typename T>
bool writeSdo(int motor_id, std::uint16_t index, std::uint8_t subindex, T value)
{
return writeSdoRaw_(motor_id, index, subindex, valueBitLength_<T>(),
toRawValue_(value));
}
template <typename T>
bool readSdo(int motor_id, std::uint16_t index, std::uint8_t subindex, T& value)
{
std::uint64_t raw = 0;
if (!readSdoRaw_(motor_id, index, subindex, valueBitLength_<T>(), raw)) {
return false;
}
value = fromRawValue_<T>(raw);
return true;
}
private:
struct PdoEntryRuntime {
EthercatPdoEntryConfig cfg;
unsigned int offset{0};
bool rx{false};
std::uint64_t value{0};
};
struct SlaveRuntime {
config::EthercatSlaveConfig cfg;
ec_slave_config_t* slave_config{nullptr};
std::unordered_map<std::uint32_t, PdoEntryRuntime> pdo_entries;
};
bool configureSlave_(SlaveRuntime& slave);
bool configureDc_();
bool waitSlavesOperational_();
void cyclicLoop_();
void readFeedbackLocked_();
void writeCommandsLocked_();
void releaseMaster_();
bool hasValidPdoMapping_() const;
bool writePdoRaw_(int motor_id, std::uint16_t index, std::uint8_t subindex,
std::uint8_t bit_len, std::uint64_t value);
bool readPdoRaw_(int motor_id, std::uint16_t index, std::uint8_t subindex,
std::uint8_t bit_len, std::uint64_t& value) const;
bool writeSdoRaw_(int motor_id, std::uint16_t index, std::uint8_t subindex,
std::uint8_t bit_len, std::uint64_t value);
bool readSdoRaw_(int motor_id, std::uint16_t index, std::uint8_t subindex,
std::uint8_t bit_len, std::uint64_t& value);
template <typename T>
static constexpr std::uint8_t valueBitLength_()
{
using ValueType = std::remove_cv_t<T>;
static_assert(std::is_integral_v<ValueType>, "EtherCAT object values must be integral");
static_assert(!std::is_same_v<ValueType, bool>, "bool is not a valid EtherCAT object value");
static_assert(sizeof(ValueType) == 1 || sizeof(ValueType) == 2 || sizeof(ValueType) == 4,
"only 8/16/32-bit EtherCAT object values are supported");
return static_cast<std::uint8_t>(sizeof(ValueType) * 8);
}
template <typename T>
static std::uint64_t toRawValue_(T value)
{
using ValueType = std::remove_cv_t<T>;
using UnsignedType = std::make_unsigned_t<ValueType>;
return static_cast<std::uint64_t>(static_cast<UnsignedType>(value));
}
template <typename T>
static T fromRawValue_(std::uint64_t raw)
{
using ValueType = std::remove_cv_t<T>;
using UnsignedType = std::make_unsigned_t<ValueType>;
const auto unsigned_value = static_cast<UnsignedType>(raw);
ValueType value{};
std::memcpy(&value, &unsigned_value, sizeof(ValueType));
return value;
}
static std::uint32_t pdoEntryKey_(std::uint16_t index, std::uint8_t subindex);
static std::string hexIndex_(std::uint32_t index);
static std::uint64_t maskValue_(std::uint64_t value, std::uint8_t bit_len);
static bool isSupportedBitLength_(std::uint8_t bit_len);
static std::uint64_t readEntryValue_(const std::uint8_t* domain_data,
const PdoEntryRuntime& entry);
static void writeEntryValue_(std::uint8_t* domain_data,
const PdoEntryRuntime& entry);
static std::uint64_t steadyTimeNs_();
static std::uint64_t timePointNs_(std::chrono::steady_clock::time_point time_point);
static std::uint32_t usToNs_(std::uint32_t value_us);
static std::int32_t usToNs_(std::int32_t value_us);
std::string id_;
config::EtherCATConfig config_;
std::unordered_map<int, const config::EthercatSlaveConfig*> slaves_by_motor_id_;
EthercatPdoMapping pdo_mapping_;
std::unordered_map<int, SlaveRuntime> slaves_by_motor_id_;
ec_master_t* master_{nullptr};
ec_domain_t* domain_{nullptr};
std::uint8_t* domain_data_{nullptr};
mutable std::mutex data_mutex_;
std::thread cyclic_thread_;
std::atomic<bool> running_{false};
bool initialized_{false};
bool started_{false};
};

View File

@ -0,0 +1,35 @@
#ifndef CMVR_ES_ETHERCAT_PDO_MAPPING_H
#define CMVR_ES_ETHERCAT_PDO_MAPPING_H
#include <cstdint>
#include <string>
#include <vector>
namespace cmvr::device {
struct EthercatPdoEntryConfig {
std::uint16_t index{0};
std::uint8_t subindex{0};
std::uint8_t bit_len{0};
std::string name;
bool padding{false};
};
struct EthercatPdoConfig {
std::uint16_t index{0};
std::uint8_t sync_manager{0};
bool rx{false};
std::vector<EthercatPdoEntryConfig> entries;
};
struct EthercatPdoMapping {
std::uint32_t vendor_id{0};
std::uint32_t product_code{0};
std::string name;
std::vector<EthercatPdoConfig> rx_pdos;
std::vector<EthercatPdoConfig> tx_pdos;
};
} // namespace cmvr::device
#endif // CMVR_ES_ETHERCAT_PDO_MAPPING_H

View File

@ -1,7 +1,18 @@
#include "motor/bus_runtime/ethercat/include/ethercat_motor_bus_runtime.h"
#include <algorithm>
#include <array>
#include <chrono>
#include <cstddef>
#include <map>
#include <sstream>
#include <thread>
#include <utility>
#include "common/base/logging/logger.h"
#include <ecrt.h>
namespace cmvr::device {
bool EthercatMotorBusRuntime::init(const config::MotorGroupConfig& group_cfg)
@ -21,14 +32,34 @@ bool EthercatMotorBusRuntime::init(const config::MotorGroupConfig& group_cfg)
}
config_ = group_cfg.ethercat();
if (config_.master_id().empty()) {
CMVR_LOG(ERROR) << "[EthercatMotorBusRuntime] master_id is empty: " << id_;
return false;
}
if (config_.cycle_us() <= 0) {
CMVR_LOG(ERROR) << "[EthercatMotorBusRuntime] cycle_us must be positive: " << id_;
return false;
}
if (!config_.has_slave_op_timeout_ms()) {
CMVR_LOG(ERROR) << "[EthercatMotorBusRuntime] missing slave_op_timeout_ms: " << id_;
return false;
}
if (config_.slave_op_timeout_ms() == 0) {
CMVR_LOG(ERROR) << "[EthercatMotorBusRuntime] slave_op_timeout_ms must be positive: "
<< id_;
return false;
}
if (!config_.has_slave_state_poll_period_ms()) {
CMVR_LOG(ERROR) << "[EthercatMotorBusRuntime] missing slave_state_poll_period_ms: "
<< id_;
return false;
}
if (config_.slave_state_poll_period_ms() == 0) {
CMVR_LOG(ERROR) << "[EthercatMotorBusRuntime] slave_state_poll_period_ms must be positive: "
<< id_;
return false;
}
if (!hasValidPdoMapping_()) {
CMVR_LOG(ERROR) << "[EthercatMotorBusRuntime] PDO mapping is not configured: "
<< id_;
return false;
}
slaves_by_motor_id_.clear();
for (const auto& slave : config_.slaves()) {
@ -36,34 +67,106 @@ bool EthercatMotorBusRuntime::init(const config::MotorGroupConfig& group_cfg)
CMVR_LOG(ERROR) << "[EthercatMotorBusRuntime] invalid motor_id in slave config: " << id_;
return false;
}
if (slave.slave_index() < 0) {
CMVR_LOG(ERROR) << "[EthercatMotorBusRuntime] invalid slave_index for motor "
<< slave.motor_id() << " in group: " << id_;
return false;
}
if (slaves_by_motor_id_.count(slave.motor_id()) > 0) {
CMVR_LOG(ERROR) << "[EthercatMotorBusRuntime] duplicate slave motor_id: "
<< slave.motor_id() << " in group: " << id_;
return false;
}
slaves_by_motor_id_[slave.motor_id()] = &slave;
SlaveRuntime runtime;
runtime.cfg = slave;
slaves_by_motor_id_.emplace(slave.motor_id(), std::move(runtime));
}
if (slaves_by_motor_id_.empty()) {
CMVR_LOG(ERROR) << "[EthercatMotorBusRuntime] no EtherCAT slaves configured: " << id_;
return false;
}
master_ = ecrt_request_master(config_.master_index());
if (!master_) {
CMVR_LOG(ERROR) << "[EthercatMotorBusRuntime] failed to request EtherCAT master "
<< config_.master_index() << ": " << id_;
return false;
}
domain_ = ecrt_master_create_domain(master_);
if (!domain_) {
CMVR_LOG(ERROR) << "[EthercatMotorBusRuntime] failed to create EtherCAT domain: " << id_;
releaseMaster_();
return false;
}
for (auto& [motor_id, slave] : slaves_by_motor_id_) {
(void)motor_id;
if (!configureSlave_(slave)) {
releaseMaster_();
return false;
}
}
if (!configureDc_()) {
releaseMaster_();
return false;
}
initialized_ = true;
return true;
}
void EthercatMotorBusRuntime::setPdoMapping(EthercatPdoMapping mapping)
{
pdo_mapping_ = std::move(mapping);
}
bool EthercatMotorBusRuntime::start()
{
if (started_) {
return true;
}
CMVR_LOG(ERROR) << "[EthercatMotorBusRuntime] EtherCAT master is not implemented yet: " << id_;
return false;
if (!initialized_ || !master_ || !domain_) {
CMVR_LOG(ERROR) << "[EthercatMotorBusRuntime] runtime is not initialized: " << id_;
return false;
}
if (ecrt_master_activate(master_) != 0) {
CMVR_LOG(ERROR) << "[EthercatMotorBusRuntime] failed to activate EtherCAT master: " << id_;
return false;
}
domain_data_ = ecrt_domain_data(domain_);
if (!domain_data_) {
CMVR_LOG(ERROR) << "[EthercatMotorBusRuntime] failed to get EtherCAT domain data: " << id_;
return false;
}
{
std::lock_guard<std::mutex> lock(data_mutex_);
writeCommandsLocked_();
}
running_.store(true);
cyclic_thread_ = std::thread(&EthercatMotorBusRuntime::cyclicLoop_, this);
started_ = true;
if (!waitSlavesOperational_()) {
stop();
return false;
}
CMVR_LOG(INFO) << "[EthercatMotorBusRuntime] started EtherCAT runtime: " << id_;
return true;
}
void EthercatMotorBusRuntime::stop()
{
running_.store(false);
if (cyclic_thread_.joinable()) {
cyclic_thread_.join();
}
started_ = false;
initialized_ = false;
domain_data_ = nullptr;
releaseMaster_();
}
const config::EthercatSlaveConfig* EthercatMotorBusRuntime::slaveForMotor(const int motor_id) const
@ -72,7 +175,780 @@ const config::EthercatSlaveConfig* EthercatMotorBusRuntime::slaveForMotor(const
if (it == slaves_by_motor_id_.end()) {
return nullptr;
}
return it->second;
return &it->second.cfg;
}
bool EthercatMotorBusRuntime::hasMotor(const int motor_id) const
{
return slaves_by_motor_id_.count(motor_id) > 0;
}
bool EthercatMotorBusRuntime::hasPdoEntry(const int motor_id,
const std::uint16_t index,
const std::uint8_t subindex) const
{
std::lock_guard<std::mutex> lock(data_mutex_);
const auto slave_it = slaves_by_motor_id_.find(motor_id);
if (slave_it == slaves_by_motor_id_.end()) {
return false;
}
return slave_it->second.pdo_entries.count(pdoEntryKey_(index, subindex)) > 0;
}
bool EthercatMotorBusRuntime::configureSlave_(SlaveRuntime& slave)
{
slave.slave_config = ecrt_master_slave_config(master_,
static_cast<std::uint16_t>(slave.cfg.alias()),
static_cast<std::uint16_t>(slave.cfg.position()),
pdo_mapping_.vendor_id,
pdo_mapping_.product_code);
if (!slave.slave_config) {
CMVR_LOG(ERROR) << "[EthercatMotorBusRuntime] failed to get slave config: group=" << id_
<< ", motor_id=" << slave.cfg.motor_id()
<< ", alias=" << slave.cfg.alias()
<< ", position=" << slave.cfg.position()
<< ", vendor=" << hexIndex_(pdo_mapping_.vendor_id)
<< ", product=" << hexIndex_(pdo_mapping_.product_code);
return false;
}
struct SyncBuild {
bool rx{false};
std::vector<std::uint16_t> pdo_indices;
std::vector<std::vector<ec_pdo_entry_info_t>> entry_storage;
std::vector<ec_pdo_info_t> pdo_infos;
};
auto add_pdo_to_sync_build = [](std::map<std::uint8_t, SyncBuild>& builds,
const EthercatPdoConfig& pdo) {
auto& build = builds[pdo.sync_manager];
build.rx = pdo.rx;
build.pdo_indices.push_back(pdo.index);
auto& entries = build.entry_storage.emplace_back();
entries.reserve(pdo.entries.size());
for (const auto& entry : pdo.entries) {
entries.push_back({entry.index, entry.subindex, entry.bit_len});
}
};
std::map<std::uint8_t, SyncBuild> sync_builds;
for (const auto& pdo : pdo_mapping_.rx_pdos) {
add_pdo_to_sync_build(sync_builds, pdo);
}
for (const auto& pdo : pdo_mapping_.tx_pdos) {
add_pdo_to_sync_build(sync_builds, pdo);
}
for (auto& [sync_manager, build] : sync_builds) {
(void)sync_manager;
build.pdo_infos.reserve(build.entry_storage.size());
for (std::size_t i = 0; i < build.entry_storage.size(); ++i) {
auto& entries = build.entry_storage[i];
build.pdo_infos.push_back({
build.pdo_indices[i],
static_cast<unsigned int>(entries.size()),
entries.data(),
});
}
}
std::vector<ec_sync_info_t> sync_infos;
sync_infos.push_back({0, EC_DIR_OUTPUT, 0, nullptr, EC_WD_DISABLE});
sync_infos.push_back({1, EC_DIR_INPUT, 0, nullptr, EC_WD_DISABLE});
for (auto& [sync_manager, build] : sync_builds) {
const auto direction = build.rx ? EC_DIR_OUTPUT : EC_DIR_INPUT;
const auto watchdog = build.rx ? EC_WD_ENABLE : EC_WD_DISABLE;
sync_infos.push_back({
sync_manager,
direction,
static_cast<unsigned int>(build.pdo_infos.size()),
build.pdo_infos.data(),
watchdog,
});
}
sync_infos.push_back({0xff});
if (ecrt_slave_config_pdos(slave.slave_config, EC_END, sync_infos.data()) != 0) {
CMVR_LOG(ERROR) << "[EthercatMotorBusRuntime] failed to configure PDOs: group=" << id_
<< ", motor_id=" << slave.cfg.motor_id()
<< ", position=" << slave.cfg.position();
return false;
}
auto register_entry = [&](const EthercatPdoEntryConfig& entry, const bool rx) -> bool {
if (entry.padding) {
return true;
}
if (!isSupportedBitLength_(entry.bit_len)) {
CMVR_LOG(ERROR) << "[EthercatMotorBusRuntime] unsupported PDO entry bit length: "
<< static_cast<int>(entry.bit_len)
<< ", entry=" << hexIndex_(entry.index)
<< ":" << static_cast<int>(entry.subindex)
<< ", motor_id=" << slave.cfg.motor_id()
<< ", group=" << id_;
return false;
}
const auto key = pdoEntryKey_(entry.index, entry.subindex);
if (slave.pdo_entries.count(key) > 0) {
CMVR_LOG(ERROR) << "[EthercatMotorBusRuntime] duplicate PDO entry: "
<< hexIndex_(entry.index)
<< ":" << static_cast<int>(entry.subindex)
<< ", motor_id=" << slave.cfg.motor_id()
<< ", group=" << id_;
return false;
}
const int result = ecrt_slave_config_reg_pdo_entry(
slave.slave_config, entry.index, entry.subindex, domain_, nullptr);
if (result < 0) {
CMVR_LOG(ERROR) << "[EthercatMotorBusRuntime] failed to register PDO entry "
<< hexIndex_(entry.index) << ":" << static_cast<int>(entry.subindex)
<< ", motor_id=" << slave.cfg.motor_id()
<< ", group=" << id_;
return false;
}
PdoEntryRuntime runtime;
runtime.cfg = entry;
runtime.offset = static_cast<unsigned int>(result);
runtime.rx = rx;
slave.pdo_entries.emplace(key, std::move(runtime));
return true;
};
for (const auto& pdo : pdo_mapping_.rx_pdos) {
for (const auto& entry : pdo.entries) {
if (!register_entry(entry, true)) {
return false;
}
}
}
for (const auto& pdo : pdo_mapping_.tx_pdos) {
for (const auto& entry : pdo.entries) {
if (!register_entry(entry, false)) {
return false;
}
}
}
return true;
}
bool EthercatMotorBusRuntime::configureDc_()
{
if (!config_.has_dc()) {
CMVR_LOG(ERROR) << "[EthercatMotorBusRuntime] missing explicit DC config: "
<< id_ << ". Add dc { enable: false } or a complete enabled DC config.";
return false;
}
const auto& dc = config_.dc();
if (!dc.has_enable()) {
CMVR_LOG(ERROR) << "[EthercatMotorBusRuntime] missing DC enable: " << id_;
return false;
}
if (!dc.enable()) {
return true;
}
if (!dc.has_reference_motor_id()) {
CMVR_LOG(ERROR) << "[EthercatMotorBusRuntime] missing DC reference_motor_id: "
<< id_;
return false;
}
if (!dc.has_sync0_cycle_us()) {
CMVR_LOG(ERROR) << "[EthercatMotorBusRuntime] missing DC sync0_cycle_us: "
<< id_;
return false;
}
if (!dc.has_sync0_shift_us()) {
CMVR_LOG(ERROR) << "[EthercatMotorBusRuntime] missing DC sync0_shift_us: "
<< id_;
return false;
}
if (!dc.has_sync_reference_clock_period()) {
CMVR_LOG(ERROR) << "[EthercatMotorBusRuntime] missing DC sync_reference_clock_period: "
<< id_;
return false;
}
if (!dc.has_assign_activate()) {
CMVR_LOG(ERROR) << "[EthercatMotorBusRuntime] missing DC assign_activate: "
<< id_;
return false;
}
if (!dc.has_sync_monitor_period_ms()) {
CMVR_LOG(ERROR) << "[EthercatMotorBusRuntime] missing DC sync_monitor_period_ms: "
<< id_;
return false;
}
if (dc.reference_motor_id() <= 0) {
CMVR_LOG(ERROR) << "[EthercatMotorBusRuntime] DC reference_motor_id must be positive: "
<< id_;
return false;
}
if (dc.sync0_cycle_us() == 0) {
CMVR_LOG(ERROR) << "[EthercatMotorBusRuntime] DC sync0_cycle_us must be positive: "
<< id_;
return false;
}
if (dc.sync_reference_clock_period() == 0) {
CMVR_LOG(ERROR) << "[EthercatMotorBusRuntime] DC sync_reference_clock_period must be positive: "
<< id_;
return false;
}
if (dc.assign_activate() == 0 || dc.assign_activate() > 0xFFFFU) {
CMVR_LOG(ERROR) << "[EthercatMotorBusRuntime] DC assign_activate must be in [1, 0xFFFF]: "
<< id_ << ", value=" << hexIndex_(dc.assign_activate());
return false;
}
if (dc.sync_monitor_period_ms() == 0) {
CMVR_LOG(ERROR) << "[EthercatMotorBusRuntime] DC sync_monitor_period_ms must be positive: "
<< id_;
return false;
}
const auto reference_it = slaves_by_motor_id_.find(dc.reference_motor_id());
if (reference_it == slaves_by_motor_id_.end() || !reference_it->second.slave_config) {
CMVR_LOG(ERROR) << "[EthercatMotorBusRuntime] DC reference motor is not configured: "
<< id_ << ", reference_motor_id=" << dc.reference_motor_id();
return false;
}
const int select_result = ecrt_master_select_reference_clock(
master_, reference_it->second.slave_config);
if (select_result != 0) {
CMVR_LOG(ERROR) << "[EthercatMotorBusRuntime] failed to select DC reference clock: "
<< id_ << ", reference_motor_id=" << dc.reference_motor_id()
<< ", result=" << select_result;
return false;
}
const auto assign_activate = static_cast<std::uint16_t>(dc.assign_activate());
const auto sync0_cycle_ns = usToNs_(dc.sync0_cycle_us());
const auto sync0_shift_ns = usToNs_(dc.sync0_shift_us());
for (auto& [motor_id, slave] : slaves_by_motor_id_) {
const int result = ecrt_slave_config_dc(slave.slave_config,
assign_activate,
sync0_cycle_ns,
sync0_shift_ns,
0,
0);
if (result != 0) {
CMVR_LOG(ERROR) << "[EthercatMotorBusRuntime] failed to configure DC: "
<< id_ << ", motor_id=" << motor_id
<< ", position=" << slave.cfg.position()
<< ", result=" << result;
return false;
}
}
CMVR_LOG(INFO) << "[EthercatMotorBusRuntime] configured DC: "
<< id_
<< ", reference_motor_id=" << dc.reference_motor_id()
<< ", sync0_cycle_ns=" << sync0_cycle_ns
<< ", sync0_shift_ns=" << sync0_shift_ns
<< ", sync_reference_clock_period=" << dc.sync_reference_clock_period()
<< ", assign_activate=" << hexIndex_(assign_activate)
<< ", sync_monitor_period_ms=" << dc.sync_monitor_period_ms();
return true;
}
bool EthercatMotorBusRuntime::waitSlavesOperational_()
{
const auto deadline = std::chrono::steady_clock::now() +
std::chrono::milliseconds(config_.slave_op_timeout_ms());
const auto poll_period =
std::chrono::milliseconds(config_.slave_state_poll_period_ms());
ec_domain_state_t last_domain_state{};
int last_domain_result = 0;
do {
bool all_slaves_operational = true;
for (const auto& [motor_id, slave] : slaves_by_motor_id_) {
(void)motor_id;
ec_slave_config_state_t state{};
const int result = ecrt_slave_config_state(slave.slave_config, &state);
if (result != 0 ||
!state.online ||
!state.operational ||
state.al_state != EC_AL_STATE_OP) {
all_slaves_operational = false;
break;
}
}
last_domain_result = ecrt_domain_state(domain_, &last_domain_state);
const bool domain_complete =
last_domain_result == 0 &&
last_domain_state.wc_state == EC_WC_COMPLETE;
if (all_slaves_operational && domain_complete) {
CMVR_LOG(INFO) << "[EthercatMotorBusRuntime] all EtherCAT slaves operational: "
<< id_
<< ", working_counter=" << last_domain_state.working_counter;
return true;
}
std::this_thread::sleep_for(poll_period);
} while (std::chrono::steady_clock::now() < deadline);
CMVR_LOG(ERROR) << "[EthercatMotorBusRuntime] timeout waiting for EtherCAT slaves OP: "
<< id_
<< ", timeout_ms=" << config_.slave_op_timeout_ms()
<< ", domain_result=" << last_domain_result
<< ", domain_wc_state=" << static_cast<int>(last_domain_state.wc_state)
<< ", working_counter=" << last_domain_state.working_counter;
for (const auto& [motor_id, slave] : slaves_by_motor_id_) {
ec_slave_config_state_t state{};
const int result = ecrt_slave_config_state(slave.slave_config, &state);
CMVR_LOG(ERROR) << "[EthercatMotorBusRuntime] slave state: "
<< id_
<< ", motor_id=" << motor_id
<< ", position=" << slave.cfg.position()
<< ", result=" << result
<< ", online=" << state.online
<< ", operational=" << state.operational
<< ", al_state=" << static_cast<int>(state.al_state);
}
return false;
}
void EthercatMotorBusRuntime::cyclicLoop_()
{
const auto period = std::chrono::microseconds(config_.cycle_us());
const bool dc_enabled = config_.dc().enable();
const auto dc_sync_period = dc_enabled ? config_.dc().sync_reference_clock_period() : 0U;
const auto dc_monitor_period =
dc_enabled ? std::chrono::milliseconds(config_.dc().sync_monitor_period_ms())
: std::chrono::milliseconds(0);
std::uint32_t dc_sync_counter = 0;
bool dc_monitor_queued = false;
auto cycle_time = std::chrono::steady_clock::now();
auto next_dc_monitor_time = cycle_time + dc_monitor_period;
while (running_.load()) {
if (dc_enabled) {
ecrt_master_application_time(master_, timePointNs_(cycle_time));
}
ecrt_master_receive(master_);
ecrt_domain_process(domain_);
if (dc_enabled && dc_monitor_queued) {
const std::uint32_t dc_sync_diff_ns =
ecrt_master_sync_monitor_process(master_);
if (dc_sync_diff_ns == static_cast<std::uint32_t>(-1)) {
CMVR_LOG(WARNING) << "[EthercatMotorBusRuntime] DC sync monitor failed: "
<< id_;
} else {
CMVR_LOG(INFO) << "[EthercatMotorBusRuntime] dc_sync_diff_ns="
<< dc_sync_diff_ns
<< ", group=" << id_;
}
dc_monitor_queued = false;
}
{
std::lock_guard<std::mutex> lock(data_mutex_);
readFeedbackLocked_();
writeCommandsLocked_();
}
ecrt_domain_queue(domain_);
if (dc_enabled) {
++dc_sync_counter;
if (dc_sync_counter >= dc_sync_period) {
dc_sync_counter = 0;
ecrt_master_sync_reference_clock(master_);
}
ecrt_master_sync_slave_clocks(master_);
if (cycle_time >= next_dc_monitor_time) {
const int monitor_result = ecrt_master_sync_monitor_queue(master_);
if (monitor_result == 0) {
dc_monitor_queued = true;
} else {
CMVR_LOG(WARNING) << "[EthercatMotorBusRuntime] failed to queue DC sync monitor: "
<< id_ << ", result=" << monitor_result;
}
do {
next_dc_monitor_time += dc_monitor_period;
} while (cycle_time >= next_dc_monitor_time);
}
}
ecrt_master_send(master_);
cycle_time += period;
std::this_thread::sleep_until(cycle_time);
}
}
void EthercatMotorBusRuntime::readFeedbackLocked_()
{
if (!domain_data_) {
return;
}
for (auto& [motor_id, slave] : slaves_by_motor_id_) {
(void)motor_id;
for (auto& [key, entry] : slave.pdo_entries) {
(void)key;
if (!entry.rx) {
entry.value = readEntryValue_(domain_data_, entry);
}
}
}
}
void EthercatMotorBusRuntime::writeCommandsLocked_()
{
if (!domain_data_) {
return;
}
for (const auto& [motor_id, slave] : slaves_by_motor_id_) {
(void)motor_id;
for (const auto& [key, entry] : slave.pdo_entries) {
(void)key;
if (entry.rx) {
writeEntryValue_(domain_data_, entry);
}
}
}
}
void EthercatMotorBusRuntime::releaseMaster_()
{
if (master_) {
ecrt_release_master(master_);
}
master_ = nullptr;
domain_ = nullptr;
domain_data_ = nullptr;
for (auto& [motor_id, slave] : slaves_by_motor_id_) {
(void)motor_id;
slave.slave_config = nullptr;
slave.pdo_entries.clear();
}
}
bool EthercatMotorBusRuntime::hasValidPdoMapping_() const
{
return pdo_mapping_.vendor_id != 0 &&
pdo_mapping_.product_code != 0 &&
!pdo_mapping_.rx_pdos.empty() &&
!pdo_mapping_.tx_pdos.empty();
}
bool EthercatMotorBusRuntime::writePdoRaw_(const int motor_id,
const std::uint16_t index,
const std::uint8_t subindex,
const std::uint8_t bit_len,
const std::uint64_t value)
{
std::lock_guard<std::mutex> lock(data_mutex_);
auto slave_it = slaves_by_motor_id_.find(motor_id);
if (slave_it == slaves_by_motor_id_.end()) {
return false;
}
auto entry_it = slave_it->second.pdo_entries.find(pdoEntryKey_(index, subindex));
if (entry_it == slave_it->second.pdo_entries.end()) {
return false;
}
auto& entry = entry_it->second;
if (!entry.rx || entry.cfg.bit_len != bit_len) {
return false;
}
entry.value = maskValue_(value, entry.cfg.bit_len);
if (domain_data_) {
writeEntryValue_(domain_data_, entry);
}
return true;
}
bool EthercatMotorBusRuntime::readPdoRaw_(const int motor_id,
const std::uint16_t index,
const std::uint8_t subindex,
const std::uint8_t bit_len,
std::uint64_t& value) const
{
std::lock_guard<std::mutex> lock(data_mutex_);
const auto slave_it = slaves_by_motor_id_.find(motor_id);
if (slave_it == slaves_by_motor_id_.end()) {
return false;
}
const auto entry_it = slave_it->second.pdo_entries.find(pdoEntryKey_(index, subindex));
if (entry_it == slave_it->second.pdo_entries.end()) {
return false;
}
const auto& entry = entry_it->second;
if (entry.rx || entry.cfg.bit_len != bit_len) {
return false;
}
value = maskValue_(entry.value, entry.cfg.bit_len);
return true;
}
bool EthercatMotorBusRuntime::writeSdoRaw_(const int motor_id,
const std::uint16_t index,
const std::uint8_t subindex,
const std::uint8_t bit_len,
const std::uint64_t value)
{
if (!isSupportedBitLength_(bit_len)) {
CMVR_LOG(ERROR) << "[EthercatMotorBusRuntime] unsupported SDO write bit length: "
<< static_cast<int>(bit_len)
<< ", object=" << hexIndex_(index)
<< ":" << static_cast<int>(subindex)
<< ", motor_id=" << motor_id;
return false;
}
auto slave_it = slaves_by_motor_id_.find(motor_id);
if (slave_it == slaves_by_motor_id_.end()) {
CMVR_LOG(ERROR) << "[EthercatMotorBusRuntime] missing motor for SDO write: "
<< motor_id << ", object=" << hexIndex_(index)
<< ":" << static_cast<int>(subindex);
return false;
}
auto& slave = slave_it->second;
if (!slave.slave_config || !master_) {
CMVR_LOG(ERROR) << "[EthercatMotorBusRuntime] runtime is not initialized for SDO write: "
<< id_ << ", motor_id=" << motor_id
<< ", object=" << hexIndex_(index)
<< ":" << static_cast<int>(subindex);
return false;
}
const auto raw_value = maskValue_(value, bit_len);
if (!started_) {
int result = -1;
switch (bit_len) {
case 8:
result = ecrt_slave_config_sdo8(slave.slave_config, index, subindex,
static_cast<std::uint8_t>(raw_value));
break;
case 16:
result = ecrt_slave_config_sdo16(slave.slave_config, index, subindex,
static_cast<std::uint16_t>(raw_value));
break;
case 32:
result = ecrt_slave_config_sdo32(slave.slave_config, index, subindex,
static_cast<std::uint32_t>(raw_value));
break;
default:
break;
}
if (result != 0) {
CMVR_LOG(ERROR) << "[EthercatMotorBusRuntime] failed to configure startup SDO: "
<< "group=" << id_ << ", motor_id=" << motor_id
<< ", object=" << hexIndex_(index)
<< ":" << static_cast<int>(subindex)
<< ", bit_len=" << static_cast<int>(bit_len)
<< ", value=" << raw_value
<< ", result=" << result;
return false;
}
return true;
}
std::array<std::uint8_t, 4> data{};
switch (bit_len) {
case 8:
EC_WRITE_U8(data.data(), static_cast<std::uint8_t>(raw_value));
break;
case 16:
EC_WRITE_U16(data.data(), static_cast<std::uint16_t>(raw_value));
break;
case 32:
EC_WRITE_U32(data.data(), static_cast<std::uint32_t>(raw_value));
break;
default:
break;
}
const auto data_size = static_cast<std::size_t>(bit_len / 8);
std::uint32_t abort_code = 0;
const int result = ecrt_master_sdo_download(
master_,
static_cast<std::uint16_t>(slave.cfg.position()),
index,
subindex,
data.data(),
data_size,
&abort_code);
if (result != 0) {
CMVR_LOG(ERROR) << "[EthercatMotorBusRuntime] failed to write SDO: "
<< "group=" << id_ << ", motor_id=" << motor_id
<< ", slave_position=" << slave.cfg.position()
<< ", object=" << hexIndex_(index)
<< ":" << static_cast<int>(subindex)
<< ", bit_len=" << static_cast<int>(bit_len)
<< ", value=" << raw_value
<< ", result=" << result
<< ", abort_code=" << hexIndex_(abort_code);
return false;
}
return true;
}
bool EthercatMotorBusRuntime::readSdoRaw_(const int motor_id,
const std::uint16_t index,
const std::uint8_t subindex,
const std::uint8_t bit_len,
std::uint64_t& value)
{
if (!isSupportedBitLength_(bit_len)) {
CMVR_LOG(ERROR) << "[EthercatMotorBusRuntime] unsupported SDO read bit length: "
<< static_cast<int>(bit_len)
<< ", object=" << hexIndex_(index)
<< ":" << static_cast<int>(subindex)
<< ", motor_id=" << motor_id;
return false;
}
auto slave_it = slaves_by_motor_id_.find(motor_id);
if (slave_it == slaves_by_motor_id_.end()) {
CMVR_LOG(ERROR) << "[EthercatMotorBusRuntime] missing motor for SDO read: "
<< motor_id << ", object=" << hexIndex_(index)
<< ":" << static_cast<int>(subindex);
return false;
}
const auto& slave = slave_it->second;
if (!master_) {
CMVR_LOG(ERROR) << "[EthercatMotorBusRuntime] runtime is not initialized for SDO read: "
<< id_ << ", motor_id=" << motor_id
<< ", object=" << hexIndex_(index)
<< ":" << static_cast<int>(subindex);
return false;
}
std::array<std::uint8_t, 4> data{};
const auto data_size = static_cast<std::size_t>(bit_len / 8);
std::size_t result_size = 0;
std::uint32_t abort_code = 0;
const int result = ecrt_master_sdo_upload(
master_,
static_cast<std::uint16_t>(slave.cfg.position()),
index,
subindex,
data.data(),
data_size,
&result_size,
&abort_code);
if (result != 0 || result_size != data_size) {
CMVR_LOG(ERROR) << "[EthercatMotorBusRuntime] failed to read SDO: "
<< "group=" << id_ << ", motor_id=" << motor_id
<< ", slave_position=" << slave.cfg.position()
<< ", object=" << hexIndex_(index)
<< ":" << static_cast<int>(subindex)
<< ", bit_len=" << static_cast<int>(bit_len)
<< ", result=" << result
<< ", result_size=" << result_size
<< ", abort_code=" << hexIndex_(abort_code);
return false;
}
switch (bit_len) {
case 8:
value = EC_READ_U8(data.data());
break;
case 16:
value = EC_READ_U16(data.data());
break;
case 32:
value = EC_READ_U32(data.data());
break;
default:
return false;
}
value = maskValue_(value, bit_len);
return true;
}
std::uint32_t EthercatMotorBusRuntime::pdoEntryKey_(const std::uint16_t index,
const std::uint8_t subindex)
{
return (static_cast<std::uint32_t>(index) << 8U) | subindex;
}
std::string EthercatMotorBusRuntime::hexIndex_(const std::uint32_t index)
{
std::ostringstream oss;
oss << "0x" << std::hex << std::uppercase << index;
return oss.str();
}
std::uint64_t EthercatMotorBusRuntime::maskValue_(const std::uint64_t value,
const std::uint8_t bit_len)
{
switch (bit_len) {
case 8:
return value & 0xFFU;
case 16:
return value & 0xFFFFU;
case 32:
return value & 0xFFFFFFFFULL;
default:
return value;
}
}
bool EthercatMotorBusRuntime::isSupportedBitLength_(const std::uint8_t bit_len)
{
return bit_len == 8 || bit_len == 16 || bit_len == 32;
}
std::uint64_t EthercatMotorBusRuntime::readEntryValue_(const std::uint8_t* domain_data,
const PdoEntryRuntime& entry)
{
switch (entry.cfg.bit_len) {
case 8:
return EC_READ_U8(domain_data + entry.offset);
case 16:
return EC_READ_U16(domain_data + entry.offset);
case 32:
return EC_READ_U32(domain_data + entry.offset);
default:
return 0;
}
}
void EthercatMotorBusRuntime::writeEntryValue_(std::uint8_t* domain_data,
const PdoEntryRuntime& entry)
{
switch (entry.cfg.bit_len) {
case 8:
EC_WRITE_U8(domain_data + entry.offset, static_cast<std::uint8_t>(entry.value));
break;
case 16:
EC_WRITE_U16(domain_data + entry.offset, static_cast<std::uint16_t>(entry.value));
break;
case 32:
EC_WRITE_U32(domain_data + entry.offset, static_cast<std::uint32_t>(entry.value));
break;
default:
break;
}
}
std::uint64_t EthercatMotorBusRuntime::steadyTimeNs_()
{
return timePointNs_(std::chrono::steady_clock::now());
}
std::uint64_t EthercatMotorBusRuntime::timePointNs_(
const std::chrono::steady_clock::time_point time_point)
{
const auto time_since_epoch = time_point.time_since_epoch();
return static_cast<std::uint64_t>(
std::chrono::duration_cast<std::chrono::nanoseconds>(time_since_epoch).count());
}
std::uint32_t EthercatMotorBusRuntime::usToNs_(const std::uint32_t value_us)
{
return value_us * 1000U;
}
std::int32_t EthercatMotorBusRuntime::usToNs_(const std::int32_t value_us)
{
return value_us * 1000;
}
} // namespace cmvr::device

View File

@ -0,0 +1,102 @@
#include "devices/motor/bus_runtime/ethercat/include/ethercat_motor_bus_runtime.h"
#include <algorithm>
#include <chrono>
#include <cstdint>
#include <iostream>
#include <string>
#include <thread>
#include <gtest/gtest.h>
#include "cmvr/msgs/cia402.pb.h"
#include "devices/motor/drivers/ethercat_motor/include/vendor/eyou/eyou_cia402_pdo_mapping.h"
namespace cmvr::device {
namespace {
config::MotorGroupConfig createSingleSlaveGroup()
{
config::MotorGroupConfig group;
group.set_id("ethercat_real_test");
group.set_bus_type(config::MOTOR_BUS_ETHERCAT);
group.set_vendor(config::MOTOR_VENDOR_EYOU);
group.set_protocol(config::MOTOR_PROTOCOL_ETHERCAT_CIA402);
auto* ethercat = group.mutable_ethercat();
ethercat->set_master_index(0);
ethercat->set_cycle_us(1000);
ethercat->set_slave_op_timeout_ms(12000);
ethercat->set_slave_state_poll_period_ms(10);
auto* dc = ethercat->mutable_dc();
dc->set_enable(true);
dc->set_reference_motor_id(1);
dc->set_sync0_cycle_us(1000);
dc->set_sync0_shift_us(0);
dc->set_sync_reference_clock_period(1);
dc->set_assign_activate(768);
dc->set_sync_monitor_period_ms(1000);
auto* slave = ethercat->add_slaves();
slave->set_motor_id(1);
slave->set_alias(0);
slave->set_position(0);
return group;
}
} // namespace
TEST(EthercatMotorBusRuntimeRealTest, InitStartAndReadStatusword)
{
EthercatMotorBusRuntime runtime;
runtime.setPdoMapping(createEyouCia402PdoMapping());
ASSERT_TRUE(runtime.init(createSingleSlaveGroup()));
EXPECT_EQ(runtime.busType(), config::MOTOR_BUS_ETHERCAT);
EXPECT_TRUE(runtime.hasMotor(1));
EXPECT_NE(runtime.slaveForMotor(1), nullptr);
EXPECT_TRUE(runtime.hasPdoEntry(1, msgs::CIA402_CONTROL_WORD_6040, 0x00));
EXPECT_TRUE(runtime.hasPdoEntry(1, msgs::CIA402_STATUS_WORD_6041, 0x00));
EXPECT_TRUE(runtime.hasPdoEntry(1, msgs::CIA402_OPERATION_MODE_6060, 0x00));
EXPECT_TRUE(runtime.hasPdoEntry(1, msgs::CIA402_MODE_DISPLAY_6061, 0x00));
const bool started = runtime.start();
EXPECT_TRUE(started);
if (!started) {
runtime.stop();
return;
}
EXPECT_TRUE(runtime.writePdo<std::uint16_t>(1, msgs::CIA402_CONTROL_WORD_6040, 0x00, 0x0000));
EXPECT_TRUE(runtime.writePdo<std::int8_t>(1, msgs::CIA402_OPERATION_MODE_6060, 0x00, 0));
const int settle_ms = 1000;
std::this_thread::sleep_for(std::chrono::milliseconds(settle_ms));
std::uint16_t statusword = 0;
EXPECT_TRUE(runtime.readPdo<std::uint16_t>(1, msgs::CIA402_STATUS_WORD_6041, 0x00, statusword));
std::int8_t mode_display = 0;
EXPECT_TRUE(runtime.readPdo<std::int8_t>(1, msgs::CIA402_MODE_DISPLAY_6061, 0x00, mode_display));
std::cout << "CIA402 statusword: 0x" << std::hex << statusword
<< ", mode display: " << std::dec << static_cast<int>(mode_display)
<< std::endl;
const int hold_ms = 10000;
if (hold_ms > 0) {
std::cout << "Holding EtherCAT runtime for " << hold_ms
<< " ms. Check slave state in another terminal." << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(hold_ms));
}
runtime.stop();
}
} // namespace cmvr::device

View File

@ -0,0 +1,49 @@
add_library(ethercat_motor_driver SHARED
src/cia402/cia402_protocol.cpp
src/vendor/eyou/eyou_motor.cpp
src/vendor/eyou/eyou_motor_adapter.cpp
)
target_include_directories(ethercat_motor_driver
PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/include
)
target_link_libraries(ethercat_motor_driver
PUBLIC
cmvr_es::device::motor_core
cmvr_es::device::motor_bus_runtime
PRIVATE
cmvr_es::proto
glog
)
add_library(cmvr_es::device::ethercat_motor_driver ALIAS ethercat_motor_driver)
install(TARGETS ethercat_motor_driver LIBRARY DESTINATION lib)
add_executable(eyou_motor_real_test
src/vendor/eyou/eyou_motor_real_test.cpp
)
target_link_libraries(eyou_motor_real_test
PRIVATE
cmvr_es::device::ethercat_motor_driver
gtest
gtest_main
pthread
glog
)
add_executable(eyou_motor_device_manager_real_test
src/vendor/eyou/eyou_motor_device_manager_real_test.cpp
)
target_link_libraries(eyou_motor_device_manager_real_test
PRIVATE
cmvr_es::device_manager
cmvr_es::device::motor_manager
gtest
gtest_main
pthread
glog
)

View File

@ -0,0 +1,161 @@
#ifndef CMVR_ES_CIA402_OBJECTS_H
#define CMVR_ES_CIA402_OBJECTS_H
#include <cstdint>
namespace cmvr::device::cia402 {
union Controlword {
std::uint16_t value;
struct {
std::uint16_t switch_on : 1;
std::uint16_t enable_voltage : 1;
std::uint16_t quick_stop : 1;
std::uint16_t enable_operation : 1;
std::uint16_t new_set_point : 1;
std::uint16_t change_set_immediately : 1;
std::uint16_t relative : 1;
std::uint16_t fault_reset : 1;
std::uint16_t halt : 1;
std::uint16_t reserved : 2;
std::uint16_t manufacturer_specific : 5;
};
};
union Statusword {
std::uint16_t value;
struct {
std::uint16_t ready_to_switch_on : 1;
std::uint16_t switched_on : 1;
std::uint16_t operation_enabled : 1;
std::uint16_t fault : 1;
std::uint16_t voltage_enabled : 1;
std::uint16_t quick_stop : 1;
std::uint16_t switch_on_disabled : 1;
std::uint16_t warning : 1;
std::uint16_t manufacturer_specific_8 : 1;
std::uint16_t remote : 1;
std::uint16_t target_reached : 1;
std::uint16_t internal_limit_active : 1;
std::uint16_t operation_mode_specific : 2;
std::uint16_t manufacturer_specific : 2;
};
};
static_assert(sizeof(Controlword) == sizeof(std::uint16_t));
static_assert(sizeof(Statusword) == sizeof(std::uint16_t));
enum class DeviceState {
SwitchOnDisabled,
ReadyToSwitchOn,
SwitchedOn,
OperationEnabled,
};
namespace detail {
struct StateRule {
std::uint16_t relevant_bits;
std::uint16_t expected_bits;
};
inline StateRule stateRule(const DeviceState state)
{
// CiA402 device states are matched by selected 0x6041 statusword bits.
switch (state) {
case DeviceState::SwitchOnDisabled:
return {0x004F, 0x0040};
case DeviceState::ReadyToSwitchOn:
return {0x006F, 0x0021};
case DeviceState::SwitchedOn:
return {0x006F, 0x0023};
case DeviceState::OperationEnabled:
return {0x006F, 0x0027};
}
return {0x006F, 0x0000};
}
} // namespace detail
inline Controlword controlword(const std::uint16_t value)
{
Controlword cw{};
cw.value = value;
return cw;
}
inline Statusword statusword(const std::uint16_t value)
{
Statusword sw{};
sw.value = value;
return sw;
}
inline Controlword shutdownControlword()
{
Controlword cw{};
cw.quick_stop = 1;
cw.enable_voltage = 1;
return cw;
}
inline Controlword switchOnControlword()
{
auto cw = shutdownControlword();
cw.switch_on = 1;
return cw;
}
inline Controlword enableOperationControlword()
{
auto cw = switchOnControlword();
cw.enable_operation = 1;
return cw;
}
inline Controlword quickStopControlword()
{
auto cw = enableOperationControlword();
cw.quick_stop = 0;
return cw;
}
inline Controlword faultResetControlword()
{
Controlword cw{};
cw.fault_reset = 1;
return cw;
}
inline Controlword profilePositionControlword(const bool new_set_point)
{
auto cw = enableOperationControlword();
cw.change_set_immediately = 1;
cw.new_set_point = new_set_point ? 1 : 0;
return cw;
}
inline bool hasState(const Statusword status, const DeviceState state)
{
const auto rule = detail::stateRule(state);
return (status.value & rule.relevant_bits) == rule.expected_bits;
}
inline bool isSwitchOnDisabled(const Statusword status)
{
return hasState(status, DeviceState::SwitchOnDisabled);
}
inline bool isOperationEnabled(const Statusword status)
{
return hasState(status, DeviceState::OperationEnabled);
}
inline bool targetReached(const Statusword status)
{
return status.target_reached != 0;
}
} // namespace cmvr::device::cia402
#endif // CMVR_ES_CIA402_OBJECTS_H

View File

@ -0,0 +1,116 @@
#ifndef CMVR_ES_CIA402_PROTOCOL_H
#define CMVR_ES_CIA402_PROTOCOL_H
#include <cstdint>
#include <memory>
#include <unordered_map>
#include "cmvr/config/motor_config/motor_config.pb.h"
#include "devices/motor/bus_runtime/ethercat/include/ethercat_motor_bus_runtime.h"
#include "devices/motor/drivers/ethercat_motor/include/cia402/cia402_objects.h"
#include "devices/motor/motor_protocol_interface.h"
namespace cmvr::device {
class Cia402Protocol final : public MotorProtocolInterface {
public:
explicit Cia402Protocol(std::shared_ptr<EthercatMotorBusRuntime> bus_runtime,
const config::Cia402ProtocolConfig& config);
~Cia402Protocol() 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;
bool syncTargetToActualPosition(std::uint8_t node_id);
private:
struct NodeState {
msgs::RunMode mode{msgs::RUN_MODE_CYCLIC_SYNC_POSITION};
cia402::Controlword controlword{};
std::int32_t target_position{0};
std::int32_t target_velocity{0};
std::int16_t target_torque{0};
std::int32_t profile_velocity{0};
std::int32_t profile_acceleration{0};
std::int32_t profile_deceleration{0};
double limit_q_lb{0.0};
double limit_q_ub{0.0};
double limit_qd{0.0};
double limit_qdd{0.0};
double encoder_counts_per_rev{0.0};
double gear_ratio{0.0};
};
static std::int8_t toCia402Mode_(msgs::RunMode mode);
static msgs::RunMode fromCia402Mode_(std::int8_t mode);
static cia402::Controlword nextControlword_(cia402::Statusword statusword);
static bool isOperationEnabled_(cia402::Statusword statusword);
static bool targetReached_(cia402::Statusword statusword);
std::int32_t radToCounts_(double angle_rad, const NodeState& state) const;
double countsToRad_(std::int32_t counts, const NodeState& state) const;
std::int32_t radPerSecToCounts_(double velocity_rad_s, const NodeState& state) const;
std::int32_t radPerSec2ToCounts_(double acceleration_rad_s2, const NodeState& state) const;
double countsToRadPerSec_(std::int32_t velocity_counts_s, const NodeState& state) const;
NodeState& nodeState_(std::uint8_t node_id);
const NodeState* findNodeState_(std::uint8_t node_id) const;
bool hasValidConversion_(std::uint8_t node_id, const NodeState& state) const;
bool validateNodePdos_(std::uint8_t node_id) const;
bool readStatusword_(std::uint8_t node_id, std::uint16_t& statusword) const;
bool readActualPosition_(std::uint8_t node_id, std::int32_t& actual_position) const;
bool readActualVelocity_(std::uint8_t node_id, std::int32_t& actual_velocity) const;
bool readModeDisplay_(std::uint8_t node_id, std::int8_t& mode_display) const;
bool writeControlword_(std::uint8_t node_id, cia402::Controlword controlword);
bool writeControlwordAndWait_(std::uint8_t node_id,
cia402::Controlword controlword,
cia402::DeviceState target_state,
const char* state_name);
bool waitStatus_(std::uint8_t node_id,
cia402::DeviceState target_state,
const char* state_name) const;
bool waitVelocityNearZero_(std::uint8_t node_id, const char* action_name) const;
bool writePositionLimitsToDictionary_(std::uint8_t node_id, const NodeState& state) const;
bool writeVelocityLimitToDictionary_(std::uint8_t node_id, const NodeState& state) const;
bool writeAccelerationLimitsToDictionary_(std::uint8_t node_id, const NodeState& state) const;
bool prepareSafeTargetsForMode_(std::uint8_t node_id, msgs::RunMode mode, NodeState& state);
void writeTargetsForMode_(std::uint8_t node_id, msgs::RunMode mode, const NodeState& state) const;
void writeProfilePositionTarget_(std::uint8_t node_id, NodeState& state);
void writeNode_(std::uint8_t node_id, NodeState& state);
std::shared_ptr<EthercatMotorBusRuntime> bus_runtime_;
config::Cia402ProtocolConfig config_;
std::unordered_map<std::uint8_t, NodeState> nodes_;
};
} // namespace cmvr::device
#endif // CMVR_ES_CIA402_PROTOCOL_H

View File

@ -0,0 +1,81 @@
#ifndef CMVR_ES_EYOU_CIA402_PDO_MAPPING_H
#define CMVR_ES_EYOU_CIA402_PDO_MAPPING_H
#include <cstdint>
#include <string>
#include <utility>
#include "cmvr/msgs/canopen.pb.h"
#include "cmvr/msgs/cia402.pb.h"
#include "devices/motor/bus_runtime/ethercat/include/ethercat_pdo_mapping.h"
namespace cmvr::device {
namespace eyou_cia402_pdo_mapping_detail {
inline constexpr std::uint32_t VENDOR_ID = 0x00001097;
inline constexpr std::uint32_t PRODUCT_CODE = 0x00002406;
inline EthercatPdoEntryConfig entry(const std::uint16_t index,
const std::uint8_t subindex,
const std::uint8_t bit_len,
std::string name)
{
EthercatPdoEntryConfig cfg;
cfg.index = index;
cfg.subindex = subindex;
cfg.bit_len = bit_len;
cfg.name = std::move(name);
cfg.padding = index == 0 || bit_len == 0;
return cfg;
}
} // namespace eyou_cia402_pdo_mapping_detail
inline EthercatPdoMapping createEyouCia402PdoMapping()
{
using namespace eyou_cia402_pdo_mapping_detail;
EthercatPdoConfig rx_pdo;
rx_pdo.index = msgs::CANOPEN_RPDO2_MAP_1601;
rx_pdo.sync_manager = 2;
rx_pdo.rx = true;
rx_pdo.entries = {
entry(msgs::CIA402_CONTROL_WORD_6040, 0x00, 16, "Control Word"),
entry(msgs::CIA402_TARGET_POSITION_607A, 0x00, 32, "Target Position"),
entry(msgs::CIA402_TARGET_VELOCITY_60FF, 0x00, 32, "Target Velocity"),
entry(msgs::CIA402_TARGET_TORQUE_6071, 0x00, 16, "Target Torque"),
entry(msgs::CIA402_PROFILE_ACCELERATION_6083, 0x00, 32, "Profile Acceleration"),
entry(msgs::CIA402_PROFILE_DECELERATION_6084, 0x00, 32, "Profile Deceleration"),
entry(msgs::CIA402_PROFILE_VELOCITY_6081, 0x00, 32, "Profile Velocity"),
entry(msgs::CIA402_TORQUE_SLOPE_6087, 0x00, 32, "Torque Slope"),
entry(msgs::CIA402_OPERATION_MODE_6060, 0x00, 8, "Mode Of Operation"),
entry(0x0000, 0x00, 8, "Padding"),
};
EthercatPdoConfig tx_pdo;
tx_pdo.index = msgs::CANOPEN_TPDO1_MAP_1A00;
tx_pdo.sync_manager = 3;
tx_pdo.rx = false;
tx_pdo.entries = {
entry(msgs::CIA402_STATUS_WORD_6041, 0x00, 16, "Status Word"),
entry(msgs::CIA402_ACTUAL_POSITION_6064, 0x00, 32, "Actual Position"),
entry(msgs::CIA402_ACTUAL_VELOCITY_606C, 0x00, 32, "Actual Velocity"),
entry(msgs::CIA402_ACTUAL_TORQUE_6077, 0x00, 16, "Actual Torque"),
entry(msgs::CIA402_MODE_DISPLAY_6061, 0x00, 8, "Mode Of Operation Display"),
entry(msgs::CIA402_ERROR_CODE_603F, 0x00, 16, "Error Code"),
entry(0x0000, 0x00, 8, "Padding"),
};
EthercatPdoMapping mapping;
mapping.vendor_id = VENDOR_ID;
mapping.product_code = PRODUCT_CODE;
mapping.name = "EYOU ServoModule ECAT V145 CiA402";
mapping.rx_pdos.push_back(std::move(rx_pdo));
mapping.tx_pdos.push_back(std::move(tx_pdo));
return mapping;
}
} // namespace cmvr::device
#endif // CMVR_ES_EYOU_CIA402_PDO_MAPPING_H

View File

@ -0,0 +1,43 @@
#ifndef CMVR_ES_EYOU_MOTOR_H
#define CMVR_ES_EYOU_MOTOR_H
#include <cstdint>
#include <memory>
#include "cmvr/config/motor_config/motor_config.pb.h"
#include "devices/motor/abstract_motor.h"
#include "devices/motor/drivers/ethercat_motor/include/cia402/cia402_protocol.h"
#include "devices/motor/drivers/ethercat_motor/include/vendor/eyou/eyou_motor_adapter.h"
namespace cmvr::device {
class EyouMotor final : public AbstractMotor {
public:
EyouMotor(const config::MotorConfigItem& config,
std::shared_ptr<Cia402Protocol> cia402_protocol,
std::unique_ptr<EyouMotorAdapter> vendor_adapter);
std::string typeName() const override { return "EyouMotor"; }
bool init() override;
void setLimitQ(double ub, double lb) override;
void setLimitQd(double qd) override;
bool calibrateZeroQ() override;
bool brakeRelease() override;
private:
bool hasDependencies_() const;
bool hasValidConversion_() const;
bool writeVendorPositionLimits_() const;
bool writeVendorVelocityLimit_() const;
std::int32_t radToCounts_(double angle_rad) const;
std::uint32_t radPerSecToCounts_(double velocity_rad_s) const;
std::shared_ptr<Cia402Protocol> cia402_protocol_;
std::unique_ptr<EyouMotorAdapter> vendor_adapter_;
double encoder_counts_per_rev_{0.0};
double gear_ratio_{0.0};
};
} // namespace cmvr::device
#endif // CMVR_ES_EYOU_MOTOR_H

View File

@ -0,0 +1,33 @@
#ifndef CMVR_ES_EYOU_MOTOR_ADAPTER_H
#define CMVR_ES_EYOU_MOTOR_ADAPTER_H
#include <cstdint>
#include <memory>
#include "devices/motor/bus_runtime/ethercat/include/ethercat_motor_bus_runtime.h"
#include "devices/motor/drivers/ethercat_motor/include/vendor/motor_vendor_adapter.h"
namespace cmvr::device {
class EyouMotorAdapter final : public MotorVendorAdapter {
public:
explicit EyouMotorAdapter(std::shared_ptr<EthercatMotorBusRuntime> bus_runtime);
~EyouMotorAdapter() override = default;
bool initNode(std::uint8_t node_id) override;
bool writePositionLimits(std::uint8_t node_id,
std::int32_t lower_limit,
std::int32_t upper_limit) override;
bool writeVelocityLimit(std::uint8_t node_id,
std::uint32_t velocity_limit) override;
bool calibrateZero(std::uint8_t node_id,
std::int32_t& zeroed_position) override;
bool brakeRelease(std::uint8_t node_id) override;
private:
std::shared_ptr<EthercatMotorBusRuntime> bus_runtime_;
};
} // namespace cmvr::device
#endif // CMVR_ES_EYOU_MOTOR_ADAPTER_H

View File

@ -0,0 +1,20 @@
#ifndef CMVR_ES_EYOU_OBJECTS_H
#define CMVR_ES_EYOU_OBJECTS_H
#include <cstdint>
namespace cmvr::device::eyou {
inline constexpr std::uint16_t EYOU_SOFT_LIMIT_STATE_2003 = 0x2003;
inline constexpr std::uint16_t EYOU_BRAKE_CONTROL_2014 = 0x2014;
inline constexpr std::uint16_t EYOU_OVER_SPEED_THRESHOLD_2024 = 0x2024;
inline constexpr std::uint16_t EYOU_FIRST_ENCODER_VALUE_202A = 0x202A;
inline constexpr std::uint16_t EYOU_SECOND_ENCODER_VALUE_202B = 0x202B;
inline constexpr std::uint16_t EYOU_STORE_PARAMETERS_1010 = 0x1010;
} // namespace cmvr::device::eyou
#endif // CMVR_ES_EYOU_OBJECTS_H

View File

@ -0,0 +1,25 @@
#ifndef CMVR_ES_MOTOR_VENDOR_ADAPTER_H
#define CMVR_ES_MOTOR_VENDOR_ADAPTER_H
#include <cstdint>
namespace cmvr::device {
class MotorVendorAdapter {
public:
virtual ~MotorVendorAdapter() = default;
virtual bool initNode(std::uint8_t node_id) = 0;
virtual bool writePositionLimits(std::uint8_t node_id,
std::int32_t lower_limit,
std::int32_t upper_limit) = 0;
virtual bool writeVelocityLimit(std::uint8_t node_id,
std::uint32_t velocity_limit) = 0;
virtual bool calibrateZero(std::uint8_t node_id,
std::int32_t& zeroed_position) = 0;
virtual bool brakeRelease(std::uint8_t node_id) = 0;
};
} // namespace cmvr::device
#endif // CMVR_ES_MOTOR_VENDOR_ADAPTER_H

View File

@ -0,0 +1,872 @@
#include "devices/motor/drivers/ethercat_motor/include/cia402/cia402_protocol.h"
#include <algorithm>
#include <chrono>
#include <cmath>
#include <thread>
#include <utility>
#include "common/base/logging/logger.h"
#include "cmvr/msgs/cia402.pb.h"
#include "devices/motor/drivers/ethercat_motor/include/cia402/cia402_objects.h"
namespace cmvr::device {
Cia402Protocol::Cia402Protocol(std::shared_ptr<EthercatMotorBusRuntime> bus_runtime,
const config::Cia402ProtocolConfig& config)
: bus_runtime_(std::move(bus_runtime)),
config_(config)
{
comm_proto = CommProto::ETHERCAT;
}
bool Cia402Protocol::initNode(const std::uint8_t node_id)
{
if (!bus_runtime_ || !bus_runtime_->hasMotor(node_id)) {
CMVR_LOG(ERROR) << "[Cia402Protocol] missing EtherCAT motor: "
<< static_cast<int>(node_id);
return false;
}
if (!validateNodePdos_(node_id)) {
return false;
}
auto& state = nodeState_(node_id);
std::int32_t actual_position = 0;
if (readActualPosition_(node_id, actual_position)) {
state.target_position = actual_position;
}
writeNode_(node_id, state);
return true;
}
bool Cia402Protocol::commandProfilePosition(const std::uint8_t node_id,
const double target_q,
const double max_qd,
const double max_qdd)
{
auto& state = nodeState_(node_id);
if (!hasValidConversion_(node_id, state)) {
return false;
}
state.target_position = radToCounts_(target_q, state);
state.profile_velocity = std::abs(radPerSecToCounts_(max_qd, state));
if (max_qdd > 0.0) {
const auto profile_acceleration = std::abs(radPerSec2ToCounts_(max_qdd, state));
state.profile_acceleration = profile_acceleration;
state.profile_deceleration = profile_acceleration;
}
writeProfilePositionTarget_(node_id, state);
return true;
}
bool Cia402Protocol::commandProfileVelocity(const std::uint8_t node_id,
const double target_qd,
const double max_qdd)
{
auto& state = nodeState_(node_id);
if (!hasValidConversion_(node_id, state)) {
return false;
}
state.target_velocity = radPerSecToCounts_(target_qd, state);
if (max_qdd > 0.0) {
const auto profile_acceleration = std::abs(radPerSec2ToCounts_(max_qdd, state));
state.profile_acceleration = profile_acceleration;
state.profile_deceleration = profile_acceleration;
}
writeNode_(node_id, state);
return true;
}
bool Cia402Protocol::commandCyclicPosition(const std::uint8_t node_id,
const double target_q,
const double target_qd)
{
auto& state = nodeState_(node_id);
if (!hasValidConversion_(node_id, state)) {
return false;
}
state.target_position = radToCounts_(target_q, state);
state.target_velocity = radPerSecToCounts_(target_qd, state);
writeNode_(node_id, state);
return true;
}
bool Cia402Protocol::commandCyclicVelocity(const std::uint8_t node_id,
const double target_qd)
{
auto& state = nodeState_(node_id);
if (!hasValidConversion_(node_id, state)) {
return false;
}
state.target_velocity = radPerSecToCounts_(target_qd, state);
writeNode_(node_id, state);
return true;
}
bool Cia402Protocol::commandCyclicTorque(const std::uint8_t node_id,
const double target_tau)
{
(void)target_tau;
CMVR_LOG(ERROR) << "[Cia402Protocol] cyclic torque command is not implemented, node="
<< static_cast<int>(node_id);
return false;
}
void Cia402Protocol::setMode(const std::uint8_t node_id, const msgs::RunMode mode)
{
auto& state = nodeState_(node_id);
if (!prepareSafeTargetsForMode_(node_id, mode, state)) {
return;
}
writeTargetsForMode_(node_id, mode, state);
state.mode = mode;
writeNode_(node_id, state);
}
msgs::RunMode Cia402Protocol::getMode(const std::uint8_t node_id)
{
std::int8_t mode_display = 0;
if (readModeDisplay_(node_id, mode_display)) {
return fromCia402Mode_(mode_display);
}
return nodeState_(node_id).mode;
}
void Cia402Protocol::setLimitQdd(const std::uint8_t node_id,
const double u_qdd,
const double l_qdd)
{
auto& state = nodeState_(node_id);
state.limit_qdd = std::max(std::abs(u_qdd), std::abs(l_qdd));
if (hasValidConversion_(node_id, state)) {
const auto profile_acceleration = std::abs(radPerSec2ToCounts_(state.limit_qdd, state));
state.profile_acceleration = profile_acceleration;
state.profile_deceleration = profile_acceleration;
writeAccelerationLimitsToDictionary_(node_id, state);
}
}
void Cia402Protocol::setLimitQd(const std::uint8_t node_id, const double qd)
{
auto& state = nodeState_(node_id);
state.limit_qd = std::abs(qd);
if (hasValidConversion_(node_id, state)) {
state.profile_velocity = std::abs(radPerSecToCounts_(state.limit_qd, state));
writeVelocityLimitToDictionary_(node_id, state);
}
}
void Cia402Protocol::setLimitQ(const std::uint8_t node_id,
const double ub,
const double lb)
{
auto& state = nodeState_(node_id);
state.limit_q_ub = ub;
state.limit_q_lb = lb;
if (hasValidConversion_(node_id, state)) {
writePositionLimitsToDictionary_(node_id, state);
}
}
bool Cia402Protocol::calibrateZeroQ(const std::uint8_t node_id)
{
CMVR_LOG(ERROR) << "[Cia402Protocol] zero calibration is vendor-specific, node="
<< static_cast<int>(node_id);
return false;
}
bool Cia402Protocol::reachedTargetQ(const std::uint8_t node_id)
{
std::uint16_t statusword = 0;
if (!readStatusword_(node_id, statusword)) {
return false;
}
return targetReached_(cia402::statusword(statusword));
}
void Cia402Protocol::setMotorConversion(
const std::uint8_t node_id,
const double encoder_counts_per_rev,
const double gear_ratio)
{
auto& state = nodeState_(node_id);
state.encoder_counts_per_rev = encoder_counts_per_rev;
state.gear_ratio = gear_ratio;
}
bool Cia402Protocol::torqueOn(const std::uint8_t node_id)
{
if (!bus_runtime_) {
return false;
}
auto& state = nodeState_(node_id);
std::uint16_t statusword = 0;
if (readStatusword_(node_id, statusword) && cia402::statusword(statusword).fault != 0) {
if (!writeControlword_(node_id, cia402::faultResetControlword())) {
return false;
}
std::this_thread::sleep_for(
std::chrono::milliseconds(config_.status_poll_period_ms()));
}
if (!prepareSafeTargetsForMode_(node_id, msgs::RUN_MODE_PROFILE_POSITION, state)) {
return false;
}
state.mode = msgs::RUN_MODE_PROFILE_POSITION;
state.target_velocity = 0;
state.target_torque = 0;
writeTargetsForMode_(node_id, state.mode, state);
if (!bus_runtime_->writePdo<std::int8_t>(node_id, msgs::CIA402_OPERATION_MODE_6060, 0x00,
toCia402Mode_(state.mode))) {
return false;
}
if (!writeControlwordAndWait_(node_id, cia402::shutdownControlword(),
cia402::DeviceState::ReadyToSwitchOn,
"Ready To Switch On")) {
return false;
}
if (!writeControlwordAndWait_(node_id, cia402::switchOnControlword(),
cia402::DeviceState::SwitchedOn,
"Switched On")) {
return false;
}
if (!writeControlwordAndWait_(node_id, cia402::enableOperationControlword(),
cia402::DeviceState::OperationEnabled,
"Operation Enabled")) {
return false;
}
std::int32_t actual_position = 0;
if (readActualPosition_(node_id, actual_position)) {
state.target_position = actual_position;
}
writeTargetsForMode_(node_id, state.mode, state);
state.controlword = cia402::profilePositionControlword(true);
if (!writeControlword_(node_id, state.controlword)) {
return false;
}
std::this_thread::sleep_for(
std::chrono::milliseconds(config_.profile_position_trigger_delay_ms()));
state.controlword = cia402::profilePositionControlword(false);
return writeControlword_(node_id, state.controlword);
}
bool Cia402Protocol::torqueOff(const std::uint8_t node_id)
{
if (!bus_runtime_) {
return false;
}
auto& state = nodeState_(node_id);
std::int32_t actual_position = 0;
if (readActualPosition_(node_id, actual_position)) {
state.target_position = actual_position;
}
state.target_velocity = 0;
state.target_torque = 0;
writeTargetsForMode_(node_id, state.mode, state);
waitVelocityNearZero_(node_id, "torqueOff");
if (!writeControlwordAndWait_(node_id, cia402::switchOnControlword(),
cia402::DeviceState::SwitchedOn,
"Switched On")) {
return false;
}
if (!writeControlwordAndWait_(node_id, cia402::shutdownControlword(),
cia402::DeviceState::ReadyToSwitchOn,
"Ready To Switch On")) {
return false;
}
return true;
}
bool Cia402Protocol::brakeRelease(const std::uint8_t node_id)
{
CMVR_LOG(ERROR) << "[Cia402Protocol] brake release is vendor-specific, node="
<< static_cast<int>(node_id);
return false;
}
bool Cia402Protocol::quickStop(const std::uint8_t node_id)
{
if (!bus_runtime_) {
return false;
}
auto& state = nodeState_(node_id);
state.target_velocity = 0;
state.target_torque = 0;
writeTargetsForMode_(node_id, state.mode, state);
state.controlword = cia402::quickStopControlword();
if (!writeControlword_(node_id, state.controlword)) {
return false;
}
return waitVelocityNearZero_(node_id, "quickStop");
}
double Cia402Protocol::getQ(const std::uint8_t node_id)
{
std::int32_t actual_position = 0;
if (!readActualPosition_(node_id, actual_position)) {
return 0.0;
}
const auto& state = nodeState_(node_id);
if (!hasValidConversion_(node_id, state)) {
return 0.0;
}
return countsToRad_(actual_position, state);
}
double Cia402Protocol::getQd(const std::uint8_t node_id)
{
std::int32_t actual_velocity = 0;
if (!readActualVelocity_(node_id, actual_velocity)) {
return 0.0;
}
const auto& state = nodeState_(node_id);
if (!hasValidConversion_(node_id, state)) {
return 0.0;
}
return countsToRadPerSec_(actual_velocity, state);
}
bool Cia402Protocol::syncTargetToActualPosition(const std::uint8_t node_id)
{
std::int32_t actual_position = 0;
if (!readActualPosition_(node_id, actual_position)) {
CMVR_LOG(ERROR) << "[Cia402Protocol] failed to read actual position, node="
<< static_cast<int>(node_id);
return false;
}
auto& state = nodeState_(node_id);
state.target_position = actual_position;
state.target_velocity = 0;
state.target_torque = 0;
writeTargetsForMode_(node_id, state.mode, state);
return true;
}
std::int8_t Cia402Protocol::toCia402Mode_(const msgs::RunMode mode)
{
switch (mode) {
case msgs::RUN_MODE_PROFILE_POSITION:
return 1;
case msgs::RUN_MODE_PROFILE_VELOCITY:
return 3;
case msgs::RUN_MODE_HOMING:
return 6;
case msgs::RUN_MODE_CYCLIC_SYNC_POSITION:
return 8;
case msgs::RUN_MODE_CYCLIC_SYNC_VELOCITY:
return 9;
case msgs::RUN_MODE_CYCLIC_SYNC_CURRENT:
return 10;
default:
return 8;
}
}
msgs::RunMode Cia402Protocol::fromCia402Mode_(const std::int8_t mode)
{
switch (mode) {
case 1:
return msgs::RUN_MODE_PROFILE_POSITION;
case 3:
return msgs::RUN_MODE_PROFILE_VELOCITY;
case 6:
return msgs::RUN_MODE_HOMING;
case 8:
return msgs::RUN_MODE_CYCLIC_SYNC_POSITION;
case 9:
return msgs::RUN_MODE_CYCLIC_SYNC_VELOCITY;
case 10:
return msgs::RUN_MODE_CYCLIC_SYNC_CURRENT;
default:
return msgs::RUN_MODE_UNSPECIFIED;
}
}
cia402::Controlword Cia402Protocol::nextControlword_(const cia402::Statusword statusword)
{
if (statusword.fault != 0) {
return cia402::faultResetControlword();
}
if (cia402::hasState(statusword, cia402::DeviceState::SwitchOnDisabled)) {
return cia402::shutdownControlword();
}
if (cia402::hasState(statusword, cia402::DeviceState::ReadyToSwitchOn)) {
return cia402::switchOnControlword();
}
if (cia402::hasState(statusword, cia402::DeviceState::SwitchedOn)) {
return cia402::enableOperationControlword();
}
if (cia402::hasState(statusword, cia402::DeviceState::OperationEnabled)) {
return cia402::enableOperationControlword();
}
return cia402::shutdownControlword();
}
bool Cia402Protocol::isOperationEnabled_(const cia402::Statusword statusword)
{
return cia402::isOperationEnabled(statusword);
}
bool Cia402Protocol::targetReached_(const cia402::Statusword statusword)
{
return cia402::targetReached(statusword);
}
std::int32_t Cia402Protocol::radToCounts_(const double angle_rad,
const NodeState& state) const
{
const double rev = angle_rad / (2.0 * M_PI);
return static_cast<std::int32_t>(
std::llround(rev * state.gear_ratio * state.encoder_counts_per_rev));
}
double Cia402Protocol::countsToRad_(const std::int32_t counts,
const NodeState& state) const
{
return static_cast<double>(counts) /
(state.gear_ratio * state.encoder_counts_per_rev) * 2.0 * M_PI;
}
std::int32_t Cia402Protocol::radPerSecToCounts_(
const double velocity_rad_s,
const NodeState& state) const
{
const double rev_per_sec = velocity_rad_s / (2.0 * M_PI);
return static_cast<std::int32_t>(
std::llround(rev_per_sec * state.gear_ratio * state.encoder_counts_per_rev));
}
std::int32_t Cia402Protocol::radPerSec2ToCounts_(
const double acceleration_rad_s2,
const NodeState& state) const
{
const double rev_per_sec2 = acceleration_rad_s2 / (2.0 * M_PI);
return static_cast<std::int32_t>(
std::llround(rev_per_sec2 * state.gear_ratio * state.encoder_counts_per_rev));
}
double Cia402Protocol::countsToRadPerSec_(
const std::int32_t velocity_counts_s,
const NodeState& state) const
{
return static_cast<double>(velocity_counts_s) /
(state.gear_ratio * state.encoder_counts_per_rev) * 2.0 * M_PI;
}
Cia402Protocol::NodeState& Cia402Protocol::nodeState_(const std::uint8_t node_id)
{
return nodes_[node_id];
}
const Cia402Protocol::NodeState* Cia402Protocol::findNodeState_(
const std::uint8_t node_id) const
{
const auto it = nodes_.find(node_id);
if (it == nodes_.end()) {
return nullptr;
}
return &it->second;
}
bool Cia402Protocol::hasValidConversion_(const std::uint8_t node_id,
const NodeState& state) const
{
if (state.encoder_counts_per_rev > 0.0 && state.gear_ratio > 0.0) {
return true;
}
CMVR_LOG(ERROR) << "[Cia402Protocol] missing conversion config for node "
<< static_cast<int>(node_id)
<< ": encoder_counts_per_rev=" << state.encoder_counts_per_rev
<< ", gear_ratio=" << state.gear_ratio;
return false;
}
bool Cia402Protocol::validateNodePdos_(const std::uint8_t node_id) const
{
if (!bus_runtime_) {
return false;
}
struct RequiredEntry {
std::uint16_t index;
std::uint8_t subindex;
const char* name;
};
const RequiredEntry required[] = {
{msgs::CIA402_CONTROL_WORD_6040, 0x00, "controlword"},
{msgs::CIA402_TARGET_POSITION_607A, 0x00, "target position"},
{msgs::CIA402_TARGET_VELOCITY_60FF, 0x00, "target velocity"},
{msgs::CIA402_TARGET_TORQUE_6071, 0x00, "target torque"},
{msgs::CIA402_PROFILE_ACCELERATION_6083, 0x00, "profile acceleration"},
{msgs::CIA402_PROFILE_DECELERATION_6084, 0x00, "profile deceleration"},
{msgs::CIA402_PROFILE_VELOCITY_6081, 0x00, "profile velocity"},
{msgs::CIA402_OPERATION_MODE_6060, 0x00, "operation mode"},
{msgs::CIA402_STATUS_WORD_6041, 0x00, "statusword"},
{msgs::CIA402_ACTUAL_POSITION_6064, 0x00, "actual position"},
{msgs::CIA402_ACTUAL_VELOCITY_606C, 0x00, "actual velocity"},
{msgs::CIA402_ACTUAL_TORQUE_6077, 0x00, "actual torque"},
{msgs::CIA402_MODE_DISPLAY_6061, 0x00, "mode display"},
{msgs::CIA402_ERROR_CODE_603F, 0x00, "error code"},
};
for (const auto& entry : required) {
if (!bus_runtime_->hasPdoEntry(node_id, entry.index, entry.subindex)) {
CMVR_LOG(ERROR) << "[Cia402Protocol] missing PDO entry for node "
<< static_cast<int>(node_id)
<< ": " << entry.name
<< " 0x" << std::hex << entry.index
<< ":" << static_cast<int>(entry.subindex) << std::dec;
return false;
}
}
return true;
}
bool Cia402Protocol::readStatusword_(const std::uint8_t node_id,
std::uint16_t& statusword) const
{
return bus_runtime_ &&
bus_runtime_->readPdo<std::uint16_t>(node_id, msgs::CIA402_STATUS_WORD_6041, 0x00,
statusword);
}
bool Cia402Protocol::readActualPosition_(const std::uint8_t node_id,
std::int32_t& actual_position) const
{
return bus_runtime_ &&
bus_runtime_->readPdo<std::int32_t>(node_id, msgs::CIA402_ACTUAL_POSITION_6064, 0x00,
actual_position);
}
bool Cia402Protocol::readActualVelocity_(const std::uint8_t node_id,
std::int32_t& actual_velocity) const
{
return bus_runtime_ &&
bus_runtime_->readPdo<std::int32_t>(node_id, msgs::CIA402_ACTUAL_VELOCITY_606C, 0x00,
actual_velocity);
}
bool Cia402Protocol::readModeDisplay_(const std::uint8_t node_id,
std::int8_t& mode_display) const
{
return bus_runtime_ &&
bus_runtime_->readPdo<std::int8_t>(node_id, msgs::CIA402_MODE_DISPLAY_6061, 0x00,
mode_display);
}
bool Cia402Protocol::writeControlword_(const std::uint8_t node_id,
const cia402::Controlword controlword)
{
if (!bus_runtime_) {
return false;
}
auto& state = nodeState_(node_id);
state.controlword = controlword;
return bus_runtime_->writePdo<std::uint16_t>(node_id, msgs::CIA402_CONTROL_WORD_6040,
0x00, controlword.value);
}
bool Cia402Protocol::writeControlwordAndWait_(
const std::uint8_t node_id,
const cia402::Controlword controlword,
const cia402::DeviceState target_state,
const char* state_name)
{
if (!writeControlword_(node_id, controlword)) {
return false;
}
return waitStatus_(node_id, target_state, state_name);
}
bool Cia402Protocol::waitStatus_(const std::uint8_t node_id,
const cia402::DeviceState target_state,
const char* state_name) const
{
const auto deadline = std::chrono::steady_clock::now() +
std::chrono::milliseconds(config_.state_transition_timeout_ms());
std::uint16_t last_statusword = 0;
do {
if (readStatusword_(node_id, last_statusword) &&
cia402::hasState(cia402::statusword(last_statusword), target_state)) {
return true;
}
std::this_thread::sleep_for(
std::chrono::milliseconds(config_.status_poll_period_ms()));
} while (std::chrono::steady_clock::now() < deadline);
CMVR_LOG(ERROR) << "[Cia402Protocol] timeout waiting for "
<< state_name << ", node=" << static_cast<int>(node_id)
<< ", last_statusword=0x" << std::hex << last_statusword << std::dec;
return false;
}
bool Cia402Protocol::waitVelocityNearZero_(const std::uint8_t node_id,
const char* action_name) const
{
const auto* state = findNodeState_(node_id);
if (state == nullptr || !hasValidConversion_(node_id, *state)) {
return false;
}
const auto tolerance_counts = std::max<std::int32_t>(
1, std::abs(radPerSecToCounts_(config_.stopped_velocity_tolerance_rad_s(), *state)));
const auto deadline = std::chrono::steady_clock::now() +
std::chrono::milliseconds(config_.velocity_stop_timeout_ms());
std::int32_t last_velocity = 0;
do {
if (readActualVelocity_(node_id, last_velocity) &&
std::abs(last_velocity) <= tolerance_counts) {
return true;
}
std::this_thread::sleep_for(
std::chrono::milliseconds(config_.status_poll_period_ms()));
} while (std::chrono::steady_clock::now() < deadline);
CMVR_LOG(ERROR) << "[Cia402Protocol] timeout waiting velocity near zero "
<< "during " << action_name
<< ", node=" << static_cast<int>(node_id)
<< ", last_velocity=" << last_velocity
<< ", tolerance=" << tolerance_counts;
return false;
}
bool Cia402Protocol::writePositionLimitsToDictionary_(
const std::uint8_t node_id,
const NodeState& state) const
{
if (!bus_runtime_) {
return false;
}
if (!std::isfinite(state.limit_q_lb) || !std::isfinite(state.limit_q_ub) ||
state.limit_q_ub <= state.limit_q_lb) {
return true;
}
const auto lower_limit = radToCounts_(state.limit_q_lb, state);
const auto upper_limit = radToCounts_(state.limit_q_ub, state);
const bool ok =
bus_runtime_->writeSdo<std::int32_t>(node_id, msgs::CIA402_SOFTWARE_POSITION_LIMIT_607D,
0x01, lower_limit) &&
bus_runtime_->writeSdo<std::int32_t>(node_id, msgs::CIA402_SOFTWARE_POSITION_LIMIT_607D,
0x02, upper_limit);
if (!ok) {
CMVR_LOG(ERROR) << "[Cia402Protocol] failed to write software position "
<< "limits to dictionary, node=" << static_cast<int>(node_id)
<< ", lower=" << lower_limit
<< ", upper=" << upper_limit;
}
return ok;
}
bool Cia402Protocol::writeVelocityLimitToDictionary_(
const std::uint8_t node_id,
const NodeState& state) const
{
if (!bus_runtime_) {
return false;
}
if (!std::isfinite(state.limit_qd) || state.limit_qd <= 0.0) {
return true;
}
const auto velocity_limit =
static_cast<std::uint32_t>(std::abs(radPerSecToCounts_(state.limit_qd, state)));
const bool ok =
bus_runtime_->writeSdo<std::uint32_t>(node_id, msgs::CIA402_MAX_PROFILE_VELOCITY_607F,
0x00, velocity_limit);
if (!ok) {
CMVR_LOG(ERROR) << "[Cia402Protocol] failed to write velocity limit "
<< "to dictionary, node=" << static_cast<int>(node_id)
<< ", velocity_limit=" << velocity_limit;
}
return ok;
}
bool Cia402Protocol::writeAccelerationLimitsToDictionary_(
const std::uint8_t node_id,
const NodeState& state) const
{
if (!bus_runtime_) {
return false;
}
if (!std::isfinite(state.limit_qdd) || state.limit_qdd <= 0.0) {
return true;
}
const auto acceleration_limit =
static_cast<std::uint32_t>(std::abs(radPerSec2ToCounts_(state.limit_qdd, state)));
const bool ok =
bus_runtime_->writeSdo<std::uint32_t>(node_id, msgs::CIA402_PROFILE_ACCELERATION_6083,
0x00, acceleration_limit) &&
bus_runtime_->writeSdo<std::uint32_t>(node_id, msgs::CIA402_PROFILE_DECELERATION_6084,
0x00, acceleration_limit);
if (!ok) {
CMVR_LOG(ERROR) << "[Cia402Protocol] failed to write acceleration limits "
<< "to dictionary, node=" << static_cast<int>(node_id)
<< ", acceleration_limit=" << acceleration_limit;
}
return ok;
}
void Cia402Protocol::writeProfilePositionTarget_(const std::uint8_t node_id,
NodeState& state)
{
if (!bus_runtime_) {
return;
}
if (state.profile_velocity <= 0 && state.limit_qd > 0.0) {
state.profile_velocity = std::abs(radPerSecToCounts_(state.limit_qd, state));
}
if (state.profile_acceleration <= 0 && state.limit_qdd > 0.0) {
const auto profile_acceleration = std::abs(radPerSec2ToCounts_(state.limit_qdd, state));
state.profile_acceleration = profile_acceleration;
state.profile_deceleration = profile_acceleration;
}
state.controlword = cia402::enableOperationControlword();
writeNode_(node_id, state);
state.controlword = cia402::profilePositionControlword(true);
writeControlword_(node_id, state.controlword);
std::this_thread::sleep_for(
std::chrono::milliseconds(config_.profile_position_trigger_delay_ms()));
state.controlword = cia402::profilePositionControlword(false);
writeControlword_(node_id, state.controlword);
}
bool Cia402Protocol::prepareSafeTargetsForMode_(const std::uint8_t node_id,
const msgs::RunMode mode,
NodeState& state)
{
state.target_velocity = 0;
state.target_torque = 0;
switch (mode) {
case msgs::RUN_MODE_PROFILE_POSITION:
case msgs::RUN_MODE_CYCLIC_SYNC_POSITION: {
std::int32_t actual_position = 0;
if (!readActualPosition_(node_id, actual_position)) {
CMVR_LOG(ERROR) << "[Cia402Protocol] failed to read actual "
<< "position before switching mode, node="
<< static_cast<int>(node_id)
<< ", mode=" << static_cast<int>(mode);
return false;
}
state.target_position = actual_position;
break;
}
case msgs::RUN_MODE_PROFILE_VELOCITY:
case msgs::RUN_MODE_CYCLIC_SYNC_VELOCITY:
case msgs::RUN_MODE_CYCLIC_SYNC_CURRENT:
case msgs::RUN_MODE_HOMING:
case msgs::RUN_MODE_UNSPECIFIED:
default:
break;
}
return true;
}
void Cia402Protocol::writeTargetsForMode_(const std::uint8_t node_id,
const msgs::RunMode mode,
const NodeState& state) const
{
if (!bus_runtime_) {
return;
}
switch (mode) {
case msgs::RUN_MODE_PROFILE_POSITION:
bus_runtime_->writePdo<std::int32_t>(node_id, msgs::CIA402_TARGET_POSITION_607A,
0x00, state.target_position);
if (bus_runtime_->hasPdoEntry(node_id, msgs::CIA402_PROFILE_VELOCITY_6081, 0x00)) {
bus_runtime_->writePdo<std::int32_t>(node_id, msgs::CIA402_PROFILE_VELOCITY_6081,
0x00, state.profile_velocity);
}
if (bus_runtime_->hasPdoEntry(node_id, msgs::CIA402_PROFILE_ACCELERATION_6083, 0x00)) {
bus_runtime_->writePdo<std::int32_t>(node_id, msgs::CIA402_PROFILE_ACCELERATION_6083,
0x00, state.profile_acceleration);
}
if (bus_runtime_->hasPdoEntry(node_id, msgs::CIA402_PROFILE_DECELERATION_6084, 0x00)) {
bus_runtime_->writePdo<std::int32_t>(node_id, msgs::CIA402_PROFILE_DECELERATION_6084,
0x00, state.profile_deceleration);
}
break;
case msgs::RUN_MODE_PROFILE_VELOCITY:
bus_runtime_->writePdo<std::int32_t>(node_id, msgs::CIA402_TARGET_VELOCITY_60FF,
0x00, state.target_velocity);
if (bus_runtime_->hasPdoEntry(node_id, msgs::CIA402_PROFILE_ACCELERATION_6083, 0x00)) {
bus_runtime_->writePdo<std::int32_t>(node_id, msgs::CIA402_PROFILE_ACCELERATION_6083,
0x00, state.profile_acceleration);
}
if (bus_runtime_->hasPdoEntry(node_id, msgs::CIA402_PROFILE_DECELERATION_6084, 0x00)) {
bus_runtime_->writePdo<std::int32_t>(node_id, msgs::CIA402_PROFILE_DECELERATION_6084,
0x00, state.profile_deceleration);
}
break;
case msgs::RUN_MODE_CYCLIC_SYNC_POSITION:
bus_runtime_->writePdo<std::int32_t>(node_id, msgs::CIA402_TARGET_POSITION_607A,
0x00, state.target_position);
bus_runtime_->writePdo<std::int32_t>(node_id, msgs::CIA402_TARGET_VELOCITY_60FF,
0x00, state.target_velocity);
break;
case msgs::RUN_MODE_CYCLIC_SYNC_VELOCITY:
bus_runtime_->writePdo<std::int32_t>(node_id, msgs::CIA402_TARGET_VELOCITY_60FF,
0x00, state.target_velocity);
break;
case msgs::RUN_MODE_CYCLIC_SYNC_CURRENT:
bus_runtime_->writePdo<std::int16_t>(node_id, msgs::CIA402_TARGET_TORQUE_6071,
0x00, state.target_torque);
break;
default:
break;
}
}
void Cia402Protocol::writeNode_(const std::uint8_t node_id, NodeState& state)
{
if (!bus_runtime_) {
return;
}
std::uint16_t statusword = 0;
if (readStatusword_(node_id, statusword)) {
const auto status = cia402::statusword(statusword);
state.controlword = nextControlword_(status);
std::int32_t actual_position = 0;
if (!isOperationEnabled_(status) &&
readActualPosition_(node_id, actual_position) &&
actual_position != 0) {
state.target_position = actual_position;
}
}
bus_runtime_->writePdo<std::uint16_t>(node_id, msgs::CIA402_CONTROL_WORD_6040, 0x00,
state.controlword.value);
bus_runtime_->writePdo<std::int8_t>(node_id, msgs::CIA402_OPERATION_MODE_6060, 0x00,
toCia402Mode_(state.mode));
writeTargetsForMode_(node_id, state.mode, state);
}
} // namespace cmvr::device

View File

@ -0,0 +1,174 @@
#include "devices/motor/drivers/ethercat_motor/include/vendor/eyou/eyou_motor.h"
#include <cmath>
#include <cstdint>
#include <utility>
#include "common/base/logging/logger.h"
namespace cmvr::device {
EyouMotor::EyouMotor(const config::MotorConfigItem& config,
std::shared_ptr<Cia402Protocol> cia402_protocol,
std::unique_ptr<EyouMotorAdapter> vendor_adapter)
: cia402_protocol_(std::move(cia402_protocol)),
vendor_adapter_(std::move(vendor_adapter))
{
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();
encoder_counts_per_rev_ = config.encoder_counts_per_rev();
gear_ratio_ = config.gear_ratio();
node_id_ = static_cast<std::uint8_t>(info_.id);
id_ = info_.joint_name;
protocol_ = cia402_protocol_;
}
bool EyouMotor::init()
{
std::scoped_lock lock(mtx_);
if (!hasDependencies_() || !hasValidConversion_()) {
return false;
}
if (!cia402_protocol_->initNode(node_id_)) {
CMVR_LOG(ERROR) << "[EyouMotor] failed to init CiA402 node: " << info_.joint_name;
return false;
}
if (!vendor_adapter_->initNode(node_id_)) {
CMVR_LOG(ERROR) << "[EyouMotor] failed to init vendor adapter: " << info_.joint_name;
return false;
}
cia402_protocol_->setMotorConversion(node_id_, encoder_counts_per_rev_, gear_ratio_);
cia402_protocol_->setLimitQd(node_id_, info_.limit_qd);
if (!writeVendorVelocityLimit_()) {
return false;
}
if (info_.limit_qdd > 0.0) {
cia402_protocol_->setLimitQdd(node_id_, info_.limit_qdd, -info_.limit_qdd);
}
if (!writeVendorPositionLimits_()) {
return false;
}
return true;
}
void EyouMotor::setLimitQ(const double ub, const double lb)
{
std::scoped_lock lock(mtx_);
info_.limit_q_ub = ub;
info_.limit_q_lb = lb;
if (!hasDependencies_() || !hasValidConversion_()) {
return;
}
writeVendorPositionLimits_();
}
void EyouMotor::setLimitQd(const double qd)
{
std::scoped_lock lock(mtx_);
info_.limit_qd = qd;
if (!hasDependencies_() || !hasValidConversion_()) {
return;
}
cia402_protocol_->setLimitQd(node_id_, info_.limit_qd);
writeVendorVelocityLimit_();
}
bool EyouMotor::calibrateZeroQ()
{
std::scoped_lock lock(mtx_);
if (!hasDependencies_() || !hasValidConversion_()) {
return false;
}
std::int32_t zeroed_position = 0;
if (!vendor_adapter_->calibrateZero(node_id_, zeroed_position)) {
CMVR_LOG(ERROR) << "[EyouMotor] zero calibration failed: " << info_.joint_name;
return false;
}
if (!cia402_protocol_->syncTargetToActualPosition(node_id_)) {
return false;
}
if (!writeVendorPositionLimits_()) {
return false;
}
return true;
}
bool EyouMotor::brakeRelease()
{
std::scoped_lock lock(mtx_);
if (!hasDependencies_()) {
return false;
}
return vendor_adapter_->brakeRelease(node_id_);
}
bool EyouMotor::hasDependencies_() const
{
if (!cia402_protocol_ || !vendor_adapter_) {
CMVR_LOG(ERROR) << "[EyouMotor] missing protocol or vendor adapter: "
<< info_.joint_name;
return false;
}
if (cia402_protocol_->comm_proto != MotorProtocolInterface::CommProto::ETHERCAT) {
CMVR_LOG(ERROR) << "[EyouMotor] invalid protocol for motor: " << info_.joint_name;
return false;
}
return true;
}
bool EyouMotor::hasValidConversion_() const
{
if (encoder_counts_per_rev_ > 0.0 && gear_ratio_ > 0.0) {
return true;
}
CMVR_LOG(ERROR) << "[EyouMotor] missing encoder conversion config: "
<< info_.joint_name
<< ", encoder_counts_per_rev=" << encoder_counts_per_rev_
<< ", gear_ratio=" << gear_ratio_;
return false;
}
bool EyouMotor::writeVendorPositionLimits_() const
{
if (!std::isfinite(info_.limit_q_lb) || !std::isfinite(info_.limit_q_ub) ||
info_.limit_q_ub <= info_.limit_q_lb) {
return true;
}
cia402_protocol_->setLimitQ(node_id_, info_.limit_q_ub, info_.limit_q_lb);
return vendor_adapter_->writePositionLimits(node_id_,
radToCounts_(info_.limit_q_lb),
radToCounts_(info_.limit_q_ub));
}
bool EyouMotor::writeVendorVelocityLimit_() const
{
if (!std::isfinite(info_.limit_qd) || info_.limit_qd <= 0.0) {
return true;
}
return vendor_adapter_->writeVelocityLimit(node_id_, radPerSecToCounts_(info_.limit_qd));
}
std::int32_t EyouMotor::radToCounts_(const double angle_rad) const
{
const double rev = angle_rad / (2.0 * M_PI);
return static_cast<std::int32_t>(
std::llround(rev * gear_ratio_ * encoder_counts_per_rev_));
}
std::uint32_t EyouMotor::radPerSecToCounts_(const double velocity_rad_s) const
{
const double rev_per_sec = std::abs(velocity_rad_s) / (2.0 * M_PI);
return static_cast<std::uint32_t>(
std::llround(rev_per_sec * gear_ratio_ * encoder_counts_per_rev_));
}
} // namespace cmvr::device

View File

@ -0,0 +1,221 @@
#include "devices/motor/drivers/ethercat_motor/include/vendor/eyou/eyou_motor_adapter.h"
#include <chrono>
#include <cmath>
#include <cstdlib>
#include <limits>
#include <thread>
#include <utility>
#include "cmvr/msgs/cia402.pb.h"
#include "common/base/logging/logger.h"
#include "devices/motor/drivers/ethercat_motor/include/vendor/eyou/eyou_objects.h"
namespace cmvr::device {
EyouMotorAdapter::EyouMotorAdapter(
std::shared_ptr<EthercatMotorBusRuntime> bus_runtime)
: bus_runtime_(std::move(bus_runtime))
{
}
bool EyouMotorAdapter::initNode(const std::uint8_t node_id)
{
return bus_runtime_ && bus_runtime_->hasMotor(node_id);
}
bool EyouMotorAdapter::writePositionLimits(const std::uint8_t node_id,
const std::int32_t lower_limit,
const std::int32_t upper_limit)
{
if (!bus_runtime_) {
return false;
}
const auto write_limits = [&]() {
return bus_runtime_->writeSdo<std::uint32_t>(node_id, eyou::EYOU_SOFT_LIMIT_STATE_2003,
0x00, 0) &&
bus_runtime_->writeSdo<std::int32_t>(
node_id, msgs::CIA402_SOFTWARE_POSITION_LIMIT_607D,
0x02, upper_limit) &&
bus_runtime_->writeSdo<std::int32_t>(
node_id, msgs::CIA402_SOFTWARE_POSITION_LIMIT_607D,
0x01, lower_limit) &&
bus_runtime_->writeSdo<std::uint32_t>(
node_id, eyou::EYOU_SOFT_LIMIT_STATE_2003,
0x00, 0x4C494D54);
};
const auto readback_matches = [&]() {
std::int32_t actual_lower = 0;
std::int32_t actual_upper = 0;
return bus_runtime_->readSdo<std::int32_t>(
node_id, msgs::CIA402_SOFTWARE_POSITION_LIMIT_607D,
0x01, actual_lower) &&
bus_runtime_->readSdo<std::int32_t>(
node_id, msgs::CIA402_SOFTWARE_POSITION_LIMIT_607D,
0x02, actual_upper) &&
actual_lower == lower_limit &&
actual_upper == upper_limit;
};
const bool ok = write_limits() && readback_matches();
if (!ok) {
CMVR_LOG(ERROR) << "[EyouMotorAdapter] failed to write software position "
<< "limits, node=" << static_cast<int>(node_id)
<< ", soft_limit_state=" << 0x4C494D54
<< ", lower=" << lower_limit
<< ", upper=" << upper_limit;
}
return ok;
}
bool EyouMotorAdapter::writeVelocityLimit(const std::uint8_t node_id,
const std::uint32_t velocity_limit)
{
if (!bus_runtime_) {
return false;
}
std::uint32_t actual_velocity_limit = 0;
const bool ok =
bus_runtime_->writeSdo<std::uint32_t>(node_id, eyou::EYOU_OVER_SPEED_THRESHOLD_2024,
0x00, velocity_limit) &&
bus_runtime_->readSdo<std::uint32_t>(node_id, eyou::EYOU_OVER_SPEED_THRESHOLD_2024,
0x00, actual_velocity_limit) &&
actual_velocity_limit == velocity_limit;
if (!ok) {
CMVR_LOG(ERROR) << "[EyouMotorAdapter] failed to write over speed "
<< "threshold, node=" << static_cast<int>(node_id)
<< ", expected=" << velocity_limit
<< ", actual=" << actual_velocity_limit;
}
return ok;
}
bool EyouMotorAdapter::calibrateZero(const std::uint8_t node_id,
std::int32_t& zeroed_position)
{
if (!bus_runtime_) {
return false;
}
if (!bus_runtime_->writeSdo<std::uint32_t>(node_id, eyou::EYOU_SOFT_LIMIT_STATE_2003,
0x00, 0)) {
CMVR_LOG(ERROR) << "[EyouMotorAdapter] failed to disable software position "
<< "limit before home offset calibration, node="
<< static_cast<int>(node_id);
return false;
}
if (!bus_runtime_->writeSdo<std::int32_t>(node_id, msgs::CIA402_HOME_OFFSET_607C,
0x00, 0)) {
CMVR_LOG(ERROR) << "[EyouMotorAdapter] failed to clear home offset, node="
<< static_cast<int>(node_id);
return false;
}
std::this_thread::sleep_for(std::chrono::milliseconds{50});
std::int32_t actual_position = 0;
if (!bus_runtime_->readSdo<std::int32_t>(node_id, msgs::CIA402_ACTUAL_POSITION_6064,
0x00, actual_position)) {
CMVR_LOG(ERROR) << "[EyouMotorAdapter] failed to read actual position "
<< "after clearing home offset, node="
<< static_cast<int>(node_id);
return false;
}
if (actual_position == std::numeric_limits<std::int32_t>::min()) {
CMVR_LOG(ERROR) << "[EyouMotorAdapter] invalid actual position for home "
<< "offset calibration, node=" << static_cast<int>(node_id)
<< ", actual_position=" << actual_position;
return false;
}
const auto home_offset = static_cast<std::int32_t>(-actual_position);
if (!bus_runtime_->writeSdo<std::int32_t>(node_id, msgs::CIA402_HOME_OFFSET_607C,
0x00, home_offset)) {
CMVR_LOG(ERROR) << "[EyouMotorAdapter] failed to write home offset, node="
<< static_cast<int>(node_id)
<< ", home_offset=" << home_offset;
return false;
}
if (!bus_runtime_->writeSdo<std::uint32_t>(
node_id, eyou::EYOU_STORE_PARAMETERS_1010,
0x01,
0x65766173)) {
CMVR_LOG(ERROR) << "[EyouMotorAdapter] failed to save home offset parameter, node="
<< static_cast<int>(node_id);
return false;
}
std::this_thread::sleep_for(std::chrono::milliseconds{50});
std::int32_t home_offset_readback = 0;
if (!bus_runtime_->readSdo<std::int32_t>(node_id, msgs::CIA402_HOME_OFFSET_607C,
0x00, home_offset_readback)) {
CMVR_LOG(ERROR) << "[EyouMotorAdapter] failed to read back home offset, node="
<< static_cast<int>(node_id);
return false;
}
if (home_offset_readback != home_offset) {
CMVR_LOG(ERROR) << "[EyouMotorAdapter] home offset readback mismatch, node="
<< static_cast<int>(node_id)
<< ", expected=" << home_offset
<< ", actual=" << home_offset_readback;
return false;
}
if (!bus_runtime_->readSdo<std::int32_t>(node_id, msgs::CIA402_ACTUAL_POSITION_6064,
0x00, zeroed_position)) {
CMVR_LOG(ERROR) << "[EyouMotorAdapter] failed to read actual position "
<< "after writing home offset, node="
<< static_cast<int>(node_id);
return false;
}
if (std::abs(static_cast<long long>(zeroed_position)) > 10000) {
CMVR_LOG(ERROR) << "[EyouMotorAdapter] home offset did not zero actual position, "
<< "node=" << static_cast<int>(node_id)
<< ", actual_position_before=" << actual_position
<< ", home_offset=" << home_offset
<< ", home_offset_readback=" << home_offset_readback
<< ", actual_position_after=" << zeroed_position
<< ", tolerance_counts=" << 10000;
return false;
}
return true;
}
bool EyouMotorAdapter::brakeRelease(const std::uint8_t node_id)
{
if (!bus_runtime_) {
return false;
}
if (!bus_runtime_->writeSdo<std::uint8_t>(
node_id, eyou::EYOU_BRAKE_CONTROL_2014,
0x01,
1)) {
CMVR_LOG(ERROR) << "[EyouMotorAdapter] failed to release brake, node="
<< static_cast<int>(node_id);
return false;
}
const auto deadline =
std::chrono::steady_clock::now() + std::chrono::milliseconds{1000};
do {
std::uint8_t brake_state = 0;
if (bus_runtime_->readSdo<std::uint8_t>(
node_id, eyou::EYOU_BRAKE_CONTROL_2014,
0x02, brake_state) &&
(brake_state == 1 || brake_state == 2)) {
return true;
}
std::this_thread::sleep_for(std::chrono::milliseconds{10});
} while (std::chrono::steady_clock::now() < deadline);
CMVR_LOG(ERROR) << "[EyouMotorAdapter] brake release timeout, node="
<< static_cast<int>(node_id);
return false;
}
} // namespace cmvr::device

View File

@ -0,0 +1,309 @@
#include <array>
#include <algorithm>
#include <chrono>
#include <cmath>
#include <cstdint>
#include <iostream>
#include <limits>
#include <memory>
#include <thread>
#include <gtest/gtest.h>
#include "cmvr/config/device_manager_config/device_manager_config.pb.h"
#include "common/config/config_files.h"
#include "devices/motor/abstract_motor.h"
#include "devices/motor/manager/include/motor_manager.h"
#include "manager/device_manager/include/device_manager.h"
namespace cmvr::device {
namespace {
constexpr const char* kMotorManagerId = "ethercat_motors";
constexpr const char* kMotorConfigFile =
"devices/motor/ethercat_motors_two_real_test.pb.txt";
constexpr std::array<int, 4> kFourMotorIds{1, 2, 3, 4};
constexpr std::chrono::milliseconds kCyclicCommandPeriod{1};
constexpr std::chrono::milliseconds kPrintPeriod{100};
constexpr std::chrono::milliseconds kStatsSamplePeriod{10};
constexpr std::chrono::milliseconds kHoldAfterTrajectoryDuration{500};
constexpr std::chrono::milliseconds kFourMotorTrajectoryDuration{20000};
constexpr double kPi = 3.14159265358979323846;
constexpr double kFourMotorAmplitudeRad = 0.2;
constexpr double kFourMotorPeriodS = 1.0;
constexpr std::array<double, 4> kFourMotorPhaseRad{0.0, 0.0, 0.0, 0.0};
class DeviceManagerDestroyGuard {
public:
~DeviceManagerDestroyGuard()
{
DeviceManager::destroyInstance();
}
};
struct TrackingErrorStats {
std::int64_t sample_count{0};
double sum_error{0.0};
double sum_error_sq{0.0};
double max_abs_error{0.0};
double sin_projection{0.0};
double cos_projection{0.0};
void add(const double error, const double theta)
{
++sample_count;
sum_error += error;
sum_error_sq += error * error;
max_abs_error = std::max(max_abs_error, std::fabs(error));
sin_projection += error * std::sin(theta);
cos_projection += error * std::cos(theta);
}
double mean() const
{
return sample_count > 0 ? sum_error / static_cast<double>(sample_count) : 0.0;
}
double rms() const
{
return sample_count > 0
? std::sqrt(sum_error_sq / static_cast<double>(sample_count))
: 0.0;
}
double fundamentalAmplitude() const
{
if (sample_count == 0) {
return 0.0;
}
const double scale = 2.0 / static_cast<double>(sample_count);
return scale * std::sqrt(sin_projection * sin_projection +
cos_projection * cos_projection);
}
double phaseRad() const
{
return std::atan2(cos_projection, sin_projection);
}
};
double normalizePhaseRad(double phase)
{
while (phase > kPi) {
phase -= 2.0 * kPi;
}
while (phase < -kPi) {
phase += 2.0 * kPi;
}
return phase;
}
double radToDeg(const double rad)
{
return rad * 180.0 / kPi;
}
config::DeviceManagerConfig createEthercatOnlyDeviceManagerConfig()
{
config::DeviceManagerConfig config;
config.set_name("eyou_motor_device_manager_real_test");
config.set_version("test");
config.set_init_all_motors_when_no_active_joints(true);
auto* motor_entry = config.add_devices();
motor_entry->set_id(kMotorManagerId);
motor_entry->set_type(config::DeviceConfigEntry::DEVICE_TYPE_MOTOR_SYSTEM);
motor_entry->set_config_file(kMotorConfigFile);
motor_entry->set_enable(true);
return config;
}
void printMotorState(const int motor_id, const std::shared_ptr<AbstractMotor>& motor)
{
ASSERT_NE(motor, nullptr);
std::cout << "motor_id=" << motor_id
<< ", joint_name=" << motor->jointName()
<< ", q=" << motor->getQ() << " rad"
<< ", qd=" << motor->getQd() << " rad/s"
<< std::endl;
}
} // namespace
TEST(EyouMotorDeviceManagerRealTest, InitFourEthercatMotorsAndPrintState)
{
ConfigHelper::setConfigRootFromFile("cmvr-es/config/cmvr_es.pb.txt");
DeviceManagerDestroyGuard guard;
auto& device_manager =
DeviceManager::getInstance(createEthercatOnlyDeviceManagerConfig());
auto motor_manager = device_manager.getDevice<MotorManager>(kMotorManagerId);
ASSERT_NE(motor_manager, nullptr);
// for (int motor_id = 1; motor_id <= 4; ++motor_id) {
// printMotorState(motor_id, motor_manager->getMotor(motor_id));
// }
auto motor = motor_manager->getMotor(4);
motor->calibrateZeroQ();
ASSERT_TRUE(motor->torqueOn());
motor->setMode(msgs::RUN_MODE_PROFILE_VELOCITY);
motor->commandProfileVelocity(-2,5);
for (int i = 1; i <= 50; ++i)
{
auto q = motor->getQ();
auto qd = motor->getQd();
std::cout << "q=" << q << ", qd=" << qd << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
motor->quickStop();
}
TEST(EyouMotorDeviceManagerRealTest, CommandFourCyclicPositionSinTrajectory)
{
ConfigHelper::setConfigRootFromFile("cmvr-es/config/cmvr_es.pb.txt");
DeviceManagerDestroyGuard guard;
auto& device_manager =
DeviceManager::getInstance(createEthercatOnlyDeviceManagerConfig());
auto motor_manager = device_manager.getDevice<MotorManager>(kMotorManagerId);
ASSERT_NE(motor_manager, nullptr);
std::array<std::shared_ptr<AbstractMotor>, kFourMotorIds.size()> motors;
for (std::size_t i = 0; i < kFourMotorIds.size(); ++i) {
const int motor_id = kFourMotorIds[i];
motors[i] = motor_manager->getMotor(static_cast<std::uint8_t>(motor_id));
ASSERT_NE(motors[i], nullptr);
printMotorState(motor_id, motors[i]);
}
std::cout << "calibrate zero for four EtherCAT motors" << std::endl;
for (std::size_t i = 0; i < motors.size(); ++i) {
const int motor_id = kFourMotorIds[i];
std::cout << "before calibrateZeroQ: ";
printMotorState(motor_id, motors[i]);
ASSERT_TRUE(motors[i]->calibrateZeroQ());
std::cout << "after calibrateZeroQ: ";
printMotorState(motor_id, motors[i]);
}
for (const auto& motor : motors) {
ASSERT_TRUE(motor->torqueOn());
}
for (const auto& motor : motors) {
motor->setMode(msgs::RUN_MODE_CYCLIC_SYNC_POSITION);
}
std::array<double, kFourMotorIds.size()> center_q{};
for (std::size_t i = 0; i < motors.size(); ++i) {
center_q[i] = motors[i]->getQ();
}
const double omega = 2.0 * kPi / kFourMotorPeriodS;
std::cout << "command four motors in CSP, duration="
<< kFourMotorTrajectoryDuration.count()
<< " ms, command_period=" << kCyclicCommandPeriod.count()
<< " ms, amplitude=" << kFourMotorAmplitudeRad
<< " rad, period=" << kFourMotorPeriodS
<< " s" << std::endl;
const auto start_time = std::chrono::steady_clock::now();
const auto total_ticks = kFourMotorTrajectoryDuration / kCyclicCommandPeriod;
std::array<TrackingErrorStats, kFourMotorIds.size()> error_stats;
double max_error_spread_rad = 0.0;
double sum_error_spread_sq = 0.0;
std::int64_t error_spread_sample_count = 0;
for (std::int64_t tick = 0; tick <= total_ticks; ++tick) {
const auto elapsed = tick * kCyclicCommandPeriod;
const double t_s = static_cast<double>(elapsed.count()) / 1000.0;
std::array<double, kFourMotorIds.size()> target_q{};
std::array<double, kFourMotorIds.size()> target_qd{};
for (std::size_t i = 0; i < motors.size(); ++i) {
const double theta = omega * t_s + kFourMotorPhaseRad[i];
target_q[i] = center_q[i] + kFourMotorAmplitudeRad * (1.0 - std::cos(theta));
target_qd[i] = kFourMotorAmplitudeRad * omega * std::sin(theta);
ASSERT_TRUE(motors[i]->commandCyclicPosition(target_q[i], target_qd[i]));
}
if (elapsed.count() % kStatsSamplePeriod.count() == 0) {
double min_error = std::numeric_limits<double>::max();
double max_error = std::numeric_limits<double>::lowest();
for (std::size_t i = 0; i < motors.size(); ++i) {
const double theta = omega * t_s + kFourMotorPhaseRad[i];
const double error = motors[i]->getQ() - target_q[i];
error_stats[i].add(error, theta);
min_error = std::min(min_error, error);
max_error = std::max(max_error, error);
}
const double error_spread = max_error - min_error;
max_error_spread_rad = std::max(max_error_spread_rad, error_spread);
sum_error_spread_sq += error_spread * error_spread;
++error_spread_sample_count;
}
// if (elapsed.count() % kPrintPeriod.count() == 0) {
// std::cout << "t=" << elapsed.count() << " ms" << std::endl;
// for (std::size_t i = 0; i < motors.size(); ++i) {
// std::cout << " motor_id=" << kFourMotorIds[i]
// << ", target_q=" << target_q[i]
// << " rad, target_qd=" << target_qd[i]
// << " rad/s, q=" << motors[i]->getQ()
// << " rad, qd=" << motors[i]->getQd()
// << " rad/s" << std::endl;
// }
// }
std::this_thread::sleep_until(start_time + (tick + 1) * kCyclicCommandPeriod);
}
const auto hold_start_time = std::chrono::steady_clock::now();
const auto hold_ticks = kHoldAfterTrajectoryDuration / kCyclicCommandPeriod;
for (std::int64_t tick = 0; tick <= hold_ticks; ++tick) {
for (std::size_t i = 0; i < motors.size(); ++i) {
ASSERT_TRUE(motors[i]->commandCyclicPosition(center_q[i], 0.0));
}
std::this_thread::sleep_until(hold_start_time + (tick + 1) * kCyclicCommandPeriod);
}
std::cout << "after four motor CSP trajectory" << std::endl;
for (std::size_t i = 0; i < motors.size(); ++i) {
printMotorState(kFourMotorIds[i], motors[i]);
}
const double reference_phase = error_stats.front().phaseRad();
const double rms_error_spread =
error_spread_sample_count > 0
? std::sqrt(sum_error_spread_sq / static_cast<double>(error_spread_sample_count))
: 0.0;
std::cout << "four motor CSP tracking error statistics, sample_period="
<< kStatsSamplePeriod.count()
<< " ms, samples=" << error_stats.front().sample_count
<< ", max_error_spread=" << max_error_spread_rad
<< " rad, rms_error_spread=" << rms_error_spread
<< " rad" << std::endl;
for (std::size_t i = 0; i < motors.size(); ++i) {
const double phase = error_stats[i].phaseRad();
const double relative_phase = normalizePhaseRad(phase - reference_phase);
const double relative_phase_ms = relative_phase / omega * 1000.0;
std::cout << " motor_id=" << kFourMotorIds[i]
<< ", mean_error=" << error_stats[i].mean()
<< " rad, rms_error=" << error_stats[i].rms()
<< " rad, max_abs_error=" << error_stats[i].max_abs_error
<< " rad, error_fundamental_amp="
<< error_stats[i].fundamentalAmplitude()
<< " rad, error_phase=" << phase
<< " rad (" << radToDeg(phase)
<< " deg), relative_phase_to_motor1=" << relative_phase
<< " rad (" << radToDeg(relative_phase)
<< " deg, " << relative_phase_ms
<< " ms)" << std::endl;
}
}
} // namespace cmvr::device

View File

@ -0,0 +1,559 @@
#include "devices/motor/bus_runtime/ethercat/include/ethercat_motor_bus_runtime.h"
#include <chrono>
#include <cmath>
#include <cstdint>
#include <iomanip>
#include <iostream>
#include <memory>
#include <sstream>
#include <thread>
#include <utility>
#include <gtest/gtest.h>
#include "cmvr/msgs/cia402.pb.h"
#include "devices/motor/abstract_motor.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"
namespace cmvr::device {
namespace {
constexpr int kMotorId = 1;
constexpr std::chrono::milliseconds kModeSettleDelay{100};
constexpr std::chrono::milliseconds kCommandSamplePeriod{100};
constexpr std::chrono::milliseconds kCyclicCommandPeriod{1};
constexpr std::chrono::milliseconds kFeedbackSampleDuration{5000};
constexpr double kDefaultGearRatio = 101.0;
constexpr double kEncoderCountsPerMotorRev = 65536.0;
constexpr double kPi = 3.14159265358979323846;
config::MotorGroupConfig createSingleSlaveGroup()
{
config::MotorGroupConfig group;
group.set_id("eyou_motor_real_test");
group.set_bus_type(config::MOTOR_BUS_ETHERCAT);
group.set_vendor(config::MOTOR_VENDOR_EYOU);
group.set_protocol(config::MOTOR_PROTOCOL_ETHERCAT_CIA402);
auto* ethercat = group.mutable_ethercat();
ethercat->set_master_index(0);
ethercat->set_cycle_us(1000);
ethercat->set_slave_op_timeout_ms(12000);
ethercat->set_slave_state_poll_period_ms(10);
auto* cia402 = ethercat->mutable_cia402();
cia402->set_profile_position_trigger_delay_ms(2);
cia402->set_state_transition_timeout_ms(1200);
cia402->set_velocity_stop_timeout_ms(2000);
cia402->set_status_poll_period_ms(10);
cia402->set_stopped_velocity_tolerance_rad_s(0.001);
auto* dc = ethercat->mutable_dc();
dc->set_enable(true);
dc->set_reference_motor_id(kMotorId);
dc->set_sync0_cycle_us(1000);
dc->set_sync0_shift_us(0);
dc->set_sync_reference_clock_period(1);
dc->set_assign_activate(768);
dc->set_sync_monitor_period_ms(1000);
auto* slave = ethercat->add_slaves();
slave->set_motor_id(kMotorId);
slave->set_alias(0);
slave->set_position(0);
return group;
}
class RuntimeStopGuard {
public:
explicit RuntimeStopGuard(std::shared_ptr<EthercatMotorBusRuntime> runtime)
: runtime_(std::move(runtime))
{
}
~RuntimeStopGuard()
{
if (runtime_) {
runtime_->stop();
}
}
private:
std::shared_ptr<EthercatMotorBusRuntime> runtime_;
};
std::shared_ptr<EthercatMotorBusRuntime> startRuntime()
{
auto runtime = std::make_shared<EthercatMotorBusRuntime>();
runtime->setPdoMapping(createEyouCia402PdoMapping());
if (!runtime->init(createSingleSlaveGroup())) {
return nullptr;
}
if (!runtime->start()) {
runtime->stop();
return nullptr;
}
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
return runtime;
}
std::shared_ptr<Cia402Protocol> createProtocol(
const std::shared_ptr<EthercatMotorBusRuntime>& runtime)
{
return std::make_shared<Cia402Protocol>(runtime, runtime->config().cia402());
}
config::MotorConfigItem createMotorConfig()
{
config::MotorConfigItem config;
config.set_id(kMotorId);
config.set_joint_name("ethercat_test_joint");
config.set_limit_q_lb(-6.14);
config.set_limit_q_ub(6.14);
config.set_limit_qd(10.0);
config.set_limit_qdd(10.0);
config.set_encoder_counts_per_rev(kEncoderCountsPerMotorRev);
config.set_gear_ratio(kDefaultGearRatio);
return config;
}
std::unique_ptr<AbstractMotor> createMotor(
const std::shared_ptr<EthercatMotorBusRuntime>& runtime)
{
auto motor = std::make_unique<EyouMotor>(
createMotorConfig(),
createProtocol(runtime),
std::make_unique<EyouMotorAdapter>(runtime));
if (!motor->init()) {
return nullptr;
}
return motor;
}
void printMotorState(const char* label, AbstractMotor& motor)
{
std::cout << label
<< ": motor_q=" << motor.getQ() << " rad"
<< ", motor_qd=" << motor.getQd() << " rad/s"
<< std::endl;
}
std::string hex16(const std::uint16_t value)
{
std::ostringstream oss;
oss << "0x" << std::uppercase << std::hex << std::setw(4) << std::setfill('0')
<< value;
return oss.str();
}
void printRawEthercatFeedback(const char* label,
const std::shared_ptr<EthercatMotorBusRuntime>& runtime)
{
std::uint16_t statusword = 0;
std::int8_t mode_display = 0;
std::int32_t actual_position = 0;
std::int32_t actual_velocity = 0;
std::int16_t actual_torque = 0;
std::uint16_t error_code = 0;
runtime->readPdo<std::uint16_t>(kMotorId, msgs::CIA402_STATUS_WORD_6041, 0x00, statusword);
runtime->readPdo<std::int8_t>(kMotorId, msgs::CIA402_MODE_DISPLAY_6061, 0x00, mode_display);
runtime->readPdo<std::int32_t>(kMotorId, msgs::CIA402_ACTUAL_POSITION_6064, 0x00,
actual_position);
runtime->readPdo<std::int32_t>(kMotorId, msgs::CIA402_ACTUAL_VELOCITY_606C, 0x00,
actual_velocity);
runtime->readPdo<std::int16_t>(kMotorId, msgs::CIA402_ACTUAL_TORQUE_6077, 0x00,
actual_torque);
runtime->readPdo<std::uint16_t>(kMotorId, msgs::CIA402_ERROR_CODE_603F, 0x00, error_code);
std::cout << label
<< ": statusword=" << hex16(statusword)
<< ", mode_display=" << static_cast<int>(mode_display)
<< ", actual_position=" << actual_position
<< ", actual_velocity=" << actual_velocity
<< ", actual_torque=" << actual_torque
<< ", error_code=" << hex16(error_code)
<< std::endl;
}
void sampleMotorState(AbstractMotor& motor,
const std::chrono::milliseconds duration)
{
for (auto elapsed = std::chrono::milliseconds{0};
elapsed < duration;
elapsed += kCommandSamplePeriod) {
std::this_thread::sleep_for(kCommandSamplePeriod);
std::cout << "t=" << (elapsed + kCommandSamplePeriod).count() << " ms";
printMotorState("", motor);
}
}
double nearbySafeTarget(const double current_q, const double delta_rad)
{
return current_q + (current_q > 0.0 ? -std::abs(delta_rad) : std::abs(delta_rad));
}
} // namespace
TEST(EyouMotorRealTest, ReadMotorStateOnly)
{
auto runtime = startRuntime();
ASSERT_NE(runtime, nullptr);
RuntimeStopGuard runtime_guard(runtime);
auto motor = createMotor(runtime);
ASSERT_NE(motor, nullptr);
ASSERT_TRUE(motor->torqueOn());
printMotorState("motor state", *motor);
printRawEthercatFeedback("raw feedback", runtime);
for (int i = 1; i <= 10; ++i) {
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
printMotorState("motor state", *motor);
printRawEthercatFeedback("raw feedback", runtime);
}
}
TEST(EyouMotorRealTest, CalibrateZeroQPrintBeforeAndAfter)
{
auto runtime = startRuntime();
ASSERT_NE(runtime, nullptr);
RuntimeStopGuard runtime_guard(runtime);
auto motor = createMotor(runtime);
ASSERT_NE(motor, nullptr);
ASSERT_TRUE(motor->torqueOff());
printMotorState("before calibrateZeroQ", *motor);
ASSERT_TRUE(motor->calibrateZeroQ());
printMotorState("after calibrateZeroQ", *motor);
ASSERT_TRUE(motor->torqueOn());
}
TEST(EyouMotorRealTest, CommandProfilePosition)
{
auto runtime = startRuntime();
ASSERT_NE(runtime, nullptr);
RuntimeStopGuard runtime_guard(runtime);
auto motor = createMotor(runtime);
ASSERT_NE(motor, nullptr);
ASSERT_TRUE(motor->torqueOn());
motor->setMode(msgs::RUN_MODE_PROFILE_POSITION);
std::this_thread::sleep_for(kModeSettleDelay);
ASSERT_TRUE(motor->commandProfilePosition(-3.0, 0.5, 1.0));
sampleMotorState(*motor, kFeedbackSampleDuration);
}
TEST(EyouMotorRealTest, CommandProfileVelocity)
{
auto runtime = startRuntime();
ASSERT_NE(runtime, nullptr);
RuntimeStopGuard runtime_guard(runtime);
auto motor = createMotor(runtime);
ASSERT_NE(motor, nullptr);
ASSERT_TRUE(motor->torqueOn());
motor->setMode(msgs::RUN_MODE_PROFILE_VELOCITY);
std::this_thread::sleep_for(kModeSettleDelay);
std::cout << "motor.commandProfileVelocity(0.3 rad/s, 1.0 rad/s^2)" << std::endl;
ASSERT_TRUE(motor->commandProfileVelocity(0.3, 1.0));
sampleMotorState(*motor, kFeedbackSampleDuration);
std::cout << "motor.commandProfileVelocity(0 rad/s, 1.0 rad/s^2)" << std::endl;
ASSERT_TRUE(motor->commandProfileVelocity(0.0, 1.0));
sampleMotorState(*motor, std::chrono::milliseconds{1000});
}
TEST(EyouMotorRealTest, CommandCyclicPosition)
{
auto runtime = startRuntime();
ASSERT_NE(runtime, nullptr);
RuntimeStopGuard runtime_guard(runtime);
auto motor = createMotor(runtime);
ASSERT_NE(motor, nullptr);
ASSERT_TRUE(motor->torqueOn());
motor->setMode(msgs::RUN_MODE_CYCLIC_SYNC_POSITION);
std::this_thread::sleep_for(kModeSettleDelay);
const std::chrono::milliseconds trajectory_duration{15000};
const double period_s = 6.0;
const double amplitude_rad = 3;
const double phase_rad = 0.0;
const double center_q = motor->getQ();
const double omega = 2.0 * kPi / period_s;
std::cout << "motor.commandCyclicPosition(sin), center_q=" << center_q
<< " rad, period=" << period_s
<< " s, amplitude=" << amplitude_rad
<< " rad, phase=" << phase_rad
<< " rad, command_period=" << kCyclicCommandPeriod.count()
<< " ms" << std::endl;
const auto start_time = std::chrono::steady_clock::now();
const auto total_ticks = trajectory_duration / kCyclicCommandPeriod;
for (std::int64_t tick = 0; tick <= total_ticks; ++tick) {
const auto elapsed = tick * kCyclicCommandPeriod;
const double t_s = static_cast<double>(elapsed.count()) / 1000.0;
const double theta = omega * t_s + phase_rad;
const double target_q = center_q + amplitude_rad * std::sin(theta);
const double target_qd = amplitude_rad * omega * std::cos(theta);
ASSERT_TRUE(motor->commandCyclicPosition(target_q, target_qd));
if (elapsed.count() % kCommandSamplePeriod.count() == 0) {
std::cout << "t=" << elapsed.count()
<< " ms, target_q=" << target_q
<< " rad, target_qd=" << target_qd
<< " rad/s";
printMotorState("", *motor);
}
std::this_thread::sleep_until(start_time + (tick + 1) * kCyclicCommandPeriod);
}
}
TEST(EyouMotorRealTest, CommandCyclicVelocity)
{
auto runtime = startRuntime();
ASSERT_NE(runtime, nullptr);
RuntimeStopGuard runtime_guard(runtime);
auto motor = createMotor(runtime);
ASSERT_NE(motor, nullptr);
ASSERT_TRUE(motor->torqueOn());
motor->setMode(msgs::RUN_MODE_CYCLIC_SYNC_VELOCITY);
std::this_thread::sleep_for(kModeSettleDelay);
const std::chrono::milliseconds trajectory_duration{15000};
const double period_s = 6.0;
const double velocity_amplitude_rad_s = 5.0;
const double phase_rad = 0.0;
const double omega = 2.0 * kPi / period_s;
std::cout << "motor.commandCyclicVelocity(sin), period=" << period_s
<< " s, velocity_amplitude=" << velocity_amplitude_rad_s
<< " rad/s, phase=" << phase_rad
<< " rad, command_period=" << kCyclicCommandPeriod.count()
<< " ms" << std::endl;
const auto start_time = std::chrono::steady_clock::now();
const auto total_ticks = trajectory_duration / kCyclicCommandPeriod;
for (std::int64_t tick = 0; tick <= total_ticks; ++tick) {
const auto elapsed = tick * kCyclicCommandPeriod;
const double t_s = static_cast<double>(elapsed.count()) / 1000.0;
const double theta = omega * t_s + phase_rad;
const double target_qd = velocity_amplitude_rad_s * std::sin(theta);
ASSERT_TRUE(motor->commandCyclicVelocity(target_qd));
if (elapsed.count() % kCommandSamplePeriod.count() == 0) {
std::cout << "t=" << elapsed.count()
<< " ms, target_qd=" << target_qd
<< " rad/s";
printMotorState("", *motor);
}
std::this_thread::sleep_until(start_time + (tick + 1) * kCyclicCommandPeriod);
}
std::cout << "motor.commandCyclicVelocity(0 rad/s)" << std::endl;
ASSERT_TRUE(motor->commandCyclicVelocity(0.0));
sampleMotorState(*motor, std::chrono::milliseconds{500});
}
TEST(EyouMotorRealTest, QuickStopAfterTwoSeconds)
{
auto runtime = startRuntime();
ASSERT_NE(runtime, nullptr);
RuntimeStopGuard runtime_guard(runtime);
auto motor = createMotor(runtime);
ASSERT_NE(motor, nullptr);
ASSERT_TRUE(motor->torqueOff());
ASSERT_TRUE(motor->calibrateZeroQ());
ASSERT_TRUE(motor->torqueOn());
motor->setMode(msgs::RUN_MODE_CYCLIC_SYNC_VELOCITY);
std::this_thread::sleep_for(kModeSettleDelay);
const std::chrono::milliseconds run_duration{2000};
const double period_s = 6.0;
const double velocity_amplitude_rad_s = 4.5;
const double phase_rad = 0.0;
const double omega = 2.0 * kPi / period_s;
std::cout << "motor.commandCyclicVelocity(sin), then quickStop at "
<< run_duration.count()
<< " ms, period=" << period_s
<< " s, velocity_amplitude=" << velocity_amplitude_rad_s
<< " rad/s, phase=" << phase_rad
<< " rad, command_period=" << kCyclicCommandPeriod.count()
<< " ms" << std::endl;
const auto start_time = std::chrono::steady_clock::now();
const auto total_ticks = run_duration / kCyclicCommandPeriod;
for (std::int64_t tick = 0; tick <= total_ticks; ++tick) {
const auto elapsed = tick * kCyclicCommandPeriod;
const double t_s = static_cast<double>(elapsed.count()) / 1000.0;
const double theta = omega * t_s + phase_rad;
const double target_qd = velocity_amplitude_rad_s * std::sin(theta);
ASSERT_TRUE(motor->commandCyclicVelocity(target_qd));
if (elapsed.count() % kCommandSamplePeriod.count() == 0) {
std::cout << "t=" << elapsed.count()
<< " ms, target_qd=" << target_qd
<< " rad/s";
printMotorState("", *motor);
printRawEthercatFeedback("raw feedback", runtime);
}
std::this_thread::sleep_until(start_time + (tick + 1) * kCyclicCommandPeriod);
}
std::cout << "motor.quickStop()" << std::endl;
ASSERT_TRUE(motor->quickStop());
printMotorState("after quickStop", *motor);
printRawEthercatFeedback("raw feedback", runtime);
sampleMotorState(*motor, std::chrono::milliseconds{1000});
printRawEthercatFeedback("raw feedback", runtime);
}
TEST(EyouMotorRealTest, QuickStopInProfilePosition)
{
auto runtime = startRuntime();
ASSERT_NE(runtime, nullptr);
RuntimeStopGuard runtime_guard(runtime);
auto motor = createMotor(runtime);
ASSERT_NE(motor, nullptr);
ASSERT_TRUE(motor->torqueOff());
ASSERT_TRUE(motor->calibrateZeroQ());
ASSERT_TRUE(motor->torqueOn());
motor->setMode(msgs::RUN_MODE_PROFILE_POSITION);
std::this_thread::sleep_for(kModeSettleDelay);
const std::chrono::milliseconds quick_stop_time{1000};
const double start_q = motor->getQ();
const double target_q = nearbySafeTarget(start_q, 4.0);
const double max_qd = 2.0;
const double max_qdd = 10.0;
std::cout << "motor.commandProfilePosition(" << target_q
<< " rad, " << max_qd
<< " rad/s, " << max_qdd
<< " rad/s^2), then quickStop at "
<< quick_stop_time.count() << " ms" << std::endl;
ASSERT_TRUE(motor->commandProfilePosition(target_q, max_qd, max_qdd));
std::cout << "wait " << quick_stop_time.count()
<< " ms before quickStop" << std::endl;
sampleMotorState(*motor, quick_stop_time);
printRawEthercatFeedback("raw feedback before quickStop", runtime);
std::cout << "motor.quickStop()" << std::endl;
ASSERT_TRUE(motor->quickStop());
printMotorState("after quickStop", *motor);
printRawEthercatFeedback("raw feedback", runtime);
sampleMotorState(*motor, std::chrono::milliseconds{1000});
printRawEthercatFeedback("raw feedback", runtime);
}
TEST(EyouMotorRealTest, QuickStopInProfileVelocity)
{
auto runtime = startRuntime();
ASSERT_NE(runtime, nullptr);
RuntimeStopGuard runtime_guard(runtime);
auto motor = createMotor(runtime);
ASSERT_NE(motor, nullptr);
ASSERT_TRUE(motor->torqueOff());
ASSERT_TRUE(motor->calibrateZeroQ());
ASSERT_TRUE(motor->torqueOn());
motor->setMode(msgs::RUN_MODE_PROFILE_VELOCITY);
std::this_thread::sleep_for(kModeSettleDelay);
const std::chrono::milliseconds quick_stop_time{2000};
const double target_qd = motor->getQ() > 0.0 ? -2.0 : 2.0;
const double max_qdd = 10.0;
std::cout << "motor.commandProfileVelocity(" << target_qd
<< " rad/s, " << max_qdd
<< " rad/s^2), then quickStop at "
<< quick_stop_time.count() << " ms" << std::endl;
ASSERT_TRUE(motor->commandProfileVelocity(target_qd, max_qdd));
std::cout << "wait " << quick_stop_time.count()
<< " ms before quickStop" << std::endl;
sampleMotorState(*motor, quick_stop_time);
printRawEthercatFeedback("raw feedback before quickStop", runtime);
std::cout << "motor.quickStop()" << std::endl;
ASSERT_TRUE(motor->quickStop());
printMotorState("after quickStop", *motor);
printRawEthercatFeedback("raw feedback", runtime);
sampleMotorState(*motor, std::chrono::milliseconds{1000});
printRawEthercatFeedback("raw feedback", runtime);
}
TEST(EyouMotorRealTest, QuickStopInCyclicPosition)
{
auto runtime = startRuntime();
ASSERT_NE(runtime, nullptr);
RuntimeStopGuard runtime_guard(runtime);
auto motor = createMotor(runtime);
ASSERT_NE(motor, nullptr);
ASSERT_TRUE(motor->torqueOff());
ASSERT_TRUE(motor->calibrateZeroQ());
ASSERT_TRUE(motor->torqueOn());
motor->setMode(msgs::RUN_MODE_CYCLIC_SYNC_POSITION);
std::this_thread::sleep_for(kModeSettleDelay);
const std::chrono::milliseconds run_duration{2000};
const double start_q = motor->getQ();
const double target_qd = start_q > 0.0 ? -2.0 : 2.0;
std::cout << "motor.commandCyclicPosition(linear), start_q=" << start_q
<< " rad, target_qd=" << target_qd
<< " rad/s, then quickStop at "
<< run_duration.count()
<< " ms, command_period=" << kCyclicCommandPeriod.count()
<< " ms" << std::endl;
const auto start_time = std::chrono::steady_clock::now();
const auto total_ticks = run_duration / kCyclicCommandPeriod;
for (std::int64_t tick = 0; tick <= total_ticks; ++tick) {
const auto elapsed = tick * kCyclicCommandPeriod;
const double t_s = static_cast<double>(elapsed.count()) / 1000.0;
const double target_q = start_q + target_qd * t_s;
ASSERT_TRUE(motor->commandCyclicPosition(target_q, target_qd));
if (elapsed.count() % kCommandSamplePeriod.count() == 0) {
std::cout << "t=" << elapsed.count()
<< " ms, target_q=" << target_q
<< " rad, target_qd=" << target_qd
<< " rad/s";
printMotorState("", *motor);
printRawEthercatFeedback("raw feedback", runtime);
}
std::this_thread::sleep_until(start_time + (tick + 1) * kCyclicCommandPeriod);
}
std::cout << "motor.quickStop()" << std::endl;
ASSERT_TRUE(motor->quickStop());
printMotorState("after quickStop", *motor);
printRawEthercatFeedback("raw feedback", runtime);
sampleMotorState(*motor, std::chrono::milliseconds{1000});
printRawEthercatFeedback("raw feedback", runtime);
}
} // namespace cmvr::device

View File

@ -23,19 +23,25 @@ public:
void setMode(msgs::RunMode mode) override;
msgs::RunMode getMode() override;
void torqueOff() override;
bool torqueOn() override;
bool torqueOff() override;
bool brakeRelease() override;
bool quickStop() override;
void setLimitQ(double ub, double lb) override;
void setLimitQd(double qd) override;
void setLimitQdd(double u_qdd, double l_qdd) override;
void brake() override;
void setQ(double q) override;
void setTarget(double q, double qd) override;
void setTarget(double qd) override;
bool calibrateZeroQ() override;
bool reachedTargetQ() override;
void setQd(double qd) override;
bool commandProfilePosition(double target_q,
double max_qd = 0.0,
double max_qdd = 0.0) override;
bool commandProfileVelocity(double target_qd, double max_qdd = 0.0) override;
bool commandCyclicPosition(double target_q,
double target_qd = 0.0) override;
bool commandCyclicVelocity(double target_qd) override;
bool commandCyclicTorque(double target_tau) override;
double getQ() override;
double getQd() override;
@ -44,6 +50,7 @@ public:
const std::vector<double>& velocities);
private:
bool holdPosition_();
double clampQ_(double q) const;
double clampQd_(double qd) const;
std::shared_ptr<simulate::MujocoWorld> worldLocked_() const;

View File

@ -54,11 +54,27 @@ msgs::RunMode MujocoMotor::getMode()
return mode_;
}
void MujocoMotor::torqueOff()
bool MujocoMotor::torqueOn()
{
brake();
return holdPosition_();
}
bool MujocoMotor::torqueOff()
{
const bool ok = holdPosition_();
std::scoped_lock lock(mtx_);
mode_ = msgs::RUN_MODE_UNSPECIFIED;
return ok;
}
bool MujocoMotor::brakeRelease()
{
return true;
}
bool MujocoMotor::quickStop()
{
return holdPosition_();
}
void MujocoMotor::setLimitQ(const double ub, const double lb)
@ -82,38 +98,78 @@ void MujocoMotor::setLimitQdd(const double u_qdd, const double l_qdd)
info_.limit_qdd = std::max(limit_qdd_upper_, std::abs(limit_qdd_lower_));
}
void MujocoMotor::brake()
bool MujocoMotor::holdPosition_()
{
const auto world = worldLocked_();
double q = 0.0;
if (!world || !world->getJointPosition(info_.joint_name, q)) {
return;
return false;
}
std::scoped_lock lock(mtx_);
target_q_ = q;
mode_ = msgs::RUN_MODE_CYCLIC_SYNC_POSITION;
world->setJointTargetState(info_.joint_name, q, 0.0);
return true;
}
void MujocoMotor::setQ(const double q)
bool MujocoMotor::commandProfilePosition(const double target_q,
const double max_qd,
const double max_qdd)
{
setTarget(q, 0.0);
(void)max_qdd;
std::scoped_lock lock(mtx_);
const auto world = worldLocked_();
if (!world) {
return false;
}
mode_ = msgs::RUN_MODE_PROFILE_POSITION;
target_q_ = clampQ_(target_q);
const double profile_qd = max_qd > 0.0 ? max_qd : info_.limit_qd;
return world->setJointTargetState(info_.joint_name, target_q_, clampQd_(profile_qd));
}
void MujocoMotor::setTarget(const double q, const double qd)
bool MujocoMotor::commandProfileVelocity(const double target_qd, const double max_qdd)
{
(void)max_qdd;
std::scoped_lock lock(mtx_);
const auto world = worldLocked_();
if (!world) {
return false;
}
mode_ = msgs::RUN_MODE_PROFILE_VELOCITY;
return world->setJointTargetVelocity(info_.joint_name, clampQd_(target_qd));
}
bool MujocoMotor::commandCyclicPosition(const double target_q,
const double target_qd)
{
std::scoped_lock lock(mtx_);
const auto world = worldLocked_();
if (!world) {
return;
return false;
}
target_q_ = clampQ_(q);
world->setJointTargetState(info_.joint_name, target_q_, clampQd_(qd));
mode_ = msgs::RUN_MODE_CYCLIC_SYNC_POSITION;
target_q_ = clampQ_(target_q);
return world->setJointTargetState(info_.joint_name, target_q_, clampQd_(target_qd));
}
void MujocoMotor::setTarget(const double qd)
bool MujocoMotor::commandCyclicVelocity(const double target_qd)
{
setQd(qd);
std::scoped_lock lock(mtx_);
const auto world = worldLocked_();
if (!world) {
return false;
}
mode_ = msgs::RUN_MODE_CYCLIC_SYNC_VELOCITY;
return world->setJointTargetVelocity(info_.joint_name, clampQd_(target_qd));
}
bool MujocoMotor::commandCyclicTorque(const double target_tau)
{
(void)target_tau;
CMVR_LOG(ERROR) << "[MujocoMotor] cyclic torque command is not implemented: "
<< info_.joint_name;
return false;
}
bool MujocoMotor::calibrateZeroQ()
@ -140,16 +196,6 @@ bool MujocoMotor::reachedTargetQ()
}
}
void MujocoMotor::setQd(const double qd)
{
std::scoped_lock lock(mtx_);
const auto world = worldLocked_();
if (!world) {
return;
}
world->setJointTargetVelocity(info_.joint_name, clampQd_(qd));
}
double MujocoMotor::getQ()
{
const auto world = worldLocked_();

View File

@ -22,6 +22,8 @@ namespace cmvr {
info_.limit_q_ub = config.limit_q_ub();
info_.limit_qd = config.limit_qd() > 0.0 ? config.limit_qd() : 0.5;
info_.limit_qdd = config.limit_qdd() > 0.0 ? config.limit_qdd() : 10.0;
encoder_counts_per_rev_ = config.encoder_counts_per_rev();
gear_ratio_ = config.gear_ratio();
node_id_ = info_.id;
}
@ -37,6 +39,19 @@ namespace cmvr {
}
if (protocol_->comm_proto == MotorProtocolInterface::CommProto::CANOPEN ) {
auto canopen_protocol = std::dynamic_pointer_cast<Ti5MotorCanopenProtocol>(protocol_);
if (!canopen_protocol) {
CMVR_LOG(ERROR) << "[Ti5Motor] invalid CANopen protocol for motor: "
<< info_.joint_name;
return false;
}
if (encoder_counts_per_rev_ <= 0.0 || gear_ratio_ <= 0.0) {
CMVR_LOG(ERROR) << "[Ti5Motor] missing encoder conversion config: "
<< info_.joint_name
<< ", encoder_counts_per_rev=" << encoder_counts_per_rev_
<< ", gear_ratio=" << gear_ratio_;
return false;
}
protocol_->setMotorConversion(node_id_, encoder_counts_per_rev_, gear_ratio_);
// torqueOff(node_id_);
canopen_protocol->seedNmtRequest(node_id_,msgs::NMT_RESET_COMMUNICATION);
// canopen_protocol->torqueOff(node_id_);
@ -45,8 +60,8 @@ namespace cmvr {
canopen_protocol->seedNmtRequest(node_id_,msgs::NMT_ENTER_PRE_OPERATIONAL);
canopen_protocol->seedNmtRequest(node_id_,msgs::NMT_START_REMOTE_NODE);
canopen_protocol->setMode(node_id_,msgs::RUN_MODE_CYCLIC_SYNC_POSITION);
// canopen_protocol->seedSdoRequest(node_id_, msgs::CS_WRITE_TWO_BYTES, msgs::CONTROL_WORD_6040, msgs::SUB_INDEX_0, 0x06,15);
// canopen_protocol->seedSdoRequest(node_id_, msgs::CS_WRITE_TWO_BYTES, msgs::CONTROL_WORD_6040, msgs::SUB_INDEX_0, 0x0F,15);
// canopen_protocol->seedSdoRequest(node_id_, msgs::CS_WRITE_TWO_BYTES, msgs::CIA402_CONTROL_WORD_6040, msgs::SUB_INDEX_0, 0x06,15);
// canopen_protocol->seedSdoRequest(node_id_, msgs::CS_WRITE_TWO_BYTES, msgs::CIA402_CONTROL_WORD_6040, msgs::SUB_INDEX_0, 0x0F,15);
canopen_protocol->setLimitQ(node_id_, info_.limit_q_ub, info_.limit_q_lb);
canopen_protocol->setLimitQd(node_id_, info_.limit_qd);
canopen_protocol->setLimitQdd(node_id_, info_.limit_qdd, -info_.limit_qdd);
@ -55,6 +70,10 @@ namespace cmvr {
}
return true;
}
private:
double encoder_counts_per_rev_{0.0};
double gear_ratio_{0.0};
};

View File

@ -18,6 +18,7 @@
#include "motor/drivers/ti5_canopen/include/protocol/ti5_motor_rpdo1.h"
#include "motor/drivers/ti5_canopen/include/protocol/ti5_motor_rpdo2.h"
#include <cmath>
#include <unordered_map>
namespace cmvr {
namespace device {
@ -29,28 +30,40 @@ namespace cmvr {
bool initNode(uint8_t node_id) override;
void setMode(uint8_t node_id, msgs::RunMode mode);
void setTarget(uint8_t node_id, double angle_rad, double vel) override;
void setTarget(uint8_t node_id, double vel) override;
void setQ(uint8_t node_id, double angle_rad) override;
void setMode(uint8_t node_id, msgs::RunMode mode) override;
void setLimitQ(uint8_t node_id, double ub, double lb) override;
void setLimitQd(uint8_t node_id, double qd) override;
void setLimitQdd(uint8_t node_id, double u_qdd,double l_qdd) override;
bool calibrateZeroQ(uint8_t node_id) override;
void brake(uint8_t node_id) override;
bool torqueOn(uint8_t node_id) override;
bool torqueOff(uint8_t node_id) override;
bool brakeRelease(uint8_t node_id) override;
bool quickStop(uint8_t node_id) override;
bool reachedTargetQ(uint8_t node_id) override;
double getQ(uint8_t node_id) override;
double getQd(uint8_t node_id) override;
void setQd(uint8_t node_id, double qd) override;
void setQdd(uint8_t node_id, double qdd) override;
void torqueOff(uint8_t node_id) override;
bool commandProfilePosition(uint8_t node_id,
double target_q,
double max_qd,
double max_qdd) override;
bool commandProfileVelocity(uint8_t node_id,
double target_qd,
double max_qdd) override;
bool commandCyclicPosition(uint8_t node_id,
double target_q,
double target_qd) override;
bool commandCyclicVelocity(uint8_t node_id,
double target_qd) override;
bool commandCyclicTorque(uint8_t node_id, double target_tau) override;
void setMotorConversion(uint8_t node_id,
double encoder_counts_per_rev,
double gear_ratio) override;
void seedNmtRequest(uint8_t node_id, msgs::NmtCommand command, uint32_t delay_ms = 10);
void seedSdoRequest(uint8_t node_id, msgs::CommandSpecifier cs, msgs::ObIndex index,
msgs::ObSubIndex sub_index, uint32_t data, uint32_t delay_ms = 10);
void seedSdoRequest(uint8_t node_id, msgs::CommandSpecifier cs, uint32_t index,
uint32_t sub_index, uint32_t data, uint32_t delay_ms = 10);
void configProfile(uint8_t node_id, uint32_t speed, uint32_t accel, uint32_t decel);
void configPdo(uint8_t node_id);
@ -70,14 +83,20 @@ namespace cmvr {
private:
static constexpr double GearRatio = 101.0; // 电机减速比
static constexpr double RADTODEG = 180.0 / M_PI;
static constexpr double Ti5VelocityUnitScale = 100.0;
static constexpr double Ti5AccelerationTimeScale = 1000.0;
struct MotorConversion {
double encoder_counts_per_rev{0.0};
double gear_ratio{0.0};
};
std::shared_ptr<AbstractCanbus> can_client_{nullptr};
// key node_id
// std::unordered_map<uint8_t,msgs::RunMode> cur_mode_{};
std::unordered_map<uint8_t,uint32_t> last_Qd_{};
std::unordered_map<uint8_t,uint32_t> last_Qdd_{};
std::unordered_map<uint8_t, MotorConversion> motor_conversions_{};
std::shared_ptr<device::CanSender<msgs::RobotDetail> > can_sender_{nullptr};
std::shared_ptr<device::MessageManager<msgs::RobotDetail> > message_manager_{nullptr};
@ -94,11 +113,7 @@ namespace cmvr {
std::map<uint8_t, motor::Ti5MotorRPDO1 *> rpdo1_commands_{};
std::map<uint8_t, motor::Ti5MotorRPDO2 *> rpdo2_commands_{};
void setPPTargetPosBySdo(uint8_t node_id, int32_t pos);
void setPPTargetPosByPdo(uint8_t node_id, int32_t pos);
void setCSPTargetPosByPdo(uint8_t node_id, int32_t pos);
void writeProfilePositionTargetBySdo(uint8_t node_id, int32_t pos);
void configTPDO1(uint8_t node_id);
@ -107,6 +122,14 @@ namespace cmvr {
void configRPDO1(uint8_t node_id, bool enable);
void configRPDO2(uint8_t node_id, bool enable);
const MotorConversion* conversionForNode(uint8_t node_id) const;
double radToCounts(double angle_rad, const MotorConversion& conversion) const;
double countsToRad(int32_t counts, const MotorConversion& conversion) const;
double radPerSecToVelocityRaw(double velocity_rad_s, const MotorConversion& conversion) const;
uint32_t radPerSec2ToAccelerationRaw(double acceleration_rad_s2,
const MotorConversion& conversion) const;
double velocityRawToRadPerSec(int32_t velocity_raw, const MotorConversion& conversion) const;
bool waitUntil(std::function<bool()> condition, int timeout_ms) {
auto start = std::chrono::steady_clock::now();

View File

@ -3,6 +3,7 @@
// Created by lgv on 2025/7/24.
//
#include "cmvr/msgs/cia402.pb.h"
#include "motor/drivers/ti5_canopen/include/protocol/ti5_motor_sdo_response.h"
using namespace cmvr::device::motor;
@ -19,17 +20,17 @@ void Ti5MotorSdoResponse::ParseSdoData(const msgs::SdoFrame &sdo_response,
switch (sdo_response.index()) {
case msgs::CONTROL_WORD_6040:
case msgs::CIA402_CONTROL_WORD_6040:
motor_status->set_ctrl_word(sdo_response.data());
break;
case msgs::STATUS_WORD_6041:
case msgs::CIA402_STATUS_WORD_6041:
motor_status->set_status_word(sdo_response.data());
break;
case msgs::ACTUAL_POSITION_6064:
case msgs::CIA402_ACTUAL_POSITION_6064:
motor_status->set_position(static_cast<int32_t>(sdo_response.data()));
CMVR_LOG(INFO) << "pos = " << motor_status->position();
break;
case msgs::POSITION_OFFSET_2008:
case msgs::CANOPEN_POSITION_OFFSET_2008:
motor_status->set_position_offset(sdo_response.data());
}
//

View File

@ -19,16 +19,4 @@ void Ti5MotorTPDO2::Parse(const std::uint8_t *bytes, int32_t length, msgs::Robot
motor_status->set_position(bytes[3] << 24 | bytes[2] << 16 | bytes[1] << 8 | bytes[0]);
motor_status->set_speed(bytes[7] << 24 | bytes[6] << 16 | bytes[5] << 8 | bytes[4]);
double gearRatio = 101.0;
double radToDeg = 180.0 / M_PI;
auto speed = (motor_status->speed() * 360.0) / (radToDeg * gearRatio * 100.0);
auto angle_rad = (motor_status->position() * 360.0) / (gearRatio * 65536.0 * radToDeg);
// CMVR_LOG(INFO) << " Motor ID " << int(this->node_id_) << " pos = " << angle_rad << " rad speed = " << speed << " rad/s";
}

View File

@ -3,6 +3,7 @@
// Created by lgv on 2025/8/1.
//
#include "cmvr/msgs/cia402.pb.h"
#include "motor/drivers/ti5_canopen/include/ti5_motor_canopen_protocol.h"
#include "canbus/canopen/register.h"
#include "motor/drivers/ti5_canopen/include/protocol/ti5_motor_tpdo1.h"
@ -94,45 +95,153 @@ bool Ti5MotorCanopenProtocol::initNode(uint8_t node_id) {
return ErrorCode::OK;
}
void Ti5MotorCanopenProtocol::setMotorConversion(
const uint8_t node_id,
const double encoder_counts_per_rev,
const double gear_ratio) {
motor_conversions_[node_id] = {encoder_counts_per_rev, gear_ratio};
}
void Ti5MotorCanopenProtocol::seedSdoRequest(uint8_t node_id, CommandSpecifier cs, ObIndex index, ObSubIndex sub_index,
const Ti5MotorCanopenProtocol::MotorConversion*
Ti5MotorCanopenProtocol::conversionForNode(const uint8_t node_id) const {
const auto it = motor_conversions_.find(node_id);
if (it != motor_conversions_.end() &&
it->second.encoder_counts_per_rev > 0.0 &&
it->second.gear_ratio > 0.0) {
return &it->second;
}
CMVR_LOG(ERROR) << "[Ti5MotorCanopenProtocol] missing conversion config for node "
<< static_cast<int>(node_id);
return nullptr;
}
double Ti5MotorCanopenProtocol::radToCounts(
const double angle_rad,
const MotorConversion& conversion) const {
return (angle_rad * RADTODEG) / 360.0 *
conversion.gear_ratio * conversion.encoder_counts_per_rev;
}
double Ti5MotorCanopenProtocol::countsToRad(
const int32_t counts,
const MotorConversion& conversion) const {
return (counts * 360.0) /
(conversion.gear_ratio * conversion.encoder_counts_per_rev * RADTODEG);
}
double Ti5MotorCanopenProtocol::radPerSecToVelocityRaw(
const double velocity_rad_s,
const MotorConversion& conversion) const {
return ((velocity_rad_s * RADTODEG) * conversion.gear_ratio * Ti5VelocityUnitScale) /
360.0;
}
uint32_t Ti5MotorCanopenProtocol::radPerSec2ToAccelerationRaw(
const double acceleration_rad_s2,
const MotorConversion& conversion) const {
const auto raw = ((std::abs(acceleration_rad_s2) * RADTODEG) *
conversion.gear_ratio * Ti5VelocityUnitScale) /
360.0 / Ti5AccelerationTimeScale;
return static_cast<uint32_t>(std::abs(raw));
}
double Ti5MotorCanopenProtocol::velocityRawToRadPerSec(
const int32_t velocity_raw,
const MotorConversion& conversion) const {
return (velocity_raw * 360.0) /
(conversion.gear_ratio * Ti5VelocityUnitScale * RADTODEG);
}
void Ti5MotorCanopenProtocol::seedSdoRequest(uint8_t node_id, CommandSpecifier cs, uint32_t index, uint32_t sub_index,
uint32_t data, uint32_t delay_ms) {
sdo_commands_[node_id]->SetFrameData(cs, index, sub_index, data);
can_sender_->Update(sdo_commands_[node_id]->ID());
std::this_thread::sleep_for(std::chrono::milliseconds(delay_ms));
}
void Ti5MotorCanopenProtocol::setQ(uint8_t node_id, double angle_rad) {
auto cmd = (angle_rad * RADTODEG) / 360.0 * GearRatio * 65536.0;
switch (getMode(node_id)) {
// case RUN_MODE_CYCLIC_SYNC_POSITION:
// setCSPTargetPosByPdo(node_id, static_cast<int32_t>(cmd));
// break;
case RUN_MODE_PROFILE_POSITION:
// setPPTargetPosByPdo(node_id, static_cast<int32_t>(cmd));
setPPTargetPosBySdo(node_id, static_cast<int32_t>(cmd));
break;
bool Ti5MotorCanopenProtocol::commandProfilePosition(uint8_t node_id,
double target_q,
double max_qd,
double max_qdd) {
const auto* conversion = conversionForNode(node_id);
if (!conversion) {
return false;
}
if (max_qd > 0.0) {
seedSdoRequest(node_id, CS_WRITE_FOUR_BYTES, CIA402_PROFILE_VELOCITY_6081,
SUB_INDEX_0,
static_cast<uint32_t>(std::abs(radPerSecToVelocityRaw(max_qd, *conversion))),
0);
}
if (max_qdd > 0.0) {
const auto accel = radPerSec2ToAccelerationRaw(max_qdd, *conversion);
seedSdoRequest(node_id, CS_WRITE_FOUR_BYTES, CIA402_PROFILE_ACCELERATION_6083,
SUB_INDEX_0, accel, 0);
seedSdoRequest(node_id, CS_WRITE_FOUR_BYTES, CIA402_PROFILE_DECELERATION_6084,
SUB_INDEX_0, accel, 0);
}
writeProfilePositionTargetBySdo(node_id, static_cast<int32_t>(radToCounts(target_q, *conversion)));
return true;
}
void Ti5MotorCanopenProtocol::setTarget(uint8_t node_id, double angle_rad, double vel) {
auto pos_cmd = (angle_rad * RADTODEG) / 360.0 * GearRatio * 65536.0;
auto speed = ((vel * RADTODEG) * GearRatio * 100.0) / 360.0;
bool Ti5MotorCanopenProtocol::commandProfileVelocity(uint8_t node_id,
double target_qd,
double max_qdd) {
const auto* conversion = conversionForNode(node_id);
if (!conversion) {
return false;
}
if (max_qdd > 0.0) {
const auto accel = radPerSec2ToAccelerationRaw(max_qdd, *conversion);
seedSdoRequest(node_id, CS_WRITE_FOUR_BYTES, CIA402_PROFILE_ACCELERATION_6083,
SUB_INDEX_0, accel, 0);
seedSdoRequest(node_id, CS_WRITE_FOUR_BYTES, CIA402_PROFILE_DECELERATION_6084,
SUB_INDEX_0, accel, 0);
}
const auto speed = radPerSecToVelocityRaw(target_qd, *conversion);
seedSdoRequest(node_id, CS_WRITE_FOUR_BYTES, CIA402_TARGET_VELOCITY_60FF,
SUB_INDEX_0,
static_cast<uint32_t>(static_cast<int32_t>(std::llround(speed))),
0);
return true;
}
bool Ti5MotorCanopenProtocol::commandCyclicPosition(uint8_t node_id,
double target_q,
double target_qd) {
const auto* conversion = conversionForNode(node_id);
if (!conversion) {
return false;
}
auto pos_cmd = radToCounts(target_q, *conversion);
auto speed = radPerSecToVelocityRaw(target_qd, *conversion);
rpdo1_commands_[node_id]->SetTargetPos(pos_cmd);
rpdo1_commands_[node_id]->SetTargetVel(uint32_t(std::abs(speed)));
can_sender_->Update(rpdo1_commands_[node_id]->ID());
return true;
}
void Ti5MotorCanopenProtocol::setTarget(uint8_t node_id, double vel) {
auto speed = ((vel * RADTODEG) * GearRatio * 100.0) / 360.0;
bool Ti5MotorCanopenProtocol::commandCyclicVelocity(uint8_t node_id,
double target_qd) {
const auto* conversion = conversionForNode(node_id);
if (!conversion) {
return false;
}
auto speed = radPerSecToVelocityRaw(target_qd, *conversion);
rpdo2_commands_[node_id]->SetTargetVel(int16_t(speed));
can_sender_->Update(rpdo2_commands_[node_id]->ID());
return true;
}
bool Ti5MotorCanopenProtocol::commandCyclicTorque(uint8_t node_id, double target_tau) {
(void)target_tau;
CMVR_LOG(ERROR) << "[Ti5MotorCanopenProtocol] cyclic torque command is not implemented, node="
<< static_cast<int>(node_id);
return false;
}
void Ti5MotorCanopenProtocol::setPPTargetPosBySdo(uint8_t node_id, int32_t pos) {
void Ti5MotorCanopenProtocol::writeProfilePositionTargetBySdo(uint8_t node_id, int32_t pos) {
controlword_t cw = {};
cw.switch_on = 1;
cw.enable_voltage = 1;
@ -141,44 +250,19 @@ void Ti5MotorCanopenProtocol::setPPTargetPosBySdo(uint8_t node_id, int32_t pos)
cw.change_set_immediately = 1;
// 1. 设置目标位置
seedSdoRequest(node_id, CS_WRITE_FOUR_BYTES, TARGET_POSITION_607A, SUB_INDEX_0, pos);
seedSdoRequest(node_id, CS_WRITE_FOUR_BYTES, CIA402_TARGET_POSITION_607A, SUB_INDEX_0, pos);
// 2. 设置触发位bit4 = 1
cw.new_set_point = 1;
seedSdoRequest(node_id, CS_WRITE_TWO_BYTES, CONTROL_WORD_6040, SUB_INDEX_0, cw.value);
seedSdoRequest(node_id, CS_WRITE_TWO_BYTES, CIA402_CONTROL_WORD_6040, SUB_INDEX_0, cw.value);
// 3. 清除触发位bit4 = 0准备下一次触发
cw.new_set_point = 0;
seedSdoRequest(node_id, CS_WRITE_TWO_BYTES, CONTROL_WORD_6040, SUB_INDEX_0, cw.value);
seedSdoRequest(node_id, CS_WRITE_TWO_BYTES, CIA402_CONTROL_WORD_6040, SUB_INDEX_0, cw.value);
}
void Ti5MotorCanopenProtocol::setPPTargetPosByPdo(uint8_t node_id, int32_t pos) {
// 触发目标位置运动
controlword_t cw;
cw.value = 0x0F;
cw.new_set_point = 1;
cw.change_set_immediately = 1;
rpdo1_commands_[node_id]->SetTargetPos(pos);
rpdo1_commands_[node_id]->SetCtrlWord(cw.value);
can_sender_->Update(rpdo1_commands_[node_id]->ID());
std::this_thread::sleep_for(std::chrono::milliseconds(10));
cw.new_set_point = 0;
rpdo1_commands_[node_id]->SetCtrlWord(cw.value);
can_sender_->Update(rpdo1_commands_[node_id]->ID());
}
void Ti5MotorCanopenProtocol::setCSPTargetPosByPdo(uint8_t node_id, int32_t pos) {
rpdo1_commands_[node_id]->SetTargetPos(pos);
rpdo1_commands_[node_id]->SetCtrlWord(0x0F);
can_sender_->Update(rpdo1_commands_[node_id]->ID());
}
void Ti5MotorCanopenProtocol::setMode(uint8_t node_id, msgs::RunMode mode) {
// cur_mode_[node_id] = mode;
@ -188,36 +272,36 @@ void Ti5MotorCanopenProtocol::setMode(uint8_t node_id, msgs::RunMode mode) {
controlword_t cw = {};
cw.quick_stop = 1;
cw.enable_voltage = 1;
seedSdoRequest(node_id, CS_WRITE_TWO_BYTES, CONTROL_WORD_6040, SUB_INDEX_0, cw.value, 20);
seedSdoRequest(node_id, CS_WRITE_TWO_BYTES, CIA402_CONTROL_WORD_6040, SUB_INDEX_0, cw.value, 20);
// configRPDO1(node_id, false);
// configRPDO2(node_id, false);
// 1 : 先设置模式
auto data = static_cast<uint32_t>(mode);
seedSdoRequest(node_id, CS_WRITE_ONE_BYTE, OPERATION_MODE_6060, SUB_INDEX_0, data);
seedSdoRequest(node_id, CS_WRITE_ONE_BYTE, CIA402_OPERATION_MODE_6060, SUB_INDEX_0, data);
// 3 : 状态机步进 —— Switch On & Enable Operation0x0F
cw.switch_on = 1;
cw.enable_operation = 1;
seedSdoRequest(node_id, CS_WRITE_TWO_BYTES, CONTROL_WORD_6040, SUB_INDEX_0, cw.value, 20);
seedSdoRequest(node_id, CS_WRITE_TWO_BYTES, CIA402_CONTROL_WORD_6040, SUB_INDEX_0, cw.value, 20);
switch (mode) {
case RUN_MODE_PROFILE_POSITION: {
// 4 : 设置目标位置(为当前位置)
auto cur_pos = GetRobotDetail()->motors().at(node_id).position();
seedSdoRequest(node_id, CS_WRITE_FOUR_BYTES, TARGET_POSITION_607A, SUB_INDEX_0, cur_pos);
seedSdoRequest(node_id, CS_WRITE_FOUR_BYTES, CIA402_TARGET_POSITION_607A, SUB_INDEX_0, cur_pos);
// 5 : 触发位置运动new_set_point 翻转)
cw.new_set_point = 1;
cw.change_set_immediately = 1;
seedSdoRequest(node_id, CS_WRITE_TWO_BYTES, CONTROL_WORD_6040, SUB_INDEX_0, cw.value);
seedSdoRequest(node_id, CS_WRITE_TWO_BYTES, CIA402_CONTROL_WORD_6040, SUB_INDEX_0, cw.value);
// 6 : 清除 new_set_point必须不清除则无法再次触发新目标
cw.new_set_point = 0;
seedSdoRequest(node_id, CS_WRITE_TWO_BYTES, CONTROL_WORD_6040, SUB_INDEX_0, cw.value);
seedSdoRequest(node_id, CS_WRITE_TWO_BYTES, CIA402_CONTROL_WORD_6040, SUB_INDEX_0, cw.value);
break;
}
@ -225,19 +309,19 @@ void Ti5MotorCanopenProtocol::setMode(uint8_t node_id, msgs::RunMode mode) {
// configRPDO1(node_id, true);
// 设置目标位置为当前位置
auto cur_pos = GetRobotDetail()->motors().at(node_id).position();
seedSdoRequest(node_id, CS_WRITE_FOUR_BYTES, TARGET_POSITION_607A, SUB_INDEX_0, cur_pos);
seedSdoRequest(node_id, CS_WRITE_FOUR_BYTES, CIA402_TARGET_POSITION_607A, SUB_INDEX_0, cur_pos);
//3 : 使能 15
cw.enable_operation = 1;
cw.switch_on = 1;
seedSdoRequest(node_id, CS_WRITE_TWO_BYTES, CONTROL_WORD_6040, SUB_INDEX_0, cw.value);
seedSdoRequest(node_id, CS_WRITE_TWO_BYTES, CIA402_CONTROL_WORD_6040, SUB_INDEX_0, cw.value);
break;
}
case RUN_MODE_PROFILE_VELOCITY: {
cw.enable_operation = 1;
cw.switch_on = 1;
seedSdoRequest(node_id, CS_WRITE_TWO_BYTES, CONTROL_WORD_6040, SUB_INDEX_0, cw.value);
seedSdoRequest(node_id, CS_WRITE_TWO_BYTES, CIA402_CONTROL_WORD_6040, SUB_INDEX_0, cw.value);
break;
}
@ -245,7 +329,7 @@ void Ti5MotorCanopenProtocol::setMode(uint8_t node_id, msgs::RunMode mode) {
// configRPDO2(node_id, true);
cw.enable_operation = 1;
cw.switch_on = 1;
seedSdoRequest(node_id, CS_WRITE_TWO_BYTES, CONTROL_WORD_6040, SUB_INDEX_0, cw.value);
seedSdoRequest(node_id, CS_WRITE_TWO_BYTES, CIA402_CONTROL_WORD_6040, SUB_INDEX_0, cw.value);
break;
}
default:
@ -262,9 +346,9 @@ void Ti5MotorCanopenProtocol::seedNmtRequest(uint8_t node_id, msgs::NmtCommand c
void Ti5MotorCanopenProtocol::configProfile(uint8_t node_id, uint32_t speed, uint32_t accel, uint32_t decel) {
seedSdoRequest(node_id, CS_WRITE_FOUR_BYTES, PROFILE_SPEED_6081, SUB_INDEX_0, speed);
seedSdoRequest(node_id, CS_WRITE_FOUR_BYTES, PROFILE_ACCELERATION_6083, SUB_INDEX_0, accel);
seedSdoRequest(node_id, CS_WRITE_FOUR_BYTES, PROFILE_DECELERATION_6084, SUB_INDEX_0, decel);
seedSdoRequest(node_id, CS_WRITE_FOUR_BYTES, CIA402_PROFILE_VELOCITY_6081, SUB_INDEX_0, speed);
seedSdoRequest(node_id, CS_WRITE_FOUR_BYTES, CIA402_PROFILE_ACCELERATION_6083, SUB_INDEX_0, accel);
seedSdoRequest(node_id, CS_WRITE_FOUR_BYTES, CIA402_PROFILE_DECELERATION_6084, SUB_INDEX_0, decel);
}
@ -272,128 +356,128 @@ void Ti5MotorCanopenProtocol::configTPDO1(uint8_t node_id) {
//TDPO1 配置 状态字 和 控制字
// 1: 失能 pdo
uint32_t cob_id = TPDO1_BASE_ID_180 + node_id;
seedSdoRequest(node_id, CS_WRITE_FOUR_BYTES, TPDO1_COMM_1800, SUB_INDEX_1, cob_id | (1U << 31));
seedSdoRequest(node_id, CS_WRITE_ONE_BYTE, TPDO1_MAP_1A00, SUB_INDEX_0, 0);
seedSdoRequest(node_id, CS_WRITE_FOUR_BYTES, CANOPEN_TPDO1_COMM_1800, SUB_INDEX_1, cob_id | (1U << 31));
seedSdoRequest(node_id, CS_WRITE_ONE_BYTE, CANOPEN_TPDO1_MAP_1A00, SUB_INDEX_0, 0);
// 2: 配置为异步
seedSdoRequest(node_id, CS_WRITE_ONE_BYTE, TPDO1_COMM_1800, SUB_INDEX_2, SYNC_EVENT_DRIVEN);
seedSdoRequest(node_id, CS_WRITE_ONE_BYTE, CANOPEN_TPDO1_COMM_1800, SUB_INDEX_2, SYNC_EVENT_DRIVEN);
// 3配置约束时间 unit:0.1ms
seedSdoRequest(node_id, CS_WRITE_TWO_BYTES, TPDO1_COMM_1800, SUB_INDEX_3, 10);
seedSdoRequest(node_id, CS_WRITE_TWO_BYTES, CANOPEN_TPDO1_COMM_1800, SUB_INDEX_3, 10);
// 4 : 配置周期发送时间 unit : ms 0 为 数据改变时发送
seedSdoRequest(node_id, CS_WRITE_TWO_BYTES, TPDO1_COMM_1800, SUB_INDEX_5, 0);
seedSdoRequest(node_id, CS_WRITE_TWO_BYTES, CANOPEN_TPDO1_COMM_1800, SUB_INDEX_5, 0);
// 5 :映射控制字
seedSdoRequest(node_id, CS_WRITE_FOUR_BYTES, TPDO1_MAP_1A00, SUB_INDEX_1,
CONTROL_WORD_6040 << 16 | SUB_INDEX_0 << 8 | 16);
seedSdoRequest(node_id, CS_WRITE_FOUR_BYTES, CANOPEN_TPDO1_MAP_1A00, SUB_INDEX_1,
CIA402_CONTROL_WORD_6040 << 16 | SUB_INDEX_0 << 8 | 16);
//6 : 映射状态字
seedSdoRequest(node_id, CS_WRITE_FOUR_BYTES, TPDO1_MAP_1A00, SUB_INDEX_2,
STATUS_WORD_6041 << 16 | SUB_INDEX_0 << 8 | 16);
seedSdoRequest(node_id, CS_WRITE_FOUR_BYTES, CANOPEN_TPDO1_MAP_1A00, SUB_INDEX_2,
CIA402_STATUS_WORD_6041 << 16 | SUB_INDEX_0 << 8 | 16);
//7 : 映射模式
seedSdoRequest(node_id, CS_WRITE_FOUR_BYTES, TPDO1_MAP_1A00, SUB_INDEX_3,
MODE_DISPLAY_6061 << 16 | SUB_INDEX_0 << 8 | 8);
seedSdoRequest(node_id, CS_WRITE_FOUR_BYTES, CANOPEN_TPDO1_MAP_1A00, SUB_INDEX_3,
CIA402_MODE_DISPLAY_6061 << 16 | SUB_INDEX_0 << 8 | 8);
//8 映射错误码
seedSdoRequest(node_id, CS_WRITE_FOUR_BYTES, TPDO1_MAP_1A00, SUB_INDEX_4,
ERROR_CODE_603F << 16 | SUB_INDEX_0 << 8 | 16);
seedSdoRequest(node_id, CS_WRITE_FOUR_BYTES, CANOPEN_TPDO1_MAP_1A00, SUB_INDEX_4,
CIA402_ERROR_CODE_603F << 16 | SUB_INDEX_0 << 8 | 16);
//9 写入该PDO映射对象总个数
seedSdoRequest(node_id, CS_WRITE_ONE_BYTE, TPDO1_MAP_1A00, SUB_INDEX_0, 4);
seedSdoRequest(node_id, CS_WRITE_ONE_BYTE, CANOPEN_TPDO1_MAP_1A00, SUB_INDEX_0, 4);
//10 使能
seedSdoRequest(node_id, CS_WRITE_FOUR_BYTES, TPDO1_COMM_1800, SUB_INDEX_1, cob_id | (0U << 31));
seedSdoRequest(node_id, CS_WRITE_FOUR_BYTES, CANOPEN_TPDO1_COMM_1800, SUB_INDEX_1, cob_id | (0U << 31));
}
void Ti5MotorCanopenProtocol::configTPDO2(uint8_t node_id) {
// 1: 失能 pdo
uint32_t cob_id = TPDO2_BASE_ID_280 + node_id;
seedSdoRequest(node_id, CS_WRITE_FOUR_BYTES, TPDO2_COMM_1801, SUB_INDEX_1, cob_id | (1U << 31));
seedSdoRequest(node_id, CS_WRITE_ONE_BYTE, TPDO2_MAP_1A01, SUB_INDEX_0, 0);
seedSdoRequest(node_id, CS_WRITE_FOUR_BYTES, CANOPEN_TPDO2_COMM_1801, SUB_INDEX_1, cob_id | (1U << 31));
seedSdoRequest(node_id, CS_WRITE_ONE_BYTE, CANOPEN_TPDO2_MAP_1A01, SUB_INDEX_0, 0);
// 2: 配置为异步
seedSdoRequest(node_id, CS_WRITE_ONE_BYTE, TPDO2_COMM_1801, SUB_INDEX_2, SYNC_EVENT_DRIVEN);
seedSdoRequest(node_id, CS_WRITE_ONE_BYTE, CANOPEN_TPDO2_COMM_1801, SUB_INDEX_2, SYNC_EVENT_DRIVEN);
// 3配置约束时间 unit:0.1ms
seedSdoRequest(node_id, CS_WRITE_TWO_BYTES, TPDO2_COMM_1801, SUB_INDEX_3, 100);
seedSdoRequest(node_id, CS_WRITE_TWO_BYTES, CANOPEN_TPDO2_COMM_1801, SUB_INDEX_3, 100);
// 4 : 配置周期发送时间 unit : ms
seedSdoRequest(node_id, CS_WRITE_TWO_BYTES, TPDO2_COMM_1801, SUB_INDEX_5, 0);
seedSdoRequest(node_id, CS_WRITE_TWO_BYTES, CANOPEN_TPDO2_COMM_1801, SUB_INDEX_5, 0);
// 5 :映射当前位置
seedSdoRequest(node_id, CS_WRITE_FOUR_BYTES, TPDO2_MAP_1A01, SUB_INDEX_1,
ACTUAL_POSITION_6064 << 16 | SUB_INDEX_0 << 8 | 32);
seedSdoRequest(node_id, CS_WRITE_FOUR_BYTES, CANOPEN_TPDO2_MAP_1A01, SUB_INDEX_1,
CIA402_ACTUAL_POSITION_6064 << 16 | SUB_INDEX_0 << 8 | 32);
//6 : 映射当前速度
seedSdoRequest(node_id, CS_WRITE_FOUR_BYTES, TPDO2_MAP_1A01, SUB_INDEX_2,
ACTUAL_SPEED_606C << 16 | SUB_INDEX_0 << 8 | 32);
seedSdoRequest(node_id, CS_WRITE_FOUR_BYTES, CANOPEN_TPDO2_MAP_1A01, SUB_INDEX_2,
CIA402_ACTUAL_VELOCITY_606C << 16 | SUB_INDEX_0 << 8 | 32);
//9 写入该PDO映射对象总个数
seedSdoRequest(node_id, CS_WRITE_ONE_BYTE, TPDO2_MAP_1A01, SUB_INDEX_0, 2);
seedSdoRequest(node_id, CS_WRITE_ONE_BYTE, CANOPEN_TPDO2_MAP_1A01, SUB_INDEX_0, 2);
//10 使能
seedSdoRequest(node_id, CS_WRITE_FOUR_BYTES, TPDO2_COMM_1801, SUB_INDEX_1, cob_id | (0U << 31));
seedSdoRequest(node_id, CS_WRITE_FOUR_BYTES, CANOPEN_TPDO2_COMM_1801, SUB_INDEX_1, cob_id | (0U << 31));
}
void Ti5MotorCanopenProtocol::configRPDO1(uint8_t node_id, bool enable) {
// 1: 失能 pdo
uint32_t cob_id = RPDO1_BASE_ID_200 + node_id;
seedSdoRequest(node_id, CS_WRITE_FOUR_BYTES, RPDO1_COMM_1400, SUB_INDEX_1, cob_id | (1U << 31));
seedSdoRequest(node_id, CS_WRITE_ONE_BYTE, RPDO1_MAP_1600, SUB_INDEX_0, 0);
seedSdoRequest(node_id, CS_WRITE_FOUR_BYTES, CANOPEN_RPDO1_COMM_1400, SUB_INDEX_1, cob_id | (1U << 31));
seedSdoRequest(node_id, CS_WRITE_ONE_BYTE, CANOPEN_RPDO1_MAP_1600, SUB_INDEX_0, 0);
// 2: 配置为
seedSdoRequest(node_id, CS_WRITE_ONE_BYTE, RPDO1_COMM_1400, SUB_INDEX_2, SYNC_EVENT_DRIVEN);
seedSdoRequest(node_id, CS_WRITE_ONE_BYTE, CANOPEN_RPDO1_COMM_1400, SUB_INDEX_2, SYNC_EVENT_DRIVEN);
// // 3配置约束时间 unit:0.1ms
// seedSdoRequest(node_id,CS_WRITE_TWO_BYTES,RPDO1_COMM_1400,SUB_INDEX_3,10);
// seedSdoRequest(node_id,CS_WRITE_TWO_BYTES,CANOPEN_RPDO1_COMM_1400,SUB_INDEX_3,10);
//
// // 4 : 配置周期发送时间 unit : ms 0 为 数据改变时发送
// seedSdoRequest(node_id,CS_WRITE_TWO_BYTES,RPDO1_COMM_1400,SUB_INDEX_5,0);
// seedSdoRequest(node_id,CS_WRITE_TWO_BYTES,CANOPEN_RPDO1_COMM_1400,SUB_INDEX_5,0);
// 5 :映射位置
seedSdoRequest(node_id, CS_WRITE_FOUR_BYTES, RPDO1_MAP_1600, SUB_INDEX_1,
TARGET_POSITION_607A << 16 | SUB_INDEX_0 << 8 | 32);
seedSdoRequest(node_id, CS_WRITE_FOUR_BYTES, CANOPEN_RPDO1_MAP_1600, SUB_INDEX_1,
CIA402_TARGET_POSITION_607A << 16 | SUB_INDEX_0 << 8 | 32);
//6 : 映射控制字
seedSdoRequest(node_id, CS_WRITE_FOUR_BYTES, RPDO1_MAP_1600, SUB_INDEX_2,
PROFILE_SPEED_6081 << 16 | SUB_INDEX_0 << 8 | 32);
seedSdoRequest(node_id, CS_WRITE_FOUR_BYTES, CANOPEN_RPDO1_MAP_1600, SUB_INDEX_2,
CIA402_PROFILE_VELOCITY_6081 << 16 | SUB_INDEX_0 << 8 | 32);
//7 写入该PDO映射对象总个数
seedSdoRequest(node_id, CS_WRITE_ONE_BYTE, RPDO1_MAP_1600, SUB_INDEX_0, 2);
seedSdoRequest(node_id, CS_WRITE_ONE_BYTE, CANOPEN_RPDO1_MAP_1600, SUB_INDEX_0, 2);
//8 使能
seedSdoRequest(node_id, CS_WRITE_FOUR_BYTES, RPDO1_COMM_1400, SUB_INDEX_1, cob_id | (0U << 31));
seedSdoRequest(node_id, CS_WRITE_FOUR_BYTES, CANOPEN_RPDO1_COMM_1400, SUB_INDEX_1, cob_id | (0U << 31));
}
void Ti5MotorCanopenProtocol::configRPDO2(uint8_t node_id, bool enable) {
// 1: 失能 pdo
uint32_t cob_id = RPDO2_BASE_ID_300 + node_id;
seedSdoRequest(node_id, CS_WRITE_FOUR_BYTES, RPDO2_COMM_1401, SUB_INDEX_1, cob_id | (1U << 31));
seedSdoRequest(node_id, CS_WRITE_ONE_BYTE, RPDO2_MAP_1601, SUB_INDEX_0, 0);
seedSdoRequest(node_id, CS_WRITE_FOUR_BYTES, CANOPEN_RPDO2_COMM_1401, SUB_INDEX_1, cob_id | (1U << 31));
seedSdoRequest(node_id, CS_WRITE_ONE_BYTE, CANOPEN_RPDO2_MAP_1601, SUB_INDEX_0, 0);
if (!enable) return;
// 2: 配置为
seedSdoRequest(node_id, CS_WRITE_ONE_BYTE, RPDO2_COMM_1401, SUB_INDEX_2, SYNC_EVENT_DRIVEN);
seedSdoRequest(node_id, CS_WRITE_ONE_BYTE, CANOPEN_RPDO2_COMM_1401, SUB_INDEX_2, SYNC_EVENT_DRIVEN);
// 5 :映射位置
seedSdoRequest(node_id, CS_WRITE_FOUR_BYTES, RPDO2_MAP_1601, SUB_INDEX_1,
TARGET_SPEED_60FF << 16 | SUB_INDEX_0 << 8 | 32);
seedSdoRequest(node_id, CS_WRITE_FOUR_BYTES, CANOPEN_RPDO2_MAP_1601, SUB_INDEX_1,
CIA402_TARGET_VELOCITY_60FF << 16 | SUB_INDEX_0 << 8 | 32);
//7 写入该PDO映射对象总个数
seedSdoRequest(node_id, CS_WRITE_ONE_BYTE, RPDO2_MAP_1601, SUB_INDEX_0, 1);
seedSdoRequest(node_id, CS_WRITE_ONE_BYTE, CANOPEN_RPDO2_MAP_1601, SUB_INDEX_0, 1);
//8 使能
seedSdoRequest(node_id, CS_WRITE_FOUR_BYTES, RPDO2_COMM_1401, SUB_INDEX_1, cob_id | (0U << 31));
seedSdoRequest(node_id, CS_WRITE_FOUR_BYTES, CANOPEN_RPDO2_COMM_1401, SUB_INDEX_1, cob_id | (0U << 31));
}
@ -405,36 +489,49 @@ void Ti5MotorCanopenProtocol::configPdo(uint8_t node_id) {
}
void Ti5MotorCanopenProtocol::setLimitQdd(uint8_t node_id, double u_qdd, double l_qdd) {
auto accel = ((u_qdd * RADTODEG) * GearRatio * 100.0) / 360.0 / 1000.0;
auto decel = ((l_qdd * RADTODEG) * GearRatio * 100.0) / 360.0 / 1000.0;
seedSdoRequest(node_id, CS_WRITE_FOUR_BYTES, PROFILE_ACCELERATION_6083, SUB_INDEX_0, std::abs(accel));
seedSdoRequest(node_id, CS_WRITE_FOUR_BYTES, PROFILE_DECELERATION_6084, SUB_INDEX_0, std::abs(decel));
const auto* conversion = conversionForNode(node_id);
if (!conversion) {
return;
}
auto accel = ((u_qdd * RADTODEG) * conversion->gear_ratio * Ti5VelocityUnitScale) /
360.0 / Ti5AccelerationTimeScale;
auto decel = ((l_qdd * RADTODEG) * conversion->gear_ratio * Ti5VelocityUnitScale) /
360.0 / Ti5AccelerationTimeScale;
seedSdoRequest(node_id, CS_WRITE_FOUR_BYTES, CIA402_PROFILE_ACCELERATION_6083, SUB_INDEX_0, std::abs(accel));
seedSdoRequest(node_id, CS_WRITE_FOUR_BYTES, CIA402_PROFILE_DECELERATION_6084, SUB_INDEX_0, std::abs(decel));
}
void Ti5MotorCanopenProtocol::setLimitQd(uint8_t node_id, double qd) {
auto speed = ((qd * RADTODEG) * GearRatio * 100.0) / 360.0;
// seedSdoRequest(node_id, CS_WRITE_FOUR_BYTES, MAX_SPEED_607F, SUB_INDEX_0, speed);
seedSdoRequest(node_id, CS_WRITE_FOUR_BYTES, PROFILE_SPEED_6081, SUB_INDEX_0, speed);
const auto* conversion = conversionForNode(node_id);
if (!conversion) {
return;
}
auto speed = radPerSecToVelocityRaw(qd, *conversion);
seedSdoRequest(node_id, CS_WRITE_FOUR_BYTES, CIA402_MAX_PROFILE_VELOCITY_607F, SUB_INDEX_0, speed);
}
void Ti5MotorCanopenProtocol::setLimitQ(uint8_t node_id, double ub, double lb) {
ub = (ub * RADTODEG) / 360.0 * GearRatio * 65536.0;
lb = (lb * RADTODEG) / 360.0 * GearRatio * 65536.0;
const auto* conversion = conversionForNode(node_id);
if (!conversion) {
return;
}
ub = radToCounts(ub, *conversion);
lb = radToCounts(lb, *conversion);
seedSdoRequest(node_id, CS_WRITE_FOUR_BYTES, SOFTWARE_POSITION_LIMIT_607D, SUB_INDEX_1, lb);
seedSdoRequest(node_id, CS_WRITE_FOUR_BYTES, SOFTWARE_POSITION_LIMIT_607D, SUB_INDEX_2, ub);
seedSdoRequest(node_id, CS_WRITE_FOUR_BYTES, CIA402_SOFTWARE_POSITION_LIMIT_607D, SUB_INDEX_1, lb);
seedSdoRequest(node_id, CS_WRITE_FOUR_BYTES, CIA402_SOFTWARE_POSITION_LIMIT_607D, SUB_INDEX_2, ub);
}
bool Ti5MotorCanopenProtocol::calibrateZeroQ(uint8_t node_id) {
// 0: 设置控制字为 0x06确保停机状态
seedSdoRequest(node_id, CS_WRITE_TWO_BYTES, CONTROL_WORD_6040, SUB_INDEX_0, 0x06, 1000);
seedSdoRequest(node_id, CS_WRITE_TWO_BYTES, CIA402_CONTROL_WORD_6040, SUB_INDEX_0, 0x06, 1000);
// 1: 清除偏置值 0x2008 ← 0
seedSdoRequest(node_id, CS_WRITE_FOUR_BYTES, POSITION_OFFSET_2008, SUB_INDEX_0, 0);
seedSdoRequest(node_id, CS_WRITE_FOUR_BYTES, CANOPEN_POSITION_OFFSET_2008, SUB_INDEX_0, 0);
// 2: 等待确认清除成功
seedSdoRequest(node_id, CS_READ_REQUEST, POSITION_OFFSET_2008, SUB_INDEX_0, 0);
seedSdoRequest(node_id, CS_READ_REQUEST, CANOPEN_POSITION_OFFSET_2008, SUB_INDEX_0, 0);
if (!waitUntil([&]() {
return GetRobotDetail()->motors().at(node_id).position_offset() == 0;
}, 1000)) {
@ -443,18 +540,18 @@ bool Ti5MotorCanopenProtocol::calibrateZeroQ(uint8_t node_id) {
}
// 3: 读取当前位置 0x6064
seedSdoRequest(node_id, CS_READ_REQUEST, ACTUAL_POSITION_6064, SUB_INDEX_0, 0, 20);
seedSdoRequest(node_id, CS_READ_REQUEST, CIA402_ACTUAL_POSITION_6064, SUB_INDEX_0, 0, 20);
auto cur_pos = GetRobotDetail()->motors().at(node_id).position();
// 4: 将当前位置写入偏置寄存器
seedSdoRequest(node_id, CS_WRITE_FOUR_BYTES, POSITION_OFFSET_2008, SUB_INDEX_0, cur_pos);
seedSdoRequest(node_id, CS_WRITE_FOUR_BYTES, CANOPEN_POSITION_OFFSET_2008, SUB_INDEX_0, cur_pos);
// 5: 保存参数到永久区0x2000 ← 1
seedSdoRequest(node_id, CS_WRITE_FOUR_BYTES, USER_SAVE_PARA_2000, SUB_INDEX_0, 1, 100);
seedSdoRequest(node_id, CS_WRITE_FOUR_BYTES, CANOPEN_USER_SAVE_PARA_2000, SUB_INDEX_0, 1, 100);
// 6: 确认写入成功
seedSdoRequest(node_id, CS_READ_REQUEST, POSITION_OFFSET_2008, SUB_INDEX_0, 0, 20);
seedSdoRequest(node_id, CS_READ_REQUEST, CANOPEN_POSITION_OFFSET_2008, SUB_INDEX_0, 0, 20);
if (!waitUntil([&]() {
return GetRobotDetail()->motors().at(node_id).position_offset() == cur_pos;
}, 500)) {
@ -465,16 +562,28 @@ bool Ti5MotorCanopenProtocol::calibrateZeroQ(uint8_t node_id) {
return true;
}
void Ti5MotorCanopenProtocol::brake(uint8_t node_id) {
bool Ti5MotorCanopenProtocol::torqueOn(uint8_t node_id) {
setMode(node_id, msgs::RUN_MODE_CYCLIC_SYNC_POSITION);
return true;
}
bool Ti5MotorCanopenProtocol::brakeRelease(uint8_t node_id) {
(void)node_id;
CMVR_LOG(ERROR) << "[Ti5MotorCanopenProtocol] brakeRelease is not implemented";
return false;
}
bool Ti5MotorCanopenProtocol::quickStop(uint8_t node_id) {
// // 开机未使能电机时调用
// seedSdoRequest(node_id, CS_WRITE_TWO_BYTES, CONTROL_WORD_6040, SUB_INDEX_0, 0x0F);
// seedSdoRequest(node_id, CS_WRITE_TWO_BYTES, CIA402_CONTROL_WORD_6040, SUB_INDEX_0, 0x0F);
// 6 抱闸 0 立即停机 自由
seedSdoRequest(node_id, CS_WRITE_FOUR_BYTES, QUICK_STOP_DECEL_6085, SUB_INDEX_0, 0XFFFFFFF0);
seedSdoRequest(node_id, CS_WRITE_TWO_BYTES, QUICK_STOP_OPTION_605A, SUB_INDEX_0, 6);
seedSdoRequest(node_id, CS_WRITE_TWO_BYTES, CONTROL_WORD_6040, SUB_INDEX_0, 0x02, 100);
seedSdoRequest(node_id, CS_WRITE_FOUR_BYTES, CIA402_QUICK_STOP_DECELERATION_6085, SUB_INDEX_0, 0XFFFFFFF0);
seedSdoRequest(node_id, CS_WRITE_TWO_BYTES, CIA402_QUICK_STOP_OPTION_605A, SUB_INDEX_0, 6);
seedSdoRequest(node_id, CS_WRITE_TWO_BYTES, CIA402_CONTROL_WORD_6040, SUB_INDEX_0, 0x02, 100);
// 必须要发送 0xf 才能按照6085中设定的减速度减速
seedSdoRequest(node_id, CS_WRITE_TWO_BYTES, CONTROL_WORD_6040, SUB_INDEX_0, 0x0F);
seedSdoRequest(node_id, CS_WRITE_TWO_BYTES, CIA402_CONTROL_WORD_6040, SUB_INDEX_0, 0x0F);
return true;
}
bool Ti5MotorCanopenProtocol::reachedTargetQ(uint8_t node_id) {
@ -483,62 +592,37 @@ bool Ti5MotorCanopenProtocol::reachedTargetQ(uint8_t node_id) {
return st.target_reached == 1;
}
void Ti5MotorCanopenProtocol::setQd(uint8_t node_id, double qd) {
auto speed = ((qd * RADTODEG) * GearRatio * 100.0) / 360.0;
switch (getMode(node_id)) {
case msgs::RUN_MODE_CYCLIC_SYNC_POSITION:
case msgs::RUN_MODE_PROFILE_POSITION: {
auto it = last_Qd_.find(node_id);
if (it == last_Qd_.end() || it->second != speed) {
seedSdoRequest(node_id, CS_WRITE_FOUR_BYTES, PROFILE_SPEED_6081, SUB_INDEX_0, uint32_t(std::abs(speed)),
0);
last_Qd_[node_id] = speed;
}
break;
}
case msgs::RUN_MODE_PROFILE_VELOCITY:
case msgs::RUN_MODE_CYCLIC_SYNC_VELOCITY: {
// 在速度模式下,直接设置目标速度
seedSdoRequest(node_id, CS_WRITE_FOUR_BYTES, TARGET_SPEED_60FF, SUB_INDEX_0, uint32_t(speed), 0);
break;
}
default:
break;
}
}
void Ti5MotorCanopenProtocol::setQdd(uint8_t node_id, double qdd) {
uint32_t accel = ((std::abs(qdd) * RADTODEG) * GearRatio * 100.0 * 65536.0) / (360.0 * 1000.0);
auto it = last_Qdd_.find(node_id);
if (it == last_Qdd_.end() || it->second != accel) {
seedSdoRequest(node_id, CS_WRITE_FOUR_BYTES, PROFILE_ACCELERATION_6083, SUB_INDEX_0, accel);
seedSdoRequest(node_id, CS_WRITE_FOUR_BYTES, PROFILE_DECELERATION_6084, SUB_INDEX_0, accel);
last_Qdd_[node_id] = accel;
}
}
void Ti5MotorCanopenProtocol::torqueOff(uint8_t node_id) {
bool Ti5MotorCanopenProtocol::torqueOff(uint8_t node_id) {
// 0 立即停机 自由
seedSdoRequest(node_id, CS_WRITE_TWO_BYTES, QUICK_STOP_OPTION_605A, SUB_INDEX_0, 0);
seedSdoRequest(node_id, CS_WRITE_TWO_BYTES, CONTROL_WORD_6040, SUB_INDEX_0, 0x02, 20);
seedSdoRequest(node_id, CS_WRITE_TWO_BYTES, CIA402_QUICK_STOP_OPTION_605A, SUB_INDEX_0, 0);
seedSdoRequest(node_id, CS_WRITE_TWO_BYTES, CIA402_CONTROL_WORD_6040, SUB_INDEX_0, 0x02, 20);
// 必须要发送 0xf 才能按照6085中设定的减速度减速
// seedSdoRequest(node_id, CS_WRITE_TWO_BYTES, CONTROL_WORD_6040, SUB_INDEX_0, 0x0F);
// seedSdoRequest(node_id, CS_WRITE_TWO_BYTES, CIA402_CONTROL_WORD_6040, SUB_INDEX_0, 0x0F);
// 停机之后,要重新使能?
// cur_mode_[node_id] = msgs::RUN_MODE_UNSPECIFIED;
return true;
}
double Ti5MotorCanopenProtocol::getQ(uint8_t node_id) {
const auto* conversion = conversionForNode(node_id);
if (!conversion) {
return 0.0;
}
auto data_ptr = std::make_unique<msgs::RobotDetail>();
message_manager_->GetSensorData(data_ptr.get());
auto cnt = data_ptr->motors().at(node_id).position();
return (cnt * 360.0) / (GearRatio * 65536.0 * RADTODEG);
return countsToRad(cnt, *conversion);
}
double Ti5MotorCanopenProtocol::getQd(uint8_t node_id) {
const auto* conversion = conversionForNode(node_id);
if (!conversion) {
return 0.0;
}
auto data_ptr = std::make_unique<msgs::RobotDetail>();
message_manager_->GetSensorData(data_ptr.get());
auto cnt = data_ptr->motors().at(node_id).speed();
return (cnt * 360.0) / (GearRatio * 100.0 * RADTODEG);
return velocityRawToRadPerSec(cnt, *conversion);
}

View File

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

View File

@ -1,21 +1,27 @@
#include "motor/manager/include/motor_manager.h"
#include "devices/motor/manager/include/motor_manager.h"
#include <chrono>
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <thread>
#include <unordered_map>
#include <utility>
#include "algorithms/kinematics/ik_solver/common/include/urdf_parser.h"
#include "common/base/logging/logger.h"
#include "common/config/config_files.h"
#include "../../bus_runtime/abstract_motor_bus_runtime.h"
#include "motor/bus_runtime/can/include/can_motor_bus_runtime.h"
#include "motor/bus_runtime/ethercat/include/ethercat_motor_bus_runtime.h"
#include "motor/bus_runtime/mujoco/include/mujoco_motor_bus_runtime.h"
#include "motor/drivers/mujoco/include/mujoco_motor.h"
#include "motor/drivers/ti5_canopen/include/ti5_motor.h"
#include "motor/drivers/ti5_canopen/include/ti5_motor_canopen_protocol.h"
#include "devices/motor/bus_runtime/abstract_motor_bus_runtime.h"
#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/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/ti5_canopen/include/ti5_motor.h"
#include "devices/motor/drivers/ti5_canopen/include/ti5_motor_canopen_protocol.h"
namespace cmvr::device {
@ -91,11 +97,17 @@ bool MotorManager::init()
all_ok = false;
continue;
}
if (!bus_runtime->start()) {
const bool start_before_motor_init =
motor_group_cfg.bus_type() == config::MOTOR_BUS_ETHERCAT;
if (start_before_motor_init && !bus_runtime->start()) {
bus_runtime->stop();
all_ok = false;
continue;
}
if (start_before_motor_init) {
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
}
auto motors = createMotors_(motor_group_cfg, selected_motor_cfgs, bus_runtime);
if (motors.empty()) {
@ -116,6 +128,11 @@ bool MotorManager::init()
all_ok = false;
continue;
}
if (!start_before_motor_init && !bus_runtime->start()) {
bus_runtime->stop();
all_ok = false;
continue;
}
bus_runtimes_.push_back(std::move(bus_runtime));
}
@ -398,8 +415,19 @@ 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_ETHERCAT:
return std::make_shared<EthercatMotorBusRuntime>();
case config::MOTOR_BUS_ETHERCAT: {
auto runtime = std::make_shared<EthercatMotorBusRuntime>();
if (group_cfg.vendor() == config::MOTOR_VENDOR_EYOU &&
group_cfg.protocol() == config::MOTOR_PROTOCOL_ETHERCAT_CIA402) {
runtime->setPdoMapping(createEyouCia402PdoMapping());
return runtime;
}
CMVR_LOG(ERROR) << "[MotorManager] unsupported EtherCAT motor: vendor="
<< config::MotorVendor_Name(group_cfg.vendor())
<< ", protocol=" << config::MotorProtocol_Name(group_cfg.protocol())
<< ", group=" << group_cfg.id();
return nullptr;
}
default:
CMVR_LOG(ERROR) << "[MotorManager] unsupported motor bus type: "
<< config::MotorBusType_Name(group_cfg.bus_type())
@ -534,6 +562,14 @@ std::vector<std::shared_ptr<AbstractMotor>> MotorManager::createEthercatMotors_(
CMVR_LOG(ERROR) << "[MotorManager] missing EtherCAT config: " << group_cfg.id();
return {};
}
if (group_cfg.vendor() != config::MOTOR_VENDOR_EYOU ||
group_cfg.protocol() != config::MOTOR_PROTOCOL_ETHERCAT_CIA402) {
CMVR_LOG(ERROR) << "[MotorManager] unsupported EtherCAT motor: vendor="
<< config::MotorVendor_Name(group_cfg.vendor())
<< ", protocol=" << config::MotorProtocol_Name(group_cfg.protocol())
<< ", group=" << group_cfg.id();
return {};
}
for (const auto& motor_cfg : motor_cfgs) {
if (!ethercat_bus_runtime->slaveForMotor(motor_cfg.id())) {
CMVR_LOG(ERROR) << "[MotorManager] missing EtherCAT slave config for motor id "
@ -542,11 +578,22 @@ std::vector<std::shared_ptr<AbstractMotor>> MotorManager::createEthercatMotors_(
}
}
CMVR_LOG(ERROR) << "[MotorManager] EtherCAT motor creation is not implemented: vendor="
<< config::MotorVendor_Name(group_cfg.vendor())
<< ", protocol=" << config::MotorProtocol_Name(group_cfg.protocol())
<< ", group=" << group_cfg.id();
return {};
auto protocol = std::make_shared<Cia402Protocol>(
ethercat_bus_runtime, group_cfg.ethercat().cia402());
std::vector<std::shared_ptr<AbstractMotor>> motors;
motors.reserve(motor_cfgs.size());
for (const auto& cfg : motor_cfgs) {
auto motor = std::make_shared<EyouMotor>(
cfg, protocol, std::make_unique<EyouMotorAdapter>(ethercat_bus_runtime));
if (!motor->init()) {
CMVR_LOG(ERROR) << "[MotorManager] failed to init EYOU EtherCAT motor: "
<< cfg.joint_name();
return {};
}
motors.push_back(std::move(motor));
}
return motors;
}
} // namespace cmvr::device

View File

@ -15,7 +15,8 @@ namespace cmvr {
public:
enum class CommProto : uint8_t {
CANOPEN = 1,
CUSTOM = 2
ETHERCAT = 2,
CUSTOM = 3
};
virtual ~MotorProtocolInterface() = default;
@ -26,9 +27,6 @@ namespace cmvr {
*/
virtual bool initNode(uint8_t node_id) = 0;
virtual void setQ(uint8_t node_id, double angle_rad) = 0;
virtual void setTarget(uint8_t node_id, double angle_rad,double vel) = 0;
virtual void setTarget(uint8_t node_id,double vel) = 0;
virtual void setMode(uint8_t node_id,msgs::RunMode mode ) = 0;
virtual msgs::RunMode getMode(uint8_t node_id) = 0;
virtual void setLimitQdd(uint8_t node_id, double u_qdd,double l_qdd) = 0;
@ -36,12 +34,35 @@ namespace cmvr {
virtual void setLimitQ(uint8_t node_id, double ub, double lb) = 0;
virtual bool calibrateZeroQ(uint8_t node_id) = 0;
virtual bool reachedTargetQ(uint8_t node_id) = 0;
virtual void setQd(uint8_t node_id, double qd) = 0;
virtual void setQdd(uint8_t node_id,double qdd) = 0;
// virtual void setVelocity(uint8_t node_id, double velocity) = 0;
// virtual void clearError(uint8_t node_id) = 0;
virtual void brake(uint8_t node_id) = 0;
virtual void torqueOff(uint8_t node_id) = 0;
// target_q: rad, max_qd: rad/s, max_qdd: rad/s^2.
// Profile Position 写入目标位置和轮廓速度/加速度,并触发一次新目标。
virtual bool commandProfilePosition(uint8_t node_id,
double target_q,
double max_qd,
double max_qdd) = 0;
// target_qd: rad/s, max_qdd: rad/s^2.
// Profile Velocity 写入目标速度和轮廓加速度。
virtual bool commandProfileVelocity(uint8_t node_id,
double target_qd,
double max_qdd) = 0;
// target_q: rad, target_qd: rad/s.
// Cyclic Position 周期写入目标位置和目标速度。
virtual bool commandCyclicPosition(uint8_t node_id,
double target_q,
double target_qd) = 0;
// target_qd: rad/s.
// Cyclic Velocity 周期写入目标速度。
virtual bool commandCyclicVelocity(uint8_t node_id,
double target_qd) = 0;
// target_tau: N*m.
virtual bool commandCyclicTorque(uint8_t node_id, double target_tau) = 0;
virtual void setMotorConversion(uint8_t node_id,
double encoder_counts_per_rev,
double gear_ratio) = 0;
virtual bool torqueOn(uint8_t node_id) = 0;
virtual bool torqueOff(uint8_t node_id) = 0;
virtual bool brakeRelease(uint8_t node_id) = 0;
virtual bool quickStop(uint8_t node_id) = 0;
virtual double getQ(uint8_t node_id) = 0;
virtual double getQd(uint8_t node_id) = 0;

View File

@ -26,6 +26,9 @@ using namespace cmvr::device;
namespace {
using GroupJointSelection = std::unordered_map<std::string, std::unordered_set<std::string>>;
using MotorJointSelections = std::unordered_map<std::string, GroupJointSelection>;
void logSection(const char* title)
{
CMVR_LOG(INFO) << "---------------- " << title << " ----------------";
@ -71,6 +74,32 @@ bool motorGroupHasJoint(const cmvr::config::MotorGroupConfig& motor_group,
return false;
}
void addAllMotorJoints(const std::string& motor_system_id,
const cmvr::config::MotorRootConfig& root_cfg,
MotorJointSelections& selections)
{
auto& group_selection = selections[motor_system_id];
for (const auto& motor_group : root_cfg.motor().motor_groups()) {
if (motor_group.id().empty()) {
continue;
}
auto& selected_joints = group_selection[motor_group.id()];
for (const auto& motor : motor_group.motors().motors()) {
if (!motor.joint_name().empty()) {
selected_joints.insert(motor.joint_name());
}
}
if (selected_joints.empty()) {
group_selection.erase(motor_group.id());
}
}
if (group_selection.empty()) {
selections.erase(motor_system_id);
}
}
} // namespace
template std::shared_ptr<AbstractAGV> DeviceManager::getDevice(const std::string& device_id);
@ -256,8 +285,7 @@ void DeviceManager::log_device_plan_() const
void DeviceManager::pre_scan_robot_arm_dependencies_() const
{
using GroupJointSelection = std::unordered_map<std::string, std::unordered_set<std::string>>;
std::unordered_map<std::string, GroupJointSelection> selections;
MotorJointSelections selections;
std::unordered_map<std::string, config::MotorRootConfig> motor_roots;
for (const auto& entry : cfg_.devices()) {
@ -387,6 +415,15 @@ void DeviceManager::pre_scan_robot_arm_dependencies_() const
}
}
if (selections.empty() && cfg_.init_all_motors_when_no_active_joints()) {
CMVR_LOG(INFO) << "[DeviceManager]: No active motor joints from RobotArm; "
<< "initialize all configured motors because "
<< "init_all_motors_when_no_active_joints=true";
for (const auto& [motor_system_id, root_cfg] : motor_roots) {
addAllMotorJoints(motor_system_id, root_cfg, selections);
}
}
MotorManager::clearActiveJoints();
for (auto& [motor_system_id, group_selection] : selections) {
MotorManager::setActiveJoints(motor_system_id, std::move(group_selection));

Binary file not shown.

View File

@ -0,0 +1,3 @@
MASTER0_DEVICE="a0:ad:9f:c4:c2:2c"
DEVICE_MODULES="generic"
UPDOWN_INTERFACES="eno1"

View File

@ -0,0 +1,113 @@
#------------------------------------------------------------------------------
#
# EtherCAT master configuration file for use with ethercatctl.
#
# vim: spelllang=en spell tw=78
#
#------------------------------------------------------------------------------
#
# Main Ethernet devices.
#
# The MASTER<X>_DEVICE variable specifies the Ethernet device for a master
# with index 'X'.
#
# Specify the MAC address (hexadecimal with colons) of the Ethernet device to
# use. Example: "00:00:08:44:ab:66"
#
# Alternatively, a network interface name can be specified. The interface
# name will be resolved to a MAC address using the 'ip' command.
# Example: "eth0"
#
# The broadcast address "ff:ff:ff:ff:ff:ff" has a special meaning: It tells
# the master to accept the first device offered by any Ethernet driver.
#
# The MASTER<X>_DEVICE variables also determine, how many masters will be
# created: A non-empty variable MASTER0_DEVICE will create one master, adding
# a non-empty variable MASTER1_DEVICE will create a second master, and so on.
#
# Examples:
# MASTER0_DEVICE="00:00:08:44:ab:66"
# MASTER0_DEVICE="eth0"
#
MASTER0_DEVICE=""
#MASTER1_DEVICE=""
#
# Backup Ethernet devices
#
# The MASTER<X>_BACKUP variables specify the devices used for redundancy. They
# behaves nearly the same as the MASTER<X>_DEVICE variable, except that it
# does not interpret the ff:ff:ff:ff:ff:ff address.
#
#MASTER0_BACKUP=""
#
# Ethernet driver modules to use for EtherCAT operation.
#
# Specify a non-empty list of Ethernet drivers, that shall be used for
# EtherCAT operation.
#
# Except for the generic Ethernet driver module, the init script will try to
# unload the usual Ethernet driver modules in the list and replace them with
# the EtherCAT-capable ones. If a certain (EtherCAT-capable) driver is not
# found, a warning will appear.
#
# Possible values: 8139too, e100, e1000, e1000e, r8169, generic, ccat, igb,
# igc, genet, dwmac-intel, stmmac-pci.
# Separate multiple drivers with spaces.
# A list of all matching kernel versions can be found here:
# https://docs.etherlab.org/ethercat/1.6/doxygen/devicedrivers.html
#
# Note: The e100, e1000, e1000e, r8169, ccat, igb and igc drivers are not
# built by default. Enable them with the --enable-<driver> configure switches.
#
DEVICE_MODULES=""
# If you have any issues about network interfaces not being configured
# properly, systemd may need some additional infos about your setup.
# Have a look at the service file, you'll find some details there.
#
#
# List of interfaces to bring up and down automatically.
#
# Specify a space-separated list of interface names (such as eth0 or
# enp0s1) that shall be brought up on `ethercatctl start` and down on
# `ethercatctl stop`.
#
# When using the generic driver, the corresponding Ethernet device has to be
# activated before the master is started, otherwise all frames will time out.
# This the perfect use-case for `UPDOWN_INTERFACES`.
#
UPDOWN_INTERFACES=""
#
# Default SII caching method.
#
# Set the start-up caching method for all masters. The integer value
# determines, which fields are used to look up a cached SII page. It is a
# bit-field consisting of the following flags. A value of zero disables SII
# caching (default). A typical value is 7 (use vendor ID, product code and
# revision number for lookup).
#
# - 1: Vendor ID (always used)
# - 2: Product code (always used)
# - 4: Revision number
# - 8: Serial number
# - 16: Alias address
#
# Please keep in mind that in case serial number or alias address is enabled,
# only slaves with a non-zero serial number or alias benefit from caching.
#
SII_CACHING=0
#
# Flags for loading kernel modules.
#
# This can usually be left empty. Adjust this variable, if you have problems
# with module loading.
#
#MODPROBE_FLAGS="-b"
#------------------------------------------------------------------------------

View File

@ -0,0 +1,3 @@
MASTER0_DEVICE="a0:ad:9f:c4:c2:2c"
DEVICE_MODULES="generic"
UPDOWN_INTERFACES="eno1"

View File

@ -0,0 +1,123 @@
#!/bin/sh
#------------------------------------------------------------------------------
#
# Init script for EtherCAT
#
# Copyright (C) 2006-2021 Florian Pose, Ingenieurgemeinschaft IgH
#
# This file is part of the IgH EtherCAT Master.
#
# The IgH EtherCAT Master is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 2, as
# published by the Free Software Foundation.
#
# The IgH EtherCAT Master is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
# Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with the IgH EtherCAT Master; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
#
#
# vim: expandtab
#
#------------------------------------------------------------------------------
### BEGIN INIT INFO
# Provides: ethercat
# Required-Start: $local_fs $syslog $network
# Should-Start: $time ntp
# Required-Stop: $local_fs $syslog $network
# Should-Stop: $time ntp
# Default-Start: 3 5
# Default-Stop: 0 1 2 6
# Short-Description: EtherCAT master
# Description: EtherCAT master 1.7.0
### END INIT INFO
#------------------------------------------------------------------------------
ETHERCATCTL="/home/lgv/cmvr/0-workspace/cmvr-es/dependency/x86/third_party/ethercat/v1.7.0/sbin/ethercatctl -c /home/lgv/cmvr/0-workspace/cmvr-es/dependency/x86/third_party/ethercat/v1.7.0/etc/sysconfig/ethercat"
#------------------------------------------------------------------------------
exit_success() {
if [ -r /etc/rc.status ]; then
rc_reset
rc_status -v
rc_exit
else
echo " done"
exit 0
fi
}
#------------------------------------------------------------------------------
exit_fail() {
if [ -r /etc/rc.status ]; then
rc_failed
rc_status -v
rc_exit
else
echo " failed"
exit 1
fi
}
#------------------------------------------------------------------------------
if [ -r /etc/rc.status ]; then
. /etc/rc.status
rc_reset
fi
case "${1}" in
start)
echo -n "Starting EtherCAT master 1.7.0 "
if $ETHERCATCTL start; then
exit_success
else
exit_fail
fi
;;
stop)
echo -n "Shutting down EtherCAT master 1.7.0 "
if $ETHERCATCTL stop; then
exit_success
else
exit_fail
fi
;;
restart)
$0 stop || exit 1
sleep 1
$0 start
;;
status)
$ETHERCATCTL status
exit $?
;;
*)
echo "USAGE: $0 {start|stop|restart|status}"
;;
esac
if [ -r /etc/rc.status ]; then
rc_exit
else
exit 1
fi
#------------------------------------------------------------------------------

View File

@ -0,0 +1,113 @@
#------------------------------------------------------------------------------
#
# EtherCAT master configuration file for use with init.d.
#
# vim: spelllang=en spell tw=78
#
#------------------------------------------------------------------------------
#
# Main Ethernet devices.
#
# The MASTER<X>_DEVICE variable specifies the Ethernet device for a master
# with index 'X'.
#
# Specify the MAC address (hexadecimal with colons) of the Ethernet device to
# use. Example: "00:00:08:44:ab:66"
#
# Alternatively, a network interface name can be specified. The interface
# name will be resolved to a MAC address using the 'ip' command.
# Example: "eth0"
#
# The broadcast address "ff:ff:ff:ff:ff:ff" has a special meaning: It tells
# the master to accept the first device offered by any Ethernet driver.
#
# The MASTER<X>_DEVICE variables also determine, how many masters will be
# created: A non-empty variable MASTER0_DEVICE will create one master, adding
# a non-empty variable MASTER1_DEVICE will create a second master, and so on.
#
# Examples:
# MASTER0_DEVICE="00:00:08:44:ab:66"
# MASTER0_DEVICE="eth0"
#
MASTER0_DEVICE=""
#MASTER1_DEVICE=""
#
# Backup Ethernet devices
#
# The MASTER<X>_BACKUP variables specify the devices used for redundancy. They
# behaves nearly the same as the MASTER<X>_DEVICE variable, except that it
# does not interpret the ff:ff:ff:ff:ff:ff address.
#
#MASTER0_BACKUP=""
#
# Ethernet driver modules to use for EtherCAT operation.
#
# Specify a non-empty list of Ethernet drivers, that shall be used for
# EtherCAT operation.
#
# Except for the generic Ethernet driver module, the init script will try to
# unload the usual Ethernet driver modules in the list and replace them with
# the EtherCAT-capable ones. If a certain (EtherCAT-capable) driver is not
# found, a warning will appear.
#
# Possible values: 8139too, e100, e1000, e1000e, r8169, generic, ccat, igb,
# igc, genet, dwmac-intel, stmmac-pci.
# Separate multiple drivers with spaces.
# A list of all matching kernel versions can be found here:
# https://docs.etherlab.org/ethercat/1.6/doxygen/devicedrivers.html
#
# Note: The e100, e1000, e1000e, r8169, ccat, igb and igc drivers are not
# built by default. Enable them with the --enable-<driver> configure switches.
#
DEVICE_MODULES=""
# If you have any issues about network interfaces not being configured
# properly, systemd may need some additional infos about your setup.
# Have a look at the service file, you'll find some details there.
#
#
# List of interfaces to bring up and down automatically.
#
# Specify a space-separated list of interface names (such as eth0 or
# enp0s1) that shall be brought up on `ethercatctl start` and down on
# `ethercatctl stop`.
#
# When using the generic driver, the corresponding Ethernet device has to be
# activated before the master is started, otherwise all frames will time out.
# This the perfect use-case for `UPDOWN_INTERFACES`.
#
UPDOWN_INTERFACES=""
#
# Default SII caching method.
#
# Set the start-up caching method for all masters. The integer value
# determines, which fields are used to look up a cached SII page. It is a
# bit-field consisting of the following flags. A value of zero disables SII
# caching (default). A typical value is 7 (use vendor ID, product code and
# revision number for lookup).
#
# - 1: Vendor ID (always used)
# - 2: Product code (always used)
# - 4: Revision number
# - 8: Serial number
# - 16: Alias address
#
# Please keep in mind that in case serial number or alias address is enabled,
# only slaves with a non-zero serial number or alias benefit from caching.
#
SII_CACHING=0
#
# Flags for loading kernel modules.
#
# This can usually be left empty. Adjust this variable, if you have problems
# with module loading.
#
#MODPROBE_FLAGS="-b"
#------------------------------------------------------------------------------

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,106 @@
/*****************************************************************************
*
* Copyright (C) 2006-2008 Florian Pose, Ingenieurgemeinschaft IgH
*
* This file is part of the IgH EtherCAT master userspace library.
*
* The IgH EtherCAT master userspace library is free software; you can
* redistribute it and/or modify it under the terms of the GNU Lesser General
* Public License as published by the Free Software Foundation; version 2.1
* of the License.
*
* The IgH EtherCAT master userspace library is distributed in the hope that
* it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with the IgH EtherCAT master userspace library. If not, see
* <http://www.gnu.org/licenses/>.
*
****************************************************************************/
/** \file
*
* EtherCAT virtual TTY interface.
*
* \defgroup TTYInterface EtherCAT Virtual TTY Interface
*
* @{
*/
/****************************************************************************/
#ifndef __ECTTY_H__
#define __ECTTY_H__
#include <linux/termios.h>
/*****************************************************************************
* Data types
****************************************************************************/
struct ec_tty;
typedef struct ec_tty ec_tty_t; /**< \see ec_tty */
/** Operations on the virtual TTY interface.
*/
typedef struct {
int (*cflag_changed)(void *, tcflag_t); /**< Called when the serial
* settings shall be changed. The
* \a cflag argument contains the
* new settings. */
} ec_tty_operations_t;
/*****************************************************************************
* Global functions
****************************************************************************/
/** Create a virtual TTY interface.
*
* \param ops Set of callbacks.
* \param cb_data Arbitrary data, that is passed to any callback.
*
* \return Pointer to the interface object, otherwise an ERR_PTR value.
*/
ec_tty_t *ectty_create(
const ec_tty_operations_t *ops,
void *cb_data
);
/*****************************************************************************
* TTY interface methods
****************************************************************************/
/** Releases a virtual TTY interface.
*/
void ectty_free(
ec_tty_t *tty /**< TTY interface. */
);
/** Reads data to send from the TTY interface.
*
* If there are data to send, they are copied into the \a buffer. At maximum,
* \a size bytes are copied. The actual number of bytes copied is returned.
*
* \return Number of bytes copied.
*/
unsigned int ectty_tx_data(
ec_tty_t *tty, /**< TTY interface. */
uint8_t *buffer, /**< Buffer for data to transmit. */
size_t size /**< Available space in \a buffer. */
);
/** Pushes received data to the TTY interface.
*/
void ectty_rx_data(
ec_tty_t *tty, /**< TTY interface. */
const uint8_t *buffer, /**< Buffer with received data. */
size_t size /**< Number of bytes in \a buffer. */
);
/****************************************************************************/
/** @} */
#endif

View File

@ -0,0 +1,43 @@
#----------------------------------------------------------------------------
#
# Copyright (C) 2021 Bjarne von Horn, Ingenieurgemeinschaft IgH
#
# This file is part of the IgH EtherCAT Master.
#
# The IgH EtherCAT Master is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 2, as
# published by the Free Software Foundation.
#
# The IgH EtherCAT Master is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
# Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with the IgH EtherCAT Master; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
#
# vim: tw=78
#
#----------------------------------------------------------------------------
find_library(EtherCAT_LIBRARY
NAMES ethercat
PATHS /home/lgv/cmvr/0-workspace/cmvr-es/dependency/x86/third_party/ethercat/v1.7.0/lib
)
find_path(EtherCAT_INCLUDE_DIR
NAMES ecrt.h
PATHS /home/lgv/cmvr/0-workspace/cmvr-es/dependency/x86/third_party/ethercat/v1.7.0/include
)
mark_as_advanced(EtherCAT_LIBRARY EtherCAT_INCLUDE_DIR)
if(NOT TARGET EtherLab::ethercat)
add_library(EtherLab::ethercat SHARED IMPORTED)
set_target_properties(EtherLab::ethercat PROPERTIES
INTERFACE_INCLUDE_DIRECTORIES "${EtherCAT_INCLUDE_DIR}"
IMPORTED_LOCATION "${EtherCAT_LIBRARY}"
)
endif()

Binary file not shown.

View File

@ -0,0 +1,41 @@
# libethercat.la - a libtool library file
# Generated by libtool (GNU libtool) 2.4.6 Debian-2.4.6-15build2
#
# Please DO NOT delete this file!
# It is necessary for linking the library.
# The name that we can dlopen(3).
dlname='libethercat.so.1'
# Names of this library.
library_names='libethercat.so.1.2.0 libethercat.so.1 libethercat.so'
# The name of the static archive.
old_library='libethercat.a'
# Linker flags that cannot go in dependency_libs.
inherited_linker_flags=''
# Libraries that this one depends upon.
dependency_libs=''
# Names of additional weak libraries provided by this library
weak_library_names=''
# Version information for libethercat.
current=3
age=2
revision=0
# Is this an already installed library?
installed=yes
# Should we warn about portability when linking against -modules?
shouldnotlink=no
# Files to dlopen/dlpreopen
dlopen=''
dlpreopen=''
# Directory that this library needs to be installed in:
libdir='/home/lgv/cmvr/0-workspace/cmvr-es/dependency/x86/third_party/ethercat/v1.7.0/lib'

View File

@ -0,0 +1 @@
libethercat.so.1.2.0

View File

@ -0,0 +1 @@
libethercat.so.1.2.0

Binary file not shown.

View File

@ -0,0 +1,34 @@
#
# pkgconfig file for ethercat library
#
# Copyright 2021 Bjarne von Horn (vh at igh dot de)
#
# This file is part of the ethercat library.
#
# The ethercat library is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or (at your
# option) any later version.
#
# The ethercat library is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
# License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with the ethercat library. If not, see <http://www.gnu.org/licenses/>.
#
# vim: tw=78 noexpandtab
#
prefix=/home/lgv/cmvr/0-workspace/cmvr-es/dependency/x86/third_party/ethercat/v1.7.0
exec_prefix=${prefix}
libdir=/home/lgv/cmvr/0-workspace/cmvr-es/dependency/x86/third_party/ethercat/v1.7.0/lib
includedir=/home/lgv/cmvr/0-workspace/cmvr-es/dependency/x86/third_party/ethercat/v1.7.0/include
Name: libethercat
Description: Client support library for the EtherCAT Master
URL: http://www.etherlab.org
Version: 1.7.0
Libs: -L${libdir} -lethercat
Cflags: -I${includedir}

View File

@ -0,0 +1,38 @@
#
# EtherCAT master kernel modules
#
[Unit]
Description=EtherCAT Master Kernel Modules
# Fine tuning of the startup dependencies below are recommended
# to provide a reliable startup routine.
# The dependencies below can be either uncommented after copying
# this file to /etc/systemd/system or by creating overrides:
# Copy the needed dependencies into
# /etc/systemd/system/ethercat.service.d/50-dependencies.conf
# in a [Unit] section.
#
# Uncomment this, if the generic Ethernet driver is used. It assures, that the
# network interfaces are configured, before the master starts.
#
#Requires=network.target # Stop master, if network is stopped
#After=network.target # Start master, after network is ready
#
# Uncomment this, if a native Ethernet driver is used. It assures, that the
# network interfaces are configured, after the Ethernet drivers have been
# replaced. Otherwise, the networking configuration tools could be confused.
#
#Before=network-pre.target
#Wants=network-pre.target
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/home/lgv/cmvr/0-workspace/cmvr-es/dependency/x86/third_party/ethercat/v1.7.0/sbin/ethercatctl start
ExecStop=/home/lgv/cmvr/0-workspace/cmvr-es/dependency/x86/third_party/ethercat/v1.7.0/sbin/ethercatctl stop
[Install]
WantedBy=multi-user.target

View File

@ -0,0 +1,253 @@
#!/bin/bash
#------------------------------------------------------------------------------
#
# Start script for EtherCAT to use with systemd
#
# Copyright (C) 2006-2021 Florian Pose, Ingenieurgemeinschaft IgH
#
# This file is part of the IgH EtherCAT Master.
#
# The IgH EtherCAT Master is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 2, as
# published by the Free Software Foundation.
#
# The IgH EtherCAT Master is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
# Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with the IgH EtherCAT Master; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
#
# vim: expandtab sw=4 tw=78
#
#------------------------------------------------------------------------------
LSMOD="/sbin/lsmod"
MODPROBE="/sbin/modprobe"
RMMOD="/sbin/rmmod"
MODINFO="/sbin/modinfo"
IP="/sbin/ip"
ETHERCAT="/home/lgv/cmvr/0-workspace/cmvr-es/dependency/x86/third_party/ethercat/v1.7.0/bin/ethercat"
#------------------------------------------------------------------------------
if [ "$1" = "-c" ]; then
ETHERCAT_CONFIG="$2"
COMMAND="$3"
else
ETHERCAT_CONFIG="/home/lgv/cmvr/0-workspace/cmvr-es/dependency/x86/third_party/ethercat/v1.7.0/etc/ethercat.conf"
COMMAND="$1"
fi
#------------------------------------------------------------------------------
if [ ! -r ${ETHERCAT_CONFIG} ]; then
echo ${ETHERCAT_CONFIG} not existing;
exit 6
fi
# shellcheck source=/etc/ethercat.conf
. ${ETHERCAT_CONFIG}
#------------------------------------------------------------------------------
is_mac_address() {
local x='[0-9a-fA-F]'
echo "$1" | grep -qE "^($x$x:){5}$x$x\$" -
}
#------------------------------------------------------------------------------
parse_mac_address() {
local DEVICENAMETOMAC
if [ -z "${1}" ] || is_mac_address "${1}"; then
MAC="${1}"
else
DEVICENAMETOMAC=$("${IP}" address show dev "${1}" |
awk '/link\/ether/ { print $2; }')
if is_mac_address "${DEVICENAMETOMAC}"; then
MAC="${DEVICENAMETOMAC}"
else
echo Invalid MAC address or interface name \""${1}"\" \
in ${ETHERCAT_CONFIG}
exit 1
fi
fi
}
#------------------------------------------------------------------------------
case "$COMMAND" in
start)
# bring up all updown interfaces before anything else
for interface in $UPDOWN_INTERFACES; do
$IP link set dev $interface up
done
# construct DEVICES and BACKUPS from configuration variables
DEVICES=""
BACKUPS=""
MASTER_INDEX=0
while true; do
DEVICE=$(eval echo "\${MASTER${MASTER_INDEX}_DEVICE}")
BACKUP=$(eval echo "\${MASTER${MASTER_INDEX}_BACKUP}")
if [ -z "${DEVICE}" ]; then break; fi
if [ ${MASTER_INDEX} -gt 0 ]; then
DEVICES=${DEVICES},
BACKUPS=${BACKUPS},
fi
parse_mac_address "${DEVICE}"
DEVICES=${DEVICES}${MAC}
parse_mac_address "${BACKUP}"
BACKUPS=${BACKUPS}${MAC}
MASTER_INDEX=$((${MASTER_INDEX} + 1))
done
if [ -z "${DEVICES}" ]; then
echo "ERROR: No network cards for EtherCAT specified."
echo -n "Please edit ${ETHERCAT_CONFIG} with root permissions"
echo -n " and set MASTER0_DEVICE variable to either a "
echo "network interface name (like eth0) or to a MAC address."
exit 1
fi
MODULE_PARAMS=(
main_devices="${DEVICES}"
backup_devices="${BACKUPS}"
)
if [ -n "$SII_CACHING" ]; then
MODULE_PARAMS+=(sii_caching="$SII_CACHING")
fi
# load master module
if ! ${MODPROBE} ${MODPROBE_FLAGS} ec_master "${MODULE_PARAMS[@]}"; then
exit 1
fi
LOADED_MODULES=ec_master
# check for modules to replace
for MODULE in ${DEVICE_MODULES}; do
ECMODULE=ec_${MODULE}
if ! ${MODINFO} "${ECMODULE}" > /dev/null; then
continue # ec_* module not found
fi
if [ "${MODULE}" != "generic" ] && [ "${MODULE}" != "ccat" ]; then
# unload standard module and check if unloading was successful
${RMMOD} "${MODULE}" 2> /dev/null || true
if ${LSMOD} | grep "^${MODULE//-/_} " > /dev/null; then
# could not unload module
${RMMOD} ${LOADED_MODULES}
exit 1
fi
fi
if ! ${MODPROBE} ${MODPROBE_FLAGS} "${ECMODULE}"; then
if [ "${MODULE}" != "generic" ] && [ "${MODULE}" != "ccat" ]; then
${MODPROBE} ${MODPROBE_FLAGS} "${MODULE}" # try to restore
fi
${RMMOD} ${LOADED_MODULES}
exit 1
fi
LOADED_MODULES="${ECMODULE} ${LOADED_MODULES}"
done
exit 0
;;
#------------------------------------------------------------------------------
stop)
# unload EtherCAT device modules
for MODULE in ${DEVICE_MODULES} master; do
ECMODULE=ec_${MODULE}
if ! ${LSMOD} | grep -q "^${ECMODULE//-/_} "; then
continue # ec_* module not loaded
fi
if ! ${RMMOD} "${ECMODULE}"; then
exit 1
fi;
done
sleep 1
# load standard modules again
for MODULE in ${DEVICE_MODULES}; do
if [ "${MODULE}" == "generic" ] || [ "${MODULE}" == "ccat" ]; then
continue
fi
${MODPROBE} ${MODPROBE_FLAGS} "${MODULE}"
done
# bring down all updown interfaces
for interface in $UPDOWN_INTERFACES; do
$IP link set dev $interface down
done
exit 0
;;
#------------------------------------------------------------------------------
restart)
$0 stop || exit 1
sleep 1
$0 start
;;
#------------------------------------------------------------------------------
status)
echo "Checking for EtherCAT master 1.7.0 "
# count masters in configuration file
MASTER_COUNT=0
while true; do
DEVICE=$(eval echo "\${MASTER${MASTER_COUNT}_DEVICE}")
if [ -z "${DEVICE}" ]; then break; fi
MASTER_COUNT=$((${MASTER_COUNT} + 1))
done
RESULT=0
for i in $(seq 0 "$((${MASTER_COUNT} - 1))"); do
echo -n "Master${i} "
# Check if the master is in idle or operation phase
${ETHERCAT} master --master "${i}" 2>/dev/null | \
grep -qE 'Phase:[[:space:]]*Idle|Phase:[[:space:]]*Operation'
EXITCODE=$?
if [ ${EXITCODE} -eq 0 ]; then
echo " running"
else
echo " dead"
RESULT=1
fi
done
exit ${RESULT}
;;
#------------------------------------------------------------------------------
*)
echo "USAGE: $0 [-c path/to/ethercat.conf] {start|stop|restart|status}"
exit 1
;;
esac
#------------------------------------------------------------------------------

View File

@ -0,0 +1,80 @@
# Copyright (C) 2022 Bjarne von Horn, Ingenieurgemeinschaft IgH
#
# This file is part of the IgH EtherCAT master userspace library.
#
# The IgH EtherCAT master userspace library is free software; you can
# redistribute it and/or modify it under the terms of the GNU Lesser General
# Public License as published by the Free Software Foundation; version 2.1
# of the License.
#
# The IgH EtherCAT master userspace library is distributed in the hope that
# it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with the IgH EtherCAT master userspace library. If not, see
# <http://www.gnu.org/licenses/>.
#
_ethercat_completions()
{
local ethercat_commands="alias config crc cstruct data debug domains download eoe foe_read foe_write graph master pdos reg_read reg_write rescan sdos sii_read sii_write slaves soe_read soe_write states upload version xml"
local options="--help --force --quiet --verbose --master "
if [ "$COMP_CWORD" -eq 1 ] ; then
COMPREPLY=($(compgen -W "$ethercat_commands --help" -- "${COMP_WORDS[1]}"))
elif [[ "${COMP_WORDS[1]}" != "--help" && ! "${COMP_WORDS[COMP_CWORD-1]}" =~ ^-a|-p|--alias|--position$ ]] ; then
case "${COMP_WORDS[1]}" in
"alias" | "config" | "cstruct" | "slaves" | "sdos" | "sii_read" | "upload" | "xml")
options+="--alias --position"
;;
"crc")
options+="reset"
;;
"debug")
options+="0 1 2"
;;
"domains")
options+="--domain"
;;
"download" | "reg_read" | "soe_read" | "soe_write")
if [[ "${COMP_WORDS[COMP_CWORD-1]}" =~ ^-t|--type$ ]] ; then
options="bool int8 int16 int32 int64 uint8 uint16 uint32 uint64 float double string octet_string unicode_string sm8 sm16 sm32 sm64"
else
options+="--alias --position --type"
fi
;;
"foe_read" | "foe_write")
if [[ "${COMP_WORDS[COMP_CWORD-1]}" =~ ^-o|--output-file$ ]] ; then
COMPREPLY=($(compgen -o filenames -A file -- "${COMP_WORDS[$COMP_CWORD]}"))
else
options+="--alias --position --output-file"
COMPREPLY=($(compgen -o filenames -A file -W "$options" -- "${COMP_WORDS[$COMP_CWORD]}"))
fi
return
;;
"graph")
options+="DC CRC"
;;
"pdos")
if [[ "${COMP_WORDS[COMP_CWORD-1]}" =~ ^-s|--skin$ ]] ; then
options="default etherlab"
else
options+="--alias --position --skin"
fi
;;
"sii_write")
options+="--alias --position"
COMPREPLY=($(compgen -o filenames -A file -W "$options" -- "${COMP_WORDS[$COMP_CWORD]}"))
return
;;
"states")
options+="--alias --position INIT PREOP BOOT SAFEOP OP"
;;
esac
COMPREPLY+=($(compgen -W "$options" -- "${COMP_WORDS[$COMP_CWORD]}"))
fi
}
complete -F _ethercat_completions ethercat

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,25 +1,51 @@
# EtherCAT 电机接入教程
这份文档只说明新增一种 EtherCAT 电机需要改哪里、怎么写
当前已接入意优 `EYOU_ServoModule_ECAT_V145`,协议为 EtherCAT CoE + CiA402PDO 使用 `docs/EYOU_ServoModule_ECAT_V145_no_slot.xml` 中的 `0x1600/0x1A00` 映射
## 1. 增加 vendor
ESI XML 作为厂商通信说明和对照资料保存,运行时不直接解析 XML。实际 PDO 映射写在:
修改 `protos/cmvr/config/motor_config/motor_config.proto`
```proto
enum MotorVendor {
MOTOR_VENDOR_UNKNOWN = 0;
MOTOR_VENDOR_TI5 = 1;
MOTOR_VENDOR_MUJOCO = 2;
MOTOR_VENDOR_XXX = 3;
}
```text
cmvr-es/devices/motor/drivers/ethercat_motor/include/vendor/eyou/eyou_cia402_pdo_mapping.h
```
`MOTOR_VENDOR_XXX` 改成真实厂商名,例如 `MOTOR_VENDOR_FOO`。不要复用 `TI5`
## 1. 系统依赖
## 2. 写电机配置
IgH EtherCAT userspace 已安装在:
新增配置文件:
```text
dependency/x86/third_party/ethercat/v1.7.0
```
真机运行前,系统里还需要安装/加载 IgH master 内核模块。使用仓库脚本启动 EtherCAT master
```bash
sudo script/ethercat/start_ethercat.sh eno1
script/ethercat/status_ethercat.sh
```
其中 `eno1` 是连接 EtherCAT 从站的网卡。脚本会读取该网卡 MAC写入
```text
dependency/x86/third_party/ethercat/v1.7.0/etc/ethercat.conf
```
并通过 bundled IgH 的 `ethercatctl -c` 启动 master。能看到 EYOU slave 后再启动程序。
停止 EtherCAT
```bash
sudo script/ethercat/stop_ethercat.sh eno1
```
如果这张网卡要恢复给普通网络使用:
```bash
sudo script/ethercat/stop_ethercat.sh eno1 --restore-network
```
## 2. 电机配置
新增或修改:
```text
cmvr-es/config/devices/motor/ethercat_motors.pb.txt
@ -34,20 +60,28 @@ motor {
motor_groups {
id: "right_arm_ethercat"
bus_type: MOTOR_BUS_ETHERCAT
vendor: MOTOR_VENDOR_XXX
vendor: MOTOR_VENDOR_EYOU
protocol: MOTOR_PROTOCOL_ETHERCAT_CIA402
tool_frame: "R_FINGER_TIP"
ethercat {
master_id: "eth0"
master_index: 0
cycle_us: 1000
slaves { motor_id: 1 slave_index: 0 vendor_id: 0x00000000 product_code: 0x00000000 }
slaves { motor_id: 2 slave_index: 1 vendor_id: 0x00000000 product_code: 0x00000000 }
slaves { motor_id: 3 slave_index: 2 vendor_id: 0x00000000 product_code: 0x00000000 }
slaves { motor_id: 4 slave_index: 3 vendor_id: 0x00000000 product_code: 0x00000000 }
slaves { motor_id: 5 slave_index: 4 vendor_id: 0x00000000 product_code: 0x00000000 }
slaves { motor_id: 6 slave_index: 5 vendor_id: 0x00000000 product_code: 0x00000000 }
slaves { motor_id: 7 slave_index: 6 vendor_id: 0x00000000 product_code: 0x00000000 }
cia402 {
profile_position_trigger_delay_ms: 2
state_transition_timeout_ms: 1200
velocity_stop_timeout_ms: 2000
status_poll_period_ms: 10
stopped_velocity_tolerance_rad_s: 0.001
}
slaves { motor_id: 1 alias: 0 position: 0 }
slaves { motor_id: 2 alias: 0 position: 1 }
slaves { motor_id: 3 alias: 0 position: 2 }
slaves { motor_id: 4 alias: 0 position: 3 }
slaves { motor_id: 5 alias: 0 position: 4 }
slaves { motor_id: 6 alias: 0 position: 5 }
slaves { motor_id: 7 alias: 0 position: 6 }
}
joint_limits {
@ -63,21 +97,26 @@ motor {
}
motors {
motors { id: 1 joint_name: "R_SHOULDER_P" }
motors { id: 2 joint_name: "R_SHOULDER_R" }
motors { id: 3 joint_name: "R_SHOULDER_Y" }
motors { id: 4 joint_name: "R_ELBOW_R" }
motors { id: 5 joint_name: "R_WRIST_P" }
motors { id: 6 joint_name: "R_WRIST_Y" }
motors { id: 7 joint_name: "R_WRIST_R" }
motors { id: 1 joint_name: "R_SHOULDER_P" encoder_counts_per_rev: 65536 gear_ratio: 101.0 }
motors { id: 2 joint_name: "R_SHOULDER_R" encoder_counts_per_rev: 65536 gear_ratio: 101.0 }
motors { id: 3 joint_name: "R_SHOULDER_Y" encoder_counts_per_rev: 65536 gear_ratio: 101.0 }
motors { id: 4 joint_name: "R_ELBOW_R" encoder_counts_per_rev: 65536 gear_ratio: 101.0 }
motors { id: 5 joint_name: "R_WRIST_P" encoder_counts_per_rev: 65536 gear_ratio: 101.0 }
motors { id: 6 joint_name: "R_WRIST_Y" encoder_counts_per_rev: 65536 gear_ratio: 101.0 }
motors { id: 7 joint_name: "R_WRIST_R" encoder_counts_per_rev: 65536 gear_ratio: 101.0 }
}
}
}
```
`motors.motors.id` 是系统内的电机逻辑 id。`ethercat.slaves.motor_id` 必须和它对应。
字段说明:
`joint_limits``joint_name` 读取,和 CAN、MuJoCo 电机保持同一个风格。
- `master_index`IgH master 编号,第一张 EtherCAT master 是 `0`
- `alias`EtherCAT alias一般为 `0`
- `position`slave 在 EtherCAT 链路上的顺序,从 `0` 开始。
- `motor_id`:系统内电机 id必须和 `motors.motors.id` 对应。
- `encoder_counts_per_rev`:电机编码器每转 count 数,用于 `rad <-> count` 换算。
- `gear_ratio`:电机轴到关节输出轴的减速比,用于接口层 `rad/rad/s` 和驱动器 raw count/counts/s 换算。
## 3. 注册设备
@ -98,15 +137,9 @@ devices {
}
```
## 4. 机械臂使用 EtherCAT group
## 4. 机械臂使用 EtherCAT 电机
修改机械臂配置,例如:
```text
cmvr-es/config/devices/arm/arm.pb.txt
```
把 motor backend 改成:
修改机械臂配置中的 motor backend
```proto
motor {
@ -122,116 +155,66 @@ motor {
}
```
## 5. 实现 bus runtime
## 5. 代码结构
EtherCAT 总线资源放在
EtherCAT 总线运行时
```text
cmvr-es/devices/motor/bus_runtime/ethercat/include/ethercat_motor_bus_runtime.h
cmvr-es/devices/motor/bus_runtime/ethercat/src/ethercat_motor_bus_runtime.cpp
```
`EthercatMotorBusRuntime` 负责:
它负责:
- 打开 IgH master
- 创建 domain
- 使用 `vendor + protocol` 选择 PDO mapping
- 按 mapping 配置 `0x1600/0x1A00` PDO
- 注册每个非 padding PDO entry 的 offset
- 启动 cyclic loop
- 按 `motor_id + index + subindex` 提供通用 PDO 读写接口
CiA402 EtherCAT 电机驱动:
```text
读取 ethercat 配置
初始化 EtherCAT master
扫描/校验 slave_index、vendor_id、product_code
启动 cyclic loop
保存 command/feedback buffer
停止 cyclic loop
cmvr-es/devices/motor/drivers/ethercat_motor/include/cia402/cia402_protocol.h
cmvr-es/devices/motor/drivers/ethercat_motor/include/cia402/cia402_objects.h
cmvr-es/devices/motor/drivers/ethercat_motor/include/vendor/motor_vendor_adapter.h
cmvr-es/devices/motor/drivers/ethercat_motor/src/cia402/cia402_protocol.cpp
```
bus runtime 不创建具体电机,也不关心厂商;它只保存总线连接、线程和数据缓存。
## 6. 增加具体电机 driver
新增目录:
EYOU 私有适配:
```text
cmvr-es/devices/motor/drivers/xxx_ethercat/
CMakeLists.txt
include/xxx_ethercat_motor.h
include/xxx_ethercat_motor_protocol.h
src/xxx_ethercat_motor.cpp
src/xxx_ethercat_motor_protocol.cpp
cmvr-es/devices/motor/drivers/ethercat_motor/include/vendor/eyou/eyou_motor.h
cmvr-es/devices/motor/drivers/ethercat_motor/include/vendor/eyou/eyou_motor_adapter.h
cmvr-es/devices/motor/drivers/ethercat_motor/include/vendor/eyou/eyou_cia402_pdo_mapping.h
cmvr-es/devices/motor/drivers/ethercat_motor/include/vendor/eyou/eyou_objects.h
cmvr-es/devices/motor/drivers/ethercat_motor/src/vendor/eyou/eyou_motor.cpp
cmvr-es/devices/motor/drivers/ethercat_motor/src/vendor/eyou/eyou_motor_adapter.cpp
```
`XxxEthercatMotor` 继承 `AbstractMotor`
它负责:
`XxxEthercatMotorProtocol` 继承 `MotorProtocolInterface`,把 `setTarget`、`setQd`、`getQ` 等接口转换成 EtherCAT command/feedback。
- CiA402 `6040/6041` 状态机
- 设置 `6060` 运行模式
- 写 `607A/60FF/6071`
- 读 `6064/606C/6077/603F`
- `rad` 和 encoder count 的换算
## 7. 在 MotorManager 里创建 EtherCAT 电机
修改:
```text
cmvr-es/devices/motor/manager/src/motor_manager.cpp
```
`MotorManager::createEthercatMotors_()` 里按 `vendor + protocol` 创建具体电机:
```cpp
auto ethercat_bus_runtime =
std::dynamic_pointer_cast<EthercatMotorBusRuntime>(bus_runtime);
if (group_cfg.vendor() == config::MOTOR_VENDOR_XXX &&
group_cfg.protocol() == config::MOTOR_PROTOCOL_ETHERCAT_CIA402) {
auto protocol = std::make_shared<XxxEthercatMotorProtocol>(ethercat_bus_runtime);
std::vector<std::shared_ptr<AbstractMotor>> motors;
motors.reserve(motor_cfgs.size());
for (const auto& cfg : motor_cfgs) {
auto motor = std::make_shared<XxxEthercatMotor>(cfg);
motor->setProtocol(protocol);
if (!motor->init()) {
return {};
}
motors.push_back(std::move(motor));
}
return motors;
}
```
## 8. 加入 CMake
修改:
```text
cmvr-es/devices/motor/manager/CMakeLists.txt
```
`motor_manager` 链接新增的具体 EtherCAT 电机 target。
修改:
```text
cmvr-es/devices/motor/CMakeLists.txt
```
加入:
```cmake
add_subdirectory(drivers/xxx_ethercat)
```
## 9. 验证配置
先验证 proto
## 6. 编译验证
```bash
./output/bin/protoc \
--encode=cmvr.config.MotorRootConfig \
-I protos \
protos/cmvr/config/motor_config/motor_config.proto \
< cmvr-es/config/devices/motor/ethercat_motors.pb.txt \
> /tmp/ethercat_motors.pb.bin
cmake -S . -B cmake-build-debug
cmake --build cmake-build-debug --target motor_manager
```
再编译
真机验证顺序:
```bash
cmake --build cmake-build-debug --target mujoco_manual_ui_test
sudo script/ethercat/start_ethercat.sh eno1
script/ethercat/status_ethercat.sh
cmake --build cmake-build-debug --target cmvr_es
```
真机联调时先只验证初始化日志master 打开、slave 数量、vendor/product 校验、cyclic loop 启动、每个 motor 注册成功。然后再下发运动命令
第一次联调先不要大幅度运动。先看 master 是否打开、slave 是否进入 OP、状态字是否更新再给单个电机小角度目标。

Binary file not shown.

View File

@ -34,6 +34,7 @@ message DeviceManagerConfig {
string version = 2;
string description = 3;
repeated DeviceConfigEntry devices = 4;
bool init_all_motors_when_no_active_joints = 20;
}
message DeviceManagerRootConfig {
DeviceManagerConfig device_manager = 1;

View File

@ -10,6 +10,8 @@ message MotorConfigItem {
double limit_q_ub = 4;
double limit_qd = 5;
double limit_qdd = 6;
double encoder_counts_per_rev = 7;
double gear_ratio = 8;
}
message MotorList {
@ -18,9 +20,26 @@ message MotorList {
message EthercatSlaveConfig {
int32 motor_id = 1;
int32 slave_index = 2;
uint32 vendor_id = 3;
uint32 product_code = 4;
uint32 alias = 2;
uint32 position = 3;
}
message Cia402ProtocolConfig {
uint32 profile_position_trigger_delay_ms = 1;
uint32 state_transition_timeout_ms = 2;
uint32 velocity_stop_timeout_ms = 3;
uint32 status_poll_period_ms = 4;
double stopped_velocity_tolerance_rad_s = 5;
}
message EtherCATDcConfig {
optional bool enable = 1;
optional int32 reference_motor_id = 2;
optional uint32 sync0_cycle_us = 3;
optional int32 sync0_shift_us = 4;
optional uint32 sync_reference_clock_period = 5;
optional uint32 assign_activate = 6;
optional uint32 sync_monitor_period_ms = 7;
}
message SocketCanConfig {
@ -29,8 +48,12 @@ message SocketCanConfig {
}
message EtherCATConfig {
string master_id = 1;
uint32 master_index = 1;
int32 cycle_us = 2;
Cia402ProtocolConfig cia402 = 3;
EtherCATDcConfig dc = 4;
optional uint32 slave_op_timeout_ms = 5;
optional uint32 slave_state_poll_period_ms = 6;
repeated EthercatSlaveConfig slaves = 10;
}
@ -49,6 +72,7 @@ enum MotorVendor {
MOTOR_VENDOR_UNKNOWN = 0;
MOTOR_VENDOR_TI5 = 1;
MOTOR_VENDOR_MUJOCO = 2;
MOTOR_VENDOR_EYOU = 3;
}
enum MotorProtocol {
@ -63,7 +87,6 @@ message MotorGroupConfig {
MotorBusType bus_type = 2;
MotorVendor vendor = 3;
MotorProtocol protocol = 4;
string tool_frame = 5;
oneof bus_config {
SocketCanConfig can = 10;

View File

@ -5,8 +5,8 @@ package cmvr.msgs;
message SdoFrame {
uint32 node_id = 1; // ID
CommandSpecifier cs = 2; // SDO命令字
ObIndex index = 3; //
ObSubIndex sub_index = 4; //
uint32 index = 3; //
uint32 sub_index = 4; //
uint32 data = 5; //
}
@ -110,81 +110,38 @@ enum NmtCommand {
}
//
enum ObIndex {
INDEX_ZERO = 0;
USER_SAVE_PARA_2000 = 0x2000; // 1
POSITION_OFFSET_2008 = 0x2008; // 0x00
// Error Codes
ERROR_CODE_6007 = 0x6007;
ERROR_CODE_603F = 0x603F;
// CANopen communication object dictionary indexes.
// CiA402 drive-profile objects are defined in cia402.proto.
enum CanopenObjectIndex {
CANOPEN_OBJECT_INDEX_ZERO = 0;
// Control and Status
CONTROL_WORD_6040 = 0x6040;
STATUS_WORD_6041 = 0x6041;
CANOPEN_PRODUCER_HEARTBEAT_TIME_1017 = 0x1017;
// Operation Modes
OPERATION_MODE_6060 = 0x6060;
MODE_DISPLAY_6061 = 0x6061;
// Actual Values
ACTUAL_POSITION_6064 = 0x6064;
ACTUAL_SPEED_606C = 0x606C;
ACTUAL_CURRENT_6078 = 0x6078;
// Torque-related
TARGET_TORQUE_6071 = 0x6071;
MAX_TORQUE_6072 = 0x6072;
DEMAND_TORQUE_6074 = 0x6074;
// Position-related
TARGET_POSITION_607A = 0x607A;
SOFTWARE_POSITION_LIMIT_607D = 0x607D; // Sub-indexes: 1, 2
// Speed-related
MAX_SPEED_607F = 0x607F;
PROFILE_SPEED_6081 = 0x6081;
PROFILE_ACCELERATION_6083 = 0x6083;
PROFILE_DECELERATION_6084 = 0x6084;
// Same as DEMAND_TORQUE? Verify correctness.
// TORQUE_SLOPE_6074 = 0x6074;
// PID Control
CURRENT_LOOP_PID_60F6 = 0x60F6; // Sub-indexes: 1, 2
SPEED_LOOP_PID_60F9 = 0x60F9; // Sub-indexes: 1, 2
POSITION_LOOP_PID_60FB = 0x60FB; // Sub-indexes: 1, 2, 3
// Target Speed
TARGET_SPEED_60FF = 0x60FF;
QUICK_STOP_OPTION_605A = 0x605A;
QUICK_STOP_DECEL_6085 = 0x6085;
// -------------------------
// PDO Communication Object
RPDO1_COMM_1400 = 0x1400;
RPDO2_COMM_1401 = 0x1401;
RPDO3_COMM_1402 = 0x1402;
RPDO4_COMM_1403 = 0x1403;
CANOPEN_RPDO1_COMM_1400 = 0x1400;
CANOPEN_RPDO2_COMM_1401 = 0x1401;
CANOPEN_RPDO3_COMM_1402 = 0x1402;
CANOPEN_RPDO4_COMM_1403 = 0x1403;
TPDO1_COMM_1800 = 0x1800;
TPDO2_COMM_1801 = 0x1801;
TPDO3_COMM_1802 = 0x1802;
TPDO4_COMM_1803 = 0x1803;
CANOPEN_TPDO1_COMM_1800 = 0x1800;
CANOPEN_TPDO2_COMM_1801 = 0x1801;
CANOPEN_TPDO3_COMM_1802 = 0x1802;
CANOPEN_TPDO4_COMM_1803 = 0x1803;
// PDO Mapping Object
RPDO1_MAP_1600 = 0x1600;
RPDO2_MAP_1601 = 0x1601;
RPDO3_MAP_1602 = 0x1602;
RPDO4_MAP_1603 = 0x1603;
CANOPEN_RPDO1_MAP_1600 = 0x1600;
CANOPEN_RPDO2_MAP_1601 = 0x1601;
CANOPEN_RPDO3_MAP_1602 = 0x1602;
CANOPEN_RPDO4_MAP_1603 = 0x1603;
TPDO1_MAP_1A00 = 0x1A00;
TPDO2_MAP_1A01 = 0x1A01;
TPDO3_MAP_1A02 = 0x1A02;
TPDO4_MAP_1A03 = 0x1A03;
CANOPEN_TPDO1_MAP_1A00 = 0x1A00;
CANOPEN_TPDO2_MAP_1A01 = 0x1A01;
CANOPEN_TPDO3_MAP_1A02 = 0x1A02;
CANOPEN_TPDO4_MAP_1A03 = 0x1A03;
PRODUCER_HEARTBEAT_TIME = 0x1017;
// Ti5 vendor-specific objects used through CANopen SDO.
CANOPEN_USER_SAVE_PARA_2000 = 0x2000;
CANOPEN_POSITION_OFFSET_2008 = 0x2008;
}
//
@ -198,5 +155,3 @@ enum ObSubIndex {
SUB_INDEX_6 = 6;
SUB_INDEX_7 = 7;
}

View File

@ -0,0 +1,64 @@
syntax = "proto3";
package cmvr.msgs;
// CiA402 object dictionary indexes shared by CANopen and EtherCAT CoE drives.
enum Cia402ObjectIndex {
CIA402_OBJECT_INDEX_ZERO = 0;
CIA402_ERROR_CODE_603F = 0x603F;
CIA402_CONTROL_WORD_6040 = 0x6040;
CIA402_STATUS_WORD_6041 = 0x6041;
CIA402_QUICK_STOP_OPTION_605A = 0x605A;
CIA402_SHUTDOWN_OPTION_605B = 0x605B;
CIA402_DISABLE_OPERATION_OPTION_605C = 0x605C;
CIA402_HALT_OPTION_605D = 0x605D;
CIA402_FAULT_REACTION_OPTION_605E = 0x605E;
CIA402_OPERATION_MODE_6060 = 0x6060;
CIA402_MODE_DISPLAY_6061 = 0x6061;
CIA402_POSITION_DEMAND_VALUE_6062 = 0x6062;
CIA402_ACTUAL_POSITION_6064 = 0x6064;
CIA402_MAX_FOLLOWING_ERROR_6065 = 0x6065;
CIA402_POSITION_WINDOW_6067 = 0x6067;
CIA402_POSITION_WINDOW_TIME_6068 = 0x6068;
CIA402_VELOCITY_DEMAND_VALUE_606B = 0x606B;
CIA402_ACTUAL_VELOCITY_606C = 0x606C;
CIA402_VELOCITY_WINDOW_606D = 0x606D;
CIA402_VELOCITY_WINDOW_TIME_606E = 0x606E;
CIA402_VELOCITY_THRESHOLD_606F = 0x606F;
CIA402_VELOCITY_THRESHOLD_TIME_6070 = 0x6070;
CIA402_TARGET_TORQUE_6071 = 0x6071;
CIA402_MAX_TORQUE_6072 = 0x6072;
CIA402_TORQUE_DEMAND_VALUE_6074 = 0x6074;
CIA402_MOTOR_RATED_TORQUE_6076 = 0x6076;
CIA402_ACTUAL_TORQUE_6077 = 0x6077;
CIA402_ACTUAL_CURRENT_6078 = 0x6078;
CIA402_DC_LINK_VOLTAGE_6079 = 0x6079;
CIA402_TARGET_POSITION_607A = 0x607A;
CIA402_HOME_OFFSET_607C = 0x607C;
CIA402_SOFTWARE_POSITION_LIMIT_607D = 0x607D;
CIA402_MAX_PROFILE_VELOCITY_607F = 0x607F;
CIA402_PROFILE_VELOCITY_6081 = 0x6081;
CIA402_PROFILE_ACCELERATION_6083 = 0x6083;
CIA402_PROFILE_DECELERATION_6084 = 0x6084;
CIA402_QUICK_STOP_DECELERATION_6085 = 0x6085;
CIA402_TORQUE_SLOPE_6087 = 0x6087;
CIA402_GEAR_RATIO_6091 = 0x6091;
CIA402_VELOCITY_OFFSET_60B1 = 0x60B1;
CIA402_TORQUE_OFFSET_60B2 = 0x60B2;
CIA402_INTERPOLATION_DATA_RECORD_60C1 = 0x60C1;
CIA402_INTERPOLATION_TIME_PERIOD_60C2 = 0x60C2;
CIA402_FOLLOWING_ERROR_ACTUAL_VALUE_60F4 = 0x60F4;
CIA402_TARGET_VELOCITY_60FF = 0x60FF;
CIA402_SUPPORTED_DRIVE_MODES_6502 = 0x6502;
}

View File

@ -114,5 +114,3 @@ message MotorStatus {
uint32 status_word = 44;
}

View File

@ -1,6 +1,5 @@
syntax = "proto3";
import "cmvr/msgs/canopen.proto";
import "cmvr/msgs/motor.proto";
package cmvr.msgs;

View File

@ -33,5 +33,6 @@ third_party/modbus/3.1.11
third_party/visp/3.7.0
third_party/mainif/0.0.5
third_party/matplotplusplus/1.2.0
third_party/ethercat/v1.7.0

133
script/ethercat/start_ethercat.sh Executable file
View File

@ -0,0 +1,133 @@
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'USAGE'
Usage:
sudo script/ethercat/start_ethercat.sh [iface] [ethercat_dev] [start_wait_sec]
Any pure numeric argument is treated as start_wait_sec.
Example:
sudo script/ethercat/start_ethercat.sh eno1
sudo script/ethercat/start_ethercat.sh 10
sudo script/ethercat/start_ethercat.sh eno1 10
sudo script/ethercat/start_ethercat.sh eno1 /dev/EtherCAT1
sudo script/ethercat/start_ethercat.sh eno1 /dev/EtherCAT0 10
sudo script/ethercat/start_ethercat.sh eno1 10 /dev/EtherCAT0
Environment:
DEVICE_MODULES=generic IgH device module list.
IGH_ROOT=... Override bundled IgH install path.
ETHERCAT_CONF=... Override ethercatctl config path.
ETHERCAT_DEV=/dev/EtherCAT0 Override EtherCAT character device node.
START_WAIT_SEC=5 Seconds to wait for link/slave discovery.
USAGE
}
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
usage
exit 0
fi
if [[ "$(id -u)" -ne 0 ]]; then
echo "error: please run with sudo." >&2
exit 1
fi
IFACE="${IFACE:-eno1}"
ETHERCAT_DEV="${ETHERCAT_DEV:-/dev/EtherCAT0}"
START_WAIT_SEC="${START_WAIT_SEC:-5}"
NON_NUMERIC_ARG_COUNT=0
for arg in "$@"; do
if [[ "${arg}" =~ ^[0-9]+$ ]]; then
START_WAIT_SEC="${arg}"
continue
fi
case "${NON_NUMERIC_ARG_COUNT}" in
0)
IFACE="${arg}"
;;
1)
ETHERCAT_DEV="${arg}"
;;
*)
echo "error: unexpected argument: ${arg}" >&2
usage >&2
exit 1
;;
esac
NON_NUMERIC_ARG_COUNT=$((NON_NUMERIC_ARG_COUNT + 1))
done
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
IGH_ROOT="${IGH_ROOT:-${REPO_ROOT}/dependency/x86/third_party/ethercat/v1.7.0}"
DEVICE_MODULES="${DEVICE_MODULES:-generic}"
ETHERCAT_CONF="${ETHERCAT_CONF:-${IGH_ROOT}/etc/ethercat.conf}"
ETHERCATCTL="${IGH_ROOT}/sbin/ethercatctl"
ETHERCAT="${IGH_ROOT}/bin/ethercat"
if [[ ! -d "/sys/class/net/${IFACE}" ]]; then
echo "error: network interface '${IFACE}' does not exist." >&2
exit 1
fi
if [[ ! -x "${ETHERCATCTL}" ]]; then
echo "error: ethercatctl not found: ${ETHERCATCTL}" >&2
exit 1
fi
if [[ ! -x "${ETHERCAT}" ]]; then
echo "error: ethercat command not found: ${ETHERCAT}" >&2
exit 1
fi
MAC="$(cat "/sys/class/net/${IFACE}/address")"
echo "EtherCAT interface: ${IFACE}"
echo "EtherCAT MAC: ${MAC}"
echo "EtherCAT device: ${ETHERCAT_DEV}"
echo "IgH root: ${IGH_ROOT}"
echo "IgH config: ${ETHERCAT_CONF}"
echo "Device modules: ${DEVICE_MODULES}"
echo "Start wait: ${START_WAIT_SEC}s"
mkdir -p "$(dirname "${ETHERCAT_CONF}")"
if [[ -f "${ETHERCAT_CONF}" ]]; then
echo "Stopping existing EtherCAT master with current config..."
"${ETHERCATCTL}" -c "${ETHERCAT_CONF}" stop >/dev/null 2>&1 || true
sleep 1
fi
cat >"${ETHERCAT_CONF}" <<EOF
MASTER0_DEVICE="${MAC}"
DEVICE_MODULES="${DEVICE_MODULES}"
UPDOWN_INTERFACES="${IFACE}"
EOF
if command -v nmcli >/dev/null 2>&1; then
nmcli device disconnect "${IFACE}" >/dev/null 2>&1 || true
fi
ip addr flush dev "${IFACE}"
ip link set "${IFACE}" up
"${ETHERCATCTL}" -c "${ETHERCAT_CONF}" start
if [[ -e "${ETHERCAT_DEV}" ]]; then
chmod 666 "${ETHERCAT_DEV}"
else
echo "warning: ${ETHERCAT_DEV} not found; skip chmod. Check with: ls -l /dev/EtherCAT*" >&2
fi
for ((i = 0; i < START_WAIT_SEC; ++i)); do
if "${ETHERCAT}" slaves 2>/dev/null | grep -qE '^[0-9]+[[:space:]]'; then
break
fi
sleep 1
done
"${ETHERCAT}" master
"${ETHERCAT}" slaves || true

View File

@ -0,0 +1,62 @@
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'USAGE'
Usage:
script/ethercat/status_ethercat.sh
Environment:
IGH_ROOT=... Override bundled IgH install path.
ETHERCAT_CONF=... Override ethercatctl config path.
USAGE
}
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
usage
exit 0
fi
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
IGH_ROOT="${IGH_ROOT:-${REPO_ROOT}/dependency/x86/third_party/ethercat/v1.7.0}"
ETHERCAT_CONF="${ETHERCAT_CONF:-${IGH_ROOT}/etc/ethercat.conf}"
ETHERCATCTL="${IGH_ROOT}/sbin/ethercatctl"
ETHERCAT="${IGH_ROOT}/bin/ethercat"
if [[ ! -x "${ETHERCATCTL}" ]]; then
echo "error: ethercatctl not found: ${ETHERCATCTL}" >&2
exit 1
fi
if [[ ! -x "${ETHERCAT}" ]]; then
echo "error: ethercat command not found: ${ETHERCAT}" >&2
exit 1
fi
echo "== ${ETHERCAT_CONF} =="
if [[ -f "${ETHERCAT_CONF}" ]]; then
sed -n '1,80p' "${ETHERCAT_CONF}"
else
echo "missing"
fi
echo
echo "== kernel modules =="
lsmod | grep -E '(^ec_master|^ec_generic|^ec_)' || true
echo
echo "== ethercatctl =="
"${ETHERCATCTL}" -c "${ETHERCAT_CONF}" status || true
echo
echo "== master =="
"${ETHERCAT}" master || true
echo
echo "== slaves =="
"${ETHERCAT}" slaves || true
echo
echo "== pdos =="
"${ETHERCAT}" pdos || true

View File

@ -0,0 +1,70 @@
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'USAGE'
Usage:
sudo script/ethercat/stop_ethercat.sh [iface] [--restore-network]
Examples:
sudo script/ethercat/stop_ethercat.sh eno1
sudo script/ethercat/stop_ethercat.sh eno1 --restore-network
Environment:
IGH_ROOT=... Override bundled IgH install path.
ETHERCAT_CONF=... Override ethercatctl config path.
USAGE
}
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
usage
exit 0
fi
if [[ "$(id -u)" -ne 0 ]]; then
echo "error: please run with sudo." >&2
exit 1
fi
IFACE="eno1"
RESTORE_NETWORK="false"
for arg in "$@"; do
case "${arg}" in
--restore-network)
RESTORE_NETWORK="true"
;;
-*)
echo "error: unknown option: ${arg}" >&2
usage >&2
exit 1
;;
*)
IFACE="${arg}"
;;
esac
done
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
IGH_ROOT="${IGH_ROOT:-${REPO_ROOT}/dependency/x86/third_party/ethercat/v1.7.0}"
ETHERCAT_CONF="${ETHERCAT_CONF:-${IGH_ROOT}/etc/ethercat.conf}"
ETHERCATCTL="${IGH_ROOT}/sbin/ethercatctl"
if [[ ! -x "${ETHERCATCTL}" ]]; then
echo "error: ethercatctl not found: ${ETHERCATCTL}" >&2
exit 1
fi
"${ETHERCATCTL}" -c "${ETHERCAT_CONF}" stop
if [[ "${RESTORE_NETWORK}" == "true" ]]; then
ip link set "${IFACE}" up
if command -v nmcli >/dev/null 2>&1; then
nmcli device connect "${IFACE}" || true
fi
echo "Stopped EtherCAT and requested normal network restore on ${IFACE}."
else
echo "Stopped EtherCAT. Normal network restore skipped."
echo "Use '--restore-network' if this interface should return to NetworkManager."
fi