Commit b145d527 authored by 唐永康's avatar 唐永康

Add RealMan RM75 demos and docs

parent 723933d0
build build
cmake-build-*/
.vscode/ .vscode/
linker_hand/build linker_hand/build
TODO.md TODO.md
PROJECT_ANALYSIS.md PROJECT_ANALYSIS.md
realman_slow_motion_demo
reports/realman_hardware_tests/*.log
reports/realman_hardware_tests/*.pid
This diff is collapsed.
{
"version": 3,
"cmakeMinimumRequired": {
"major": 3,
"minor": 15,
"patch": 0
},
"configurePresets": [
{
"name": "clion-debug",
"displayName": "CLion Debug",
"description": "Debug build for CLion. Tests are disabled by default to keep the RealMan demo build self-contained.",
"generator": "Unix Makefiles",
"binaryDir": "${sourceDir}/cmake-build-debug",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug",
"CMAKE_EXPORT_COMPILE_COMMANDS": "ON",
"BUILD_TESTING": "OFF",
"REALMAN_BUILD_HARDWARE_TESTS": "ON",
"REALMAN_REGISTER_HARDWARE_TESTS": "ON",
"REALMAN_RM_API2_ROOT": "/home/mashiro/RM_API2/C++"
}
},
{
"name": "clion-release",
"displayName": "CLion Release",
"description": "Release build for CLion. Tests are disabled by default to keep the RealMan demo build self-contained.",
"generator": "Unix Makefiles",
"binaryDir": "${sourceDir}/cmake-build-clion-release",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release",
"CMAKE_EXPORT_COMPILE_COMMANDS": "ON",
"BUILD_TESTING": "OFF",
"REALMAN_BUILD_HARDWARE_TESTS": "ON",
"REALMAN_REGISTER_HARDWARE_TESTS": "ON",
"REALMAN_RM_API2_ROOT": "/home/mashiro/RM_API2/C++"
}
}
],
"buildPresets": [
{
"name": "realman-demo-debug",
"configurePreset": "clion-debug",
"targets": [
"realman_minimal_demo"
]
},
{
"name": "realman-slow-motion-debug",
"configurePreset": "clion-debug",
"targets": [
"realman_slow_motion_demo"
]
},
{
"name": "realman-joint-pose-config-debug",
"configurePreset": "clion-debug",
"targets": [
"realman_joint_pose_config_demo"
]
},
{
"name": "realman-metrics-test-debug",
"configurePreset": "clion-debug",
"targets": [
"realman_metrics_test"
]
},
{
"name": "realman-trajectory-record-debug",
"configurePreset": "clion-debug",
"targets": [
"realman_trajectory_record"
]
},
{
"name": "realman-trajectory-replay-debug",
"configurePreset": "clion-debug",
"targets": [
"realman_trajectory_replay"
]
},
{
"name": "realman-hw-readonly-tests-debug",
"configurePreset": "clion-debug",
"targets": [
"realman_hardware_readonly_tests"
]
},
{
"name": "rm75-acceptance-readonly-debug",
"configurePreset": "clion-debug",
"targets": [
"rm75_acceptance_readonly_tests"
]
},
{
"name": "realman-hw-tests-debug",
"configurePreset": "clion-debug",
"targets": [
"realman_hardware_tests"
]
},
{
"name": "realman-hw-run-readonly-tests-debug",
"configurePreset": "clion-debug",
"targets": [
"run_realman_hardware_readonly_tests"
]
},
{
"name": "rm75-acceptance-run-readonly-debug",
"configurePreset": "clion-debug",
"targets": [
"run_rm75_acceptance_readonly_tests"
]
},
{
"name": "realman-hw-run-tests-debug",
"configurePreset": "clion-debug",
"targets": [
"run_realman_hardware_tests"
]
},
{
"name": "realman-hw-force-retreat-debug",
"configurePreset": "clion-debug",
"targets": [
"realman_hw_test_force_retreat"
]
},
{
"name": "realman-hw-force-retreat-hold-debug",
"configurePreset": "clion-debug",
"targets": [
"realman_hw_test_force_retreat_hold"
]
},
{
"name": "realman-hw-run-manual-tests-debug",
"configurePreset": "clion-debug",
"targets": [
"run_realman_hardware_manual_tests"
]
},
{
"name": "realman-demo-release",
"configurePreset": "clion-release",
"targets": [
"realman_minimal_demo"
]
},
{
"name": "realman-slow-motion-release",
"configurePreset": "clion-release",
"targets": [
"realman_slow_motion_demo"
]
},
{
"name": "realman-joint-pose-config-release",
"configurePreset": "clion-release",
"targets": [
"realman_joint_pose_config_demo"
]
},
{
"name": "realman-metrics-test-release",
"configurePreset": "clion-release",
"targets": [
"realman_metrics_test"
]
},
{
"name": "realman-trajectory-record-release",
"configurePreset": "clion-release",
"targets": [
"realman_trajectory_record"
]
},
{
"name": "realman-trajectory-replay-release",
"configurePreset": "clion-release",
"targets": [
"realman_trajectory_replay"
]
},
{
"name": "realman-hw-readonly-tests-release",
"configurePreset": "clion-release",
"targets": [
"realman_hardware_readonly_tests"
]
},
{
"name": "rm75-acceptance-readonly-release",
"configurePreset": "clion-release",
"targets": [
"rm75_acceptance_readonly_tests"
]
},
{
"name": "realman-hw-tests-release",
"configurePreset": "clion-release",
"targets": [
"realman_hardware_tests"
]
},
{
"name": "realman-hw-run-readonly-tests-release",
"configurePreset": "clion-release",
"targets": [
"run_realman_hardware_readonly_tests"
]
},
{
"name": "rm75-acceptance-run-readonly-release",
"configurePreset": "clion-release",
"targets": [
"run_rm75_acceptance_readonly_tests"
]
},
{
"name": "realman-hw-run-tests-release",
"configurePreset": "clion-release",
"targets": [
"run_realman_hardware_tests"
]
},
{
"name": "realman-hw-force-retreat-release",
"configurePreset": "clion-release",
"targets": [
"realman_hw_test_force_retreat"
]
},
{
"name": "realman-hw-force-retreat-hold-release",
"configurePreset": "clion-release",
"targets": [
"realman_hw_test_force_retreat_hold"
]
},
{
"name": "realman-hw-run-manual-tests-release",
"configurePreset": "clion-release",
"targets": [
"run_realman_hardware_manual_tests"
]
}
]
}
...@@ -237,6 +237,18 @@ auto state_arc = hand.getStateArc(); ...@@ -237,6 +237,18 @@ auto state_arc = hand.getStateArc();
更多示例请参考 `examples/` 目录。详细说明请查看 [示例代码文档](examples/README.md) 更多示例请参考 `examples/` 目录。详细说明请查看 [示例代码文档](examples/README.md)
睿尔曼 RealMan C++ SDK 的 CLion 搭建说明请查看 [CLion 搭建睿尔曼 RealMan C++ SDK](docs/REALMAN_CLION_SETUP.md)
睿尔曼 RealMan 机械臂最小连接 Demo 请查看 [睿尔曼机械臂最小 Demo](docs/REALMAN_MINIMAL_DEMO.md),CLion 可直接构建目标 `realman_minimal_demo`
睿尔曼 RealMan 机械臂低速小幅运动 Demo 请查看 [睿尔曼机械臂低速运动 Demo 操作手册](docs/REALMAN_SLOW_MOTION_MANUAL.md),CLion 可直接构建目标 `realman_slow_motion_demo`
睿尔曼 RealMan 机械臂源码参数调姿态 Demo 请查看 [睿尔曼机械臂源码参数调姿态 Demo](docs/REALMAN_JOINT_POSE_CONFIG_DEMO.md),CLion 可直接构建目标 `realman_joint_pose_config_demo`
睿尔曼 RealMan 机械臂力/力矩与关节指标测试请查看 [睿尔曼机械臂力/力矩与关节指标测试 README](docs/REALMAN_METRICS_TEST_README.md),CLion 可直接构建目标 `realman_metrics_test`,运行后生成 `reports/REALMAN_METRICS_TEST_RESULT.md`
睿尔曼 RealMan 机械臂独立硬件模块测试请查看 [睿尔曼 RealMan 硬件模块测试](tests/realman_hardware/README.md),包含连接、关节遥测、六维力/力矩、控制器状态、低速运动跟踪、外力方向退让等独立测试。
## 📚 API 文档 ## 📚 API 文档
详细的 API 文档请参考:[API 参考文档](docs/API-Reference.md) 详细的 API 文档请参考:[API 参考文档](docs/API-Reference.md)
......
build/
cmake-build-*/
cmake_minimum_required(VERSION 3.16)
# RM 是独立机械臂子工程,和仓库根目录的 LinkerHand 灵巧手工程分开构建。
# 这样后续更换机械臂品牌时,不会影响灵巧手 SDK 的 CMake 目标。
project(RMJsonControl
VERSION 0.1.0
LANGUAGES CXX
DESCRIPTION "Standalone JSON-command RealMan robotic arm control project"
)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
option(RM_BUILD_TESTING "Build RM JSON command parser tests" ON)
# core 库只包含 JSON 解析和命令执行流程,不链接任何真实硬件 SDK。
# 这样 parser/runner 的单元测试可以在没有机械臂的机器上运行。
add_library(rm_json_core
src/config/simple_json.cpp
src/config/json_command.cpp
src/core/json_command_runner.cpp
)
target_include_directories(rm_json_core
PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/include
)
target_compile_features(rm_json_core PUBLIC cxx_std_17)
add_library(rm_trajectory_core
src/modules/trajectory/trajectory_file_store.cpp
src/core/drag_teach_recorder.cpp
)
target_include_directories(rm_trajectory_core
PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/include
)
target_compile_features(rm_trajectory_core PUBLIC cxx_std_17)
target_compile_definitions(rm_trajectory_core
PUBLIC
RM_PROJECT_ROOT="${CMAKE_CURRENT_SOURCE_DIR}"
)
add_executable(rm_trajectory_convert
src/trajectory_convert_main.cpp
)
target_link_libraries(rm_trajectory_convert
PRIVATE
rm_trajectory_core
)
set(REALMAN_RM_API2_ROOT "" CACHE PATH "Path to RealMan RM_API2 C++ SDK root, for example /home/mashiro/RM_API2/C++")
set(REALMAN_ARM_ROOT "")
set(REALMAN_ARM_INCLUDE_DIR "")
set(REALMAN_ARM_LIB_DIR "")
set(REALMAN_ARM_LIB "")
set(REALMAN_ARM_CANDIDATE_ROOTS)
if(REALMAN_RM_API2_ROOT)
list(APPEND REALMAN_ARM_CANDIDATE_ROOTS ${REALMAN_RM_API2_ROOT})
endif()
list(APPEND REALMAN_ARM_CANDIDATE_ROOTS
/home/mashiro/RM_API2/C++
${CMAKE_CURRENT_LIST_DIR}/../third_party/Robotic_Arm
)
list(REMOVE_DUPLICATES REALMAN_ARM_CANDIDATE_ROOTS)
foreach(CANDIDATE_ROOT ${REALMAN_ARM_CANDIDATE_ROOTS})
# 兼容本机 SDK 路径和仓库 third_party 备份路径。
if(REALMAN_ARM_LIB)
break()
endif()
set(CANDIDATE_INCLUDE_DIR ${CANDIDATE_ROOT}/include)
if(NOT EXISTS ${CANDIDATE_INCLUDE_DIR}/rm_interface.h)
continue()
endif()
set(CANDIDATE_LIB_DIRS)
if(CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|amd64|AMD64")
file(GLOB CANDIDATE_RELEASE_LIB_DIRS CONFIGURE_DEPENDS ${CANDIDATE_ROOT}/linux/linux_x86_c++_vv*)
file(GLOB CANDIDATE_DEBUG_LIB_DIRS CONFIGURE_DEPENDS ${CANDIDATE_ROOT}/linux/linux_x86_c++_debug*)
list(APPEND CANDIDATE_LIB_DIRS
${CANDIDATE_RELEASE_LIB_DIRS}
${CANDIDATE_DEBUG_LIB_DIRS}
${CANDIDATE_ROOT}/linux/lib
)
elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|arm64")
file(GLOB CANDIDATE_RELEASE_LIB_DIRS CONFIGURE_DEPENDS ${CANDIDATE_ROOT}/linux/linux_arm64_c++_vv*)
file(GLOB CANDIDATE_DEBUG_LIB_DIRS CONFIGURE_DEPENDS ${CANDIDATE_ROOT}/linux/linux_arm64_c++_debug*)
list(APPEND CANDIDATE_LIB_DIRS
${CANDIDATE_RELEASE_LIB_DIRS}
${CANDIDATE_DEBUG_LIB_DIRS}
${CANDIDATE_ROOT}/linux/lib
)
else()
file(GLOB CANDIDATE_LIB_DIRS CONFIGURE_DEPENDS
${CANDIDATE_ROOT}/linux/*c++*
${CANDIDATE_ROOT}/linux/lib
)
endif()
unset(REALMAN_ARM_LIB_CANDIDATE CACHE)
find_library(REALMAN_ARM_LIB_CANDIDATE
NAMES api_cpp libapi_cpp
PATHS ${CANDIDATE_LIB_DIRS}
NO_DEFAULT_PATH
)
if(REALMAN_ARM_LIB_CANDIDATE)
set(REALMAN_ARM_ROOT ${CANDIDATE_ROOT})
set(REALMAN_ARM_INCLUDE_DIR ${CANDIDATE_INCLUDE_DIR})
get_filename_component(REALMAN_ARM_LIB_DIR ${REALMAN_ARM_LIB_CANDIDATE} DIRECTORY)
set(REALMAN_ARM_LIB ${REALMAN_ARM_LIB_CANDIDATE})
endif()
endforeach()
if(REALMAN_ARM_LIB AND EXISTS ${REALMAN_ARM_INCLUDE_DIR}/rm_interface.h)
# 不同版本 RM_API2 的销毁函数可能拼写不同:
# 有的版本是 rm_destroy,有的版本是 rm_destory。
# 这里在配置阶段读取头文件,给 C++ 代码注入对应宏。
file(READ ${REALMAN_ARM_INCLUDE_DIR}/rm_interface.h REALMAN_INTERFACE_HEADER)
if(REALMAN_INTERFACE_HEADER MATCHES "rm_destroy")
set(REALMAN_DESTROY_COMPILE_DEFINITION REALMAN_RM_API_HAS_RM_DESTROY=1)
else()
set(REALMAN_DESTROY_COMPILE_DEFINITION REALMAN_RM_API_HAS_RM_DESTORY=1)
endif()
add_library(realman_api_cpp SHARED IMPORTED GLOBAL)
set_target_properties(realman_api_cpp PROPERTIES
IMPORTED_LOCATION ${REALMAN_ARM_LIB}
INTERFACE_INCLUDE_DIRECTORIES ${REALMAN_ARM_INCLUDE_DIR}
INTERFACE_COMPILE_DEFINITIONS ${REALMAN_DESTROY_COMPILE_DEFINITION}
)
add_library(rm_realman_driver
src/drivers/realman/realman_robot_arm.cpp
)
target_link_libraries(rm_realman_driver
PUBLIC
rm_json_core
realman_api_cpp
PRIVATE
pthread
)
add_executable(rm_json_runner
src/main.cpp
)
target_link_libraries(rm_json_runner
PRIVATE
rm_json_core
rm_realman_driver
)
set_target_properties(rm_json_runner PROPERTIES
BUILD_RPATH ${REALMAN_ARM_LIB_DIR}
INSTALL_RPATH ${REALMAN_ARM_LIB_DIR}
)
add_executable(rm_drag_teach_record
src/drag_teach_record_main.cpp
)
target_link_libraries(rm_drag_teach_record
PRIVATE
rm_trajectory_core
rm_realman_driver
)
set_target_properties(rm_drag_teach_record PROPERTIES
BUILD_RPATH ${REALMAN_ARM_LIB_DIR}
INSTALL_RPATH ${REALMAN_ARM_LIB_DIR}
)
message(STATUS "Found RealMan RM_API2 root: ${REALMAN_ARM_ROOT}")
message(STATUS "Found RealMan RM_API2 library: ${REALMAN_ARM_LIB}")
message(STATUS "Found RealMan RM_API2 headers: ${REALMAN_ARM_INCLUDE_DIR}")
else()
message(WARNING "RealMan RM_API2 library or headers not found; rm_json_runner will not be built.")
endif()
if(RM_BUILD_TESTING)
# 当前测试只测 JSON/命令解析,不访问真实机械臂。
enable_testing()
add_executable(rm_json_command_tests
tests/test_json_command_parser.cpp
)
target_link_libraries(rm_json_command_tests
PRIVATE
rm_json_core
)
add_test(NAME rm_json_command_tests COMMAND rm_json_command_tests)
add_executable(rm_trajectory_file_store_tests
tests/test_trajectory_file_store.cpp
)
target_link_libraries(rm_trajectory_file_store_tests
PRIVATE
rm_trajectory_core
)
target_compile_definitions(rm_trajectory_file_store_tests
PRIVATE
RM_TEST_OUTPUT_DIR="${CMAKE_CURRENT_BINARY_DIR}/test_outputs"
)
add_test(NAME rm_trajectory_file_store_tests COMMAND rm_trajectory_file_store_tests)
endif()
# RM JSON 机械臂控制子工程
这个目录只放机械臂相关代码,和上层 LinkerHand 灵巧手代码分开维护。后续如果机械臂从睿尔曼换成其他品牌,优先保留 `interfaces/``core/``config/`,只新增或替换 `drivers/<vendor>/`
## 目录结构
```text
RM/
├── CMakeLists.txt
├── README.md
├── config/
│ ├── sample_readonly.json
│ └── sample_movej_delta_dry_run.json
├── include/rm_control/
│ ├── common/:Result、Pose、RobotInfo 等通用类型
│ ├── interfaces/:IRobotArm 抽象机械臂接口
│ ├── config/:JSON 解析和命令结构
│ ├── core/:JsonCommandRunner、DragTeachRecorder 等流程编排
│ ├── modules/trajectory/:轨迹目录、CSV、C++ waypoint 转换
│ └── drivers/realman/:睿尔曼 RM_API2 适配层
├── src/
│ ├── config/
│ ├── core/
│ ├── modules/trajectory/
│ ├── drivers/realman/
│ ├── main.cpp
│ ├── drag_teach_record_main.cpp
│ └── trajectory_convert_main.cpp
├── data/trajectories/
│ ├── csv/
│ ├── vendor/
│ ├── converted/
│ └── reports/
└── tests/:不依赖真实硬件的命令解析测试
```
## JSON 指令格式
顶层字段:
```json
{
"connection": {
"ip": "192.168.1.18",
"port": 8080,
"timeout_ms": 1000
},
"safety": {
"dry_run": true,
"allow_motion": false,
"max_speed_percent": 10,
"max_joint_delta_deg": 2.0,
"max_cartesian_step_m": 0.02,
"max_euler_delta_rad": 0.2
},
"commands": []
}
```
安全字段含义:
| 字段 | 含义 |
|---|---|
| `dry_run` | 为 `true` 时只校验运动指令,不发送 `rm_movej/rm_movel` |
| `allow_motion` | 为 `false` 时禁止真实运动 |
| `max_speed_percent` | 运动速度上限 |
| `max_joint_delta_deg` | 单次关节运动最大角度差 |
| `max_cartesian_step_m` | 单次直线运动最大 xyz 位移 |
| `max_euler_delta_rad` | 单次直线运动最大欧拉角变化 |
支持的 `commands`
```json
{"type": "get_robot_info"}
```
```json
{"type": "get_current_arm_state"}
```
```json
{"type": "get_force_data"}
```
```json
{"type": "sleep_ms", "duration_ms": 500}
```
绝对关节目标,单位是度:
```json
{
"type": "movej",
"joints_deg": [0, 0, 0, 0, 0, 0, 0],
"speed_percent": 5,
"blocking": true
}
```
相对关节增量,单位是度。推荐初期用这个:
```json
{
"type": "movej_delta",
"delta_deg": [0, 1, 0, 0, 0, 0, 0],
"speed_percent": 5,
"blocking": true
}
```
绝对笛卡尔直线目标,位置单位是米,姿态单位是弧度:
```json
{
"type": "movel",
"pose": {
"x": 0.30,
"y": 0.00,
"z": 0.25,
"rx": 3.14,
"ry": 0.00,
"rz": 0.00
},
"speed_percent": 5,
"blocking": true
}
```
## 编译
```bash
cd /home/mashiro/project/linkerhand-cpp-sdk
cmake -S RM -B RM/build -DCMAKE_BUILD_TYPE=Debug
cmake --build RM/build -j
ctest --test-dir RM/build --output-on-failure
```
本机默认会优先查找:
```text
/home/mashiro/RM_API2/C++
RM/../third_party/Robotic_Arm
```
如果 SDK 在其他位置:
```bash
cmake -S RM -B RM/build -DREALMAN_RM_API2_ROOT=/path/to/RM_API2/C++
```
## 运行
只校验 JSON,不连接 SDK:
```bash
./RM/build/rm_json_runner RM/config/sample_readonly.json --validate-only
```
执行只读指令,读取机器人信息、当前状态和六维力:
```bash
./RM/build/rm_json_runner RM/config/sample_readonly.json
```
校验一个相对关节运动,但不发送运动:
```bash
./RM/build/rm_json_runner RM/config/sample_movej_delta_dry_run.json
```
需要真实运动时,把 JSON 改成:
```json
"safety": {
"dry_run": false,
"allow_motion": true,
"max_speed_percent": 10,
"max_joint_delta_deg": 2.0
}
```
第一次真实运动必须清空工作空间,低速、小角度,并保证急停可触达。
## 拖动示教轨迹
录制 RM75 末端六维力位置+姿态拖动示教,同步保存 CSV:
```bash
./RM/build/rm_drag_teach_record demo_pick_path 15 100
```
默认写入:
```text
RM/data/trajectories/csv/demo_pick_path.csv
RM/data/trajectories/vendor/demo_pick_path_vendor.txt
```
把 CSV 转成 C++ TCP waypoint 表:
```bash
./RM/build/rm_trajectory_convert RM/data/trajectories/csv/demo_pick_path.csv
```
默认输出:
```text
RM/data/trajectories/converted/demo_pick_path_tcp_path.cpp
```
维护边界:
- `drivers/realman` 只封装 RM SDK 调用。
- `core/drag_teach_recorder` 负责编排拖动示教、采样、停止和保存。
- `modules/trajectory` 负责目录、CSV 和 C++ 文件转换,不访问硬件。
## 后续换机械臂怎么改
不要改 `JsonCommandRunner` 和 JSON 格式。新增一个驱动:
```text
include/rm_control/drivers/new_vendor/new_vendor_robot_arm.h
src/drivers/new_vendor/new_vendor_robot_arm.cpp
```
新驱动实现 `IRobotArm`
```cpp
class NewVendorRobotArm final : public rm_control::interfaces::IRobotArm {
// connect / getCurrentArmState / moveJ / moveL ...
};
```
然后在 `src/main.cpp` 里把 `RealManRobotArm` 换成新驱动即可。上层 JSON 指令、解析、校验和执行流程不需要重写。
{
"connection": {
"ip": "192.168.1.18",
"port": 8080,
"timeout_ms": 1000
},
"safety": {
"dry_run": false,
"allow_motion": true,
"max_speed_percent": 20,
"max_joint_delta_deg": 1.2,
"max_cartesian_step_m": 0.02,
"max_euler_delta_rad": 0.2
},
"commands": [
{
"type": "get_current_arm_state",
"name": "read current state before 20 degree stepped motion"
},
{
"type": "movej_delta",
"name": "J2 positive step 01 of 20",
"delta_deg": [0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0],
"speed_percent": 20,
"blocking": true
},
{
"type": "movej_delta",
"name": "J2 positive step 02 of 20",
"delta_deg": [0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0],
"speed_percent": 20,
"blocking": true
},
{
"type": "movej_delta",
"name": "J2 positive step 03 of 20",
"delta_deg": [0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0],
"speed_percent": 20,
"blocking": true
},
{
"type": "movej_delta",
"name": "J2 positive step 04 of 20",
"delta_deg": [0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0],
"speed_percent": 20,
"blocking": true
},
{
"type": "movej_delta",
"name": "J2 positive step 05 of 20",
"delta_deg": [0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0],
"speed_percent": 20,
"blocking": true
},
{
"type": "movej_delta",
"name": "J2 positive step 06 of 20",
"delta_deg": [0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0],
"speed_percent": 20,
"blocking": true
},
{
"type": "movej_delta",
"name": "J2 positive step 07 of 20",
"delta_deg": [0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0],
"speed_percent": 20,
"blocking": true
},
{
"type": "movej_delta",
"name": "J2 positive step 08 of 20",
"delta_deg": [0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0],
"speed_percent": 20,
"blocking": true
},
{
"type": "movej_delta",
"name": "J2 positive step 09 of 20",
"delta_deg": [0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0],
"speed_percent": 20,
"blocking": true
},
{
"type": "movej_delta",
"name": "J2 positive step 10 of 20",
"delta_deg": [0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0],
"speed_percent": 20,
"blocking": true
},
{
"type": "movej_delta",
"name": "J2 positive step 11 of 20",
"delta_deg": [0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0],
"speed_percent": 20,
"blocking": true
},
{
"type": "movej_delta",
"name": "J2 positive step 12 of 20",
"delta_deg": [0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0],
"speed_percent": 20,
"blocking": true
},
{
"type": "movej_delta",
"name": "J2 positive step 13 of 20",
"delta_deg": [0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0],
"speed_percent": 20,
"blocking": true
},
{
"type": "movej_delta",
"name": "J2 positive step 14 of 20",
"delta_deg": [0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0],
"speed_percent": 20,
"blocking": true
},
{
"type": "movej_delta",
"name": "J2 positive step 15 of 20",
"delta_deg": [0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0],
"speed_percent": 20,
"blocking": true
},
{
"type": "movej_delta",
"name": "J2 positive step 16 of 20",
"delta_deg": [0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0],
"speed_percent": 20,
"blocking": true
},
{
"type": "movej_delta",
"name": "J2 positive step 17 of 20",
"delta_deg": [0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0],
"speed_percent": 20,
"blocking": true
},
{
"type": "movej_delta",
"name": "J2 positive step 18 of 20",
"delta_deg": [0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0],
"speed_percent": 20,
"blocking": true
},
{
"type": "movej_delta",
"name": "J2 positive step 19 of 20",
"delta_deg": [0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0],
"speed_percent": 20,
"blocking": true
},
{
"type": "movej_delta",
"name": "J2 positive step 20 of 20",
"delta_deg": [0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0],
"speed_percent": 20,
"blocking": true
},
{
"type": "sleep_ms",
"name": "observe hold at 20 degree peak",
"duration_ms": 8000
},
{
"type": "movej_delta",
"name": "J2 return step 01 of 20",
"delta_deg": [0.0, -1.0, 0.0, 0.0, 0.0, 0.0, 0.0],
"speed_percent": 20,
"blocking": true
},
{
"type": "movej_delta",
"name": "J2 return step 02 of 20",
"delta_deg": [0.0, -1.0, 0.0, 0.0, 0.0, 0.0, 0.0],
"speed_percent": 20,
"blocking": true
},
{
"type": "movej_delta",
"name": "J2 return step 03 of 20",
"delta_deg": [0.0, -1.0, 0.0, 0.0, 0.0, 0.0, 0.0],
"speed_percent": 20,
"blocking": true
},
{
"type": "movej_delta",
"name": "J2 return step 04 of 20",
"delta_deg": [0.0, -1.0, 0.0, 0.0, 0.0, 0.0, 0.0],
"speed_percent": 20,
"blocking": true
},
{
"type": "movej_delta",
"name": "J2 return step 05 of 20",
"delta_deg": [0.0, -1.0, 0.0, 0.0, 0.0, 0.0, 0.0],
"speed_percent": 20,
"blocking": true
},
{
"type": "movej_delta",
"name": "J2 return step 06 of 20",
"delta_deg": [0.0, -1.0, 0.0, 0.0, 0.0, 0.0, 0.0],
"speed_percent": 20,
"blocking": true
},
{
"type": "movej_delta",
"name": "J2 return step 07 of 20",
"delta_deg": [0.0, -1.0, 0.0, 0.0, 0.0, 0.0, 0.0],
"speed_percent": 20,
"blocking": true
},
{
"type": "movej_delta",
"name": "J2 return step 08 of 20",
"delta_deg": [0.0, -1.0, 0.0, 0.0, 0.0, 0.0, 0.0],
"speed_percent": 20,
"blocking": true
},
{
"type": "movej_delta",
"name": "J2 return step 09 of 20",
"delta_deg": [0.0, -1.0, 0.0, 0.0, 0.0, 0.0, 0.0],
"speed_percent": 20,
"blocking": true
},
{
"type": "movej_delta",
"name": "J2 return step 10 of 20",
"delta_deg": [0.0, -1.0, 0.0, 0.0, 0.0, 0.0, 0.0],
"speed_percent": 20,
"blocking": true
},
{
"type": "movej_delta",
"name": "J2 return step 11 of 20",
"delta_deg": [0.0, -1.0, 0.0, 0.0, 0.0, 0.0, 0.0],
"speed_percent": 20,
"blocking": true
},
{
"type": "movej_delta",
"name": "J2 return step 12 of 20",
"delta_deg": [0.0, -1.0, 0.0, 0.0, 0.0, 0.0, 0.0],
"speed_percent": 20,
"blocking": true
},
{
"type": "movej_delta",
"name": "J2 return step 13 of 20",
"delta_deg": [0.0, -1.0, 0.0, 0.0, 0.0, 0.0, 0.0],
"speed_percent": 20,
"blocking": true
},
{
"type": "movej_delta",
"name": "J2 return step 14 of 20",
"delta_deg": [0.0, -1.0, 0.0, 0.0, 0.0, 0.0, 0.0],
"speed_percent": 20,
"blocking": true
},
{
"type": "movej_delta",
"name": "J2 return step 15 of 20",
"delta_deg": [0.0, -1.0, 0.0, 0.0, 0.0, 0.0, 0.0],
"speed_percent": 20,
"blocking": true
},
{
"type": "movej_delta",
"name": "J2 return step 16 of 20",
"delta_deg": [0.0, -1.0, 0.0, 0.0, 0.0, 0.0, 0.0],
"speed_percent": 20,
"blocking": true
},
{
"type": "movej_delta",
"name": "J2 return step 17 of 20",
"delta_deg": [0.0, -1.0, 0.0, 0.0, 0.0, 0.0, 0.0],
"speed_percent": 20,
"blocking": true
},
{
"type": "movej_delta",
"name": "J2 return step 18 of 20",
"delta_deg": [0.0, -1.0, 0.0, 0.0, 0.0, 0.0, 0.0],
"speed_percent": 20,
"blocking": true
},
{
"type": "movej_delta",
"name": "J2 return step 19 of 20",
"delta_deg": [0.0, -1.0, 0.0, 0.0, 0.0, 0.0, 0.0],
"speed_percent": 20,
"blocking": true
},
{
"type": "movej_delta",
"name": "J2 return step 20 of 20",
"delta_deg": [0.0, -1.0, 0.0, 0.0, 0.0, 0.0, 0.0],
"speed_percent": 20,
"blocking": true
},
{
"type": "get_current_arm_state",
"name": "read final state after 20 degree stepped motion"
}
]
}
{
"connection": {
"ip": "192.168.1.18",
"port": 8080,
"timeout_ms": 1000
},
"safety": {
"dry_run": true,
"allow_motion": false,
"max_speed_percent": 10,
"max_joint_delta_deg": 2.0,
"max_cartesian_step_m": 0.02,
"max_euler_delta_rad": 0.2
},
"commands": [
{
"type": "get_current_arm_state",
"name": "read current state before relative motion"
},
{
"type": "movej_delta",
"name": "dry-run small J2 positive delta",
"delta_deg": [0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0],
"speed_percent": 5,
"blocking": true
},
{
"type": "sleep_ms",
"name": "wait after dry run",
"duration_ms": 500
}
]
}
{
"connection": {
"ip": "192.168.1.18",
"port": 8080,
"timeout_ms": 1000
},
"safety": {
"dry_run": true,
"allow_motion": false,
"max_speed_percent": 20,
"max_joint_delta_deg": 5.0,
"max_cartesian_step_m": 0.02,
"max_euler_delta_rad": 0.2
},
"commands": [
{
"type": "get_robot_info",
"name": "read robot model and controller"
},
{
"type": "get_current_arm_state",
"name": "read current joints and tcp pose"
},
{
"type": "get_force_data",
"name": "read six-axis force data"
}
]
}
{
"connection": {
"ip": "192.168.1.18",
"port": 8080,
"timeout_ms": 1000
},
"safety": {
"dry_run": false,
"allow_motion": true,
"max_speed_percent": 20,
"max_joint_delta_deg": 1.2,
"max_cartesian_step_m": 0.02,
"max_euler_delta_rad": 0.2
},
"commands": [
{
"type": "get_current_arm_state",
"name": "read current state before visible J4 motion"
},
{
"type": "movej_delta_steps",
"name": "visible J4 positive 20 degree stepped motion",
"delta_deg": [0.0, 0.0, 0.0, 20.0, 0.0, 0.0, 0.0],
"steps": 20,
"step_sleep_ms": 20,
"speed_percent": 20,
"blocking": true
},
{
"type": "sleep_ms",
"name": "observe hold at visible J4 peak",
"duration_ms": 8000
},
{
"type": "movej_delta_steps",
"name": "visible J4 return 20 degree stepped motion",
"delta_deg": [0.0, 0.0, 0.0, -20.0, 0.0, 0.0, 0.0],
"steps": 20,
"step_sleep_ms": 20,
"speed_percent": 20,
"blocking": true
},
{
"type": "get_current_arm_state",
"name": "read final state after visible J4 motion"
}
]
}
#pragma once
#include <string>
namespace rm_control::common {
// 工程内部统一返回类型:
// - ok=true 表示调用成功,value 存放结果;
// - ok=false 表示调用失败,error_message 存放可打印的错误原因。
// 这样上层不用混用异常、错误码和 bool,便于测试和排查硬件调用失败。
template <typename T>
struct Result {
bool ok = false;
T value{};
std::string error_message;
static Result<T> success(const T &value) {
return Result<T>{true, value, ""};
}
static Result<T> failure(const std::string &message) {
return Result<T>{false, T{}, message};
}
};
// void 特化用于“只关心成功/失败、不需要返回值”的动作类接口,
// 例如 connect、moveJ、moveL。
template <>
struct Result<void> {
bool ok = false;
std::string error_message;
static Result<void> success() {
return Result<void>{true, ""};
}
static Result<void> failure(const std::string &message) {
return Result<void>{false, message};
}
};
} // namespace rm_control::common
#pragma once
#include <array>
#include <string>
#include <vector>
namespace rm_control::common {
// 机械臂连接参数。当前默认值是本地 RM75 控制器,
// 后续换机械臂或换 IP 时应优先通过 JSON 覆盖,而不是改驱动代码。
struct RobotConnectionConfig {
std::string ip = "192.168.1.18";
int port = 8080;
int timeout_ms = 1000;
};
// 通用 TCP 位姿结构,位置单位为 m,欧拉角单位为 rad。
// 这里不用 rm_pose_t,是为了让 core/config 不直接依赖睿尔曼 SDK。
struct Pose {
float x = 0.0F;
float y = 0.0F;
float z = 0.0F;
float rx = 0.0F;
float ry = 0.0F;
float rz = 0.0F;
};
// 只保留上层业务真正需要的机器人基础信息,避免把 RM SDK 结构体泄漏到 core。
struct RobotInfo {
int arm_dof = 0;
int arm_model = 0;
int force_type = 0;
int controller_version = 0;
};
// 当前机械臂状态:关节角、TCP 位姿、错误码。
// joints_deg 的长度由真实机械臂自由度决定,RM75 当前是 7。
struct ArmState {
std::vector<float> joints_deg;
Pose tcp_pose;
std::vector<int> error_codes;
};
// 六维力数据。数组顺序固定为 Fx, Fy, Fz, Mx, My, Mz。
// raw/zero/work_zero/tool_zero 对应 RM_API2 返回的四组力数据。
struct ForceData {
std::array<float, 6> raw{};
std::array<float, 6> zero{};
std::array<float, 6> work_zero{};
std::array<float, 6> tool_zero{};
};
enum class DragTeachMode {
OrdinaryRecorded,
SixDofForcePositionAndOrientation,
};
struct DragTeachStartOptions {
DragTeachMode mode = DragTeachMode::SixDofForcePositionAndOrientation;
bool record_vendor_trajectory = true;
bool enable_singular_wall = true;
};
struct SavedVendorTrajectory {
std::string path;
int point_count = 0;
};
struct TrajectorySample {
long long elapsed_ms = 0;
std::vector<float> joints_deg;
Pose tcp_pose;
};
} // namespace rm_control::common
#pragma once
#include "rm_control/common/result.h"
#include "rm_control/common/robot_types.h"
#include <string>
#include <vector>
namespace rm_control::config {
// JSON 支持的动作类型。
// 这里把“只读命令”和“运动命令”放在同一个枚举里,执行器可以集中做安全校验。
enum class CommandType {
GetRobotInfo,
GetCurrentArmState,
GetForceData,
SleepMs,
MoveJ,
MoveJDelta,
MoveJDeltaSteps,
MoveL,
};
// 单条运动命令的通用选项,对应 RM_API2 moveJ/moveL 的速度、平滑半径、
// 轨迹连接和阻塞执行参数。
struct MotionOptions {
int speed_percent = 5;
int blend_radius = 0;
int trajectory_connect = 0;
bool blocking = true;
};
// JSON 中的一条命令被解析后的结构体。
// 不同命令只使用其中一部分字段,例如 movej 使用 joints_deg,
// movej_delta_steps 使用 joint_delta_deg + steps。
struct ArmCommand {
std::string name;
CommandType type = CommandType::GetRobotInfo;
std::vector<float> joints_deg;
std::vector<float> joint_delta_deg;
common::Pose pose;
int duration_ms = 0;
int steps = 1;
int step_sleep_ms = 0;
MotionOptions motion;
};
// 全局安全限制。默认 dry_run=true 且 allow_motion=false,
// 是为了避免首次运行 JSON 文件时误发真实运动指令。
struct SafetyOptions {
bool dry_run = true;
bool allow_motion = false;
int max_speed_percent = 20;
float max_joint_delta_deg = 5.0F;
float max_cartesian_step_m = 0.02F;
float max_euler_delta_rad = 0.2F;
};
// 一个完整 JSON 指令文件:连接参数 + 安全策略 + 命令列表。
struct JsonCommandProgram {
common::RobotConnectionConfig connection;
SafetyOptions safety;
std::vector<ArmCommand> commands;
};
// 从字符串或文件加载 JSON 指令,并完成字段类型校验和默认值填充。
common::Result<JsonCommandProgram> parseCommandProgramText(const std::string &text);
common::Result<JsonCommandProgram> loadCommandProgramFromFile(const std::string &path);
const char *commandTypeName(CommandType type);
} // namespace rm_control::config
#pragma once
#include "rm_control/common/result.h"
#include <map>
#include <string>
#include <vector>
namespace rm_control::config {
// 轻量 JSON 值对象。
// 这里没有引入 nlohmann/json,是因为本机环境没有现成依赖;
// 为了让 RM 子工程离线可编译,只实现命令文件需要的 JSON 子集。
class JsonValue {
public:
enum class Type {
Null,
Bool,
Number,
String,
Array,
Object,
};
using Array = std::vector<JsonValue>;
using Object = std::map<std::string, JsonValue>;
JsonValue() = default;
explicit JsonValue(bool value);
explicit JsonValue(double value);
explicit JsonValue(std::string value);
explicit JsonValue(Array value);
explicit JsonValue(Object value);
Type type() const;
bool isNull() const;
bool isBool() const;
bool isNumber() const;
bool isString() const;
bool isArray() const;
bool isObject() const;
bool asBool() const;
double asNumber() const;
const std::string &asString() const;
const Array &asArray() const;
const Object &asObject() const;
const JsonValue *find(const std::string &key) const;
private:
Type type_ = Type::Null;
bool bool_value_ = false;
double number_value_ = 0.0;
std::string string_value_;
Array array_value_;
Object object_value_;
};
// 解析标准 JSON 文本。返回 Result,避免解析失败时直接抛异常给 main。
common::Result<JsonValue> parseJson(const std::string &text);
} // namespace rm_control::config
#pragma once
#include "rm_control/common/result.h"
#include "rm_control/common/robot_types.h"
#include "rm_control/interfaces/robot_arm.h"
#include "rm_control/modules/trajectory/trajectory_file_store.h"
#include <iosfwd>
#include <string>
namespace rm_control::core {
struct DragTeachRecordConfig {
std::string storage_root;
std::string session_name;
int duration_seconds = 15;
int sample_period_ms = 100;
bool save_vendor_trajectory = true;
common::DragTeachStartOptions drag_options;
};
struct DragTeachRecordSummary {
modules::trajectory::TrajectoryFileSet files;
int sample_count = 0;
bool vendor_trajectory_saved = false;
std::string vendor_save_message;
};
class DragTeachRecorder {
public:
explicit DragTeachRecorder(interfaces::IRobotArm &robot);
common::Result<DragTeachRecordSummary> record(const DragTeachRecordConfig &config,
std::ostream &out);
private:
common::Result<void> validateConfig(const DragTeachRecordConfig &config) const;
interfaces::IRobotArm &robot_;
};
} // namespace rm_control::core
#pragma once
#include "rm_control/common/result.h"
#include "rm_control/config/json_command.h"
#include "rm_control/interfaces/robot_arm.h"
#include <iosfwd>
namespace rm_control::core {
// JSON 指令执行器。
// 这个类只负责流程编排和安全校验,不直接包含 rm_interface.h。
// 真实硬件动作通过 IRobotArm 注入,保证后续可替换机械臂驱动。
class JsonCommandRunner {
public:
explicit JsonCommandRunner(interfaces::IRobotArm &robot);
common::Result<void> run(const config::JsonCommandProgram &program, std::ostream &out);
private:
// 所有运动命令发送前都必须经过这里。
// 绝对 movej 检查“目标与当前值差值”,相对 movej_delta 检查“单次增量”,
// movej_delta_steps 检查“每步增量”,避免 JSON 写错导致大幅跳动。
common::Result<void> validateMotionCommand(const config::JsonCommandProgram &program,
const config::ArmCommand &command,
const common::ArmState &current_state) const;
interfaces::IRobotArm &robot_;
};
} // namespace rm_control::core
#pragma once
#include "rm_control/interfaces/robot_arm.h"
#include "rm_interface.h"
namespace rm_control::drivers::realman {
// 睿尔曼 RM_API2 的具体驱动实现。
// 这是 RM 子工程中唯一直接依赖 rm_interface.h 的位置之一;
// core/config/interfaces 都不应该 include 睿尔曼 SDK 头文件。
class RealManRobotArm final : public interfaces::IRobotArm {
public:
RealManRobotArm() = default;
RealManRobotArm(const RealManRobotArm &) = delete;
RealManRobotArm &operator=(const RealManRobotArm &) = delete;
~RealManRobotArm() override;
common::Result<void> connect(const common::RobotConnectionConfig &config) override;
void disconnect() override;
common::Result<common::RobotInfo> getRobotInfo() override;
common::Result<common::ArmState> getCurrentArmState() override;
common::Result<common::ForceData> getForceData() override;
common::Result<void> moveJ(const std::vector<float> &joints_deg,
int speed_percent,
int blend_radius,
int trajectory_connect,
bool blocking) override;
common::Result<void> moveL(const common::Pose &pose,
int speed_percent,
int blend_radius,
int trajectory_connect,
bool blocking) override;
common::Result<void> startDragTeach(const common::DragTeachStartOptions &options) override;
common::Result<void> stopDragTeach() override;
common::Result<common::SavedVendorTrajectory>
saveDragTeachTrajectory(const std::string &path) override;
private:
// 所有 SDK 读写前先检查连接状态,防止空句柄传入 C SDK。
common::Result<void> ensureConnected() const;
// RM_API2 使用裸指针句柄,因此必须由本类析构函数集中释放。
rm_robot_handle *handle_ = nullptr;
bool initialized_ = false;
int dof_ = 0;
};
} // namespace rm_control::drivers::realman
#pragma once
#include "rm_control/common/result.h"
#include "rm_control/common/robot_types.h"
#include <string>
#include <vector>
namespace rm_control::interfaces {
// 机械臂抽象接口。
// core 只依赖这个接口,不直接依赖 RealManRobotArm 或 rm_interface.h。
// 后续如果更换机械臂品牌,只要新增一个实现 IRobotArm 的 driver,
// JSON 指令解析和 JsonCommandRunner 都可以继续复用。
class IRobotArm {
public:
virtual ~IRobotArm() = default;
// 建立/释放硬件连接。具体 SDK 初始化细节放在 drivers 层。
virtual common::Result<void> connect(const common::RobotConnectionConfig &config) = 0;
virtual void disconnect() = 0;
// 只读接口:适合自动诊断和状态显示,不会造成机械臂运动。
virtual common::Result<common::RobotInfo> getRobotInfo() = 0;
virtual common::Result<common::ArmState> getCurrentArmState() = 0;
virtual common::Result<common::ForceData> getForceData() = 0;
// 关节空间运动。joints_deg 是绝对关节角,单位为度。
virtual common::Result<void> moveJ(const std::vector<float> &joints_deg,
int speed_percent,
int blend_radius,
int trajectory_connect,
bool blocking) = 0;
// 笛卡尔直线运动。pose 是绝对 TCP 位姿,单位见 common::Pose。
virtual common::Result<void> moveL(const common::Pose &pose,
int speed_percent,
int blend_radius,
int trajectory_connect,
bool blocking) = 0;
// 拖动示教生命周期。上层只表达“开始/停止/保存”,具体使用普通拖动、
// 六维力复合拖动或厂商文件格式都由 driver 实现。
virtual common::Result<void> startDragTeach(const common::DragTeachStartOptions &options) = 0;
virtual common::Result<void> stopDragTeach() = 0;
virtual common::Result<common::SavedVendorTrajectory>
saveDragTeachTrajectory(const std::string &path) = 0;
};
} // namespace rm_control::interfaces
#pragma once
#include "rm_control/common/result.h"
#include "rm_control/common/robot_types.h"
#include <string>
#include <vector>
namespace rm_control::modules::trajectory {
struct TrajectoryStoragePaths {
std::string root_dir;
std::string csv_dir;
std::string vendor_dir;
std::string converted_dir;
std::string reports_dir;
};
struct TrajectoryFileSet {
std::string session_name;
std::string csv_path;
std::string vendor_path;
std::string converted_cpp_path;
std::string report_path;
};
std::string timestampString();
std::string sanitizeSessionName(const std::string &raw_name);
std::string defaultTrajectoryStorageRoot();
common::Result<TrajectoryStoragePaths> ensureTrajectoryStorage(const std::string &root_dir);
common::Result<TrajectoryFileSet> makeTrajectoryFileSet(const std::string &root_dir,
const std::string &session_name);
common::Result<void> writeTrajectoryCsv(const std::string &csv_path,
const std::vector<common::TrajectorySample> &samples);
common::Result<std::vector<common::TrajectorySample>> loadTrajectoryCsv(const std::string &csv_path);
common::Result<void> exportTcpPathCpp(const std::string &cpp_path,
const std::vector<common::TrajectorySample> &samples,
const std::string &session_name);
} // namespace rm_control::modules::trajectory
This diff is collapsed.
This diff is collapsed.
#include "rm_control/core/drag_teach_recorder.h"
#include <chrono>
#include <iostream>
#include <thread>
#include <vector>
namespace rm_control::core {
namespace {
constexpr int kMinimumSamplePeriodMs = 50;
constexpr int kMaximumDurationSeconds = 3600;
constexpr int kSafetyCountdownSeconds = 5;
common::TrajectorySample makeSample(long long elapsed_ms, const common::ArmState &state) {
common::TrajectorySample sample;
sample.elapsed_ms = elapsed_ms;
sample.joints_deg = state.joints_deg;
sample.tcp_pose = state.tcp_pose;
return sample;
}
void printCountdown(std::ostream &out) {
out << "Prepare for six-dof force drag teach. Clear workspace and keep E-stop reachable.\n";
for (int second = kSafetyCountdownSeconds; second > 0; --second) {
out << "Drag teach starts in " << second << "...\n";
std::this_thread::sleep_for(std::chrono::seconds(1));
}
}
} // namespace
DragTeachRecorder::DragTeachRecorder(interfaces::IRobotArm &robot)
: robot_(robot) {}
common::Result<DragTeachRecordSummary> DragTeachRecorder::record(const DragTeachRecordConfig &config,
std::ostream &out) {
auto validation = validateConfig(config);
if (!validation.ok) {
return common::Result<DragTeachRecordSummary>::failure(validation.error_message);
}
auto files = modules::trajectory::makeTrajectoryFileSet(config.storage_root, config.session_name);
if (!files.ok) {
return common::Result<DragTeachRecordSummary>::failure(files.error_message);
}
printCountdown(out);
auto start_result = robot_.startDragTeach(config.drag_options);
if (!start_result.ok) {
return common::Result<DragTeachRecordSummary>::failure(start_result.error_message);
}
std::vector<common::TrajectorySample> samples;
const auto start_time = std::chrono::steady_clock::now();
const int total_samples = (config.duration_seconds * 1000) / config.sample_period_ms + 1;
common::Result<void> sample_result = common::Result<void>::success();
for (int sample_index = 0; sample_index < total_samples; ++sample_index) {
const auto target_time = start_time + std::chrono::milliseconds(sample_index * config.sample_period_ms);
std::this_thread::sleep_until(target_time);
auto state = robot_.getCurrentArmState();
if (!state.ok) {
sample_result = common::Result<void>::failure(state.error_message);
break;
}
const auto now = std::chrono::steady_clock::now();
const long long elapsed_ms =
std::chrono::duration_cast<std::chrono::milliseconds>(now - start_time).count();
samples.push_back(makeSample(elapsed_ms, state.value));
if (sample_index == 0 || sample_index == total_samples - 1) {
out << "sample " << sample_index << "/" << (total_samples - 1)
<< " elapsed_ms=" << elapsed_ms << '\n';
}
}
auto stop_result = robot_.stopDragTeach();
if (!sample_result.ok) {
return common::Result<DragTeachRecordSummary>::failure(sample_result.error_message);
}
if (!stop_result.ok) {
return common::Result<DragTeachRecordSummary>::failure(stop_result.error_message);
}
auto write_result = modules::trajectory::writeTrajectoryCsv(files.value.csv_path, samples);
if (!write_result.ok) {
return common::Result<DragTeachRecordSummary>::failure(write_result.error_message);
}
DragTeachRecordSummary summary;
summary.files = files.value;
summary.sample_count = static_cast<int>(samples.size());
if (config.save_vendor_trajectory) {
auto vendor_result = robot_.saveDragTeachTrajectory(files.value.vendor_path);
if (vendor_result.ok) {
summary.vendor_trajectory_saved = true;
summary.vendor_save_message = "saved " + std::to_string(vendor_result.value.point_count) +
" vendor points";
} else {
summary.vendor_save_message = vendor_result.error_message;
}
} else {
summary.vendor_save_message = "vendor trajectory save disabled";
}
out << "CSV saved: " << summary.files.csv_path << '\n';
out << "Vendor trajectory: " << summary.vendor_save_message << '\n';
return common::Result<DragTeachRecordSummary>::success(summary);
}
common::Result<void> DragTeachRecorder::validateConfig(const DragTeachRecordConfig &config) const {
if (config.storage_root.empty()) {
return common::Result<void>::failure("storage_root must not be empty");
}
if (config.session_name.empty()) {
return common::Result<void>::failure("session_name must not be empty");
}
if (config.duration_seconds <= 0 || config.duration_seconds > kMaximumDurationSeconds) {
return common::Result<void>::failure("duration_seconds must be in range 1.." +
std::to_string(kMaximumDurationSeconds));
}
if (config.sample_period_ms < kMinimumSamplePeriodMs) {
return common::Result<void>::failure("sample_period_ms must be at least " +
std::to_string(kMinimumSamplePeriodMs));
}
return common::Result<void>::success();
}
} // namespace rm_control::core
This diff is collapsed.
#include "rm_control/core/drag_teach_recorder.h"
#include "rm_control/drivers/realman/realman_robot_arm.h"
#include "rm_control/modules/trajectory/trajectory_file_store.h"
#include <cstdlib>
#include <iostream>
#include <string>
namespace {
using rm_control::core::DragTeachRecordConfig;
void printUsage(const char *program_name) {
std::cout << "Usage: " << program_name
<< " [session_name] [duration_seconds] [sample_period_ms] [storage_root]"
<< " [--no-vendor-save] [--ordinary-recorded] [--no-singular-wall]\n"
<< "\n"
<< "Default mode is RM75 six-dof force drag teach with position and orientation enabled.\n"
<< "The program records synchronized CSV as the primary artifact.\n";
}
bool parseInt(const char *value, int *out) {
if (value == nullptr || out == nullptr || value[0] == '\0') {
return false;
}
char *end = nullptr;
const long parsed = std::strtol(value, &end, 10);
if (end == value || *end != '\0') {
return false;
}
*out = static_cast<int>(parsed);
return true;
}
std::string defaultSessionName() {
return "rm75_drag_pose_" + rm_control::modules::trajectory::timestampString();
}
bool parseArguments(int argc, char **argv, DragTeachRecordConfig *config) {
if (config == nullptr) {
return false;
}
if (argc > 1 && (std::string(argv[1]) == "-h" || std::string(argv[1]) == "--help")) {
printUsage(argv[0]);
std::exit(0);
}
config->session_name = defaultSessionName();
config->storage_root = rm_control::modules::trajectory::defaultTrajectoryStorageRoot();
config->drag_options.mode = rm_control::common::DragTeachMode::SixDofForcePositionAndOrientation;
config->drag_options.enable_singular_wall = true;
config->drag_options.record_vendor_trajectory = true;
int positional_index = 0;
for (int i = 1; i < argc; ++i) {
const std::string arg = argv[i] == nullptr ? "" : argv[i];
if (arg == "--no-vendor-save") {
config->save_vendor_trajectory = false;
continue;
}
if (arg == "--ordinary-recorded") {
config->drag_options.mode = rm_control::common::DragTeachMode::OrdinaryRecorded;
continue;
}
if (arg == "--no-singular-wall") {
config->drag_options.enable_singular_wall = false;
continue;
}
if (!arg.empty() && arg[0] == '-') {
std::cerr << "Unknown option: " << arg << '\n';
return false;
}
++positional_index;
if (positional_index == 1) {
config->session_name = arg;
} else if (positional_index == 2) {
if (!parseInt(arg.c_str(), &config->duration_seconds)) {
std::cerr << "Invalid duration_seconds: " << arg << '\n';
return false;
}
} else if (positional_index == 3) {
if (!parseInt(arg.c_str(), &config->sample_period_ms)) {
std::cerr << "Invalid sample_period_ms: " << arg << '\n';
return false;
}
} else if (positional_index == 4) {
config->storage_root = arg;
} else {
std::cerr << "Too many positional arguments.\n";
return false;
}
}
return true;
}
} // namespace
int main(int argc, char **argv) {
DragTeachRecordConfig config;
if (!parseArguments(argc, argv, &config)) {
printUsage(argv[0]);
return 64;
}
rm_control::drivers::realman::RealManRobotArm robot;
rm_control::common::RobotConnectionConfig connection;
auto connect_result = robot.connect(connection);
if (!connect_result.ok) {
std::cerr << "Failed to connect RM75: " << connect_result.error_message << '\n';
return 1;
}
rm_control::core::DragTeachRecorder recorder(robot);
auto summary = recorder.record(config, std::cout);
if (!summary.ok) {
std::cerr << "Drag teach record failed: " << summary.error_message << '\n';
return 2;
}
std::cout << "Recorded samples: " << summary.value.sample_count << '\n'
<< "CSV: " << summary.value.files.csv_path << '\n'
<< "Vendor: " << summary.value.files.vendor_path << '\n'
<< "Converted C++ target path: " << summary.value.files.converted_cpp_path << '\n';
return 0;
}
This diff is collapsed.
#include "rm_control/config/json_command.h"
#include "rm_control/core/json_command_runner.h"
#include "rm_control/drivers/realman/realman_robot_arm.h"
#include <cstdlib>
#include <iostream>
#include <string>
namespace {
void printUsage(const char *program_name) {
std::cout << "Usage: " << program_name << " <commands.json> [--validate-only]\n"
<< "\n"
<< "Examples:\n"
<< " " << program_name << " ../config/sample_commands.json --validate-only\n"
<< " " << program_name << " ../config/sample_commands.json\n"
<< "\n"
<< "Safety:\n"
<< " JSON defaults should keep dry_run=true and allow_motion=false first.\n"
<< " Set safety.dry_run=false and safety.allow_motion=true only after checking the target.\n";
}
} // namespace
int main(int argc, char **argv) {
// main 只负责参数解析、加载 JSON、创建具体驱动并启动 runner。
// 具体安全校验和硬件调用都放在 core/drivers 中,避免入口文件膨胀。
if (argc < 2 || argc > 3) {
printUsage(argv[0]);
return 64;
}
const std::string first_arg = argv[1] == nullptr ? "" : argv[1];
if (first_arg == "-h" || first_arg == "--help") {
printUsage(argv[0]);
return 0;
}
bool validate_only = false;
if (argc == 3) {
const std::string option = argv[2] == nullptr ? "" : argv[2];
if (option != "--validate-only") {
std::cerr << "Unknown option: " << option << '\n';
printUsage(argv[0]);
return 64;
}
validate_only = true;
}
auto program = rm_control::config::loadCommandProgramFromFile(first_arg);
if (!program.ok) {
std::cerr << "Failed to load JSON command file: " << program.error_message << '\n';
return 1;
}
std::cout << "JSON command file parsed successfully. command_count="
<< program.value.commands.size() << '\n';
if (validate_only) {
// 只校验 JSON 结构,不连接 SDK、不访问真实机械臂。
// 调试新 JSON 时建议先跑这个模式。
std::cout << "validate-only mode: no SDK connection or command execution.\n";
return 0;
}
// 这里选择 RealMan 驱动。以后更换机械臂品牌,只需要替换这个具体实现,
// JsonCommandRunner 和 JSON 格式不需要变。
rm_control::drivers::realman::RealManRobotArm robot;
rm_control::core::JsonCommandRunner runner(robot);
auto result = runner.run(program.value, std::cout);
if (!result.ok) {
std::cerr << "Command execution failed: " << result.error_message << '\n';
return 2;
}
std::cout << "\nAll JSON commands finished.\n";
return 0;
}
This diff is collapsed.
#include "rm_control/modules/trajectory/trajectory_file_store.h"
#include <iostream>
#include <string>
namespace {
void printUsage(const char *program_name) {
std::cout << "Usage: " << program_name << " <input_csv> [output_cpp]\n"
<< "\n"
<< "Converts recorded CSV into a C++ TCP waypoint table. It does not connect to the robot.\n";
}
std::string fileStem(const std::string &path) {
const size_t slash = path.find_last_of('/');
const size_t begin = slash == std::string::npos ? 0 : slash + 1;
const size_t dot = path.find_last_of('.');
const size_t end = dot == std::string::npos || dot < begin ? path.size() : dot;
return path.substr(begin, end - begin);
}
std::string defaultOutputPath(const std::string &input_csv) {
const std::string session_name = rm_control::modules::trajectory::sanitizeSessionName(fileStem(input_csv));
auto files = rm_control::modules::trajectory::makeTrajectoryFileSet(
rm_control::modules::trajectory::defaultTrajectoryStorageRoot(), session_name);
if (!files.ok) {
return session_name + "_tcp_path.cpp";
}
return files.value.converted_cpp_path;
}
} // namespace
int main(int argc, char **argv) {
if (argc < 2 || argc > 3) {
printUsage(argv[0]);
return 64;
}
const std::string first_arg = argv[1] == nullptr ? "" : argv[1];
if (first_arg == "-h" || first_arg == "--help") {
printUsage(argv[0]);
return 0;
}
const std::string input_csv = first_arg;
const std::string output_cpp = argc == 3 ? std::string(argv[2]) : defaultOutputPath(input_csv);
auto samples = rm_control::modules::trajectory::loadTrajectoryCsv(input_csv);
if (!samples.ok) {
std::cerr << "Failed to load CSV: " << samples.error_message << '\n';
return 1;
}
const std::string session_name = rm_control::modules::trajectory::sanitizeSessionName(fileStem(input_csv));
auto export_result = rm_control::modules::trajectory::exportTcpPathCpp(
output_cpp, samples.value, session_name);
if (!export_result.ok) {
std::cerr << "Failed to export C++: " << export_result.error_message << '\n';
return 2;
}
std::cout << "Loaded samples: " << samples.value.size() << '\n'
<< "C++ TCP path: " << output_cpp << '\n';
return 0;
}
#include "rm_control/config/json_command.h"
#include <iostream>
#include <string>
namespace {
int g_failures = 0;
void expect(bool condition, const std::string &message) {
// 这里不用 gtest,保持 RM 子工程测试零外部依赖,方便离线编译。
if (!condition) {
++g_failures;
std::cerr << "[FAIL] " << message << '\n';
}
}
void testParseReadonlyProgram() {
// 只读命令必须能在不打开真实硬件的情况下完成解析。
const std::string text = R"json(
{
"connection": {
"ip": "192.168.1.18",
"port": 8080,
"timeout_ms": 1000
},
"safety": {
"dry_run": true,
"allow_motion": false,
"max_speed_percent": 20,
"max_joint_delta_deg": 5
},
"commands": [
{"type": "get_robot_info"},
{"type": "get_current_arm_state"},
{"type": "get_force_data"},
{"type": "sleep_ms", "duration_ms": 10}
]
}
)json";
auto program = rm_control::config::parseCommandProgramText(text);
expect(program.ok, "read-only JSON should parse");
if (!program.ok) {
return;
}
expect(program.value.connection.ip == "192.168.1.18", "connection IP parsed");
expect(program.value.commands.size() == 4, "command count parsed");
expect(program.value.safety.dry_run, "dry_run parsed");
expect(!program.value.safety.allow_motion, "allow_motion parsed");
}
void testParseMoveJDeltaProgram() {
// 相对关节命令是人工调试最常用格式,必须保证 delta 和速度能正确解析。
const std::string text = R"json(
{
"commands": [
{
"type": "movej_delta",
"name": "small J2 test",
"delta_deg": [0, 1, 0, 0, 0, 0, 0],
"speed_percent": 5,
"blocking": true
}
]
}
)json";
auto program = rm_control::config::parseCommandProgramText(text);
expect(program.ok, "movej_delta JSON should parse");
if (!program.ok) {
return;
}
expect(program.value.commands[0].joint_delta_deg.size() == 7, "delta size parsed");
expect(program.value.commands[0].motion.speed_percent == 5, "speed parsed");
}
void testParseMoveJDeltaStepsProgram() {
// 阶梯运动用于把较大总幅度拆成小步,核心字段是 steps 和 step_sleep_ms。
const std::string text = R"json(
{
"commands": [
{
"type": "movej_delta_steps",
"name": "visible J4 test",
"delta_deg": [0, 0, 0, 20, 0, 0, 0],
"steps": 20,
"step_sleep_ms": 20,
"speed_percent": 20,
"blocking": true
}
]
}
)json";
auto program = rm_control::config::parseCommandProgramText(text);
expect(program.ok, "movej_delta_steps JSON should parse");
if (!program.ok) {
return;
}
expect(program.value.commands[0].steps == 20, "steps parsed");
expect(program.value.commands[0].step_sleep_ms == 20, "step sleep parsed");
}
void testRejectInvalidProgram() {
// 缺少必要字段时必须拒绝,避免不完整 JSON 被发送到真实机械臂。
const std::string text = R"json({"commands":[{"type":"movej"}]})json";
auto program = rm_control::config::parseCommandProgramText(text);
expect(!program.ok, "movej without joints_deg should be rejected");
}
} // namespace
int main() {
testParseReadonlyProgram();
testParseMoveJDeltaProgram();
testParseMoveJDeltaStepsProgram();
testRejectInvalidProgram();
if (g_failures == 0) {
std::cout << "[PASS] rm_json_command_tests\n";
return 0;
}
std::cerr << g_failures << " test(s) failed\n";
return 1;
}
#include "rm_control/modules/trajectory/trajectory_file_store.h"
#include <fstream>
#include <iostream>
#include <sstream>
#include <string>
#include <vector>
#ifndef RM_TEST_OUTPUT_DIR
#define RM_TEST_OUTPUT_DIR "."
#endif
namespace {
int g_failures = 0;
void expect(bool condition, const std::string &message) {
if (!condition) {
++g_failures;
std::cerr << "[FAIL] " << message << '\n';
}
}
rm_control::common::TrajectorySample makeSample(long long elapsed_ms, float j2_deg) {
rm_control::common::TrajectorySample sample;
sample.elapsed_ms = elapsed_ms;
sample.joints_deg = {0.0F, j2_deg, 0.0F, 0.0F, 0.0F, 0.0F, 0.0F};
sample.tcp_pose.x = 0.1F;
sample.tcp_pose.y = 0.2F;
sample.tcp_pose.z = 0.3F;
sample.tcp_pose.rx = 0.01F;
sample.tcp_pose.ry = 0.02F;
sample.tcp_pose.rz = 0.03F;
return sample;
}
std::string readTextFile(const std::string &path) {
std::ifstream input(path);
std::ostringstream buffer;
buffer << input.rdbuf();
return buffer.str();
}
void testStoragePathsAndCsvRoundTrip() {
const std::string root = std::string(RM_TEST_OUTPUT_DIR) + "/trajectory_store_test";
auto files = rm_control::modules::trajectory::makeTrajectoryFileSet(root, "demo session");
expect(files.ok, "file set should be created");
if (!files.ok) {
return;
}
expect(files.value.session_name == "demo_session", "session name should be sanitized");
std::vector<rm_control::common::TrajectorySample> samples;
samples.push_back(makeSample(0, 0.0F));
samples.push_back(makeSample(100, 1.0F));
auto write_result = rm_control::modules::trajectory::writeTrajectoryCsv(files.value.csv_path, samples);
expect(write_result.ok, "CSV should be written");
auto loaded = rm_control::modules::trajectory::loadTrajectoryCsv(files.value.csv_path);
expect(loaded.ok, "CSV should be loaded");
if (!loaded.ok) {
return;
}
expect(loaded.value.size() == 2, "loaded sample count should match");
expect(loaded.value[1].joints_deg[1] == 1.0F, "loaded J2 should match");
}
void testExportTcpPathCpp() {
const std::string root = std::string(RM_TEST_OUTPUT_DIR) + "/trajectory_export_test";
auto files = rm_control::modules::trajectory::makeTrajectoryFileSet(root, "tcp demo");
expect(files.ok, "file set should be created for export");
if (!files.ok) {
return;
}
std::vector<rm_control::common::TrajectorySample> samples;
samples.push_back(makeSample(0, 0.0F));
samples.push_back(makeSample(100, 1.0F));
auto export_result = rm_control::modules::trajectory::exportTcpPathCpp(
files.value.converted_cpp_path, samples, files.value.session_name);
expect(export_result.ok, "C++ TCP path should be exported");
const std::string text = readTextFile(files.value.converted_cpp_path);
expect(text.find("struct TcpWaypoint") != std::string::npos, "export should contain TcpWaypoint");
expect(text.find("kTcpPath_tcp_demo") != std::string::npos, "export should contain sanitized variable");
}
} // namespace
int main() {
testStoragePathsAndCsvRoundTrip();
testExportTcpPathCpp();
if (g_failures == 0) {
std::cout << "[PASS] rm_trajectory_file_store_tests\n";
return 0;
}
std::cerr << g_failures << " test(s) failed\n";
return 1;
}
# CLion 搭建睿尔曼 RealMan C++ SDK
本文档说明当前项目中睿尔曼 RealMan RM_API2 C++ SDK 的 CLion 配置方式。
## 当前配置
本项目的 CMake 已接入睿尔曼 C++ SDK:
| 项目 | 当前值 |
| --- | --- |
| 首选 SDK 根目录 | `/home/mashiro/RM_API2/C++` |
| 头文件目录 | `/home/mashiro/RM_API2/C++/include` |
| x86_64 动态库 | `/home/mashiro/RM_API2/C++/linux/linux_x86_c++_vv1.1.5/libapi_cpp.so` |
| fallback SDK | `third_party/Robotic_Arm` |
| CMake imported target | `realman_api_cpp` |
CMake 会优先使用 `REALMAN_RM_API2_ROOT` 指定的 SDK;如果该路径不可用,会回退到项目内置的 `third_party/Robotic_Arm`
## CLion 打开项目
1. 打开 CLion。
2. 选择 `Open`
3. 打开目录:
```bash
/home/mashiro/project/linkerhand-cpp-sdk
```
4. 选择 CMake preset:
```text
CLion Debug
```
或:
```text
CLion Release
```
5. 点击 `Reload CMake Project`
## 可运行目标
CLion 中会出现以下睿尔曼相关 targets:
| Target | 说明 |
| --- | --- |
| `realman_minimal_demo` | 连接机械臂,读取 API/软件/基础信息,不发送运动指令 |
| `realman_slow_motion_demo` | 低速小幅关节往返运动,默认 `J2 ±5°`,速度 `10%` |
| `realman_joint_pose_config_demo` | 在源码顶部修改关节目标/偏移参数,用于 CLion 内调姿态 |
| `realman_metrics_test` | 采集六维力/力矩、关节电流、电压、温度和低速运动跟踪指标,并生成报告 |
| `realman_trajectory_record` | 录制关节角/TCP 位姿到 CSV,只读不发送运动指令 |
| `realman_trajectory_replay` | 从 CSV 低速逐点回放关节角,默认 dry-run,显式 execute 才运动 |
| `realman_hw_test_connection` | 独立连接/基础信息硬件测试 |
| `realman_hw_test_joint_telemetry` | 独立关节遥测硬件测试 |
| `realman_hw_test_force_sensor` | 独立六维力/力矩传感器硬件测试 |
| `realman_hw_test_controller_state` | 独立控制器状态硬件测试 |
| `realman_hw_test_motion_tracking` | 独立低速运动跟踪硬件测试 |
| `realman_hw_test_force_retreat` | 手动外力方向退让测试,检测末端受力后小步后退 |
| `realman_hw_test_force_retreat_hold` | 持续外力方向退让保持模式,被推就小步退让,直到手动停止 |
## CMake Presets
`CMakePresets.json` 已配置:
```json
"REALMAN_RM_API2_ROOT": "/home/mashiro/RM_API2/C++"
```
命令行也可以直接构建:
```bash
cd /home/mashiro/project/linkerhand-cpp-sdk
cmake --preset clion-debug
cmake --build --preset realman-demo-debug
cmake --build --preset realman-slow-motion-debug
cmake --build --preset realman-joint-pose-config-debug
cmake --build --preset realman-metrics-test-debug
cmake --build --preset realman-trajectory-record-debug
cmake --build --preset realman-trajectory-replay-debug
cmake --build --preset realman-hw-tests-debug
cmake --build --preset realman-hw-force-retreat-debug
cmake --build --preset realman-hw-force-retreat-hold-debug
```
Release:
```bash
cd /home/mashiro/project/linkerhand-cpp-sdk
cmake --preset clion-release
cmake --build --preset realman-demo-release
cmake --build --preset realman-slow-motion-release
cmake --build --preset realman-joint-pose-config-release
cmake --build --preset realman-metrics-test-release
cmake --build --preset realman-trajectory-record-release
cmake --build --preset realman-trajectory-replay-release
cmake --build --preset realman-hw-tests-release
cmake --build --preset realman-hw-force-retreat-release
cmake --build --preset realman-hw-force-retreat-hold-release
```
## 最小连接验证
在 CLion 里运行:
```text
realman_minimal_demo
```
默认参数等价于:
```bash
./cmake-build-debug/realman_minimal_demo 192.168.1.18 8080 1000
```
成功时会打印机械臂型号、自由度、软件版本等信息。
当前环境已验证:
| 项目 | 结果 |
| --- | --- |
| 运行目标 | `realman_minimal_demo` |
| 动态库 | `/home/mashiro/RM_API2/C++/linux/linux_x86_c++_vv1.1.5/libapi_cpp.so` |
| API 版本 | `v1.1.5` |
| 控制器地址 | `192.168.1.18:8080` |
| 机械臂型号 | `RM75-6FB` |
| 自由度 | `7` |
| 运行结果 | 成功 |
## 低速运动验证
在 CLion 里运行:
```text
realman_slow_motion_demo
```
默认参数等价于:
```bash
./cmake-build-debug/realman_slow_motion_demo 192.168.1.18 8080 1000 2 5 10
```
含义:
| 参数 | 含义 |
| --- | --- |
| `192.168.1.18` | 机械臂控制器 IP |
| `8080` | SDK 控制端口 |
| `1000` | SDK 超时时间,单位 ms |
| `2` | 关节序号 J2 |
| `5` | 运动幅度 5° |
| `10` | 速度比例 10% |
## 源码参数调姿态
在 CLion 中运行:
```text
realman_joint_pose_config_demo
```
修改文件顶部的参数:
```text
examples/realman_joint_pose_config_demo.cpp
```
常用修改点:
| 参数 | 说明 |
| --- | --- |
| `kTargetMode` | `kRelativeJointOffset` 为相对当前姿态偏移,`kAbsoluteJointAngles` 为绝对关节角 |
| `kRelativeJointOffsetsDeg` | 相对偏移,单位度,例如 `{0, 5, 0, 0, 0, 0, 0}` 表示 `J2 +5°` |
| `kAbsoluteTargetJointsDeg` | 绝对目标关节角,单位度 |
| `kSpeedPercent` | 速度比例 |
| `kReturnToStartAfterMove` | 是否执行后回到起点 |
| `kDryRunOnly` | 只打印目标,不发送运动指令 |
详细说明见 [睿尔曼机械臂源码参数调姿态 Demo](REALMAN_JOINT_POSE_CONFIG_DEMO.md)。
## 力/力矩与关节指标测试
在 CLion 中运行:
```text
realman_metrics_test
```
运行后生成:
```text
reports/REALMAN_METRICS_TEST_RESULT.md
reports/realman_metrics_latest.csv
```
详细说明见 [睿尔曼机械臂力/力矩与关节指标测试 README](REALMAN_METRICS_TEST_README.md)。
## 轨迹录制与回放
在 CLion 中可以分别运行:
```text
realman_trajectory_record
realman_trajectory_replay
```
命令行示例:
```bash
./cmake-build-debug/realman_trajectory_record reports/realman_trajectories/demo.csv 15 100
./cmake-build-debug/realman_trajectory_replay reports/realman_trajectories/demo.csv dry-run
./cmake-build-debug/realman_trajectory_replay reports/realman_trajectories/demo.csv execute 5 5 5
```
详细说明见 [RM75 轨迹录制与回放](REALMAN_TRAJECTORY_RECORD_REPLAY.md)。
## 独立硬件模块测试
这套测试类似单元测试组织方式,但对象是真实机械臂硬件。每个模块都是独立进程,单独连接、单独断开、单独出报告。
只运行只读测试:
```bash
ctest --test-dir cmake-build-debug --output-on-failure --label-regex readonly
```
运行单个模块:
```bash
ctest --test-dir cmake-build-debug --output-on-failure -R realman_hw_test_force_sensor
```
运行全部硬件测试:
```bash
ctest --test-dir cmake-build-debug --output-on-failure --label-regex realman
```
手动运行外力方向退让测试:
```bash
ctest --test-dir cmake-build-debug --output-on-failure -R realman_hw_test_force_retreat
```
持续保持外力方向退让状态:
```bash
./cmake-build-debug/realman_hw_test_force_retreat_hold
```
详细说明见 [睿尔曼 RealMan 硬件模块测试](../tests/realman_hardware/README.md)。
## Web 示教器
控制器 Web 示教器入口:
```text
http://192.168.1.18/
```
8080 是 SDK 控制端口,不是 Web 页面入口。
## 更换 SDK 路径
如果后续 SDK 放到其他目录,在 CLion 的 CMake cache 或 `CMakePresets.json` 中修改:
```text
REALMAN_RM_API2_ROOT=/path/to/RM_API2/C++
```
目录应包含:
```text
include/rm_interface.h
linux/linux_x86_c++_*/libapi_cpp.so
```
## 常见问题
| 现象 | 处理 |
| --- | --- |
| CLion 看不到 RealMan target | `Reload CMake Project` |
| 找不到 `rm_interface.h` | 检查 `REALMAN_RM_API2_ROOT` 是否指向 `RM_API2/C++` |
| 运行时报 `libapi_cpp.so` 找不到 | 使用本项目 target 构建,CMake 已设置 rpath |
| 修改源码后 CLion 仍运行旧动作 | 重新 Build `realman_joint_pose_config_demo`,不要运行旧的 `realman_slow_motion_demo` |
| 指标测试后找不到报告 | 查看项目根目录下 `reports/REALMAN_METRICS_TEST_RESULT.md` |
| 连接失败 | 检查 `192.168.1.18:8080`、网线、网段和控制器电源 |
| Web 示教器打不开 | 浏览器访问 `http://192.168.1.18/`,并绕过代理 |
# 睿尔曼机械臂源码参数调姿态 Demo
本文档说明如何使用 `realman_joint_pose_config_demo` 在 CLion 中通过修改 C++ 源码顶部参数来调整机械臂姿态。
## 文件位置
| 文件 | 说明 |
| --- | --- |
| `examples/realman_joint_pose_config_demo.cpp` | 源码参数调姿态示例 |
| `CMakeLists.txt` | 已注册 `realman_joint_pose_config_demo` target |
| `CMakePresets.json` | 已注册 Debug/Release 构建 preset |
## CLion 运行
1. 用 CLion 打开项目:
```bash
/home/mashiro/project/linkerhand-cpp-sdk
```
2. 点击 `Reload CMake Project`
3. 打开文件:
```text
examples/realman_joint_pose_config_demo.cpp
```
4. 修改源码顶部的“用户主要修改区”。
5. 运行 target:
```text
realman_joint_pose_config_demo
```
## 主要参数
默认推荐使用相对关节偏移模式:
```cpp
constexpr TargetMode kTargetMode = TargetMode::kRelativeJointOffset;
constexpr std::array<float, ARM_DOF> kRelativeJointOffsetsDeg = {
0.0F, 5.0F, 0.0F, 0.0F, 0.0F, 0.0F, 0.0F,
};
```
这表示在当前姿态基础上让 `J2 +5°`,其他关节不动。
如果要改成其他姿态,例如 `J2 +3°`、`J4 -2°`:
```cpp
constexpr std::array<float, ARM_DOF> kRelativeJointOffsetsDeg = {
0.0F, 3.0F, 0.0F, -2.0F, 0.0F, 0.0F, 0.0F,
};
```
速度参数:
```cpp
constexpr int kSpeedPercent = 10;
```
安全限幅:
```cpp
constexpr float kMaxSingleJointDeltaDeg = 10.0F;
constexpr int kMaxAllowedSpeedPercent = 20;
```
程序会拒绝超过限幅的动作,避免误把机械臂一次移动太大。
## 绝对关节角模式
需要直接给目标关节角时,改为:
```cpp
constexpr TargetMode kTargetMode = TargetMode::kAbsoluteJointAngles;
constexpr std::array<float, ARM_DOF> kAbsoluteTargetJointsDeg = {
0.0F, 5.0F, 0.0F, 0.0F, 0.0F, 0.0F, 0.0F,
};
```
首次使用绝对模式前,建议先运行一次程序,看输出的 `Current joints`,把当前角度复制到 `kAbsoluteTargetJointsDeg`,然后只小幅修改其中一个或两个关节。
## 是否回到起点
默认移动到目标姿态后停在目标位置:
```cpp
constexpr bool kReturnToStartAfterMove = false;
```
如果只是测试动作,希望执行后回到起始姿态:
```cpp
constexpr bool kReturnToStartAfterMove = true;
```
## 只预览不运动
如果只想检查目标角度,不发送运动指令:
```cpp
constexpr bool kDryRunOnly = true;
```
程序会打印当前关节角和目标关节角,但不会调用 `rm_movej`。
## 命令行构建
Debug:
```bash
cd /home/mashiro/project/linkerhand-cpp-sdk
cmake --preset clion-debug
cmake --build --preset realman-joint-pose-config-debug
```
Release:
```bash
cd /home/mashiro/project/linkerhand-cpp-sdk
cmake --preset clion-release
cmake --build --preset realman-joint-pose-config-release
```
## 安全建议
- 优先使用 `kRelativeJointOffset`
- 首次调姿态时,每个关节变化控制在 `1°..5°`
- 速度先保持 `5..10`
- 每次只改一个或两个关节,确认方向和空间余量后再组合动作。
- 运行前确认工作空间无人,急停按钮可触达。
# 睿尔曼机械臂力/力矩与关节指标测试 README
本文档说明如何使用 `realman_metrics_test` 对当前睿尔曼机械臂做保守指标测试,并生成 CSV 原始数据和 Markdown 报告。
## 测试对象
| 项目 | 当前值 |
| --- | --- |
| 控制器地址 | `192.168.1.18:8080` |
| SDK | `/home/mashiro/RM_API2/C++` |
| 动态库 | `/home/mashiro/RM_API2/C++/linux/linux_x86_c++_vv1.1.5/libapi_cpp.so` |
| CLion target | `realman_metrics_test` |
## 已测试指标
| 类别 | 指标 | 数据来源 |
| --- | --- | --- |
| 末端六维力/力矩 | `Fx/Fy/Fz/Mx/My/Mz` 原始值与清零后系统外受力 | `rm_get_force_data` |
| 关节负载代理量 | 关节电流均值、标准差、峰值、峰峰值 | `rm_get_current_joint_current` |
| 关节状态 | 关节角度、电压、温度 | `rm_get_current_arm_state``rm_get_current_joint_voltage``rm_get_current_joint_temperature` |
| 控制器状态 | 控制器电压、电流、温度、错误码 | `rm_get_controller_state` |
| 低速运动指标 | J2 `+3° -> 原位 -> -3° -> 原位` 的耗时与到位误差 | `rm_movej` + 到位后状态读取 |
## 未自动测试的指标
以下项目需要额外治具或外部传感器,本程序不会自动执行:
| 指标 | 原因 |
| --- | --- |
| 接触式力位混合控制闭环精度 | 需要固定接触面、接触方向限位和外部测力基准 |
| 外部负载下输出力矩精度 | 需要力矩传感器、已知力臂或标定负载 |
| 碰撞阈值/柔顺控制边界 | 涉及真实接触风险,需要急停监护和保护工装 |
| 裸关节力矩指令跟踪 | 当前 RM_API2 C++ 头文件未暴露直接关节力矩命令接口 |
因此本文中的“关节侧力矩相关表现”以关节电流变化作为负载代理量;末端力矩使用六维力传感器的 `Mx/My/Mz`
## 安全策略
程序默认只做低风险动作:
```text
静态采样 -> 5 秒安全倒计时 -> J2 +3° -> 原位 -> J2 -3° -> 原位 -> 静态采样
```
内置限制:
| 参数 | 默认值 | 限制 |
| --- | ---: | --- |
| `kMotionDeltaDeg` | `3.0` | 不超过 `5°` |
| `kMotionSpeedPercent` | `5` | 不超过 `10%` |
| `kSamplePeriodMs` | `50` | 不低于 `20ms` |
如需只采样、不运动,在源码中改:
```cpp
constexpr bool kRunMotionTrackingTest = false;
```
如需只预览动作、不发送 `rm_movej`
```cpp
constexpr bool kDryRunOnly = true;
```
## CLion 使用
1. 打开项目:
```bash
/home/mashiro/project/linkerhand-cpp-sdk
```
2. 点击 `Reload CMake Project`
3. 选择运行目标:
```text
realman_metrics_test
```
4. 运行结束后查看:
```text
reports/REALMAN_METRICS_TEST_RESULT.md
reports/realman_metrics_latest.csv
```
## 命令行构建与运行
Debug:
```bash
cd /home/mashiro/project/linkerhand-cpp-sdk
cmake --preset clion-debug
cmake --build --preset realman-metrics-test-debug
./cmake-build-debug/realman_metrics_test
```
Release:
```bash
cd /home/mashiro/project/linkerhand-cpp-sdk
cmake --preset clion-release
cmake --build --preset realman-metrics-test-release
./cmake-build-clion-release/realman_metrics_test
```
## 本次实测结果
已生成最新报告:
```text
reports/REALMAN_METRICS_TEST_RESULT.md
```
本次结果摘要:
| 指标 | 结果 |
| --- | --- |
| 总样本数 | `205` |
| 六维力接口失败次数 | `0` |
| 关节电流接口失败次数 | `0` |
| 控制器状态接口失败次数 | `0` |
| J2 `+3°` 到位最大误差 | `0.004°` |
| J2 返回原位最大误差 | `0.004°` |
| J2 `-3°` 到位最大误差 | `0.003°` |
| 最终返回原位最大误差 | `0.007°` |
| 控制器电压均值 | `23.3421 V` |
| 控制器温度均值 | `45.8516 C` |
详细统计以 `reports/REALMAN_METRICS_TEST_RESULT.md` 和 CSV 原始数据为准。
## 后续扩展建议
如果要进一步测试真实力控能力,建议增加以下前置条件后再启用 `rm_set_force_position``rm_force_position_move_*`
- 固定机械臂底座和接触工件。
- 明确接触方向,设置机械限位或软限位。
- 使用外部六维力传感器、力矩传感器或标定砝码做基准。
- 每次只测试一个轴向,目标力从 `1N..3N` 小值开始。
- 测试期间保持急停按钮可触达。
# 睿尔曼机械臂最小 Demo
本文档说明如何在本项目中构建并运行睿尔曼 RealMan RM_API2 最小 C++ Demo。
该 Demo 目标是验证 SDK、动态库、网络连接和控制器通信是否可用。程序只执行以下动作:
1. 初始化 RM_API2。
2. 连接机械臂控制器。
3. 打印 API 版本。
4. 读取机械臂软件版本信息。
5. 读取机械臂基础信息。
6. 断开连接并销毁 SDK 线程。
Demo 不会发送任何运动指令,适合作为首次联调入口。
## 文件位置
| 文件 | 说明 |
| --- | --- |
| `examples/realman_minimal_demo.cpp` | 最小机械臂连接与信息读取示例 |
| `/home/mashiro/RM_API2/C++/include` | 首选睿尔曼 RM_API2 C++ 头文件 |
| `/home/mashiro/RM_API2/C++/linux/linux_x86_c++_vv1.1.5/libapi_cpp.so` | 首选睿尔曼 RM_API2 Linux x86_64 动态库 |
| `third_party/Robotic_Arm` | fallback SDK 路径 |
| `CMakePresets.json` | CLion/命令行可直接使用的 CMake presets |
## 环境要求
- Ubuntu/Linux x86_64。
- GCC/G++ 7.5 或更高版本。
- CMake 3.15 或更高版本。
- 机械臂控制器与电脑在同一网络内。
- 默认控制器地址为 `192.168.1.18:8080`,可通过参数覆盖。
## CLion 使用方式
1. 打开 CLion。
2. 选择 `Open`,打开项目目录:
```bash
/home/mashiro/project/linkerhand-cpp-sdk
```
3. CLion 识别 `CMakePresets.json` 后,选择 `CLion Debug``CLion Release`
4. 在 Run/Debug Configuration 中选择目标 `realman_minimal_demo`
5. 如果机械臂 IP 不是默认值,在 Program arguments 中填写:
```text
192.168.1.18 8080
```
第三个参数可设置 SDK 超时时间,单位 ms:
```text
192.168.1.18 8080 1500
```
6. 点击 Build 或 Run。
## 命令行构建
Debug 构建:
```bash
cd /home/mashiro/project/linkerhand-cpp-sdk
cmake --preset clion-debug
cmake --build --preset realman-demo-debug
```
Release 构建:
```bash
cd /home/mashiro/project/linkerhand-cpp-sdk
cmake --preset clion-release
cmake --build --preset realman-demo-release
```
## 运行方式
使用默认地址 `192.168.1.18:8080`:
```bash
./cmake-build-debug/realman_minimal_demo
```
指定 IP 和端口:
```bash
./cmake-build-debug/realman_minimal_demo 192.168.1.18 8080
```
指定 IP、端口和超时时间:
```bash
./cmake-build-debug/realman_minimal_demo 192.168.1.18 8080 1500
```
查看帮助:
```bash
./cmake-build-debug/realman_minimal_demo --help
```
## 预期输出
连接成功时会看到类似输出:
```text
RealMan API version: ...
Connecting to 192.168.1.18:8080 with timeout 1000 ms...
Robot handle created, id: 1
================ RealMan Arm Software Info ================
Product Version: ...
Algorithm Version: ...
Control Layer Version: ...
===========================================================
================ RealMan Arm Basic Info ====================
Arm DOF: 6
Arm Model: RM_65 (0)
Force Type: Standard (0)
Controller Version: 4
===========================================================
RealMan minimal demo finished successfully.
```
如果未连接真实机械臂,程序会在连接阶段失败并提示检查 IP、端口、网络和电源状态。这说明本地程序已能启动,剩余问题在设备连接侧。
## 网络检查
运行前建议先检查网络连通性:
```bash
ping 192.168.1.18
nc -vz 192.168.1.18 8080
```
如果 `nc` 未安装:
```bash
sudo apt update
sudo apt install netcat-openbsd
```
## 常见问题
| 现象 | 可能原因 | 处理方式 |
| --- | --- | --- |
| `Failed to connect to RealMan arm` | IP/端口不对、控制器未上电、电脑不在同一网段 | 检查控制器地址、网线、网卡 IP、防火墙 |
| 找不到 `libapi_cpp.so` | 运行时动态库路径未生效 | 使用本文的 CMake target 构建;目标已设置 rpath |
| CLion 没看到 `realman_minimal_demo` | CMake 未重新加载 | 点击 Reload CMake Project |
| 构建时找不到 `rm_interface.h` | SDK 路径不对 | 确认 `REALMAN_RM_API2_ROOT` 指向 `/home/mashiro/RM_API2/C++`,或 fallback `third_party/Robotic_Arm/include/rm_interface.h` 存在 |
## 后续开发建议
- 先确认本 Demo 能稳定读取机械臂信息,再加入运动控制。
- 新增运动控制前,优先做小速度、非危险空间、急停可触达的测试。
- 把所有运动参数做成配置,不要把生产动作硬编码在 demo 中。
# 睿尔曼机械臂低速运动 Demo 操作手册
本文档说明如何使用 `realman_slow_motion_demo` 让睿尔曼机械臂做一次小幅、低速、可观察的关节空间往返运动。
该 Demo 的默认动作是:
```text
读取当前关节角 -> J2 +5 deg -> 回到起始角 -> J2 -5 deg -> 回到起始角
```
默认速度比例为 `10%`。程序使用 `rm_movej` 阻塞运动,不执行笛卡尔空间轨迹、不做轨迹融合、不连续抖动。
## 文件位置
| 文件 | 说明 |
| --- | --- |
| `examples/realman_slow_motion_demo.cpp` | 低速小幅运动 Demo |
| `CMakeLists.txt` | 已注册 `realman_slow_motion_demo` target |
| `CMakePresets.json` | 已注册 Debug/Release 构建 preset |
| `/home/mashiro/RM_API2/C++` | 首选睿尔曼 RM_API2 C++ SDK |
| `third_party/Robotic_Arm` | fallback SDK 路径 |
## 安全前置检查
运行前必须确认:
1. 机械臂工作空间内无人、无松散物体。
2. 急停按钮可触达。
3. 机械臂末端、线缆、夹具不会与桌面、支架、相机、电脑等发生碰撞。
4. 控制器 IP 和端口正确,默认是 `192.168.1.18:8080`
5. 首次运行保持默认参数,不要直接增大角度或速度。
程序内置 5 秒倒计时,倒计时期间可以用 `Ctrl+C` 取消。
## CLion 运行
1. 用 CLion 打开项目:
```bash
/home/mashiro/project/linkerhand-cpp-sdk
```
2. 选择 CMake preset:
- `CLion Debug`
- 或 `CLion Release`
3. 选择运行目标:
```text
realman_slow_motion_demo
```
4. 默认无需填写 Program arguments。若要显式指定参数:
```text
192.168.1.18 8080 1000 2 5 10
```
## 命令行构建
Debug:
```bash
cd /home/mashiro/project/linkerhand-cpp-sdk
cmake --preset clion-debug
cmake --build --preset realman-slow-motion-debug
```
Release:
```bash
cd /home/mashiro/project/linkerhand-cpp-sdk
cmake --preset clion-release
cmake --build --preset realman-slow-motion-release
```
## 命令行运行
使用默认参数:
```bash
./cmake-build-debug/realman_slow_motion_demo
```
完整参数格式:
```bash
./cmake-build-debug/realman_slow_motion_demo [robot_ip] [port] [timeout_ms] [joint] [delta_deg] [speed_percent]
```
示例:
```bash
./cmake-build-debug/realman_slow_motion_demo 192.168.1.18 8080 1000 2 5 10
```
更保守的动作:
```bash
./cmake-build-debug/realman_slow_motion_demo 192.168.1.18 8080 1000 2 3 5
```
## 参数说明
| 参数 | 默认值 | 范围 | 说明 |
| --- | --- | --- | --- |
| `robot_ip` | `192.168.1.18` | 有效 IP | 机械臂控制器 IP |
| `port` | `8080` | `1..65535` | 控制器端口 |
| `timeout_ms` | `1000` | `>0` | SDK 接收超时时间 |
| `joint` | `7` | `1..当前自由度` | 1-based 关节序号 |
| `delta_deg` | `2` | `(0, 5]` | 往返运动角度,单位度 |
| `speed_percent` | `5` | `1..20` | 速度比例。此 Demo 限制在低速范围 |
## 程序流程
1. 初始化 RM_API2,设置日志和超时时间。
2. 连接机械臂。
3. 读取机械臂自由度和当前关节角。
4. 打印起始关节角。
5. 5 秒安全倒计时。
6. 执行 `+delta`、回原位、`-delta`、回原位。
7. 读取并打印最终关节角。
8. 断开连接并销毁 SDK 线程。
任一步 `rm_movej` 失败时,程序会调用 `rm_set_arm_stop`,随后断开连接。
## 预期输出
```text
RealMan API version: 1.1.0
Connecting to 192.168.1.18:8080...
Connected. Arm DOF: 7, selected joint: J2, delta: 5 deg, speed: 10%
Current joints: [J1=..., J2=..., ...]
Safety countdown before motion. Clear the workspace and keep E-stop reachable.
Moving in 5...
...
Step 1 target (+delta): [...]
Step 2 return home: [...]
Step 3 target (-delta): [...]
Step 4 return home: [...]
Final joints: [...]
Slow motion demo finished successfully.
```
## 本机实测记录
已在当前连接的睿尔曼机械臂上执行成功:
```bash
./cmake-build-debug/realman_slow_motion_demo 192.168.1.18 8080 1000 2 5 10
```
实测信息:
| 项目 | 结果 |
| --- | --- |
| 控制器地址 | `192.168.1.18:8080` |
| API 版本 | `1.1.0` |
| 机械臂自由度 | `7` |
| 运动关节 | `J2` |
| 运动幅度 | `+5° -> 原位 -> -5° -> 原位` |
| 速度比例 | `10%` |
| 执行结果 | 成功 |
实测起始关节角:
```text
[J1=0.001, J2=0.002, J3=0.004, J4=0.000, J5=-0.001, J6=0.005, J7=-0.001] deg
```
实测结束关节角:
```text
[J1=0.001, J2=-0.001, J3=0.005, J4=0.000, J5=0.000, J6=0.001, J7=-0.001] deg
```
## 问题定位记录
如果程序返回成功但肉眼看不到动作,优先检查以下点:
1. 旧默认动作是 `J7 ±2°`。J7 是末端腕部旋转,小角度时没有工具或末端标记会很不明显。
2. 当前控制器已确认不是仿真模式:`run_mode=1`
3. 当前 7 个关节均已使能,错误码均为 `0`,抱闸均为打开状态。
4. 已用 `J2 +5° -> 回原位` 实测验证,编码器读数从 `J2=0.002°``J2=5.001°`,再回到 `J2=-0.001°`
因此默认动作已改为更容易观察的 `J2 ±5°`、速度 `10%`,并在每一步后读取实际关节角。
## 故障处理
| 现象 | 处理 |
| --- | --- |
| 连接失败 | 检查控制器 IP、端口、网线、电脑网段、电源状态 |
| 目标运行失败 | 降低 `delta_deg`,例如 `1`;降低速度,例如 `3` |
| CLion 看不到 target | 点击 `Reload CMake Project` |
| 动态库加载失败 | 使用本项目 CMake target 构建运行,target 已设置 rpath |
## 后续扩展
确认本 Demo 稳定后,再考虑新增更复杂动作。建议保持以下策略:
- 先读当前关节角,再基于当前位置做相对小幅运动。
- 优先关节空间 `movej`,避免一开始使用笛卡尔空间大轨迹。
- 运动参数配置化,不把生产动作硬编码进示例。
- 每次新增动作后先以 `delta_deg <= 2``speed_percent <= 5` 验证。
# 睿尔曼 RM75 测试项目架构说明
本文说明当前 RealMan/RM75-6F 测试代码的分层、模块职责和设计取舍。
## 总体结构
```text
linkerhand-cpp-sdk/
├── CMakeLists.txt
├── CMakePresets.json
├── examples/
│ ├── realman_minimal_demo.cpp
│ ├── realman_slow_motion_demo.cpp
│ ├── realman_joint_pose_config_demo.cpp
│ └── realman_metrics_test.cpp
├── tests/
│ ├── realman_hardware/
│ │ ├── realman_hw_test_common.hpp
│ │ ├── test_realman_connection.cpp
│ │ ├── test_realman_joint_telemetry.cpp
│ │ ├── test_realman_force_sensor.cpp
│ │ ├── test_realman_controller_state.cpp
│ │ ├── test_realman_motion_tracking.cpp
│ │ ├── test_realman_force_retreat.cpp
│ │ └── test_realman_force_retreat_hold.cpp
│ └── rm75_acceptance/
│ ├── test_rm75_acceptance_preflight.cpp
│ ├── test_rm75_acceptance_communication.cpp
│ └── README.md
├── docs/
│ ├── REALMAN_CLION_SETUP.md
│ ├── REALMAN_METRICS_TEST_README.md
│ └── REALMAN_TEST_ARCHITECTURE.md
└── reports/
└── realman_hardware_tests/
```
## 分层职责
| 层级 | 目录/文件 | 职责 |
| --- | --- | --- |
| 构建层 | `CMakeLists.txt`, `CMakePresets.json` | 发现 RM_API2 SDK,导入 `libapi_cpp.so`,暴露 CLion target 和 CTest target |
| Demo 层 | `examples/` | 给使用者提供最小连接、慢速运动、源码调参、综合指标测试 |
| 测试基础设施 | `tests/realman_hardware/realman_hw_test_common.hpp` | 统一连接参数、SDK 生命周期、断言输出、Markdown 报告路径 |
| 独立硬件模块测试 | `tests/realman_hardware/test_*.cpp` | 每个模块独立 `main()`,分别测试连接、关节、控制器、力传感器、运动、力退让 |
| 文档验收层 | `tests/rm75_acceptance/` | 按 RM75-6F 测试文档组织验收项,自动执行只读项目,手动项目写入 README |
| 报告层 | `reports/realman_hardware_tests/` | 每个测试生成 latest 和带时间戳的 Markdown/CSV 报告 |
## 为什么每个测试都是独立可执行文件
机械臂不是普通纯软件模块,它有真实状态、网络连接、运动风险和 SDK 全局初始化状态。把每个测试做成独立 `main()` 有几个好处:
- 单个模块失败不会污染后续模块。
- 每个模块都独立执行 `rm_init``rm_create_robot_arm``rm_delete_robot_arm``rm_destroy/rm_destory`
- 在 CLion 里可以像单元测试一样单独运行某个 target。
- CTest 可以通过 `RESOURCE_LOCK realman_robot` 防止多个测试并发抢同一台机械臂。
- 报告天然按模块拆分,便于定位是通信、关节、力传感器还是运动问题。
## 为什么区分只读测试和运动/手动测试
RM75-6F 测试文档要求不能绕过安全确认。项目里把测试分成三类:
| 类型 | 示例 target | 默认是否可自动运行 | 原因 |
| --- | --- | --- | --- |
| 只读测试 | `rm75_acceptance_preflight`, `realman_hw_test_force_sensor` | 是 | 只调用 `rm_get_*`,不会移动机械臂或修改控制器参数 |
| 低速运动测试 | `realman_hw_test_motion_tracking`, `realman_slow_motion_demo` | 否,需要用户明确要求 | 会发送运动指令,必须确认工作空间和急停 |
| 手动/外部设备测试 | 载荷、急停、重复定位、真实接触力控 | 否 | 需要人现场观察、治具、载荷、测量设备或监护 |
这样做的核心原因是:自动化测试应该提高效率,但不能替代物理安全确认。
## 为什么 `rm75_acceptance` 不直接复用所有硬件测试
`realman_hardware` 是“模块测试”,关注某一个硬件能力是否正常。
`rm75_acceptance` 是“验收流程”,关注测试文档里的阶段和数据字段是否被覆盖。
两者不是重复关系:
- `realman_hardware` 适合调试单个故障点。
- `rm75_acceptance` 适合给验收人员一个按文档组织的入口和报告。
- 验收层会复用公共基础设施,但不把所有模块串成强耦合流程。
## 为什么报告同时有 Markdown 和 CSV
Markdown 用于人读:
- 每个断言 PASS/FAIL。
- 每个模块的关键状态摘要。
- 适合直接发给测试人员或写入验收记录。
CSV 用于机器分析:
- `rm75_acceptance_communication` 按测试文档保留固定字段。
- 后续可以导入 Excel、Python、数据库或绘图工具。
- `latest.csv` 方便固定路径读取,带时间戳 CSV 方便追溯历史。
## 为什么 `err_len=1,codes=[0]` 不判失败
实测 RM75 控制器在 `rm_get_current_arm_state``rm_get_arm_all_state` 中可能返回:
```text
err_len=1, codes=[0]
```
这表示错误列表里只有一个零码占位,不是实际故障。代码里统一使用 `errListClear()`,只要错误码数组没有非零值,就视为无故障。这样可以避免把 SDK 的占位返回误判为验收失败。
## 为什么力退让持续模式使用绝对目标位姿
早期使用 `rm_movel_offset` 做连续退让时,当前控制器曾出现 `error=-7`。现在的持续模式流程是:
```text
读取当前 TCP 位姿
读取工作坐标系外力
根据外力方向和大小计算小步退让
生成 current_pose + delta 的绝对目标位姿
调用 rm_movel(target_pose, ...)
```
这样写的原因:
- 方向计算和数据来源都在工作坐标系语义下,减少坐标混用。
- 绝对目标位姿比连续 offset 在当前固件上更稳定。
- 每一步都能重新读取当前状态,便于遇到错误立即停止。
- 单步距离、速度和累计距离都有上限,方便人工测试。
## 为什么参数写在源码常量里
`realman_hw_test_force_retreat_hold.cpp` 里的阈值、速度、步长是 `constexpr` 常量。这样对初学者更直观:
- 打开一个文件就能看到所有关键参数。
- CLion 修改后重新编译即可验证。
- 避免命令行参数传错导致危险动作。
- 参数自检 `validateConfig()` 会在运动前拦截明显危险配置。
后续如果要做大量实验,可以再把这些常量抽成配置文件;当前阶段优先保证简单、可读、可控。
## 推荐开发流程
1. 修改代码前先确认目标模块。
2. 只读逻辑优先跑:
```bash
cmake --build --preset rm75-acceptance-readonly-debug
ctest --test-dir cmake-build-debug --output-on-failure --label-regex rm75
```
3. 修改运动逻辑时先构建,不自动运行:
```bash
cmake --build --preset realman-hw-force-retreat-hold-debug
```
4. 运行任何运动测试前,确认工作空间、急停、载荷和人工监护。
# RM75 Drag Teach Trajectory Workflow
本文档说明正式的 RM75 末端拖动示教、同步 CSV 记录、C++ TCP 路径转换和 skill 封装边界。
`examples/realman_trajectory_record.cpp``examples/realman_trajectory_replay.cpp` 是早期最小验证入口。后续维护优先看 `RM/` 子工程。
## 目标
- C++ 底层模块:在 `RM/include/rm_control``RM/src` 中维护,可编译、可测试。
- Codex skill:在 `skills/rm75-drag-trajectory``~/.codex/skills/rm75-drag-trajectory` 中维护,负责指导以后如何安全操作。
- 数据目录:用文件夹管理轨迹产物,避免 CSV、厂商文件、转换代码混在一起。
## 维护边界
| 层 | 路径 | 维护内容 |
| --- | --- | --- |
| HAL/interface | `RM/include/rm_control/interfaces/robot_arm.h` | 抽象机械臂能力,不包含 RM SDK 头文件 |
| Driver | `RM/include/rm_control/drivers/realman`, `RM/src/drivers/realman` | `rm_start_multi_drag_teach``rm_stop_drag_teach``rm_save_trajectory` 等 RM SDK 调用 |
| Core | `RM/include/rm_control/core/drag_teach_recorder.h`, `RM/src/core/drag_teach_recorder.cpp` | 拖动示教记录流程、采样、停止、保存 |
| Module | `RM/include/rm_control/modules/trajectory`, `RM/src/modules/trajectory` | CSV 读写、目录管理、C++ TCP 路径导出 |
| Application | `RM/src/drag_teach_record_main.cpp`, `RM/src/trajectory_convert_main.cpp` | 参数解析、驱动装配、启动流程 |
| Skill | `skills/rm75-drag-trajectory` | 给 Codex 的操作步骤和安全约束 |
## 数据目录
默认根目录:
```text
RM/data/trajectories/
├── csv/ # 我们同步采样的主数据源
├── vendor/ # rm_save_trajectory 尽力保存的厂商轨迹文件
├── converted/ # CSV 转成的 C++ TCP waypoint 文件
└── reports/ # 后续扩展:记录摘要、误差报告、回放日志
```
## CSV 格式
```text
sample_index,elapsed_ms,j1_deg,j2_deg,j3_deg,j4_deg,j5_deg,j6_deg,j7_deg,tcp_x_m,tcp_y_m,tcp_z_m,tcp_rx_rad,tcp_ry_rad,tcp_rz_rad
```
当前主目标是 TCP 空间路径复现,因此转换阶段会把 `tcp_*` 字段导出为 C++ waypoint 表。`j1_deg..j7_deg` 仍保留,用于复现原始姿态、做逆解种子或误差对比。
## 构建
```bash
cd /home/mashiro/project/linkerhand-cpp-sdk
cmake -S RM -B RM/build -DCMAKE_BUILD_TYPE=Debug
cmake --build RM/build -j
ctest --test-dir RM/build --output-on-failure
```
## 六维力拖动示教录制
```bash
./RM/build/rm_drag_teach_record
```
默认行为:
- 连接 `192.168.1.18:8080`
- 使用 RM75 三代控制器复合拖动示教:`mode=3`,即六维力位置和姿态同时可拖动
- 默认开启拖动奇异墙
- 录制 `15`
- 采样周期 `100 ms`
- 主产物写到 `RM/data/trajectories/csv/<session>.csv`
- 停止拖动后尝试把厂商轨迹保存到 `RM/data/trajectories/vendor/<session>_vendor.txt`
自定义 session、时长和采样周期:
```bash
./RM/build/rm_drag_teach_record \
demo_pick_path \
15 \
100
```
完整参数:
```text
rm_drag_teach_record [session_name] [duration_seconds] [sample_period_ms] [storage_root] [--no-vendor-save] [--ordinary-recorded] [--no-singular-wall]
```
## CSV 转 C++ TCP 路径
离线转换,不连接机械臂:
```bash
./RM/build/rm_trajectory_convert \
RM/data/trajectories/csv/demo_pick_path.csv
```
默认输出:
```text
RM/data/trajectories/converted/demo_pick_path_tcp_path.cpp
```
完整参数:
```text
rm_trajectory_convert <input_csv> [output_cpp]
```
## 安全边界
- `rm_drag_teach_record` 会启动拖动示教,必须清空工作空间,急停可触达。
- 当前实现不自动执行 TCP 回放;转换出来的 C++ 文件只是 waypoint 数据。
- 六维力复合拖动接口没有暴露 `trajectory_record` 开关,CSV 是主数据源;厂商轨迹保存失败只作为警告处理。
- 后续做 TCP 复现时,应先用 CSV 里的关节角作为逆解参考,再做低速、小步、dry-run 校验。
# RM75 + O6 目标软件架构与最小可行重构计划
本文承接工程理解阶段的结论,说明适合睿尔曼 RM75 机械臂与灵心巧手 O6 的目标软件架构、当前工程差距,以及最小可行重构计划。
当前阶段只定义架构和重构边界,不修改业务代码,不引入未确认的厂商 API 签名。
## 1. 目标软件架构
目标不是推翻现有工程,而是把已有 `RM/` 子工程中的机械臂接口化思路,扩展成“机械臂 + O6 手”的统一控制栈。
建议目录形态:
```text
applications/
move_arm_then_grasp_task.cpp
core/
coordinated_arm_hand_controller.h
coordinated_arm_hand_controller.cpp
motion_safety_guard.h
motion_safety_guard.cpp
modules/
arm_motion_service.h
arm_motion_service.cpp
o6_grasp_service.h
o6_grasp_service.cpp
arm_hand_task_planner.h
arm_hand_task_planner.cpp
interfaces/
robot_arm.h
dexterous_hand.h
tool_modbus_bus.h
drivers/
realman/realman_robot_arm.h
realman/realman_robot_arm.cpp
realman/realman_tool_modbus_bus.h
realman/realman_tool_modbus_bus.cpp
linkerhand/o6_modbus_hand.h
linkerhand/o6_modbus_hand.cpp
linkerhand/o6_sdk_hand.h
linkerhand/o6_sdk_hand.cpp
common/
result.h
robot_types.h
hand_types.h
```
依赖方向固定为:
```text
main/applications -> core -> modules -> interfaces <- drivers
```
### 1.1 应用层
应用层只表达任务顺序,例如:
- `runMoveArmThenGraspTask()`
- `runArmSwingWithO6GraspTask()`
应用层禁止直接包含或调用:
- `rm_interface.h`
- `LinkerHandApi.h`
- 串口 API
- TCP Socket
- Modbus 寄存器读写
- RM_API2 原生函数
### 1.2 Core 层
Core 层负责流程编排、生命周期和安全入口。
建议核心类:
- `CoordinatedArmHandController`
- `MotionSafetyGuard`
职责:
- 组合机械臂动作和 O6 动作;
- 在运动前做速度、幅度、步长限制;
- 遇到失败时简单记录错误并停止当前流程;
- 不直接依赖 RM SDK 或 LinkerHand SDK。
### 1.3 Modules 层
Modules 层负责可测试的业务语义。
建议模块:
- `ArmMotionService`
- `moveArmToJointTarget()`
- `moveArmBySmallJointDelta()`
- `returnArmToRecordedStartPose()`
- `O6GraspService`
- `openO6HandToSafePose()`
- `moveO6HandToPreGraspPose()`
- `closeO6HandForPinchGrasp()`
- `holdO6HandForObjectTransfer()`
- `ArmHandTaskPlanner`
- 生成最小抓取任务的步骤列表;
- 不访问任何真实硬件。
### 1.4 Interfaces 层
接口层定义抽象能力,不写硬件实现。
已有机械臂接口可以继续沿用:
- `IRobotArm`
建议新增:
- `IDexterousHand`
- `IToolModbusBus`
示意职责:
```text
IDexterousHand
connect()
disconnect()
readIdentity()
moveToJointPose()
openToSafePose()
IToolModbusBus
configureToolPowerAndModbus()
readInputRegisters()
writeRegisters()
writeSingleRegister()
```
### 1.5 Drivers 层
Drivers 层承接真实 SDK、通信协议和寄存器细节。
建议实现:
- `RealManRobotArm`
- 继续包装 `rm_init``rm_create_robot_arm``rm_movej``rm_movel` 等 RM_API2 调用。
- `RealManToolModbusBus`
- 包装 RM75 工具端电源和工具端 Modbus。
- 包含 `rm_set_tool_voltage(handle, 3)`
- 包含 `rm_set_modbus_mode(handle, 1, 115200, 10)`
- 包含 `rm_read_multiple_input_registers``rm_write_registers``rm_write_single_register`
- `O6ModbusHand`
- 通过 `IToolModbusBus` 操作 O6。
- 管理 O6 右手 ID `0x27`
- 管理身份寄存器 `30..35`
- 管理 6 维关节姿态寄存器。
- `O6SdkHand`
- 可选保留,用于 PC USB/CAN 直连基线。
- 直接包装 `LinkerHandApi`
## 2. 当前工程与目标架构的差距
### 2.1 已经符合目标架构的部分
`RM/` 子工程已经有比较清晰的机械臂分层:
- `IRobotArm` 已把机械臂能力抽象出来;
- `RealManRobotArm` 已把 RM_API2 细节压在 driver 层;
- `JsonCommandRunner` 已经只依赖 `IRobotArm`,并负责运动安全校验;
- `Result<T>``Pose``ArmState``ForceData` 已经具备公共类型雏形。
这些内容应复用,而不是重写。
### 2.2 主要差距
1. O6 还没有接口层。
当前根目录 `src/main.cpp` 直接构造 `LinkerHandApi` 并执行抓取流程。它把应用逻辑、O6 姿态、力传感读取和硬件 API 混在入口文件里。
2. RM75 工具端 Modbus 还没有 driver 层。
当前 `tools/realman_tool_modbus_o6_*` 里直接调用 RM_API2 的工具端电源、Modbus 模式和寄存器读写函数。这些逻辑应该进入 `RealManToolModbusBus`
3. 机械臂和手部协调逻辑还在临时工具程序里。
`tools/realman_tool_modbus_o6_grasp_with_arm_swing.cpp` 同时包含:
- 参数解析;
- RM SDK 初始化;
- 工具端 RS485 配置;
- O6 寄存器写入;
- J2/J3 摆动目标生成;
- 机械臂运动等待;
- 手部抓取动作序列。
这些职责需要拆到 application、core、modules、drivers。
4. CMake 形态尚未统一。
根目录 CMake 同时管理 LinkerHand 示例、RealMan 示例、硬件测试;`RM/` 又有较干净的独立子工程。后续应选择一个稳定边界,避免正式控制栈和临时 demo 混在一起。
5. 手部公共类型缺失。
目前只有机械臂公共类型,没有 `O6JointPose``O6HandIdentity``O6HandConfig``DexterousHandState` 等手部语义类型。
## 3. 最小可行重构计划
### Phase 1: 先建立接口和类型边界
目标:不搬业务代码,只建立未来代码该放的位置。
新增:
- `IDexterousHand`
- `IToolModbusBus`
- `O6JointPose`
- `O6HandConfig`
- `O6HandIdentity`
约束:
- 不引入未确认的新厂商 API。
- 不自动运行真实硬件。
- 不改现有 demo 行为。
验收标准:
- 新增接口可被 fake 实现用于单元测试。
- 应用层未来不需要包含 RM SDK 或 LinkerHand SDK 头文件。
### Phase 2: 抽出 RM75 工具端 Modbus driver
目标:把 RM75 工具端 RS485 访问从工具程序中移出。
新增:
- `RealManToolModbusBus`
迁移内容:
- `rm_set_tool_voltage(handle, 3)`
- `rm_set_modbus_mode(handle, 1, 115200, 10)`
- `rm_get_tool_RS485_mode`
- `rm_read_multiple_input_registers`
- `rm_write_registers`
- `rm_write_single_register`
验收标准:
- 工具端 Modbus 配置只存在于 driver 层。
- O6 driver 只依赖 `IToolModbusBus`
### Phase 3: 抽出 O6 Modbus 手部 driver
目标:把 O6 寄存器语义从工具程序移到 driver。
新增:
- `O6ModbusHand`
最小支持:
- `readIdentity()`
- `moveToJointPose()`
- `openToSafePose()`
- `moveToPreGraspPose()`
- `closeForPinchGrasp()`
约束:
- 先保留右手 ID `0x27` 作为默认配置。
- 保留寄存器地址 `30..35` 和 6 维关节姿态语义。
- 写失败时简单返回错误,不做复杂重试。
### Phase 4: 抽出协调控制和任务流程
目标:让应用层只表达“任务步骤”,不碰硬件细节。
新增:
- `ArmMotionService`
- `O6GraspService`
- `CoordinatedArmHandController`
- `MoveArmThenGraspTask`
从工具程序迁移:
- J2/J3 小幅摆动目标生成;
- pre-grasp / grasp / hold / safe-open 姿态;
- 机械臂动作启动后写手部姿态;
- 到位等待;
- 失败时停止机械臂并返回错误。
MVP 失败策略:
- 打印错误;
- 停止当前动作;
- 返回失败;
- 不做复杂恢复、不做自动重试。
### Phase 5: 增加无硬件单元测试
优先测试:
- O6 姿态长度校验;
- 抓取姿态生成;
- 安全限幅;
- fake arm + fake hand 的调用顺序;
- 失败时是否停止流程。
不自动测试:
- 真实机械臂运动;
- 真实 O6 抓取;
- 长时间力控;
- 需要人工监护的动作。
### Phase 6: 收敛 CMake
短期策略:
- 保留现有根目录目标和 `RM/` 子工程;
- 新增一个小的控制库,例如 `rm_o6_control_core`
- 不一次性重排整个仓库。
后续策略:
- 若 MVP 跑通,再决定是否把 `RM/` 子工程并入统一 `control/` 目录;
- 或继续保持 `RM/` 独立,只把 O6 协调控制作为上层应用库。
## 4. 推荐第一轮落地范围
确认后建议只做以下最小修改:
1. 新增 `IDexterousHand``IToolModbusBus` 和 O6 公共类型;
2. 新增 `RealManToolModbusBus` 骨架;
3. 不迁移工具程序;
4. 不改现有 demo;
5. 增加 fake 测试,验证接口可用。
这样第一轮风险最低,并能立即建立正确边界。
## 5. 当前硬件事实约束
当前 RM75 + O6 调试结论会影响架构实现顺序:
- PC USB-RS485 直连 O6 已验证可用;
- O6 右手 Modbus ID 为 `0x27`
- 直连路径可读取身份寄存器 `30..35`
- RM75 末端工具端 24V 和 Modbus 模式配置可成功;
- RM75 末端 RS485 到 O6 目前仍无回包,常见返回为 `-2``write_state:false`
- 该现象更像物理链路、A/B、共地或接线位置问题,不应优先继续堆寄存器编码逻辑。
因此软件重构应先把边界建好,但真实末端 RS485 动作验证仍要等硬件链路确认后再执行。
...@@ -19,6 +19,12 @@ ...@@ -19,6 +19,12 @@
| `toolset_example.cpp` | 交互式工具集,演示所有 API 功能 | 所有型号 | 中级 | | `toolset_example.cpp` | 交互式工具集,演示所有 API 功能 | 所有型号 | 中级 |
| `o6_async_reader.cpp` | O6 后台采集示例,主线程只消费快照 | O6/L6 | 中级 | | `o6_async_reader.cpp` | O6 后台采集示例,主线程只消费快照 | O6/L6 | 中级 |
| `action_group_show_l10.cpp` | L10 型号的动作组演示 | L10 | 初级 | | `action_group_show_l10.cpp` | L10 型号的动作组演示 | L10 | 初级 |
| `realman_minimal_demo.cpp` | 睿尔曼机械臂最小连接与版本/信息读取 Demo,不发送运动指令 | RealMan 机械臂 | 初级 |
| `realman_slow_motion_demo.cpp` | 睿尔曼机械臂低速小幅关节往返运动 Demo | RealMan 机械臂 | 初级 |
| `realman_joint_pose_config_demo.cpp` | 睿尔曼机械臂源码参数调姿态 Demo | RealMan 机械臂 | 初级 |
| `realman_metrics_test.cpp` | 睿尔曼机械臂六维力/力矩、关节电流和低速运动指标测试 | RealMan 机械臂 | 中级 |
| `realman_trajectory_record.cpp` | 睿尔曼机械臂关节角/TCP 位姿 CSV 录制,只读不运动 | RealMan 机械臂 | 初级 |
| `realman_trajectory_replay.cpp` | 睿尔曼机械臂 CSV 轨迹低速逐点回放,默认 dry-run | RealMan 机械臂 | 中级 |
--- ---
...@@ -43,6 +49,12 @@ make ...@@ -43,6 +49,12 @@ make
./toolset_example ./toolset_example
./o6_async_reader ./o6_async_reader
./action_group_show_l10 ./action_group_show_l10
./realman_minimal_demo 192.168.1.18 8080
./realman_slow_motion_demo 192.168.1.18 8080 1000 2 5 10
./realman_joint_pose_config_demo
./realman_metrics_test
./realman_trajectory_record
./realman_trajectory_replay reports/realman_trajectories/demo.csv dry-run
``` ```
### 方法三:直接编译示例 ### 方法三:直接编译示例
...@@ -61,10 +73,133 @@ g++ -std=c++11 examples/o6_async_reader.cpp -I./include \ ...@@ -61,10 +73,133 @@ g++ -std=c++11 examples/o6_async_reader.cpp -I./include \
./o6_async_reader ./o6_async_reader
``` ```
---
## 示例详细说明 ## 示例详细说明
### realman_minimal_demo.cpp
**功能描述**
睿尔曼 RealMan RM_API2 最小 C++ 示例,用于验证项目内置的机械臂 SDK、动态库、网络连接和控制器通信是否可用。
**主要功能**:
1. 初始化 RM_API2
2. 连接机械臂控制器
3. 打印 API 版本
4. 读取机械臂软件版本信息
5. 读取机械臂基础信息
6. 断开连接并销毁 SDK 线程
**安全边界**:
- 不发送运动指令
- 默认只读信息
- 支持通过命令行覆盖 IP、端口和超时时间
**使用示例**:
```bash
./realman_minimal_demo
./realman_minimal_demo 192.168.1.18 8080
./realman_minimal_demo 192.168.1.18 8080 1500
```
详细说明见 [睿尔曼机械臂最小 Demo](../docs/REALMAN_MINIMAL_DEMO.md)
### realman_slow_motion_demo.cpp
**功能描述**
睿尔曼 RealMan RM_API2 低速小幅运动示例,用于确认机械臂可以稳定、缓慢地执行关节空间运动。
**默认动作**:
1. 读取当前关节角
2. 第 2 关节低速 `+5°`
3. 回到起始关节角
4. 第 2 关节低速 `-5°`
5. 回到起始关节角
**安全边界**:
- 默认速度比例 `10%`
- 默认角度幅度 `5°`
- 参数校验限制速度不超过 `20%`,幅度不超过 `5°`
- 每一步阻塞等待完成,失败时发送停止指令
**使用示例**:
```bash
./realman_slow_motion_demo
./realman_slow_motion_demo 192.168.1.18 8080 1000 2 5 10
./realman_slow_motion_demo 192.168.1.18 8080 1000 2 3 5
```
详细说明见 [睿尔曼机械臂低速运动 Demo 操作手册](../docs/REALMAN_SLOW_MOTION_MANUAL.md)
### realman_joint_pose_config_demo.cpp
**功能描述**
睿尔曼 RealMan RM_API2 源码参数调姿态示例。适合在 CLion 中直接修改 C++ 文件顶部参数,然后重新构建运行。
**主要修改区**:
```cpp
constexpr TargetMode kTargetMode = TargetMode::kRelativeJointOffset;
constexpr std::array<float, ARM_DOF> kRelativeJointOffsetsDeg = {
0.0F, 5.0F, 0.0F, 0.0F, 0.0F, 0.0F, 0.0F,
};
constexpr int kSpeedPercent = 10;
```
**安全边界**:
- 默认相对当前姿态执行 `J2 +5°`
- 默认速度比例 `10%`
- 单关节单次变化超过 `10°` 会拒绝执行
- 运行前有 5 秒安全倒计时
- 可设置 `kDryRunOnly = true` 只预览目标,不发送运动指令
**使用示例**:
```bash
./realman_joint_pose_config_demo
```
详细说明见 [睿尔曼机械臂源码参数调姿态 Demo](../docs/REALMAN_JOINT_POSE_CONFIG_DEMO.md)
### realman_metrics_test.cpp
**功能描述**
睿尔曼 RealMan RM_API2 指标测试示例。程序会采集六维力/力矩、关节电流、电压、温度、控制器状态,并执行一次低速小幅 J2 往返运动,用于评估基础运动到位误差和负载代理量变化。
**默认测试流程**:
1. 静态采样
2. 5 秒安全倒计时
3. J2 `+3°`
4. 返回起始姿态
5. J2 `-3°`
6. 返回起始姿态
7. 结束静态采样
8. 写入 CSV 和 Markdown 报告
**输出文件**:
```bash
reports/REALMAN_METRICS_TEST_RESULT.md
reports/realman_metrics_latest.csv
```
**安全边界**:
- 默认速度比例 `5%`
- 默认运动幅度 `3°`
- 程序限制速度不超过 `10%`,幅度不超过 `5°`
- 不自动启用接触式力位混合控制
详细说明见 [睿尔曼机械臂力/力矩与关节指标测试 README](../docs/REALMAN_METRICS_TEST_README.md)
### realman_trajectory_record.cpp / realman_trajectory_replay.cpp
**功能描述**
睿尔曼 RealMan RM_API2 最小轨迹录制/回放方案。录制程序读取 `J1..J7` 关节角和 TCP 位姿写入 CSV;回放程序读取 CSV,默认只做 dry-run 校验,显式传 `execute` 才低速逐点调用 `rm_movej`
**使用示例**:
```bash
./realman_trajectory_record reports/realman_trajectories/demo.csv 15 100
./realman_trajectory_replay reports/realman_trajectories/demo.csv dry-run
./realman_trajectory_replay reports/realman_trajectories/demo.csv execute 5 5 5
```
详细说明见 [RM75 轨迹录制与回放](../docs/REALMAN_TRAJECTORY_RECORD_REPLAY.md)
### toolset_example.cpp ### toolset_example.cpp
**功能描述** **功能描述**
......
#include "rm_interface.h"
#include <array>
#include <chrono>
#include <cmath>
#include <cstdarg>
#include <cstdio>
#include <iomanip>
#include <iostream>
#include <thread>
namespace {
// ===== 用户主要修改区:在 CLion 里改这里,然后重新 Build/Run =====
constexpr const char *kRobotIp = "192.168.1.18";
constexpr int kRobotPort = 8080;
constexpr int kTimeoutMs = 1000;
enum class TargetMode {
kRelativeJointOffset, // 基于当前关节角做偏移,更适合调试
kAbsoluteJointAngles, // 直接移动到指定绝对关节角
};
constexpr TargetMode kTargetMode = TargetMode::kRelativeJointOffset;
// 7 自由度关节角,单位:度。RM75 使用 J1..J7。
// 相对模式:目标 = 当前关节角 + kRelativeJointOffsetsDeg。
constexpr std::array<float, ARM_DOF> kRelativeJointOffsetsDeg = {
0.0F, 5.0F, 0.0F, 0.0F, 0.0F, 0.0F, 0.0F,
};
// 绝对模式:目标 = kAbsoluteTargetJointsDeg。
// 首次使用绝对模式前,建议先把程序打印的 Current joints 复制到这里,再小幅修改。
constexpr std::array<float, ARM_DOF> kAbsoluteTargetJointsDeg = {
0.0F, 0.0F, 0.0F, 0.0F, 0.0F, 0.0F, 0.0F,
};
constexpr int kSpeedPercent = 10;
constexpr bool kReturnToStartAfterMove = false;
constexpr bool kDryRunOnly = false;
// 安全限幅。需要更大动作时,先在低速、小幅验证后再调大。
constexpr float kMaxSingleJointDeltaDeg = 10.0F;
constexpr int kMaxAllowedSpeedPercent = 20;
constexpr int kSafetyCountdownSeconds = 5;
// ===== 用户主要修改区结束 =====
void apiLogCallback(const char *message, va_list args) {
if (message == nullptr) {
return;
}
char buffer[1024] = {};
std::vsnprintf(buffer, sizeof(buffer), message, args);
std::cerr << "[rm_api] " << buffer << '\n';
}
int destroyApi() {
#if defined(REALMAN_RM_API_HAS_RM_DESTROY)
return rm_destroy();
#else
return rm_destory();
#endif
}
void cleanup(rm_robot_handle *handle) {
if (handle != nullptr) {
const int delete_result = rm_delete_robot_arm(handle);
if (delete_result != 0) {
std::cerr << "rm_delete_robot_arm failed, error code: " << delete_result << '\n';
}
}
const int destroy_result = destroyApi();
if (destroy_result != 0) {
std::cerr << "RealMan API destroy failed, error code: " << destroy_result << '\n';
}
}
void printJoints(const char *label, const float joints[ARM_DOF], int dof) {
std::cout << label;
for (int i = 0; i < dof; ++i) {
std::cout << (i == 0 ? " [" : ", ")
<< "J" << (i + 1) << "=" << std::fixed << std::setprecision(3) << joints[i];
}
std::cout << "] deg\n";
}
void printPose(const char *label, const rm_pose_t &pose) {
std::cout << label
<< " position[m]=(" << std::fixed << std::setprecision(4)
<< pose.position.x << ", " << pose.position.y << ", " << pose.position.z
<< "), euler[rad]=(" << pose.euler.rx << ", " << pose.euler.ry << ", " << pose.euler.rz
<< ")\n";
}
bool readArmState(rm_robot_handle *handle, rm_current_arm_state_t *state) {
if (handle == nullptr || state == nullptr) {
return false;
}
const int result = rm_get_current_arm_state(handle, state);
if (result != 0) {
std::cerr << "rm_get_current_arm_state failed, error code: " << result << '\n';
return false;
}
return true;
}
bool allFinite(const float joints[ARM_DOF], int dof) {
for (int i = 0; i < dof; ++i) {
if (!std::isfinite(joints[i])) {
std::cerr << "Joint J" << (i + 1) << " is not finite: " << joints[i] << '\n';
return false;
}
}
return true;
}
bool validateSpeed() {
if (kSpeedPercent < 1 || kSpeedPercent > kMaxAllowedSpeedPercent) {
std::cerr << "kSpeedPercent must be in range 1.." << kMaxAllowedSpeedPercent
<< ". Current value: " << kSpeedPercent << '\n';
return false;
}
return true;
}
bool validateTargetDelta(const float current[ARM_DOF], const float target[ARM_DOF], int dof) {
if (!allFinite(current, dof) || !allFinite(target, dof)) {
return false;
}
bool has_motion = false;
for (int i = 0; i < dof; ++i) {
const float delta = target[i] - current[i];
if (std::fabs(delta) > 0.001F) {
has_motion = true;
}
if (std::fabs(delta) > kMaxSingleJointDeltaDeg) {
std::cerr << "Refuse to move J" << (i + 1) << " by " << delta
<< " deg. Limit is +/-" << kMaxSingleJointDeltaDeg << " deg.\n";
return false;
}
}
if (!has_motion) {
std::cerr << "Target equals current joints; no motion will be sent.\n";
return false;
}
return true;
}
void buildTargetJoints(const float current[ARM_DOF], float target[ARM_DOF], int dof) {
for (int i = 0; i < dof; ++i) {
if (kTargetMode == TargetMode::kRelativeJointOffset) {
target[i] = current[i] + kRelativeJointOffsetsDeg[static_cast<size_t>(i)];
} else {
target[i] = kAbsoluteTargetJointsDeg[static_cast<size_t>(i)];
}
}
for (int i = dof; i < ARM_DOF; ++i) {
target[i] = current[i];
}
}
void countdownBeforeMotion() {
std::cout << "\nSafety countdown before motion. Clear workspace and keep E-stop reachable.\n";
for (int second = kSafetyCountdownSeconds; second > 0; --second) {
std::cout << "Moving in " << second << "...\n";
std::this_thread::sleep_for(std::chrono::seconds(1));
}
}
bool moveJointTarget(rm_robot_handle *handle,
const char *label,
const float target[ARM_DOF],
int dof) {
printJoints(label, target, dof);
if (kDryRunOnly) {
std::cout << "Dry run enabled. rm_movej was not sent.\n";
return true;
}
const int result = rm_movej(handle, target, kSpeedPercent, 0, 0, 1);
if (result != 0) {
std::cerr << "rm_movej failed, error code: " << result << '\n';
const int stop_result = rm_set_arm_stop(handle);
if (stop_result != 0) {
std::cerr << "rm_set_arm_stop failed, error code: " << stop_result << '\n';
}
return false;
}
rm_current_arm_state_t actual = {};
if (!readArmState(handle, &actual)) {
return false;
}
printJoints("Actual joints after move:", actual.joint, dof);
printPose("Actual TCP pose:", actual.pose);
return true;
}
} // namespace
int main() {
std::cout.setf(std::ios::unitbuf);
std::cerr.setf(std::ios::unitbuf);
if (!validateSpeed()) {
return 64;
}
rm_set_log_call_back(apiLogCallback, 2);
rm_set_timeout(kTimeoutMs);
const int init_result = rm_init(RM_TRIPLE_MODE_E);
if (init_result != 0) {
std::cerr << "rm_init failed, error code: " << init_result << '\n';
return 1;
}
std::cout << "RealMan API version: " << rm_api_version() << '\n'
<< "Connecting to " << kRobotIp << ':' << kRobotPort << "...\n";
rm_robot_handle *handle = rm_create_robot_arm(kRobotIp, kRobotPort);
if (handle == nullptr || handle->id < 0) {
std::cerr << "Failed to connect to RealMan arm.\n";
cleanup(handle);
return 2;
}
rm_robot_info_t robot_info = {};
int result = rm_get_robot_info(handle, &robot_info);
if (result != 0) {
std::cerr << "rm_get_robot_info failed, error code: " << result << '\n';
cleanup(handle);
return 3;
}
const int dof = static_cast<int>(robot_info.arm_dof);
if (dof <= 0 || dof > ARM_DOF) {
std::cerr << "Invalid arm DOF reported by controller: " << dof << '\n';
cleanup(handle);
return 4;
}
rm_current_arm_state_t start_state = {};
if (!readArmState(handle, &start_state)) {
cleanup(handle);
return 5;
}
float target[ARM_DOF] = {};
buildTargetJoints(start_state.joint, target, dof);
std::cout << "Connected. Arm DOF: " << dof << ", speed: " << kSpeedPercent << "%\n"
<< "Target mode: "
<< (kTargetMode == TargetMode::kRelativeJointOffset ? "relative joint offset" : "absolute joint angles")
<< '\n';
printJoints("Current joints:", start_state.joint, dof);
printPose("Current TCP pose:", start_state.pose);
printJoints("Planned target joints:", target, dof);
if (!validateTargetDelta(start_state.joint, target, dof)) {
cleanup(handle);
return 6;
}
countdownBeforeMotion();
if (!moveJointTarget(handle, "Move target:", target, dof)) {
cleanup(handle);
return 7;
}
if (kReturnToStartAfterMove) {
if (!moveJointTarget(handle, "Return to start:", start_state.joint, dof)) {
cleanup(handle);
return 8;
}
}
cleanup(handle);
std::cout << "Joint pose config demo finished successfully.\n";
return 0;
}
This diff is collapsed.
#include "rm_interface.h"
#include <cerrno>
#include <climits>
#include <cstdarg>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <string>
namespace {
constexpr const char *kDefaultRobotIp = "192.168.1.18";
constexpr int kDefaultRobotPort = 8080;
constexpr int kDefaultTimeoutMs = 1000;
struct Config {
std::string ip = kDefaultRobotIp;
int port = kDefaultRobotPort;
int timeout_ms = kDefaultTimeoutMs;
};
void printUsage(const char *program_name) {
std::cout << "Usage: " << program_name << " [robot_ip] [port] [timeout_ms]\n"
<< "\n"
<< "Examples:\n"
<< " " << program_name << "\n"
<< " " << program_name << " 192.168.1.18 8080\n"
<< " " << program_name << " 192.168.1.18 8080 1500\n"
<< "\n"
<< "This minimal demo only connects to the RealMan arm, reads version/info,\n"
<< "and disconnects. It does not send motion commands.\n";
}
bool parseInt(const char *value, int *out) {
if (value == nullptr || out == nullptr || value[0] == '\0') {
return false;
}
errno = 0;
char *end = nullptr;
const long parsed = std::strtol(value, &end, 10);
if (errno != 0 || end == value || *end != '\0' || parsed < INT_MIN || parsed > INT_MAX) {
return false;
}
*out = static_cast<int>(parsed);
return true;
}
bool parseArgs(int argc, char **argv, Config *config) {
if (config == nullptr) {
return false;
}
if (argc > 1) {
const std::string first_arg = argv[1] == nullptr ? "" : argv[1];
if (first_arg == "-h" || first_arg == "--help") {
printUsage(argv[0]);
std::exit(0);
}
config->ip = first_arg;
}
if (argc > 2 && !parseInt(argv[2], &config->port)) {
std::cerr << "Invalid port: " << argv[2] << "\n";
return false;
}
if (argc > 3 && !parseInt(argv[3], &config->timeout_ms)) {
std::cerr << "Invalid timeout_ms: " << argv[3] << "\n";
return false;
}
if (argc > 4) {
std::cerr << "Too many arguments.\n";
return false;
}
if (config->ip.empty()) {
std::cerr << "Robot IP must not be empty.\n";
return false;
}
if (config->port <= 0 || config->port > 65535) {
std::cerr << "Port must be in range 1..65535.\n";
return false;
}
if (config->timeout_ms <= 0) {
std::cerr << "timeout_ms must be greater than 0.\n";
return false;
}
return true;
}
void apiLogCallback(const char *message, va_list args) {
if (message == nullptr) {
return;
}
char buffer[1024] = {};
std::vsnprintf(buffer, sizeof(buffer), message, args);
std::cerr << "[rm_api] " << buffer << '\n';
}
const char *armModelName(rm_robot_arm_model_e model) {
switch (model) {
case RM_MODEL_RM_65_E:
return "RM_65";
case RM_MODEL_RM_75_E:
return "RM_75";
case RM_MODEL_RM_63_I_E:
return "RML_63I";
case RM_MODEL_RM_63_II_E:
return "RML_63II";
case RM_MODEL_RM_63_III_E:
return "RML_63III";
case RM_MODEL_ECO_65_E:
return "ECO_65";
case RM_MODEL_ECO_62_E:
return "ECO_62";
case RM_MODEL_GEN_72_E:
return "GEN_72";
case RM_MODEL_ECO_63_E:
return "ECO_63";
case RM_MODEL_UNIVERSAL_E:
return "UNIVERSAL";
default:
return "UNKNOWN";
}
}
const char *forceTypeName(rm_force_type_e type) {
switch (type) {
case RM_MODEL_RM_B_E:
return "Standard";
case RM_MODEL_RM_ZF_E:
return "1D force";
case RM_MODEL_RM_SF_E:
return "6D force";
case RM_MODEL_RM_ISF_E:
return "Integrated 6D force";
default:
return "Unknown";
}
}
void printSoftwareInfo(const rm_arm_software_version_t &info) {
std::cout << "\n================ RealMan Arm Software Info ================\n"
<< "Product Version: " << info.product_version << '\n'
<< "Robot Controller Version: " << info.robot_controller_version << '\n'
<< "Algorithm Version: " << info.algorithm_info.version << '\n'
<< "Control Layer Version: " << info.ctrl_info.version << '\n'
<< "Control Layer Build Time: " << info.ctrl_info.build_time << '\n'
<< "Dynamics Model Version: " << info.dynamic_info.model_version << '\n'
<< "Planning Layer Version: " << info.plan_info.version << '\n'
<< "Planning Layer Build Time: " << info.plan_info.build_time << '\n'
<< "===========================================================\n";
}
void printRobotInfo(const rm_robot_info_t &info) {
std::cout << "\n================ RealMan Arm Basic Info ====================\n"
<< "Arm DOF: " << static_cast<int>(info.arm_dof) << '\n'
<< "Arm Model: " << armModelName(info.arm_model)
<< " (" << static_cast<int>(info.arm_model) << ")\n"
<< "Force Type: " << forceTypeName(info.force_type)
<< " (" << static_cast<int>(info.force_type) << ")\n"
<< "Controller Version: " << static_cast<int>(info.robot_controller_version) << '\n'
<< "===========================================================\n";
}
int destroyApi() {
#if defined(REALMAN_RM_API_HAS_RM_DESTROY)
return rm_destroy();
#else
return rm_destory();
#endif
}
} // namespace
int main(int argc, char **argv) {
Config config;
if (!parseArgs(argc, argv, &config)) {
printUsage(argv[0]);
return 64;
}
rm_set_log_call_back(apiLogCallback, 2);
rm_set_timeout(config.timeout_ms);
const int init_result = rm_init(RM_TRIPLE_MODE_E);
if (init_result != 0) {
std::cerr << "rm_init failed, error code: " << init_result << '\n';
return 1;
}
int exit_code = 0;
rm_robot_handle *handle = nullptr;
std::cout << "RealMan API version: " << rm_api_version() << '\n'
<< "Connecting to " << config.ip << ':' << config.port
<< " with timeout " << config.timeout_ms << " ms...\n";
handle = rm_create_robot_arm(config.ip.c_str(), config.port);
if (handle == nullptr || handle->id < 0) {
std::cerr << "Failed to connect to RealMan arm. Check IP, port, network, and robot power state.\n";
exit_code = 2;
if (handle != nullptr) {
rm_delete_robot_arm(handle);
handle = nullptr;
}
destroyApi();
return exit_code;
}
std::cout << "Robot handle created, id: " << handle->id << '\n';
rm_arm_software_version_t software_info = {};
const int software_result = rm_get_arm_software_info(handle, &software_info);
if (software_result == 0) {
printSoftwareInfo(software_info);
} else {
std::cerr << "rm_get_arm_software_info failed, error code: " << software_result << '\n';
exit_code = 3;
}
rm_robot_info_t robot_info = {};
const int robot_info_result = rm_get_robot_info(handle, &robot_info);
if (robot_info_result == 0) {
printRobotInfo(robot_info);
} else {
std::cerr << "rm_get_robot_info failed, error code: " << robot_info_result << '\n';
if (exit_code == 0) {
exit_code = 4;
}
}
const int delete_result = rm_delete_robot_arm(handle);
if (delete_result != 0) {
std::cerr << "rm_delete_robot_arm failed, error code: " << delete_result << '\n';
if (exit_code == 0) {
exit_code = 5;
}
}
const int destroy_result = destroyApi();
if (destroy_result != 0) {
std::cerr << "RealMan API destroy failed, error code: " << destroy_result << '\n';
if (exit_code == 0) {
exit_code = 6;
}
}
if (exit_code == 0) {
std::cout << "RealMan minimal demo finished successfully.\n";
}
return exit_code;
}
This diff is collapsed.
This diff is collapsed.
#include "realman_trajectory_common.hpp"
#include <thread>
namespace {
using realman_trajectory::RobotConnectionConfig;
using realman_trajectory::RobotSession;
using realman_trajectory::TrajectoryPoint;
constexpr int kDefaultDurationSeconds = 10;
constexpr int kDefaultSamplePeriodMs = 100;
constexpr int kMinimumSamplePeriodMs = 50;
constexpr int kMaximumDurationSeconds = 3600;
struct RecordConfig {
std::string output_csv_path;
int duration_seconds = kDefaultDurationSeconds;
int sample_period_ms = kDefaultSamplePeriodMs;
RobotConnectionConfig robot;
};
void printUsage(const char *program_name) {
std::cout << "Usage: " << program_name
<< " [output_csv] [duration_seconds] [sample_period_ms] [robot_ip] [port] [timeout_ms]\n"
<< "\n"
<< "Examples:\n"
<< " " << program_name << "\n"
<< " " << program_name << " reports/realman_trajectories/demo.csv 15 100\n"
<< "\n"
<< "This program only records current joints and TCP pose. It does not send motion commands.\n";
}
bool parseRecordArguments(int argc, char **argv, RecordConfig *config) {
if (config == nullptr) {
return false;
}
if (argc > 1 && (std::string(argv[1]) == "-h" || std::string(argv[1]) == "--help")) {
printUsage(argv[0]);
std::exit(0);
}
if (argc > 1) {
config->output_csv_path = argv[1];
}
if (argc > 2 && !realman_trajectory::parseIntArgument(argv[2], &config->duration_seconds)) {
std::cerr << "Invalid duration_seconds: " << argv[2] << '\n';
return false;
}
if (argc > 3 && !realman_trajectory::parseIntArgument(argv[3], &config->sample_period_ms)) {
std::cerr << "Invalid sample_period_ms: " << argv[3] << '\n';
return false;
}
if (argc > 4) {
config->robot.robot_ip = argv[4];
}
if (argc > 5 && !realman_trajectory::parseIntArgument(argv[5], &config->robot.robot_port)) {
std::cerr << "Invalid port: " << argv[5] << '\n';
return false;
}
if (argc > 6 && !realman_trajectory::parseIntArgument(argv[6], &config->robot.timeout_ms)) {
std::cerr << "Invalid timeout_ms: " << argv[6] << '\n';
return false;
}
if (argc > 7) {
std::cerr << "Too many arguments.\n";
return false;
}
if (config->output_csv_path.empty()) {
config->output_csv_path = realman_trajectory::defaultRecordedTrajectoryPath();
}
return true;
}
bool validateRecordConfig(const RecordConfig &config) {
if (config.output_csv_path.empty()) {
std::cerr << "output_csv must not be empty.\n";
return false;
}
if (config.duration_seconds <= 0 || config.duration_seconds > kMaximumDurationSeconds) {
std::cerr << "duration_seconds must be in range 1.." << kMaximumDurationSeconds << ".\n";
return false;
}
if (config.sample_period_ms < kMinimumSamplePeriodMs) {
std::cerr << "sample_period_ms must be at least " << kMinimumSamplePeriodMs << ".\n";
return false;
}
if (config.robot.robot_port <= 0 || config.robot.robot_port > 65535) {
std::cerr << "port must be in range 1..65535.\n";
return false;
}
return config.robot.timeout_ms > 0;
}
TrajectoryPoint makeTrajectoryPoint(long long elapsed_ms, const rm_current_arm_state_t &state) {
TrajectoryPoint point = {};
point.elapsed_ms = elapsed_ms;
point.joint_deg = realman_trajectory::copyJointArray(state.joint);
point.tcp_pose = state.pose;
return point;
}
bool recordTrajectoryCsv(rm_robot_handle *handle, const RecordConfig &config, int dof) {
std::ofstream output(config.output_csv_path);
if (!output) {
std::cerr << "Failed to open output CSV: " << config.output_csv_path << '\n';
return false;
}
output << realman_trajectory::trajectoryCsvHeader() << '\n';
const auto start_time = std::chrono::steady_clock::now();
const int total_samples = (config.duration_seconds * 1000) / config.sample_period_ms + 1;
for (int sample_index = 0; sample_index < total_samples; ++sample_index) {
const auto target_time = start_time + std::chrono::milliseconds(sample_index * config.sample_period_ms);
std::this_thread::sleep_until(target_time);
rm_current_arm_state_t state = {};
if (!realman_trajectory::readCurrentArmState(handle, &state)) {
return false;
}
const auto now = std::chrono::steady_clock::now();
const long long elapsed_ms = std::chrono::duration_cast<std::chrono::milliseconds>(now - start_time).count();
const TrajectoryPoint point = makeTrajectoryPoint(elapsed_ms, state);
realman_trajectory::writeTrajectoryPointCsv(&output, sample_index, point);
if (sample_index == 0 || sample_index == total_samples - 1) {
std::cout << "Sample " << sample_index << " joints "
<< realman_trajectory::formatJoints(point.joint_deg, dof) << '\n';
}
}
std::cout << "Recorded " << total_samples << " samples to " << config.output_csv_path << '\n';
return true;
}
} // namespace
int main(int argc, char **argv) {
std::cout.setf(std::ios::unitbuf);
std::cerr.setf(std::ios::unitbuf);
RecordConfig config;
if (!parseRecordArguments(argc, argv, &config) || !validateRecordConfig(config)) {
printUsage(argv[0]);
return 64;
}
RobotSession session(config.robot);
if (!session.connect()) {
return 1;
}
int dof = 0;
if (!realman_trajectory::readAndValidateRobotDof(session.handle(), &dof)) {
return 2;
}
if (!recordTrajectoryCsv(session.handle(), config, dof)) {
return 3;
}
return 0;
}
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment