From e82a90237d99215eb464e3b196836b5e5f720e44 Mon Sep 17 00:00:00 2001 From: lgv Date: Tue, 17 Mar 2026 15:59:25 +0800 Subject: [PATCH] feat: add touch app --- .vscode/extensions.json | 6 + .vscode/launch.json | 32 + .vscode/settings.json | 12 + .vscode/tasks.json | 25 + cmvr-es/applications/CMakeLists.txt | 6 +- .../applications/include/touch_screen_app.h | 155 +++- cmvr-es/applications/src/touch_screen_app.cpp | 601 +++++++++++++- .../src/touch_screen_app_test.cpp | 146 ++-- .../dexhand_config/dexhand_config.pb.txt | 4 +- .../touch_screen_app_config.pb.txt | 123 +++ .../config_helper/include/config_helper.h | 11 + .../config_helper/include/config_setting.h | 3 +- .../config_helper/src/config_setting.cpp | 3 + cmvr-es/devices/dexhand/abstract_dexhand.h | 16 +- .../include/rh56dftp_dexhand.h | 1 + .../rh56dftp_dexhand/src/rh56dftp_dexhand.cpp | 25 + .../canopen/protocol/ti5_motor_tpdo1.cpp | 8 +- cmvr-es/devices/robot/abstract_robot.h | 8 +- .../humanoid_robot/include/humanoid_robot.h | 6 +- .../humanoid_robot/src/humanoid_robot.cpp | 14 +- .../src/humanoid_robot_test.cpp | 782 +++++++++--------- cmvr-es/service/CMakeLists.txt | 2 +- .../service/grpc/include/grpc_hlc_service.h | 11 +- .../service/grpc/src/grpc_hlc_client_test.cpp | 4 +- cmvr-es/service/grpc/src/grpc_hlc_service.cpp | 146 +++- data/plot1.png | Bin 57378 -> 62702 bytes .../touch_screen_app_config.proto | 122 +++ 27 files changed, 1670 insertions(+), 602 deletions(-) create mode 100644 .vscode/extensions.json create mode 100644 .vscode/launch.json create mode 100644 .vscode/settings.json create mode 100644 .vscode/tasks.json create mode 100644 cmvr-es/common/config/touch_screen_app_config/touch_screen_app_config.pb.txt create mode 100644 protos/cmvr/config/touch_screen_app_config/touch_screen_app_config.proto diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 00000000..c03d5b9f --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,6 @@ +{ + "recommendations": [ + "ms-vscode.cpptools", + "ms-vscode.cmake-tools" + ] +} diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..4c435193 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,32 @@ +{ + "configurations": [ + + { + "name": "C/C++: g++ 构建和调试活动文件", + "type": "cppdbg", + "request": "launch", + "program": "${fileDirname}/${fileBasenameNoExtension}", + "args": [], + "stopAtEntry": false, + "cwd": "${fileDirname}", + "environment": [], + "externalConsole": false, + "MIMode": "gdb", + "setupCommands": [ + { + "description": "为 gdb 启用整齐打印", + "text": "-enable-pretty-printing", + "ignoreFailures": true + }, + { + "description": "将反汇编风格设置为 Intel", + "text": "-gdb-set disassembly-flavor intel", + "ignoreFailures": true + } + ], + "preLaunchTask": "C/C++: g++ 生成活动文件", + "miDebuggerPath": "/usr/bin/gdb" + } + ], + "version": "2.0.0" +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..f2daafe2 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,12 @@ +{ + "cmake.configureOnOpen": true, + "cmake.buildDirectory": "${workspaceFolder}/cmake-build-debug", + "cmake.copyCompileCommands": "${workspaceFolder}/compile_commands.json", + "cmake.configureSettings": { + "CMAKE_EXPORT_COMPILE_COMMANDS": "ON" + }, + "C_Cpp.default.configurationProvider": "ms-vscode.cmake-tools", + "C_Cpp.default.compileCommands": "${workspaceFolder}/compile_commands.json", + "C_Cpp.default.cppStandard": "c++17", + "C_Cpp.default.intelliSenseMode": "linux-gcc-x64" +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 00000000..e9fb5f49 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,25 @@ +{ + "tasks": [ + { + "type": "cppbuild", + "label": "C/C++: g++ 生成活动文件", + "command": "/usr/bin/g++", + "args": [ + "-fdiagnostics-color=always", + "-g", + "${file}", + "-o", + "${fileDirname}/${fileBasenameNoExtension}" + ], + "options": { + "cwd": "${fileDirname}" + }, + "problemMatcher": [ + "$gcc" + ], + "group": "build", + "detail": "调试器生成的任务。" + } + ], + "version": "2.0.0" +} \ No newline at end of file diff --git a/cmvr-es/applications/CMakeLists.txt b/cmvr-es/applications/CMakeLists.txt index 7d4b7a1f..331fd67c 100644 --- a/cmvr-es/applications/CMakeLists.txt +++ b/cmvr-es/applications/CMakeLists.txt @@ -4,8 +4,12 @@ add_library(applications target_include_directories(applications PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) -target_link_libraries(applications PUBLIC +target_link_libraries(applications + PUBLIC cmvr_es::controller + cmvr_es::common + PRIVATE + cmvr_es::device_manager ) add_library(cmvr_es::applications ALIAS applications) diff --git a/cmvr-es/applications/include/touch_screen_app.h b/cmvr-es/applications/include/touch_screen_app.h index ad405585..b0fc5f9d 100644 --- a/cmvr-es/applications/include/touch_screen_app.h +++ b/cmvr-es/applications/include/touch_screen_app.h @@ -11,6 +11,7 @@ #include +#include "cmvr/config/touch_screen_app_config/touch_screen_app_config.pb.h" #include "controller/include/ibvs_controller.h" #include "devices/camera/abstract_camera.h" #include "devices/dexhand/abstract_dexhand.h" @@ -23,36 +24,37 @@ namespace cmvr::app { class TouchScreenApp { public: enum class Phase { - IDLE = 0, - ALIGNING, - ALIGN_REACHED, - TOUCHING, - DWELLING, - RETRACTING, - DONE, - FAILED + IDLE = 0, // 空闲,尚未开始任务。 + ALIGNING, // 视觉对准阶段:持续 IBVS 对齐目标点。 + ALIGN_REACHED, // 视觉对准已达到阈值,等待进入下一阶段。 + TOUCHING, // 前进触控阶段:沿设定方向向屏幕推进。 + DWELLING, // 已检测到接触,保持当前位置短暂停留。 + RETRACTING, // 回退阶段:沿设定回退方向离开屏幕。 + DONE, // 整个流程成功完成。 + FAILED // 流程失败并已停止。 }; enum class Status { - IDLE = 0, - NOT_INITIALIZED, - INVALID_CONFIG, - CONTROL_JOINT_MISMATCH, - ALIGN_WAITING_PERCEPTION, - ALIGN_WAITING_TRACK, - ALIGN_TARGET_SETUP_FAILED, - ALIGN_COMPUTE_FAILED, - ALIGN_TIMEOUT, - ALIGNING, - ALIGN_REACHED, - TOUCHING, - TOUCH_TRIGGERED, - TOUCH_TIMEOUT, - RETRACTING, - DONE, - STOPPED, - ROBOT_STATE_FAILED, - ROBOT_COMMAND_FAILED + IDLE = 0, // 空闲状态。 + NOT_INITIALIZED, // 尚未调用 init() 完成初始化。 + INVALID_CONFIG, // 配置非法,无法启动或应用参数。 + CONTROL_JOINT_MISMATCH, // 控制关节顺序与 IK 链不一致。 + ALIGN_WAITING_PERCEPTION, // 对准阶段等待相机/AprilTag 感知结果。 + ALIGN_WAITING_TRACK, // 对准阶段等待目标点跟踪恢复成功。 + ALIGN_TARGET_SETUP_FAILED,// 视觉目标设置失败,setTargetFromPointInTag 失败。 + ALIGN_COMPUTE_FAILED, // 对准阶段 IBVS 或 IK 计算失败。 + ALIGN_TIMEOUT, // 对准阶段超时仍未收敛。 + ALIGNING, // 正在执行视觉对准。 + ALIGN_REACHED, // 视觉对准完成。 + TOUCHING, // 正在向前触控。 + TACTILE_UNAVAILABLE, // 触觉数据不可用。 + TOUCH_TRIGGERED, // 已检测到接触触发。 + TOUCH_FORWARD_TIMEOUT, // 前进触控时间到,但未触发接触。 + RETRACTING, // 正在回退离开屏幕。 + DONE, // 流程成功完成。 + STOPPED, // 被外部 stop() 主动停止。 + ROBOT_STATE_FAILED, // 读取机器人状态失败。 + ROBOT_COMMAND_FAILED // 向机器人下发控制命令失败。 }; enum class TactileRegion { @@ -64,80 +66,141 @@ public: }; struct Options { + // 是否在触控流程开始前先回到指定初始关节位姿。 + bool move_to_init_position_before_start{false}; + // 是否在触控流程结束(DONE/FAILED)后回到指定初始关节位姿。 + bool move_to_init_position{false}; + // 初始关节位姿目标,在前置回位或结束后回位开启时使用。 + std::vector init_joint_positions{}; + // 回到初始位姿时的 moveJ 主导速度,单位 rad/s。 + double init_movej_vel{1.0}; + // 回到初始位姿时的 moveJ 主导加速度,单位 rad/s^2。 + double init_movej_acc{2.0}; + // IBVS / IK 初始化参数。 + // URDF 文件路径,用于初始化 IbvsController 内部 IK 求解器。 std::string urdf_path; + // IK 链基座 link 名称。 std::string base_link{"PELVIS_S"}; + // IK 链末端法兰 link 名称。 std::string flange_link{"R_WRIST_R_S"}; + // URDF 中相机 link 名称。 std::string camera_link; // 视觉感知参数。 + // AprilTag 实际边长,单位米。 double tag_size_m{0.12}; + // 感知更新时如何使用深度图:不用 / 尽量用 / 必须用。 perception::AprilTagPerception::DepthPolicy depth_policy{ perception::AprilTagPerception::DepthPolicy::NONE}; + // 从像素恢复目标点时采用 tag 平面求交,还是深度图反投影。 perception::TagRelativeTarget3D::TargetPointMethod target_point_method{ perception::TagRelativeTarget3D::TargetPointMethod::TAG_PLANE}; // 视觉阶段目标:触控点在相机坐标系中的 hover 位置。 - Eigen::Vector3d hover_target_in_camera{0.0, 0.0, 0.12}; + // 目标点在相机坐标系中的期望位置,单位米。 + Eigen::Vector3d hover_target_in_camera{0.0, 0.0, 0.40}; + // 目标 tag 姿态旋转参数,直接传给 vpRotationMatrix::buildFrom。 double target_rx{3.14159265358979323846}; + // 目标 tag 姿态旋转参数,直接传给 vpRotationMatrix::buildFrom。 double target_ry{0.0}; + // 目标 tag 姿态旋转参数,直接传给 vpRotationMatrix::buildFrom。 double target_rz{0.0}; // IBVS 参数。 + // 视觉伺服增益 lambda。 double ibvs_lambda{0.6}; + // DLS IK 阻尼系数 mu。 double ibvs_mu{0.1}; - double ibvs_qdot_max{0.5}; - std::array ibvs_vmax6{{0.04, 0.04, 0.04, 0.04, 0.04, 0.04}}; + // 单关节最大速度,单位 rad/s。 + double ibvs_qdot_max{0.15}; + // 相机 twist 六维限幅 `[vx, vy, vz, wx, wy, wz]`。 + std::array ibvs_vmax6{{0.15, 0.15, 0.20, 0.6, 0.6, 0.6}}; + // 是否启用关节限位回避。 bool enable_joint_limit_avoidance{true}; + // 关节限位回避增益。 double joint_limit_avoidance_gain{0.2}; - double joint_limit_avoidance_margin_ratio{0.05}; + // 距离关节限位多近时开始回避,按关节范围比例计算。 + double joint_limit_avoidance_margin_ratio{0.15}; + // 单关节限位回避最大推回速度。 double joint_limit_avoidance_max_push{0.25}; + // `AbstractCamera` 相机坐标系到 ViSP 相机坐标系的旋转矩阵。 Eigen::Matrix3d R_camera_to_visp{Eigen::Matrix3d::Identity()}; + // `AbstractCamera` 相机坐标系到 URDF 相机坐标系的旋转矩阵。 Eigen::Matrix3d R_camera_to_urdf{Eigen::Matrix3d::Identity()}; // 关节控制链,默认右臂 7 轴。 + // 顺序必须与 IbvsController 内部 IK 链顺序一致。 std::vector control_joint_names{ "R_SHOULDER_P", "R_SHOULDER_R", "R_SHOULDER_Y", "R_ELBOW_R", "R_WRIST_P", "R_WRIST_Y", "R_WRIST_R"}; // 视觉对准收敛判据。 + // 目标点在相机坐标系 x/y 方向的允许误差,单位米。 double align_xy_threshold_m{0.003}; + // 目标点在相机坐标系 z 方向的允许误差,单位米。 double align_z_threshold_m{0.010}; + // tag 当前姿态与目标姿态的允许夹角误差,单位弧度。 + double align_rot_threshold_rad{0.08726646259971647}; + // 连续多少帧都满足阈值,才认为对准完成。 int align_stable_frames{5}; + // 对准阶段超时时间,单位秒。 double align_timeout_s{10.0}; + // 为 true 时对准完成后暂停,不自动进入触控阶段。 bool pause_after_align_reached{false}; - // 触控阶段:speedL 目标 twist(base_link 系)。 + // 触控阶段:speedL 目标 twist。当前按末端 Tool 坐标系解释,字段名保留兼容。 + // 6 维速度命令 `[vx, vy, vz, wx, wy, wz]`,单位 m/s 和 rad/s。 + // 当前机器人上 `[0, -0.08, 0, 0, 0, 0]` 表示沿 Tool -Y 方向向前触屏。 Eigen::Matrix touch_twist_base{ - (Eigen::Matrix() << 0.0, 0.0, -0.02, 0.0, 0.0, 0.0).finished()}; - double touch_acceleration{0.6}; - double touch_timeout_s{2.0}; + (Eigen::Matrix() << 0.0, -0.08, 0.0, 0.0, 0.0, 0.0).finished()}; + // 触控阶段 speedL 的加速度参数。 + double touch_acceleration{3.0}; + // 前进触控阶段的最大持续时间,单位秒。 + // 该时间由 TouchScreenApp 状态机自行计时,不直接传给 robot->speedL(time)。 + // 大于 0 时,达到该时间后无论压力是否达阈值,都会立即进入回退阶段。 + // 小于等于 0 时,表示不启用这条限制。 + double touch_forward_duration_s{1.0}; - // 接触后停留与回退。 + // 接触后停留与回退。当前按末端 Tool 坐标系解释。 + // 检测到接触后在当前位置停留的时间,单位秒。 + // 当该值小于 0 时,表示不做停留,直接把 speedL 切换为回退。 double dwell_time_s{0.05}; + // 回退阶段的 6 维速度命令 `[vx, vy, vz, wx, wy, wz]`。 + // 当前机器人上 `[0, +0.08, 0, 0, 0, 0]` 表示沿 Tool +Y 方向向后离屏。 Eigen::Matrix retract_twist_base{ - (Eigen::Matrix() << 0.0, 0.0, 0.03, 0.0, 0.0, 0.0).finished()}; - double retract_acceleration{0.8}; - double retract_duration_s{0.20}; + (Eigen::Matrix() << 0.0, 0.08,0.0, 0.0, 0.0, 0.0).finished()}; + // 回退阶段 speedL 的加速度参数。 + double retract_acceleration{3.0}; + // 回退阶段持续时间,单位秒。 + double retract_duration_s{0.8}; // 指尖触觉判据。 + // 使用哪根手指的触觉阵列判断是否接触。 device::FingerType tactile_finger{device::FingerType::INDEX}; - TactileRegion tactile_region{TactileRegion::FINGER}; - double tactile_pressure_sum_threshold{3000.0}; + // 使用该手指的哪个触觉区域。 + TactileRegion tactile_region{TactileRegion::TIP_AND_FINGER}; + // 触觉压力和阈值;总和超过该值认为已经接触。 + double tactile_pressure_sum_threshold{100.0}; + // 触觉峰值阈值;为 0 时表示不使用峰值判据。 double tactile_pressure_peak_threshold{0.0}; }; TouchScreenApp(); ~TouchScreenApp() = default; + bool init(); + bool init(const std::shared_ptr& robot, + const std::shared_ptr& dexhand, + const std::shared_ptr& camera); bool init(const std::shared_ptr& robot, const std::shared_ptr& dexhand, const std::shared_ptr& camera, - const Options& options); + const cmvr::config::TouchScreenAppConfig& config); - void setOptions(const Options& options); - const Options& options() const { return options_; } + bool setOptionsFromConfig(); + bool setOptionsFromConfig(const cmvr::config::TouchScreenAppConfig& config); bool startFromPixel(int u, int v); bool step(); @@ -168,6 +231,9 @@ public: private: using Clock = std::chrono::steady_clock; + static bool optionsFromConfig(const cmvr::config::TouchScreenAppConfig& config, + Options& options_out); + void setOptions(const Options& options); bool applyOptions(); bool validateControlJointNames() const; bool stepAligning(); @@ -179,6 +245,7 @@ private: bool sendJointVelocity(const std::vector& qdot) const; bool sendZeroJointVelocity() const; bool holdCurrentControlledPosition() const; + bool moveToInitPositionIfEnabled() const; bool startTouchPhase(); bool startRetractPhase(Phase next_phase_after_retract, Status final_status_after_retract); diff --git a/cmvr-es/applications/src/touch_screen_app.cpp b/cmvr-es/applications/src/touch_screen_app.cpp index 90b2a703..66e53dbf 100644 --- a/cmvr-es/applications/src/touch_screen_app.cpp +++ b/cmvr-es/applications/src/touch_screen_app.cpp @@ -3,8 +3,13 @@ #include #include #include +#include #include +#include "common/utils/config_helper/include/config_helper.h" +#include "device_manager/include/device_manager.h" +#include + namespace cmvr::app { namespace { @@ -29,15 +34,250 @@ void accumulateMatrixStats(const std::vector>& } } +bool requiresReinitForOptionChange(const TouchScreenApp::Options& current, + const TouchScreenApp::Options& requested) { + const auto init_joints_differ = [&]() { + if (current.init_joint_positions.size() != requested.init_joint_positions.size()) { + return true; + } + for (size_t i = 0; i < current.init_joint_positions.size(); ++i) { + if (current.init_joint_positions[i].joint_name != requested.init_joint_positions[i].joint_name || + current.init_joint_positions[i].rad != requested.init_joint_positions[i].rad) { + return true; + } + } + return false; + }; + return current.move_to_init_position_before_start != requested.move_to_init_position_before_start || + current.move_to_init_position != requested.move_to_init_position || + init_joints_differ() || + current.init_movej_vel != requested.init_movej_vel || + current.init_movej_acc != requested.init_movej_acc || + current.urdf_path != requested.urdf_path || + current.base_link != requested.base_link || + current.flange_link != requested.flange_link || + current.camera_link != requested.camera_link || + current.control_joint_names != requested.control_joint_names; +} + +Eigen::Matrix3d rotationFromTargetRotvec(double rx, double ry, double rz) { + vpRotationMatrix R_visp; + R_visp.buildFrom(rx, ry, rz); + + Eigen::Matrix3d R = Eigen::Matrix3d::Identity(); + for (int r = 0; r < 3; ++r) { + for (int c = 0; c < 3; ++c) { + R(r, c) = R_visp[r][c]; + } + } + return R; +} + +double rotationErrorRad(const Eigen::Matrix3d& R_current, + const Eigen::Matrix3d& R_target) { + if (!R_current.allFinite() || !R_target.allFinite()) { + return std::numeric_limits::infinity(); + } + + const Eigen::Matrix3d R_err = R_current * R_target.transpose(); + const double cos_angle = std::clamp(0.5 * (R_err.trace() - 1.0), -1.0, 1.0); + return std::acos(cos_angle); +} + +device::TactileRegion toDeviceTactileRegion(const TouchScreenApp::TactileRegion region) { + switch (region) { + case TouchScreenApp::TactileRegion::TIP: return device::TactileRegion::TIP; + case TouchScreenApp::TactileRegion::FINGER: return device::TactileRegion::FINGER; + case TouchScreenApp::TactileRegion::PAD: return device::TactileRegion::PAD; + case TouchScreenApp::TactileRegion::TIP_AND_FINGER: + return device::TactileRegion::TIP_AND_FINGER; + case TouchScreenApp::TactileRegion::THUMB_MIDDLE: + return device::TactileRegion::THUMB_MIDDLE; + } + return device::TactileRegion::TIP; +} + +perception::AprilTagPerception::DepthPolicy toDepthPolicy( + const cmvr::config::TouchScreenDepthPolicy policy) { + switch (policy) { + case cmvr::config::TOUCH_SCREEN_DEPTH_POLICY_NONE: + return perception::AprilTagPerception::DepthPolicy::NONE; + case cmvr::config::TOUCH_SCREEN_DEPTH_POLICY_PREFER: + return perception::AprilTagPerception::DepthPolicy::PREFER; + case cmvr::config::TOUCH_SCREEN_DEPTH_POLICY_REQUIRE: + return perception::AprilTagPerception::DepthPolicy::REQUIRE; + } + return perception::AprilTagPerception::DepthPolicy::NONE; +} + +perception::TagRelativeTarget3D::TargetPointMethod toTargetPointMethod( + const cmvr::config::TouchScreenTargetPointMethod method) { + switch (method) { + case cmvr::config::TOUCH_SCREEN_TARGET_POINT_METHOD_TAG_PLANE: + return perception::TagRelativeTarget3D::TargetPointMethod::TAG_PLANE; + case cmvr::config::TOUCH_SCREEN_TARGET_POINT_METHOD_DEPTH_IMAGE: + return perception::TagRelativeTarget3D::TargetPointMethod::DEPTH_IMAGE; + } + return perception::TagRelativeTarget3D::TargetPointMethod::TAG_PLANE; +} + +device::FingerType toFingerType(const cmvr::config::TouchScreenFingerType finger) { + switch (finger) { + case cmvr::config::TOUCH_SCREEN_FINGER_TYPE_PINKY: + return device::FingerType::PINKY; + case cmvr::config::TOUCH_SCREEN_FINGER_TYPE_RING: + return device::FingerType::RING; + case cmvr::config::TOUCH_SCREEN_FINGER_TYPE_MIDDLE: + return device::FingerType::MIDDLE; + case cmvr::config::TOUCH_SCREEN_FINGER_TYPE_INDEX: + return device::FingerType::INDEX; + case cmvr::config::TOUCH_SCREEN_FINGER_TYPE_THUMB: + return device::FingerType::THUMB; + } + return device::FingerType::INDEX; +} + +TouchScreenApp::TactileRegion toTactileRegion( + const cmvr::config::TouchScreenTactileRegion region) { + switch (region) { + case cmvr::config::TOUCH_SCREEN_TACTILE_REGION_TIP: + return TouchScreenApp::TactileRegion::TIP; + case cmvr::config::TOUCH_SCREEN_TACTILE_REGION_FINGER: + return TouchScreenApp::TactileRegion::FINGER; + case cmvr::config::TOUCH_SCREEN_TACTILE_REGION_PAD: + return TouchScreenApp::TactileRegion::PAD; + case cmvr::config::TOUCH_SCREEN_TACTILE_REGION_TIP_AND_FINGER: + return TouchScreenApp::TactileRegion::TIP_AND_FINGER; + case cmvr::config::TOUCH_SCREEN_TACTILE_REGION_THUMB_MIDDLE: + return TouchScreenApp::TactileRegion::THUMB_MIDDLE; + } + return TouchScreenApp::TactileRegion::TIP; +} + +void applyVec3FromConfig(const cmvr::config::TouchScreenVec3& src, + Eigen::Vector3d& dst) { + if (src.has_x()) { + dst.x() = src.x(); + } + if (src.has_y()) { + dst.y() = src.y(); + } + if (src.has_z()) { + dst.z() = src.z(); + } +} + +void applyTwist6FromConfig(const cmvr::config::TouchScreenTwist6& src, + Eigen::Matrix& dst) { + if (src.has_vx()) { + dst[0] = src.vx(); + } + if (src.has_vy()) { + dst[1] = src.vy(); + } + if (src.has_vz()) { + dst[2] = src.vz(); + } + if (src.has_wx()) { + dst[3] = src.wx(); + } + if (src.has_wy()) { + dst[4] = src.wy(); + } + if (src.has_wz()) { + dst[5] = src.wz(); + } +} + +void applyMatrix3dFromConfig(const cmvr::config::TouchScreenMatrix3d& src, + Eigen::Matrix3d& dst) { + if (src.has_m00()) { + dst(0, 0) = src.m00(); + } + if (src.has_m01()) { + dst(0, 1) = src.m01(); + } + if (src.has_m02()) { + dst(0, 2) = src.m02(); + } + if (src.has_m10()) { + dst(1, 0) = src.m10(); + } + if (src.has_m11()) { + dst(1, 1) = src.m11(); + } + if (src.has_m12()) { + dst(1, 2) = src.m12(); + } + if (src.has_m20()) { + dst(2, 0) = src.m20(); + } + if (src.has_m21()) { + dst(2, 1) = src.m21(); + } + if (src.has_m22()) { + dst(2, 2) = src.m22(); + } +} + } // namespace TouchScreenApp::TouchScreenApp() : tracker_(nullptr) {} +bool TouchScreenApp::init() { + cmvr::config::TouchScreenAppConfig config; + if (!cmvr::ConfigHelper::getTouchScreenAppConfig(config)) { + initialized_ = false; + last_status_ = Status::INVALID_CONFIG; + return false; + } + + if (!config.has_robot_id() || config.robot_id().empty() || + !config.has_camera_id() || config.camera_id().empty() || + !config.has_dexhand_id() || config.dexhand_id().empty()) { + initialized_ = false; + last_status_ = Status::INVALID_CONFIG; + return false; + } + + try { + auto& dm = device::DeviceManager::getInstance(); + auto robot = dm.getDevice(config.robot_id()); + auto dexhand = dm.getDevice(config.dexhand_id()); + auto camera = dm.getDevice(config.camera_id()); + camera->start(); + return init(robot, dexhand, camera, config); + } catch (...) { + initialized_ = false; + last_status_ = Status::INVALID_CONFIG; + return false; + } +} + +bool TouchScreenApp::init(const std::shared_ptr& robot, + const std::shared_ptr& dexhand, + const std::shared_ptr& camera) { + cmvr::config::TouchScreenAppConfig config; + if (!cmvr::ConfigHelper::getTouchScreenAppConfig(config)) { + initialized_ = false; + last_status_ = Status::INVALID_CONFIG; + return false; + } + return init(robot, dexhand, camera, config); +} + bool TouchScreenApp::init(const std::shared_ptr& robot, const std::shared_ptr& dexhand, const std::shared_ptr& camera, - const Options& options) { + const cmvr::config::TouchScreenAppConfig& config) { + Options options; + if (!optionsFromConfig(config, options)) { + initialized_ = false; + last_status_ = Status::INVALID_CONFIG; + return false; + } + robot_ = robot; dexhand_ = dexhand; camera_ = camera; @@ -49,6 +289,24 @@ bool TouchScreenApp::init(const std::shared_ptr& robot, return false; } + if ((options_.move_to_init_position_before_start || options_.move_to_init_position) && + options_.init_joint_positions.empty()) { + initialized_ = false; + last_status_ = Status::INVALID_CONFIG; + return false; + } + + if (options_.move_to_init_position_before_start) { + try { + auto init_cmd = options_.init_joint_positions; + robot_->moveJ(init_cmd, options_.init_movej_vel, options_.init_movej_acc); + } catch (...) { + initialized_ = false; + last_status_ = Status::ROBOT_COMMAND_FAILED; + return false; + } + } + if (options_.urdf_path.empty() || options_.camera_link.empty() || options_.control_joint_names.empty()) { initialized_ = false; last_status_ = Status::INVALID_CONFIG; @@ -102,10 +360,53 @@ bool TouchScreenApp::init(const std::shared_ptr& robot, } void TouchScreenApp::setOptions(const Options& options) { - options_ = options; - if (initialized_) { - applyOptions(); + if (!initialized_) { + options_ = options; + return; } + + const Options previous = options_; + Options applied = options; + if (requiresReinitForOptionChange(options_, options)) { + std::cerr << "[TouchScreenApp] setOptions() ignored IK-chain option changes after init; " + "call init() again to update device/init pose/urdf/link/joint chain\n"; + applied.move_to_init_position_before_start = previous.move_to_init_position_before_start; + applied.move_to_init_position = previous.move_to_init_position; + applied.init_joint_positions = previous.init_joint_positions; + applied.init_movej_vel = previous.init_movej_vel; + applied.init_movej_acc = previous.init_movej_acc; + applied.urdf_path = previous.urdf_path; + applied.base_link = previous.base_link; + applied.flange_link = previous.flange_link; + applied.camera_link = previous.camera_link; + applied.control_joint_names = previous.control_joint_names; + } + + options_ = applied; + if (!applyOptions()) { + options_ = previous; + applyOptions(); + last_status_ = Status::INVALID_CONFIG; + } +} + +bool TouchScreenApp::setOptionsFromConfig() { + cmvr::config::TouchScreenAppConfig config; + if (!cmvr::ConfigHelper::getTouchScreenAppConfig(config)) { + last_status_ = Status::INVALID_CONFIG; + return false; + } + return setOptionsFromConfig(config); +} + +bool TouchScreenApp::setOptionsFromConfig(const cmvr::config::TouchScreenAppConfig& config) { + Options options; + if (!optionsFromConfig(config, options)) { + last_status_ = Status::INVALID_CONFIG; + return false; + } + setOptions(options); + return last_status_ != Status::INVALID_CONFIG; } bool TouchScreenApp::startFromPixel(int u, int v) { @@ -148,6 +449,10 @@ bool TouchScreenApp::step() { return false; } + if (dexhand_) { + updateTouchPressure(); + } + switch (phase_) { case Phase::IDLE: last_status_ = Status::IDLE; @@ -234,8 +539,9 @@ const char* TouchScreenApp::statusToString(const Status status) { case Status::ALIGNING: return "ALIGNING"; case Status::ALIGN_REACHED: return "ALIGN_REACHED"; case Status::TOUCHING: return "TOUCHING"; + case Status::TACTILE_UNAVAILABLE: return "TACTILE_UNAVAILABLE"; case Status::TOUCH_TRIGGERED: return "TOUCH_TRIGGERED"; - case Status::TOUCH_TIMEOUT: return "TOUCH_TIMEOUT"; + case Status::TOUCH_FORWARD_TIMEOUT: return "TOUCH_FORWARD_TIMEOUT"; case Status::RETRACTING: return "RETRACTING"; case Status::DONE: return "DONE"; case Status::STOPPED: return "STOPPED"; @@ -245,10 +551,181 @@ const char* TouchScreenApp::statusToString(const Status status) { return "UNKNOWN"; } +bool TouchScreenApp::optionsFromConfig(const cmvr::config::TouchScreenAppConfig& config, + Options& options_out) { + Options options; + + if (config.has_move_to_init_position_before_start()) { + options.move_to_init_position_before_start = config.move_to_init_position_before_start(); + } + if (config.has_move_to_init_position()) { + options.move_to_init_position = config.move_to_init_position(); + } + if (config.init_joint_positions_size() > 0) { + options.init_joint_positions.clear(); + options.init_joint_positions.reserve(static_cast(config.init_joint_positions_size())); + for (const auto& joint : config.init_joint_positions()) { + options.init_joint_positions.emplace_back( + joint.has_joint_name() ? joint.joint_name() : std::string{}, + joint.has_rad() ? joint.rad() : 0.0); + } + } + if (config.has_init_movej_vel()) { + options.init_movej_vel = config.init_movej_vel(); + } + if (config.has_init_movej_acc()) { + options.init_movej_acc = config.init_movej_acc(); + } + + if (config.has_urdf_path()) { + options.urdf_path = config.urdf_path(); + } + if (config.has_base_link()) { + options.base_link = config.base_link(); + } + if (config.has_flange_link()) { + options.flange_link = config.flange_link(); + } + if (config.has_camera_link()) { + options.camera_link = config.camera_link(); + } + + if (config.has_tag_size_m()) { + options.tag_size_m = config.tag_size_m(); + } + if (config.has_depth_policy()) { + options.depth_policy = toDepthPolicy(config.depth_policy()); + } + if (config.has_target_point_method()) { + options.target_point_method = toTargetPointMethod(config.target_point_method()); + } + + if (config.has_hover_target_in_camera()) { + applyVec3FromConfig(config.hover_target_in_camera(), options.hover_target_in_camera); + } + if (config.has_target_rx()) { + options.target_rx = config.target_rx(); + } + if (config.has_target_ry()) { + options.target_ry = config.target_ry(); + } + if (config.has_target_rz()) { + options.target_rz = config.target_rz(); + } + + if (config.has_ibvs_lambda()) { + options.ibvs_lambda = config.ibvs_lambda(); + } + if (config.has_ibvs_mu()) { + options.ibvs_mu = config.ibvs_mu(); + } + if (config.has_ibvs_qdot_max()) { + options.ibvs_qdot_max = config.ibvs_qdot_max(); + } + if (config.has_ibvs_vmax6()) { + Eigen::Matrix ibvs_vmax; + ibvs_vmax << options.ibvs_vmax6[0], options.ibvs_vmax6[1], options.ibvs_vmax6[2], + options.ibvs_vmax6[3], options.ibvs_vmax6[4], options.ibvs_vmax6[5]; + applyTwist6FromConfig(config.ibvs_vmax6(), ibvs_vmax); + for (int i = 0; i < 6; ++i) { + options.ibvs_vmax6[static_cast(i)] = ibvs_vmax[i]; + } + } + if (config.has_enable_joint_limit_avoidance()) { + options.enable_joint_limit_avoidance = config.enable_joint_limit_avoidance(); + } + if (config.has_joint_limit_avoidance_gain()) { + options.joint_limit_avoidance_gain = config.joint_limit_avoidance_gain(); + } + if (config.has_joint_limit_avoidance_margin_ratio()) { + options.joint_limit_avoidance_margin_ratio = config.joint_limit_avoidance_margin_ratio(); + } + if (config.has_joint_limit_avoidance_max_push()) { + options.joint_limit_avoidance_max_push = config.joint_limit_avoidance_max_push(); + } + + if (config.has_r_camera_to_visp()) { + applyMatrix3dFromConfig(config.r_camera_to_visp(), options.R_camera_to_visp); + } + if (config.has_r_camera_to_urdf()) { + applyMatrix3dFromConfig(config.r_camera_to_urdf(), options.R_camera_to_urdf); + } + + if (config.control_joint_names_size() > 0) { + options.control_joint_names.assign(config.control_joint_names().begin(), + config.control_joint_names().end()); + } + + if (config.has_align_xy_threshold_m()) { + options.align_xy_threshold_m = config.align_xy_threshold_m(); + } + if (config.has_align_z_threshold_m()) { + options.align_z_threshold_m = config.align_z_threshold_m(); + } + if (config.has_align_rot_threshold_rad()) { + options.align_rot_threshold_rad = config.align_rot_threshold_rad(); + } + if (config.has_align_stable_frames()) { + options.align_stable_frames = config.align_stable_frames(); + } + if (config.has_align_timeout_s()) { + options.align_timeout_s = config.align_timeout_s(); + } + if (config.has_pause_after_align_reached()) { + options.pause_after_align_reached = config.pause_after_align_reached(); + } + + if (config.has_touch_twist_base()) { + applyTwist6FromConfig(config.touch_twist_base(), options.touch_twist_base); + } + if (config.has_touch_acceleration()) { + options.touch_acceleration = config.touch_acceleration(); + } + if (config.has_touch_forward_duration_s()) { + options.touch_forward_duration_s = config.touch_forward_duration_s(); + } + + if (config.has_dwell_time_s()) { + options.dwell_time_s = config.dwell_time_s(); + } + if (config.has_retract_twist_base()) { + applyTwist6FromConfig(config.retract_twist_base(), options.retract_twist_base); + } + if (config.has_retract_acceleration()) { + options.retract_acceleration = config.retract_acceleration(); + } + if (config.has_retract_duration_s()) { + options.retract_duration_s = config.retract_duration_s(); + } + + if (config.has_tactile_finger()) { + options.tactile_finger = toFingerType(config.tactile_finger()); + } + if (config.has_tactile_region()) { + options.tactile_region = toTactileRegion(config.tactile_region()); + } + if (config.has_tactile_pressure_sum_threshold()) { + options.tactile_pressure_sum_threshold = config.tactile_pressure_sum_threshold(); + } + if (config.has_tactile_pressure_peak_threshold()) { + options.tactile_pressure_peak_threshold = config.tactile_pressure_peak_threshold(); + } + + options_out = options; + return true; +} + bool TouchScreenApp::applyOptions() { if (!perception_) { return false; } + if (!options_.R_camera_to_visp.allFinite() || !options_.R_camera_to_urdf.allFinite()) { + return false; + } + if (!std::isfinite(options_.touch_forward_duration_s) || + options_.touch_forward_duration_s < 0.0) { + return false; + } perception_->setTagSize(options_.tag_size_m); tracker_.setTargetPointMethod(options_.target_point_method); @@ -260,15 +737,8 @@ bool TouchScreenApp::applyOptions() { options_.joint_limit_avoidance_gain, options_.joint_limit_avoidance_margin_ratio, options_.joint_limit_avoidance_max_push); - // ibvs_.setAlignCameraToVisp(options_.R_camera_to_visp); - // ibvs_.setAlignCameraToUrdf(options_.R_camera_to_urdf); - - Eigen::Matrix3d RxPi; - RxPi << 1,0,0, - 0,-1,0, - 0,0,-1; - ibvs_.setAlignCameraToUrdf(RxPi); - ibvs_.setAlignCameraToVisp(RxPi); + ibvs_.setAlignCameraToVisp(options_.R_camera_to_visp); + ibvs_.setAlignCameraToUrdf(options_.R_camera_to_urdf); return true; } @@ -395,9 +865,22 @@ bool TouchScreenApp::stepAligning() { } last_align_error_camera_ = tracker_.lastTargetInCamera() - options_.hover_target_in_camera; + const auto* current_tag = perception_->findTag(tag_id); + if (!current_tag) { + sendZeroJointVelocity(); + align_stable_count_ = 0; + last_status_ = Status::ALIGN_WAITING_TRACK; + return true; + } + + const Eigen::Matrix3d R_current = current_tag->T_c_t.block<3, 3>(0, 0); + const Eigen::Matrix3d R_target = + rotationFromTargetRotvec(options_.target_rx, options_.target_ry, options_.target_rz); + const double rot_error_rad = rotationErrorRad(R_current, R_target); if (std::abs(last_align_error_camera_.x()) <= options_.align_xy_threshold_m && std::abs(last_align_error_camera_.y()) <= options_.align_xy_threshold_m && - std::abs(last_align_error_camera_.z()) <= options_.align_z_threshold_m) { + std::abs(last_align_error_camera_.z()) <= options_.align_z_threshold_m && + rot_error_rad <= options_.align_rot_threshold_rad) { ++align_stable_count_; } else { align_stable_count_ = 0; @@ -424,10 +907,38 @@ bool TouchScreenApp::stepTouching() { } } - updateTouchPressure(); - if (last_touch_pressure_sum_ >= options_.tactile_pressure_sum_threshold || - (options_.tactile_pressure_peak_threshold > 0.0 && - last_touch_pressure_peak_ >= options_.tactile_pressure_peak_threshold)) { + if (!dexhand_) { + enterFailed(Status::TACTILE_UNAVAILABLE); + return false; + } + const auto touchTriggered = [&]() { + return last_touch_pressure_sum_ >= options_.tactile_pressure_sum_threshold || + (options_.tactile_pressure_peak_threshold > 0.0 && + last_touch_pressure_peak_ >= options_.tactile_pressure_peak_threshold); + }; + const auto handleTouchTriggered = [&]() { + if (options_.dwell_time_s < 0.0) { + try { + if (!robot_->speedL(toStdVector6(options_.retract_twist_base), + options_.retract_acceleration, + 0.0, + cmvr::CartesianFrame::Tool)) { + enterFailed(Status::ROBOT_COMMAND_FAILED); + return false; + } + } catch (...) { + enterFailed(Status::ROBOT_COMMAND_FAILED); + return false; + } + phase_ = Phase::RETRACTING; + phase_after_retract_ = Phase::DONE; + final_status_after_retract_ = Status::DONE; + phase_start_time_ = Clock::now(); + retract_command_started_ = true; + last_status_ = Status::TOUCH_TRIGGERED; + return true; + } + try { robot_->stopSpeedL(); } catch (...) { @@ -438,17 +949,23 @@ bool TouchScreenApp::stepTouching() { phase_start_time_ = Clock::now(); last_status_ = Status::TOUCH_TRIGGERED; return true; + }; + + if (touchTriggered()) { + return handleTouchTriggered(); } const double elapsed = std::chrono::duration(Clock::now() - phase_start_time_).count(); - if (elapsed > options_.touch_timeout_s) { - try { - robot_->stopSpeedL(); - } catch (...) { - enterFailed(Status::ROBOT_COMMAND_FAILED); + if (options_.touch_forward_duration_s > 0.0 && + elapsed >= options_.touch_forward_duration_s) { + if (!updateTouchPressure()) { + enterFailed(Status::TACTILE_UNAVAILABLE); return false; } - if (!startRetractPhase(Phase::FAILED, Status::TOUCH_TIMEOUT)) { + if (touchTriggered()) { + return handleTouchTriggered(); + } + if (!startRetractPhase(Phase::FAILED, Status::TOUCH_FORWARD_TIMEOUT)) { enterFailed(Status::ROBOT_COMMAND_FAILED); return false; } @@ -494,6 +1011,12 @@ bool TouchScreenApp::stepRetracting() { return false; } holdCurrentControlledPosition(); + if ((phase_after_retract_ == Phase::DONE || phase_after_retract_ == Phase::FAILED) && + !moveToInitPositionIfEnabled()) { + phase_ = Phase::FAILED; + last_status_ = Status::ROBOT_COMMAND_FAILED; + return false; + } phase_ = phase_after_retract_; last_status_ = final_status_after_retract_; return phase_ != Phase::FAILED; @@ -578,6 +1101,23 @@ bool TouchScreenApp::holdCurrentControlledPosition() const { return true; } +bool TouchScreenApp::moveToInitPositionIfEnabled() const { + if (!options_.move_to_init_position) { + return true; + } + if (!robot_ || options_.init_joint_positions.empty()) { + return false; + } + + try { + auto init_cmd = options_.init_joint_positions; + robot_->moveJ(init_cmd, options_.init_movej_vel, options_.init_movej_acc); + } catch (...) { + return false; + } + return true; +} + bool TouchScreenApp::startTouchPhase() { if (!robot_) { return false; @@ -585,7 +1125,8 @@ bool TouchScreenApp::startTouchPhase() { try { if (!robot_->speedL(toStdVector6(options_.touch_twist_base), options_.touch_acceleration, - 0.0)) { + 0.0, + cmvr::CartesianFrame::Tool)) { return false; } } catch (...) { @@ -607,7 +1148,8 @@ bool TouchScreenApp::startRetractPhase(const Phase next_phase_after_retract, try { if (!robot_->speedL(toStdVector6(options_.retract_twist_base), options_.retract_acceleration, - 0.0)) { + 0.0, + cmvr::CartesianFrame::Tool)) { return false; } } catch (...) { @@ -634,7 +1176,7 @@ void TouchScreenApp::enterFailed(const Status status) { phase_ = Phase::FAILED; touch_command_started_ = false; retract_command_started_ = false; - last_status_ = status; + last_status_ = moveToInitPositionIfEnabled() ? status : Status::ROBOT_COMMAND_FAILED; } bool TouchScreenApp::updateTouchPressure() { @@ -644,7 +1186,8 @@ bool TouchScreenApp::updateTouchPressure() { return false; } - const auto& sensors = dexhand_->getSensorData(); + const auto& sensors = dexhand_->getSensorData(options_.tactile_finger, + toDeviceTactileRegion(options_.tactile_region)); double sum = 0.0; double peak = 0.0; diff --git a/cmvr-es/applications/src/touch_screen_app_test.cpp b/cmvr-es/applications/src/touch_screen_app_test.cpp index 9d2cb1eb..bccb1dfd 100644 --- a/cmvr-es/applications/src/touch_screen_app_test.cpp +++ b/cmvr-es/applications/src/touch_screen_app_test.cpp @@ -5,21 +5,13 @@ #include #include "applications/include/touch_screen_app.h" -#include "device_manager/include/device_manager.h" + +#include "include/device_manager.h" #include "service/grpc/include/server_runner.h" namespace { constexpr const char* kConfigPath = "/home/lgv/cmvr/cmvr-es/cmvr-es/common/config/cabin_robot.xml"; -// 这些 id 需要与现场配置一致;保持为示例调用中的写法。 -constexpr const char* kRobotId = "hc01"; -constexpr const char* kDexhandId = "hand2"; -constexpr const char* kCameraId = "right_hand_cam"; -constexpr const char* kUrdfPath = - "/home/lgv/cmvr/cmvr-es/model/xiaoyan_description/dual_arm.urdf"; -constexpr const char* kBaseLink = "PELVIS_S"; -constexpr const char* kFlangeLink = "R_WRIST_R_S"; -constexpr const char* kCameraLink = "R_CAM"; constexpr int kTargetU = 1280 / 2.0; constexpr int kTargetV = 720 / 2.0; @@ -27,117 +19,75 @@ constexpr int kTargetV = 720 / 2.0; void run_touch_once(int u, int v) { const XmlNode config(kConfigPath); - cmvr::service::ServerRunner runner; - runner.start(config); - if (!config.hasChild("DeviceManager")) { - std::cerr << "DeviceManager node not found\n"; - return; - } - - auto& dm = cmvr::device::DeviceManager::getInstance(config.getChild("DeviceManager")); - - auto robot = dm.getDevice(kRobotId); - // auto dexhand = dm.getDevice(kDexhandId); - auto camera = dm.getDevice(kCameraId); - std::shared_ptr dexhand = nullptr; - - camera->start(); - - std::vector init_cmd{}; - init_cmd = { - {"R_SHOULDER_P", -0.2423}, - {"R_SHOULDER_R", 1.2929}, - {"R_SHOULDER_Y", 1.61}, - {"R_ELBOW_R", 1.58}, - {"R_WRIST_P", -2.8792}, - {"R_WRIST_Y", 0.1150}, - {"R_WRIST_R", -0.08}, - }; - robot->moveJ(init_cmd,1.0,2.0); - std::this_thread::sleep_for(std::chrono::milliseconds(1000)); - + // auto& dm = cmvr::device::DeviceManager::getInstance(); + cmvr::device::DeviceManager::getInstance(config.getChild("DeviceManager")); + // cmvr::service::ServerRunner runner; + // runner.start(config); + ASSERT_TRUE(config.hasChild("DeviceManager")) << "DeviceManager node not found"; cmvr::app::TouchScreenApp app; - cmvr::app::TouchScreenApp::Options opt; + ASSERT_TRUE(app.init()) + << "TouchScreenApp init failed"; - opt.urdf_path = kUrdfPath; - opt.base_link = kBaseLink; - opt.flange_link = kFlangeLink; - opt.camera_link = kCameraLink; - opt.tag_size_m = 0.02; - opt.ibvs_vmax6 = {0.04, 0.04, 0.04, 0.04, 0.04, 0.04}; - - opt.hover_target_in_camera = Eigen::Vector3d(0.0, 0.0, 0.30); - opt.pause_after_align_reached = true; - - // 只测试视觉对齐阶段:禁止进入真实下压。 - opt.touch_twist_base << - 0.0, 0.0, 0.0, - 0.0, 0.0, 0.0; - opt.touch_acceleration = 0.6; - - opt.retract_twist_base << - 0.0, 0.0, 0.03, - 0.0, 0.0, 0.0; - opt.retract_duration_s = 0.20; - - opt.tactile_finger = cmvr::device::FingerType::INDEX; - opt.tactile_region = cmvr::app::TouchScreenApp::TactileRegion::FINGER; - opt.tactile_pressure_sum_threshold = 1e12; - opt.align_timeout_s = 100.0; - - if (!app.init(robot, dexhand, camera, opt)) { - std::cerr << "TouchScreenApp init failed\n"; - return; - } - - if (!app.startFromPixel(u, v)) { - std::cerr << "startFromPixel failed\n"; - return; - } + ASSERT_TRUE(app.startFromPixel(u, v)) << "startFromPixel failed"; + bool align_reached = false; + bool touch_triggered = false; while (app.isBusy()) { - if (!app.step()) { - std::cerr << "touch failed, status=" - << cmvr::app::TouchScreenApp::statusToString(app.lastStatus()) - << "\n"; - break; - } + const bool step_ok = app.step(); + const auto& p_c_target = app.tracker().lastTargetInCamera(); std::cout << "phase=" << cmvr::app::TouchScreenApp::phaseToString(app.phase()) << ", status=" << cmvr::app::TouchScreenApp::statusToString(app.lastStatus()) << ", active_tag=" << app.lastActiveTagId() + << ", target_c=[" << p_c_target.x() << ", " + << p_c_target.y() << ", " + << p_c_target.z() << "]" + << ", pressure_sum=" << app.lastTouchPressureSum() + << ", pressure_peak=" << app.lastTouchPressurePeak() << ", err_c=[" << app.lastAlignErrorCamera().x() << ", " << app.lastAlignErrorCamera().y() << ", " << app.lastAlignErrorCamera().z() << "]\n"; if (app.lastStatus() == cmvr::app::TouchScreenApp::Status::ALIGN_REACHED) { - std::cout << "align reached\n"; - app.stop(); - break; + std::cout << "align reached, target_c=[" << p_c_target.x() << ", " + << p_c_target.y() << ", " + << p_c_target.z() << "]\n"; + align_reached = true; } - std::this_thread::sleep_for(std::chrono::milliseconds(10)); - } - - if (app.isFinished()) { - std::cout << "touch done\n"; - } else if (app.isFailed()) { - std::cout << "touch failed: " - << cmvr::app::TouchScreenApp::statusToString(app.lastStatus()) - << "\n"; - } - - while (true) { - std::this_thread::sleep_for(std::chrono::milliseconds(1000)); + if (app.lastStatus() == cmvr::app::TouchScreenApp::Status::TOUCH_TRIGGERED) { + std::cout << "touch triggered, pressure_sum=" << app.lastTouchPressureSum() + << ", pressure_peak=" << app.lastTouchPressurePeak() << "\n"; + touch_triggered = true; + } + if (!step_ok) { + const auto failed_status = app.lastStatus(); + app.stop(); + FAIL() << "touch flow failed, status=" + << cmvr::app::TouchScreenApp::statusToString(failed_status); + } + std::this_thread::sleep_for(std::chrono::milliseconds(1)); } + const auto final_status = app.lastStatus(); + ASSERT_TRUE(align_reached) + << "align was never reached, final status=" + << cmvr::app::TouchScreenApp::statusToString(final_status); + ASSERT_TRUE(touch_triggered) + << "touch was never triggered, final status=" + << cmvr::app::TouchScreenApp::statusToString(final_status); + ASSERT_TRUE(app.isFinished()) + << "touch did not finish successfully, final status=" + << cmvr::app::TouchScreenApp::statusToString(final_status); + std::cout << "touch done, final status=" + << cmvr::app::TouchScreenApp::statusToString(final_status) << "\n"; + app.stop(); } } // namespace TEST(TouchScreenAppTest, RunTouchOnceOnRealRobot) { - run_touch_once(kTargetU, kTargetV); } diff --git a/cmvr-es/common/config/dexhand_config/dexhand_config.pb.txt b/cmvr-es/common/config/dexhand_config/dexhand_config.pb.txt index e2d2d969..57ff09fa 100644 --- a/cmvr-es/common/config/dexhand_config/dexhand_config.pb.txt +++ b/cmvr-es/common/config/dexhand_config/dexhand_config.pb.txt @@ -7,7 +7,7 @@ rh56dftp_dexhands { rh56dftp_dexhands { id: "hand2" - ip: "192.168.1.214" + ip: "192.168.1.224" port: 6000 - enable: false + enable: true } \ No newline at end of file diff --git a/cmvr-es/common/config/touch_screen_app_config/touch_screen_app_config.pb.txt b/cmvr-es/common/config/touch_screen_app_config/touch_screen_app_config.pb.txt new file mode 100644 index 00000000..f7e040f7 --- /dev/null +++ b/cmvr-es/common/config/touch_screen_app_config/touch_screen_app_config.pb.txt @@ -0,0 +1,123 @@ +robot_id: "hc01" +dexhand_id: "hand2" +camera_id: "right_hand_cam" +move_to_init_position_before_start: true +move_to_init_position: true +init_joint_positions { + joint_name: "R_SHOULDER_P" + rad: -0.2423 +} +init_joint_positions { + joint_name: "R_SHOULDER_R" + rad: 1.2929 +} +init_joint_positions { + joint_name: "R_SHOULDER_Y" + rad: 1.61 +} +init_joint_positions { + joint_name: "R_ELBOW_R" + rad: 1.58 +} +init_joint_positions { + joint_name: "R_WRIST_P" + rad: -2.8792 +} +init_joint_positions { + joint_name: "R_WRIST_Y" + rad: 0.1150 +} +init_joint_positions { + joint_name: "R_WRIST_R" + rad: -0.08 +} +init_movej_vel: 1.0 +init_movej_acc: 2.0 + +urdf_path: "/home/lgv/cmvr/cmvr-es/model/xiaoyan_description/dual_arm.urdf" +base_link: "PELVIS_S" +flange_link: "R_WRIST_R_S" +camera_link: "R_CAM" + +tag_size_m: 0.02 +depth_policy: TOUCH_SCREEN_DEPTH_POLICY_NONE +target_point_method: TOUCH_SCREEN_TARGET_POINT_METHOD_TAG_PLANE + +hover_target_in_camera { + x: 0.0 + y: 0.0 + z: 0.20 +} +target_rx: 3.14159265358979323846 +target_ry: 0.0 +target_rz: 0.0 + +ibvs_lambda: 0.6 +ibvs_mu: 0.1 +ibvs_qdot_max: 0.15 +ibvs_vmax6 { + vx: 0.15 + vy: 0.15 + vz: 0.20 + wx: 0.6 + wy: 0.6 + wz: 0.6 +} +enable_joint_limit_avoidance: true +joint_limit_avoidance_gain: 0.2 +joint_limit_avoidance_margin_ratio: 0.15 +joint_limit_avoidance_max_push: 0.25 + +r_camera_to_visp { + m00: 1.0 + m11: 1.0 + m22: 1.0 +} +r_camera_to_urdf { + m00: 1.0 + m11: 1.0 + m22: 1.0 +} + +control_joint_names: "R_SHOULDER_P" +control_joint_names: "R_SHOULDER_R" +control_joint_names: "R_SHOULDER_Y" +control_joint_names: "R_ELBOW_R" +control_joint_names: "R_WRIST_P" +control_joint_names: "R_WRIST_Y" +control_joint_names: "R_WRIST_R" + +align_xy_threshold_m: 0.003 +align_z_threshold_m: 0.010 +align_rot_threshold_rad: 0.08726646259971647 +align_stable_frames: 5 +align_timeout_s: 100.0 +pause_after_align_reached: false + +touch_twist_base { + vx: 0.0 + vy: -0.08 + vz: 0.0 + wx: 0.0 + wy: 0.0 + wz: 0.0 +} +touch_acceleration: 3.0 +touch_forward_duration_s: 1.2 + +dwell_time_s: -1.0 +retract_twist_base { + vx: 0.0 + vy: 0.08 + vz: 0.0 + wx: 0.0 + wy: 0.0 + wz: 0.0 +} +retract_acceleration: 3.0 +retract_duration_s: 1.0 + +tactile_finger: TOUCH_SCREEN_FINGER_TYPE_INDEX +tactile_region: TOUCH_SCREEN_TACTILE_REGION_TIP +tactile_pressure_sum_threshold: 300.0 +tactile_pressure_peak_threshold: 0.0 diff --git a/cmvr-es/common/utils/config_helper/include/config_helper.h b/cmvr-es/common/utils/config_helper/include/config_helper.h index 6f575d4c..7d189128 100644 --- a/cmvr-es/common/utils/config_helper/include/config_helper.h +++ b/cmvr-es/common/utils/config_helper/include/config_helper.h @@ -9,6 +9,7 @@ #include "cmvr/config/dexhand_config/dexhand_config.pb.h" #include "cmvr/config/microphone_config/microphone_config.pb.h" #include "cmvr/config/speaker_config/speaker_conifg.pb.h" +#include "cmvr/config/touch_screen_app_config/touch_screen_app_config.pb.h" #define GET_CONFIG(file, para) \ ([&]() -> bool { \ @@ -71,6 +72,16 @@ namespace cmvr return GET_CONFIG(speaker_config_file, config); } + static bool getTouchScreenAppConfig(config::TouchScreenAppConfig& config) + { + return GET_CONFIG(touch_screen_app_config_file, config); + } + + static bool setTouchScreenAppConfig(const config::TouchScreenAppConfig& config) + { + return SET_CONFIG(config, touch_screen_app_config_file); + } + private: // Make macros able to call these (macros call ::cmvr::ConfigHelper::xxx) template diff --git a/cmvr-es/common/utils/config_helper/include/config_setting.h b/cmvr-es/common/utils/config_helper/include/config_setting.h index 25080311..5d58fb9e 100644 --- a/cmvr-es/common/utils/config_helper/include/config_setting.h +++ b/cmvr-es/common/utils/config_helper/include/config_setting.h @@ -4,4 +4,5 @@ DECLARE_string(pinocchio_qp_ik_solver_config_file); DECLARE_string(camera_config_file); DECLARE_string(dexhand_config_file); DECLARE_string(microphone_config_file); -DECLARE_string(speaker_config_file); \ No newline at end of file +DECLARE_string(speaker_config_file); +DECLARE_string(touch_screen_app_config_file); diff --git a/cmvr-es/common/utils/config_helper/src/config_setting.cpp b/cmvr-es/common/utils/config_helper/src/config_setting.cpp index 2e32d6ed..a0690df6 100644 --- a/cmvr-es/common/utils/config_helper/src/config_setting.cpp +++ b/cmvr-es/common/utils/config_helper/src/config_setting.cpp @@ -35,3 +35,6 @@ DEFINE_string(speaker_config_file, basePath() + "speaker_config/speaker_config.pb.txt", "The configuration file for speaker"); +DEFINE_string(touch_screen_app_config_file, + basePath() + "touch_screen_app_config/touch_screen_app_config.pb.txt", + "The configuration file for TouchScreenApp"); diff --git a/cmvr-es/devices/dexhand/abstract_dexhand.h b/cmvr-es/devices/dexhand/abstract_dexhand.h index 803b322b..9f5d502c 100644 --- a/cmvr-es/devices/dexhand/abstract_dexhand.h +++ b/cmvr-es/devices/dexhand/abstract_dexhand.h @@ -75,6 +75,15 @@ namespace cmvr::device{ THUMB // 大拇指 }; + // 触觉区域枚举(设备层)。 + enum class TactileRegion { + TIP = 0, + FINGER, + PAD, + TIP_AND_FINGER, + THUMB_MIDDLE + }; + // 整只手的触觉传感器数据 struct HandTactileSensors { // 五指的触觉数据(每根手指包含指端、指尖、指腹) @@ -219,10 +228,15 @@ namespace cmvr::device{ virtual void execPresetAct(int action_id) {} virtual void setForce(const std::vector& finger_joint_force) {} virtual HandTactileSensors& getSensorData() { return hand_tactile_sensors_;} + virtual HandTactileSensors& getSensorData(FingerType finger_type, TactileRegion tactile_region) { + (void)finger_type; + (void)tactile_region; + return getSensorData(); + } protected: DexHandState status_; HandTactileSensors hand_tactile_sensors_; }; } -#endif //CMVR_ES_ABSTRACT_DEXHAND_H \ No newline at end of file +#endif //CMVR_ES_ABSTRACT_DEXHAND_H diff --git a/cmvr-es/devices/dexhand/rh56dftp_dexhand/include/rh56dftp_dexhand.h b/cmvr-es/devices/dexhand/rh56dftp_dexhand/include/rh56dftp_dexhand.h index 43a59ddb..9455d103 100644 --- a/cmvr-es/devices/dexhand/rh56dftp_dexhand/include/rh56dftp_dexhand.h +++ b/cmvr-es/devices/dexhand/rh56dftp_dexhand/include/rh56dftp_dexhand.h @@ -61,6 +61,7 @@ namespace cmvr::device { void execPresetAct(int action_id) override;//这个无用 void setForce(const std::vector& finger_joint_force) override; HandTactileSensors& getSensorData() override; + HandTactileSensors& getSensorData(FingerType finger_type, TactileRegion tactile_region) override; private: void updateState(); void updateSensorData(); diff --git a/cmvr-es/devices/dexhand/rh56dftp_dexhand/src/rh56dftp_dexhand.cpp b/cmvr-es/devices/dexhand/rh56dftp_dexhand/src/rh56dftp_dexhand.cpp index 764ee0c6..da2dfb83 100644 --- a/cmvr-es/devices/dexhand/rh56dftp_dexhand/src/rh56dftp_dexhand.cpp +++ b/cmvr-es/devices/dexhand/rh56dftp_dexhand/src/rh56dftp_dexhand.cpp @@ -35,6 +35,21 @@ const int TOUCH_SENSOR_END_ADDR_PALM = 5123; // Modbus 每次最多读取寄存器的数量 const int MAX_REGISTERS_PER_READ = 125; +namespace { + +std::pair fingerSensorRegisterRange(const FingerType finger_type) { + switch (finger_type) { + case PINKY: return {TOUCH_SENSOR_BASE_ADDR_PINKY, TOUCH_SENSOR_END_ADDR_PINKY}; + case RING: return {TOUCH_SENSOR_BASE_ADDR_RING, TOUCH_SENSOR_END_ADDR_RING}; + case MIDDLE: return {TOUCH_SENSOR_BASE_ADDR_MIDDLE, TOUCH_SENSOR_END_ADDR_MIDDLE}; + case INDEX: return {TOUCH_SENSOR_BASE_ADDR_INDEX, TOUCH_SENSOR_END_ADDR_INDEX}; + case THUMB: return {TOUCH_SENSOR_BASE_ADDR_THUMB, TOUCH_SENSOR_END_ADDR_THUMB}; + } + return {TOUCH_SENSOR_BASE_ADDR_INDEX, TOUCH_SENSOR_END_ADDR_INDEX}; +} + +} // namespace + ModbusController::ModbusController() { // 初始化寄存器字典 regdict = { @@ -443,3 +458,13 @@ HandTactileSensors& RH56DFTPDexhand::getSensorData() return hand_tactile_sensors_; } + +HandTactileSensors& RH56DFTPDexhand::getSensorData(const FingerType finger_type, + const TactileRegion tactile_region) +{ + (void)tactile_region; + const auto [start_addr, end_addr] = fingerSensorRegisterRange(finger_type); + const auto finger_register_values = controller_->readRegisterRange(start_addr, end_addr); + hand_tactile_sensors_.parseFinger(finger_type, finger_register_values); + return hand_tactile_sensors_; +} diff --git a/cmvr-es/devices/motor/ti5_motor/canopen/protocol/ti5_motor_tpdo1.cpp b/cmvr-es/devices/motor/ti5_motor/canopen/protocol/ti5_motor_tpdo1.cpp index a119df4b..57f83aa3 100644 --- a/cmvr-es/devices/motor/ti5_motor/canopen/protocol/ti5_motor_tpdo1.cpp +++ b/cmvr-es/devices/motor/ti5_motor/canopen/protocol/ti5_motor_tpdo1.cpp @@ -26,12 +26,12 @@ void Ti5MotorTPDO1::Parse(const std::uint8_t *bytes, int32_t length, msgs::Robot statusword_t st{}; st.value = motor_status->status_word(); - if (st.op_mode_specific > 0) { - LOG(INFO) << st.op_mode_specific ; - } + // if (st.op_mode_specific > 0) { + // LOG(INFO) << st.op_mode_specific ; + // } - LOG(INFO) << " Motor ID " << int(this->node_id_) << " mode = " << motor_status->run_mode() ; + // LOG(INFO) << " Motor ID " << int(this->node_id_) << " mode = " << motor_status->run_mode() ; diff --git a/cmvr-es/devices/robot/abstract_robot.h b/cmvr-es/devices/robot/abstract_robot.h index 09c5d62b..823caf44 100644 --- a/cmvr-es/devices/robot/abstract_robot.h +++ b/cmvr-es/devices/robot/abstract_robot.h @@ -9,6 +9,7 @@ #include "json/json.h" #include "../abstract_device.h" #include "../../utils/controller/include/cartesian_controller.h" +#include "planner/cartesian_space_planner/include/cartesian_twist_limiter.h" #include "cmvr/msgs/geometry.pb.h" #include "cmvr/msgs/motor.pb.h" @@ -108,7 +109,12 @@ namespace cmvr::device{ virtual void moveL(const std::string &base_link, const std::string &ee_link,msgs::Pose3d target_pose, double vel, double acc) { throw std::runtime_error("Not implemented"); } virtual void moveDeltaL(const std::string &base_link, const std::string &ee_link,msgs::Pose3d delta_pose, double vel, double acc) { throw std::runtime_error("Not implemented"); } - virtual bool speedL(const std::vector &xd, double acceleration = 0.25, double time = 0.0) { throw std::runtime_error("Not implemented"); } + virtual bool speedL(const std::vector &xd, + double acceleration = 0.25, + double time = 0.0, + cmvr::CartesianFrame frame = cmvr::CartesianFrame::Base) { + throw std::runtime_error("Not implemented"); + } virtual void stopSpeedL() { throw std::runtime_error("Not implemented"); } virtual void speedJ(std::string &joint_name, RobotJointIndexDirection dir, double vel, double acc=0.5) { throw std::runtime_error("Not implemented"); } diff --git a/cmvr-es/devices/robot/humanoid_robot/include/humanoid_robot.h b/cmvr-es/devices/robot/humanoid_robot/include/humanoid_robot.h index 0f881368..51bf2d4e 100644 --- a/cmvr-es/devices/robot/humanoid_robot/include/humanoid_robot.h +++ b/cmvr-es/devices/robot/humanoid_robot/include/humanoid_robot.h @@ -94,7 +94,10 @@ namespace cmvr::device{ void speedJ(std::string &joint_name, RobotJointIndexDirection dir, double vel, double acc) override; - bool speedL(const std::vector &xd, double acceleration = 0.25, double time = 0.0) override; + bool speedL(const std::vector &xd, + double acceleration = 0.25, + double time = 0.0, + cmvr::CartesianFrame frame = cmvr::CartesianFrame::Base) override; void stopSpeedL() override; Eigen::Matrix getSpeedLCommandTwistBase(); @@ -215,6 +218,7 @@ namespace cmvr::device{ Eigen::Matrix speedl_target_twist_{Eigen::Matrix::Zero()}; Eigen::Matrix speedl_last_command_twist_base_{Eigen::Matrix::Zero()}; double speedl_target_acceleration_{0.25}; + cmvr::CartesianFrame speedl_target_frame_{cmvr::CartesianFrame::Base}; double speedl_applied_acceleration_{0.25}; std::uint64_t speedl_command_version_{0}; diff --git a/cmvr-es/devices/robot/humanoid_robot/src/humanoid_robot.cpp b/cmvr-es/devices/robot/humanoid_robot/src/humanoid_robot.cpp index 140febcb..0e4341ce 100644 --- a/cmvr-es/devices/robot/humanoid_robot/src/humanoid_robot.cpp +++ b/cmvr-es/devices/robot/humanoid_robot/src/humanoid_robot.cpp @@ -87,7 +87,10 @@ void HumanoidRobot::init() { template -bool HumanoidRobot::speedL(const std::vector& xd, double acceleration, double time) +bool HumanoidRobot::speedL(const std::vector& xd, + double acceleration, + double time, + cmvr::CartesianFrame frame) { if (!ik_solver_) { throw runtime_error("speedL failed: ik_solver_ is not initialized"); @@ -115,6 +118,7 @@ bool HumanoidRobot::speedL(const std::vector& xd, double accelerati std::lock_guard lock(speedl_mutex_); speedl_target_twist_ = target_twist; speedl_target_acceleration_ = acceleration; + speedl_target_frame_ = frame; speedl_command_active_ = true; command_version = ++speedl_command_version_; } @@ -127,6 +131,7 @@ bool HumanoidRobot::speedL(const std::vector& xd, double accelerati std::lock_guard lock(speedl_mutex_); if (speedl_command_version_ == command_version) { speedl_target_twist_.setZero(); + speedl_target_frame_ = cmvr::CartesianFrame::Base; speedl_command_active_ = true; ++speedl_command_version_; should_stop = true; @@ -149,6 +154,7 @@ void HumanoidRobot::stopSpeedL() { std::lock_guard lock(speedl_mutex_); speedl_target_twist_.setZero(); + speedl_target_frame_ = cmvr::CartesianFrame::Base; speedl_command_active_ = true; ++speedl_command_version_; } @@ -215,6 +221,7 @@ void HumanoidRobot::stopSpeedLWorker_() { speedl_stop_requested_.store(true); speedl_command_active_ = false; speedl_target_twist_.setZero(); + speedl_target_frame_ = cmvr::CartesianFrame::Base; speedl_last_command_twist_base_.setZero(); } speedl_cv_.notify_all(); @@ -272,6 +279,7 @@ void HumanoidRobot::speedLWorkerLoop_() { while (true) { Eigen::Matrix target_twist = Eigen::Matrix::Zero(); double acceleration = 0.25; + cmvr::CartesianFrame target_frame = cmvr::CartesianFrame::Base; { std::unique_lock lock(speedl_mutex_); speedl_cv_.wait(lock, [&]() { @@ -283,6 +291,7 @@ void HumanoidRobot::speedLWorkerLoop_() { } target_twist = speedl_target_twist_; acceleration = speedl_target_acceleration_; + target_frame = speedl_target_frame_; } next_tick = std::chrono::steady_clock::now(); @@ -300,6 +309,7 @@ void HumanoidRobot::speedLWorkerLoop_() { } target_twist = speedl_target_twist_; acceleration = speedl_target_acceleration_; + target_frame = speedl_target_frame_; } if (!updateSpeedLAccelerationConfig_(acceleration)) { @@ -323,7 +333,7 @@ void HumanoidRobot::speedLWorkerLoop_() { dt, q_now, qd_cmd, - cmvr::CartesianFrame::Base, + target_frame, true)) { LOG(ERROR) << "speedL worker: speedLStep() failed"; send_zero(); diff --git a/cmvr-es/devices/robot/humanoid_robot/src/humanoid_robot_test.cpp b/cmvr-es/devices/robot/humanoid_robot/src/humanoid_robot_test.cpp index 22c7e947..eb26e14c 100644 --- a/cmvr-es/devices/robot/humanoid_robot/src/humanoid_robot_test.cpp +++ b/cmvr-es/devices/robot/humanoid_robot/src/humanoid_robot_test.cpp @@ -22,8 +22,11 @@ #include "../../../../device_manager/include/device_manager.h" #include +#include "common/utils/image/image_process.h" #include "controller/include/ibvs_controller.h" #include "ik_solver/include/pinocchio_dls_ik_solver.h" +#include "perception/include/apriltag_perception.h" +#include "perception/include/tag_relative_target_3d.h" #include "cmvr/msgs/can_card_parameter.grpc.pb.h" #include "../include/humanoid_robot.h" #include "motor/ti5_motor/canopen/ti5_motor_canopen_protocol.h" @@ -78,188 +81,16 @@ std::unordered_map makeRightArmQMap() { return q_map; } -constexpr std::array kRightArmQMin = { - -3.14, // R_SHOULDER_P - -0.78, // R_SHOULDER_R - -3.14, // R_SHOULDER_Y - 0.00, // R_ELBOW_R - -3.14, // R_WRIST_P - -0.78, // R_WRIST_Y - -0.55 // R_WRIST_R -}; - -constexpr std::array kRightArmQMax = { - 3.14, // R_SHOULDER_P - 1.57, // R_SHOULDER_R - 3.14, // R_SHOULDER_Y - 2.05, // R_ELBOW_R - 3.14, // R_WRIST_P - 0.78, // R_WRIST_Y - 1.57 // R_WRIST_R -}; - -double applySoftJointLimitVelocity(double q, - double v, - double q_min, - double q_max, - double soft_margin, - double hard_margin) { - if (q_max <= q_min) return 0.0; - const double hard = std::max(1e-4, hard_margin); - const double soft = std::max(hard + 1e-4, soft_margin); - - if (v < 0.0) { - const double q_hard = q_min + hard; - const double q_soft = q_min + soft; - if (q <= q_hard) return 0.0; - if (q < q_soft) { - const double s = std::clamp((q - q_hard) / (q_soft - q_hard), 0.0, 1.0); - return v * s; - } - } else if (v > 0.0) { - const double q_hard = q_max - hard; - const double q_soft = q_max - soft; - if (q >= q_hard) return 0.0; - if (q > q_soft) { - const double s = std::clamp((q_hard - q) / (q_hard - q_soft), 0.0, 1.0); - return v * s; - } - } - return v; -} - } // namespace -TEST(HumanoidRobotTest,GetState) { - std::string config_path = "/home/lgv/cmvr/cmvr-es/config/cabin_robot.xml"; - const XmlNode config(config_path); - - if (!config.hasChild("DeviceManager")){ - LOG(ERROR) << "Device Manager node not found"; - } - auto dmgr_cfg = config.getChild("DeviceManager"); - auto &dmgr = DeviceManager::getInstance(dmgr_cfg); - - auto robot = dmgr.getDevice("hc01"); - robot->torqueOff(); - std::vector joints; - - while (true) { - joints.clear(); - robot->getJointsState(joints); - for (const auto &js : joints) { - LOG(INFO) << "Joint: " << js.name - << ", Position: " << js.position - << ", Velocity: " << js.velocity - << std::endl; - } - std::this_thread::sleep_for(std::chrono::milliseconds(1000)); - } -} - - -TEST(HumanoidRobotTest,MyRobotTest) { - - std::string config_path = "/home/lgv/cmvr/cmvr-es/config/cabin_robot.xml"; - const XmlNode config(config_path); - - if (!config.hasChild("DeviceManager")){ - LOG(ERROR) << "Device Manager node not found"; - } - auto dmgr_cfg = config.getChild("DeviceManager"); - auto &dmgr = DeviceManager::getInstance(dmgr_cfg); - - auto robot = dmgr.getDevice("hc01"); - robot->calibrateZeroQ("R_WRIST_R"); - - // std::vector cmd{}; - -} - -TEST(HumanoidRobotTest,ServoJAndGetJointQSmokeTest) { - const XmlNode config("/home/lgv/cmvr/cmvr-es/cmvr-es/common/config/cabin_robot.xml"); - ASSERT_TRUE(config.hasChild("DeviceManager")) << "DeviceManager node not found"; - auto dmgr_cfg = config.getChild("DeviceManager"); - auto& dmgr = DeviceManager::getInstance(dmgr_cfg); - - auto robot = dmgr.getDevice("hc01"); - - auto q_map_now = makeRightArmQMap(); - std::vector q_now(kRightArmJointNames.size(), 0.0); - auto refreshRightArmQ = [&]() -> bool { - robot->getJointQ(q_map_now); - for (size_t i = 0; i < kRightArmJointNames.size(); ++i) { - const auto it = q_map_now.find(kRightArmJointNames[i]); - if (it == q_map_now.end()) return false; - q_now[i] = it->second; - } - return true; - }; - ASSERT_TRUE(refreshRightArmQ()) << "Failed to read right-arm joint q"; - std::vector cmd{}; - cmd = { - // {"R_SHOULDER_P", 0.0}, - // {"R_SHOULDER_R", 0.0}, - // {"R_SHOULDER_Y", 0.0}, - // {"R_ELBOW_R", 0.0}, - // {"R_WRIST_P", 0.0}, - // {"R_WRIST_Y", 0.0}, - {"R_WRIST_R", 0.2}, - }; - // auto joint_cmd = buildRightArmJointCmd(q_now, 0.8); - ASSERT_NO_THROW(robot->servoJ(cmd, 0.8, 0.01)); - while (true) { - std::this_thread::sleep_for(std::chrono::milliseconds(10000)); - } - -} - - -TEST(HumanoidRobotTest,speedJTest) { - const XmlNode config("/home/lgv/cmvr/cmvr-es/cmvr-es/common/config/cabin_robot.xml"); - ASSERT_TRUE(config.hasChild("DeviceManager")) << "DeviceManager node not found"; - auto dmgr_cfg = config.getChild("DeviceManager"); - auto& dmgr = DeviceManager::getInstance(dmgr_cfg); - - auto robot = dmgr.getDevice("hc01"); - std::vector cmd{}; - cmd = { - // {"R_SHOULDER_P", 0.0}, - // {"R_SHOULDER_R", 0.0}, - // {"R_SHOULDER_Y", 0.0}, - // {"R_ELBOW_R", 0.0}, - // {"R_WRIST_P", 0.0}, - // {"R_WRIST_Y", 0.0}, - {"R_WRIST_R", 0.2}, - }; - robot->speedJ(cmd); - std::this_thread::sleep_for(std::chrono::milliseconds(1000)); - // robot->speedJ(0); - // - std::vector cmd1{}; - cmd1 = { - // {"R_SHOULDER_P", 0.0}, - // {"R_SHOULDER_R", 0.0}, - // {"R_SHOULDER_Y", 0.0}, - // {"R_ELBOW_R", 0.0}, - // {"R_WRIST_P", 0.0}, - // {"R_WRIST_Y", 0.0}, - {"R_WRIST_R", 0}, - }; - robot->moveJ(cmd1,2.0,10.0); - // robot->eStop(); - - // -} - TEST(HumanoidRobotTest, speedLSmokeTest) { const XmlNode config("/home/lgv/cmvr/cmvr-es/cmvr-es/common/config/cabin_robot.xml"); ASSERT_TRUE(config.hasChild("DeviceManager")) << "DeviceManager node not found"; @@ -283,8 +114,8 @@ TEST(HumanoidRobotTest, speedLSmokeTest) { robot->moveJ(init_cmd,1.0,2.0); std::this_thread::sleep_for(std::chrono::milliseconds(1000)); - const std::vector twist_pos = {0.08, 0, 0, 0.00, 0, 0}; - const std::vector twist_neg = {-0.08, 0, 0, 0.00, 0, 0}; + const std::vector twist_pos = {0.00, -0.08, 0, 0.00, 0, 0}; + const std::vector twist_neg = {0.00, 0.08, 0, 0.00, 0, 0}; const double acceleration = 3.0; const double segment_time = 1.0; const double settle_time = 1.0; @@ -302,7 +133,7 @@ TEST(HumanoidRobotTest, speedLSmokeTest) { double duration, const char* phase) { LOG(INFO) << "speedLSmokeTest phase: " << phase; - ASSERT_NO_THROW(robot->speedL(target_twist, acceleration, 0.0)); + ASSERT_NO_THROW(robot->speedL(target_twist, acceleration, 0.0, cmvr::CartesianFrame::Tool)); const int steps = static_cast(std::ceil(duration / sample_dt)); for (int i = 0; i < steps; ++i) { const Eigen::Matrix cmd_twist = robot->getSpeedLCommandTwistBase(); @@ -379,177 +210,6 @@ TEST(HumanoidRobotTest, speedLSmokeTest) { // show(fig); } -TEST(HumanoidRobotTest, SpeedLOpenLoopPlannerPlot) { - cmvr::PinocchioDlsIKSolver solver( - "/home/lgv/cmvr/0-workspace/cmvr-es/model/xiaoyan_description/dual_arm.urdf", - "PELVIS_S", - "R_WRIST_R_S", - "R_FINGER_TIP_FIXED"); - ASSERT_TRUE(solver.init()) << "PinocchioDlsIKSolver init failed"; - - cmvr::PinocchioDlsIKSolver::SpeedLConfig speedl_config; - speedl_config.linear_velocity_max = 0.55; - speedl_config.linear_acceleration_max = 0.80; - speedl_config.linear_jerk_max = 3.30; - speedl_config.angular_velocity_max = 1.00; - speedl_config.angular_acceleration_max = 3.00; - speedl_config.angular_jerk_max = 12.0; - speedl_config.joint_acceleration_max = std::vector(7, 8.0); - speedl_config.linear_target_replan_threshold = 1e-4; - speedl_config.angular_target_replan_threshold = 1e-4; - speedl_config.linear_reverse_cos_threshold = -0.8660254037844386; - speedl_config.linear_reverse_switch_speed_threshold = 1e-3; - ASSERT_TRUE(solver.configureSpeedL(speedl_config)) << "configureSpeedL failed"; - - std::vector q_init = {0.25, 1.00, M_PI / 2, M_PI / 2, -M_PI / 2, 0, 0}; - solver.update_joints_state(q_init); - - const double dt = 0.002; - const int log_every = 50; - const double segment_time = 1.5; - const double stop_time = 1.0; - const double settle_time = 1.0; - const double linear_speed_cmd = 0.1; - const double z_speed_cmd = 0.10; - const double total_time = 2.0 * segment_time + stop_time; - const int active_steps = static_cast(std::ceil(total_time / dt)); - const int settle_steps = static_cast(std::ceil(settle_time / dt)); - const int total_steps = active_steps + settle_steps; - - int ok_steps = 0; - std::vector t_trace; - std::vector target_vy_trace; - std::vector target_vz_trace; - std::vector command_vx_trace; - std::vector command_vy_trace; - std::vector command_vz_trace; - std::vector> qd_cmd_trace(kRightArmJointNames.size()); - t_trace.reserve(total_steps); - target_vy_trace.reserve(total_steps); - target_vz_trace.reserve(total_steps); - command_vx_trace.reserve(total_steps); - command_vy_trace.reserve(total_steps); - command_vz_trace.reserve(total_steps); - for (auto& v : qd_cmd_trace) v.reserve(total_steps); - - for (int step = 0; step < total_steps; ++step) { - const double t = static_cast(step) * dt; - - Eigen::Matrix target_twist = Eigen::Matrix::Zero(); - if (t < segment_time) { - target_twist[1] = linear_speed_cmd; - target_twist[2] = z_speed_cmd; - - } else if (t < 2.0 * segment_time) { - target_twist[1] = -linear_speed_cmd; - target_twist[2] = -z_speed_cmd; - } else if (t < total_time) { - target_twist.setZero(); - } else { - target_twist.setZero(); - } - - std::vector qd_cmd; - ASSERT_TRUE(solver.speedLStep(target_twist, - dt, - qd_cmd, - cmvr::CartesianFrame::Base, - true)) - << "speedLStep failed at step " << step; - - t_trace.push_back(t); - target_vy_trace.push_back(target_twist[1]); - target_vz_trace.push_back(target_twist[2]); - command_vx_trace.push_back(solver.getSpeedLCommandTwistBase()[0]); - command_vy_trace.push_back(solver.getSpeedLCommandTwistBase()[1]); - command_vz_trace.push_back(solver.getSpeedLCommandTwistBase()[2]); - for (size_t i = 0; i < kRightArmJointNames.size(); ++i) { - qd_cmd_trace[i].push_back(i < qd_cmd.size() ? qd_cmd[i] : 0.0); - } - - ++ok_steps; - - if ((step % log_every) == 0) { - std::cout << "[SPEEDL_OPEN_LOOP] step=" << step - << " t=" << t - << " target_vy=" << target_twist[1] - << " target_vz=" << target_twist[2] - << " phase=" - << (t < segment_time ? "pos" : - (t < 2.0 * segment_time ? "neg" : - (t < total_time ? "stop" : "settle"))) - << " cmd_qd="; - for (const auto& v : qd_cmd) { - std::cout << v << " "; - } - std::cout << std::endl; - } - } - - if (!t_trace.empty()) { - using namespace matplot; - auto fig = figure(true); - fig->size(1600, 1200); - fig->font_size(16); - - auto ax1 = subplot(3, 1, 0); - hold(ax1, true); - auto l_target_y = plot(ax1, t_trace, target_vy_trace, "k--"); - l_target_y->line_width(2.0f); - auto l_target_z = plot(ax1, t_trace, target_vz_trace, "c--"); - l_target_z->line_width(2.0f); - auto l_cmd_x = plot(ax1, t_trace, command_vx_trace, "r-"); - l_cmd_x->line_width(2.0f); - auto l_cmd_y = plot(ax1, t_trace, command_vy_trace, "g-"); - l_cmd_y->line_width(2.0f); - auto l_cmd_z = plot(ax1, t_trace, command_vz_trace, "b-"); - l_cmd_z->line_width(2.0f); - title(ax1, "speedL target vy/vz vs command vxyz"); - xlabel(ax1, "time [s]"); - ylabel(ax1, "linear cmd [m/s]"); - legend(ax1, {"target vy", "target vz", "command vx", "command vy", "command vz"}); - grid(ax1, true); - - auto ax2 = subplot(3, 1, 1); - hold(ax2, true); - std::vector joint_labels; - joint_labels.reserve(kRightArmJointNames.size()); - for (size_t i = 0; i < kRightArmJointNames.size(); ++i) { - auto line = plot(ax2, t_trace, qd_cmd_trace[i]); - line->line_width(1.8f); - joint_labels.emplace_back(kRightArmJointNames[i]); - } - title(ax2, "solver qd_cmd"); - xlabel(ax2, "time [s]"); - ylabel(ax2, "joint vel [rad/s]"); - legend(ax2, joint_labels); - grid(ax2, true); - - auto ax3 = subplot(3, 1, 2); - hold(ax3, true); - std::vector qd_norm_trace; - qd_norm_trace.reserve(t_trace.size()); - for (size_t k = 0; k < t_trace.size(); ++k) { - double norm = 0.0; - for (size_t i = 0; i < kRightArmJointNames.size(); ++i) { - const double v = qd_cmd_trace[i][k]; - norm += v * v; - } - qd_norm_trace.push_back(std::sqrt(norm)); - } - auto qd_norm_line = plot(ax3, t_trace, qd_norm_trace, "m-"); - qd_norm_line->line_width(2.0f); - title(ax3, "solver qd_cmd norm"); - xlabel(ax3, "time [s]"); - ylabel(ax3, "norm [rad/s]"); - legend(ax3, {"||qd_cmd||"}); - grid(ax3, true); - - show(fig); - } - - EXPECT_GT(ok_steps, 0) << "No successful open-loop speedL steps."; -} TEST(HumanoidRobotTest,IBVSWithRealRobot) { const XmlNode config("/home/lgv/cmvr/cmvr-es/cmvr-es/common/config/cabin_robot.xml"); @@ -558,7 +218,7 @@ TEST(HumanoidRobotTest,IBVSWithRealRobot) { auto& dmgr = DeviceManager::getInstance(dmgr_cfg); auto robot = dmgr.getDevice("hc01"); - auto camera = dmgr.getDevice("cam2"); + auto camera = dmgr.getDevice("right_hand_cam"); ASSERT_NE(robot, nullptr); ASSERT_NE(camera, nullptr); @@ -595,28 +255,45 @@ TEST(HumanoidRobotTest,IBVSWithRealRobot) { } } guard{camera, robot}; + const double tag_size_m = 0.12; + const int tracked_tag_id = 0; + const double target_x = 0.0; + const double target_y = 0.0; + const double target_z = 0.40; + const cmvr::IbvsController::DepthMode ibvs_depth_mode = cmvr::IbvsController::DepthMode::MONOCULAR; + + auto perception = std::make_shared(camera); + perception->setTagSize(tag_size_m); + cmvr::perception::AprilTagPerception::Options perception_opt; + switch (ibvs_depth_mode) { + case cmvr::IbvsController::DepthMode::MONOCULAR: + perception_opt.depth_policy = cmvr::perception::AprilTagPerception::DepthPolicy::NONE; + break; + case cmvr::IbvsController::DepthMode::PREFER_DEPTH: + perception_opt.depth_policy = cmvr::perception::AprilTagPerception::DepthPolicy::PREFER; + break; + case cmvr::IbvsController::DepthMode::DEPTH_ONLY: + perception_opt.depth_policy = cmvr::perception::AprilTagPerception::DepthPolicy::REQUIRE; + break; + } + cmvr::IbvsController ibvs_controller; ibvs_controller.setMu(0.1); ibvs_controller.setLambda(0.6); - ibvs_controller.setQdotMax(0.6); - ibvs_controller.setJointLimitAvoidance(true, 0.2, 0.15, 0.25); - ibvs_controller.setTrackedTagId(0); - ibvs_controller.setTarget(0.0, 0.0, 0.40); - ibvs_controller.setDepthMode(cmvr::IbvsController::DepthMode::MONOCULAR); + ibvs_controller.setTrackedTagId(tracked_tag_id); + ibvs_controller.setTarget(target_x, target_y, target_z); + ibvs_controller.setDepthMode(ibvs_depth_mode); ibvs_controller.setDepthZGain(1.0); - Eigen::Matrix3d I = Eigen::Matrix3d::Identity(); - // ibvs_controller.setAlignCameraToVisp(I); // RealSense optical -> ViSP - // ibvs_controller.setAlignCameraToUrdf(I); // optical -> URDF 的 R_CAM - Eigen::Matrix3d RxPi; - RxPi << 1,0,0, - 0,-1,0, - 0,0,-1; - ibvs_controller.setAlignCameraToUrdf(RxPi); - ibvs_controller.setAlignCameraToVisp(RxPi); + // Eigen::Matrix3d RxPi; + // RxPi << 1,0,0, + // 0,-1,0, + // 0,0,-1; + // ibvs_controller.setAlignCameraToUrdf(RxPi); + // ibvs_controller.setAlignCameraToVisp(RxPi); - // ibvs_controller.setVelocityLimit6({0.15, 0.15, 0.20, 0, 0, 0}); + // ibvs_controller.setVelocityLimit6({0.3, 0.3, 0.3, 0.3, 0.3, 0.3}); // ibvs_controller.setVelocityLimit6({0.03,0.03,0.03,0.005,0.005,0.005}); ibvs_controller.setQdotMax(0.15); ibvs_controller.setJointLimitAvoidance(true, 0.2, 0.15, 0.25); @@ -629,6 +306,7 @@ TEST(HumanoidRobotTest,IBVSWithRealRobot) { "R_WRIST_R_S", "R_CAM")) << "IbvsController init failed"; + ibvs_controller.setPerception(perception); auto q_map_now = makeRightArmQMap(); robot->getJointQ(q_map_now); @@ -653,23 +331,25 @@ TEST(HumanoidRobotTest,IBVSWithRealRobot) { const int max_steps = 30000; const int log_every = 1; const int cycle_ms = 10; - const double soft_margin = 0.20; // rad - const double hard_margin = 0.04; // rad - const double qdot_deadband = 0.01; // rad/s - const double qdot_lpf_alpha = 0.75; - const double qdot_acc_limit = 1.5; // rad/s^2 - const double qdot_max_send = 0.25; // rad/s int ok_steps = 0; int fail_steps = 0; - std::vector qdot_lpf(kRightArmJointNames.size(), 0.0); - std::vector qdot_cmd_prev(kRightArmJointNames.size(), 0.0); - auto t_prev = std::chrono::steady_clock::now(); + int perception_fail_steps = 0; + int compute_fail_steps = 0; + auto sendZeroVelocity = [&]() { + robot->speedJ(0); + }; + + for (int i = 0; i < 10; ++i) { + perception->update(perception_opt); + std::this_thread::sleep_for(std::chrono::milliseconds(30)); + } for (int step = 0; step < max_steps; ++step) { robot->getJointQ(q_map_now); if (!refreshRightArmQ()) { ++fail_steps; + sendZeroVelocity(); if ((step % log_every) == 0) { std::cout << "[IBVS_REAL] step=" << step << " joint extract failed\n"; } @@ -677,33 +357,49 @@ TEST(HumanoidRobotTest,IBVSWithRealRobot) { continue; } - const auto t_now = std::chrono::steady_clock::now(); - double dt = std::chrono::duration(t_now - t_prev).count(); - t_prev = t_now; - dt = std::clamp(dt, 0.001, 0.1); + const bool perception_ok = perception->update(perception_opt); + if (!perception_ok) { + ++perception_fail_steps; + ++fail_steps; + sendZeroVelocity(); + if ((step % log_every) == 0) { + std::cout << "[IBVS_REAL] step=" << step + << " perception failed: " + << cmvr::perception::AprilTagPerception::statusToString(perception->lastStatus()) + << std::endl; + } + std::this_thread::sleep_for(std::chrono::milliseconds(cycle_ms)); + continue; + } std::vector qdot_next; const bool ok = ibvs_controller.compute(q_now, qdot_next); - - + if (!ok) { + ++compute_fail_steps; + ++fail_steps; + sendZeroVelocity(); + if ((step % log_every) == 0) { + std::cout << "[IBVS_REAL] step=" << step + << " compute failed: " + << cmvr::IbvsController::statusToString(ibvs_controller.lastComputeStatus()); + if (perception->hasTags()) { + std::cout << " visible_tags="; + for (const auto& tag : perception->tags()) { + std::cout << tag.id << " "; + } + } + std::cout << std::endl; + } + std::this_thread::sleep_for(std::chrono::milliseconds(cycle_ms)); + continue; + } std::vector qd_send; for (size_t i = 0; i < kRightArmJointNames.size(); ++i) { - qdot_lpf[i] = qdot_lpf_alpha * qdot_lpf[i] + (1.0 - qdot_lpf_alpha) * qdot_next[i]; - double v = applySoftJointLimitVelocity(q_now[i], qdot_lpf[i], - kRightArmQMin[i], kRightArmQMax[i], - soft_margin, hard_margin); - if (std::abs(v) < qdot_deadband) { - v = 0.0; - } - const double dv_max = qdot_acc_limit * dt; - v = std::clamp(v, qdot_cmd_prev[i] - dv_max, qdot_cmd_prev[i] + dv_max); - v = std::clamp(v, -qdot_max_send, qdot_max_send); - qdot_cmd_prev[i] = v; - qd_send.push_back({kRightArmJointNames[i],v}); + qd_send.push_back({kRightArmJointNames[i], qdot_next[i]}); } - robot->speedJ(qd_send); + robot->speedJ(qd_send); ++ok_steps; if ((step % log_every) == 0) { @@ -728,9 +424,315 @@ TEST(HumanoidRobotTest,IBVSWithRealRobot) { std::cout << "[IBVS_REAL] finished steps=" << max_steps << " ok_steps=" << ok_steps << " fail_steps=" << fail_steps + << " perception_fail_steps=" << perception_fail_steps + << " compute_fail_steps=" << compute_fail_steps << std::endl; robot->speedJ(0); EXPECT_GT(ok_steps, 0) << "No successful IBVS control steps."; } + +TEST(HumanoidRobotTest,IBVSWithRealRobotTrackedPoint) { + const XmlNode config("/home/lgv/cmvr/cmvr-es/cmvr-es/common/config/cabin_robot.xml"); + ASSERT_TRUE(config.hasChild("DeviceManager")) << "DeviceManager node not found"; + auto dmgr_cfg = config.getChild("DeviceManager"); + auto& dmgr = DeviceManager::getInstance(dmgr_cfg); + + auto robot = dmgr.getDevice("hc01"); + auto camera = dmgr.getDevice("right_hand_cam"); + ASSERT_NE(robot, nullptr); + ASSERT_NE(camera, nullptr); + + std::vector init_cmd{}; + init_cmd = { + {"R_SHOULDER_P", -0.2423}, + {"R_SHOULDER_R", 1.2929}, + {"R_SHOULDER_Y", 1.61}, + {"R_ELBOW_R", 1.58}, + {"R_WRIST_P", -2.8792}, + {"R_WRIST_Y", 0.1150}, + {"R_WRIST_R", -0.08}, + }; + robot->moveJ(init_cmd, 1.0, 2.0); + std::this_thread::sleep_for(std::chrono::milliseconds(1000)); + + ASSERT_NO_THROW(camera->start()); + struct RuntimeGuard { + std::shared_ptr camera; + std::shared_ptr robot; + ~RuntimeGuard() { + if (camera) { + try { + camera->stop(); + } catch (...) { + } + } + if (robot) { + try { + robot->eStop(); + } catch (...) { + } + } + } + } guard{camera, robot}; + + const double tag_size_m = 0.12; + const int preferred_tag_id = 0; + const Eigen::Vector3d hover_target_in_camera(0.0, 0.0, 0.40); + const cmvr::IbvsController::DepthMode ibvs_depth_mode = cmvr::IbvsController::DepthMode::MONOCULAR; + + auto perception = std::make_shared(camera); + perception->setTagSize(tag_size_m); + cmvr::perception::AprilTagPerception::Options perception_opt; + switch (ibvs_depth_mode) { + case cmvr::IbvsController::DepthMode::MONOCULAR: + perception_opt.depth_policy = cmvr::perception::AprilTagPerception::DepthPolicy::NONE; + break; + case cmvr::IbvsController::DepthMode::PREFER_DEPTH: + perception_opt.depth_policy = cmvr::perception::AprilTagPerception::DepthPolicy::PREFER; + break; + case cmvr::IbvsController::DepthMode::DEPTH_ONLY: + perception_opt.depth_policy = cmvr::perception::AprilTagPerception::DepthPolicy::REQUIRE; + break; + } + + cmvr::perception::TagRelativeTarget3D tracker(perception); + tracker.setTargetPointMethod(cmvr::perception::TagRelativeTarget3D::TargetPointMethod::TAG_PLANE); + tracker.setActiveTagSwitchPolicy(4, 1.2); + tracker.setTrackingCandidateScoreWeights(1.0, 1.0, 0.08); + + cmvr::IbvsController ibvs_controller; + ibvs_controller.setMu(0.1); + ibvs_controller.setLambda(0.6); + ibvs_controller.setDepthMode(ibvs_depth_mode); + ibvs_controller.setDepthZGain(1.0); + + ibvs_controller.setQdotMax(0.15); + ibvs_controller.setJointLimitAvoidance(true, 0.2, 0.15, 0.25); + + ASSERT_TRUE(ibvs_controller.init( + "/home/lgv/cmvr/cmvr-es/model/xiaoyan_description/dual_arm.urdf", + "PELVIS_S", + "R_WRIST_R_S", + "R_CAM")) + << "IbvsController init failed"; + ibvs_controller.setPerception(perception); + + auto q_map_now = makeRightArmQMap(); + robot->getJointQ(q_map_now); + std::vector q_now(kRightArmJointNames.size(), 0.0); + auto refreshRightArmQ = [&]() -> bool { + for (size_t i = 0; i < kRightArmJointNames.size(); ++i) { + const auto it = q_map_now.find(kRightArmJointNames[i]); + if (it == q_map_now.end()) { + return false; + } + q_now[i] = it->second; + } + return true; + }; + + ASSERT_TRUE(refreshRightArmQ()) + << "Failed to extract right-arm 7 joints from robot state"; + ibvs_controller.reset(q_now); + + auto sendZeroVelocity = [&]() { + robot->speedJ(0); + }; + + auto projectPreferredTagCenter = [&](int& u, int& v) -> bool { + const auto* tag = perception->findTag(preferred_tag_id); + if (!tag) return false; + Eigen::Vector2d uv = Eigen::Vector2d::Zero(); + const Eigen::Vector3d p_c_tag = tag->T_c_t.block<3, 1>(0, 3); + if (!cmvr::ImageProcess::projectCameraPointToPixel(perception->intrinsics(), p_c_tag, uv)) { + return false; + } + u = static_cast(std::lround(uv.x())); + v = static_cast(std::lround(uv.y())); + const cv::Mat& color = perception->color(); + return !color.empty() && u >= 0 && v >= 0 && u < color.cols && v < color.rows; + }; + + for (int i = 0; i < 10; ++i) { + perception->update(perception_opt); + std::this_thread::sleep_for(std::chrono::milliseconds(30)); + } + + const int max_steps = 30000; + const int log_every = 1; + const int cycle_ms = 10; + + int ok_steps = 0; + int fail_steps = 0; + int perception_fail_steps = 0; + int tracking_fail_steps = 0; + int compute_fail_steps = 0; + bool target_locked = false; + bool ibvs_target_initialized = false; + int lock_u = -1; + int lock_v = -1; + + for (int step = 0; step < max_steps; ++step) { + robot->getJointQ(q_map_now); + if (!refreshRightArmQ()) { + ++fail_steps; + sendZeroVelocity(); + if ((step % log_every) == 0) { + std::cout << "[IBVS_REAL_TRACK] step=" << step << " joint extract failed\n"; + } + std::this_thread::sleep_for(std::chrono::milliseconds(cycle_ms)); + continue; + } + + const bool perception_ok = perception->update(perception_opt); + if (!perception_ok) { + ++perception_fail_steps; + ++fail_steps; + sendZeroVelocity(); + if ((step % log_every) == 0) { + std::cout << "[IBVS_REAL_TRACK] step=" << step + << " perception failed: " + << cmvr::perception::AprilTagPerception::statusToString(perception->lastStatus()) + << std::endl; + } + std::this_thread::sleep_for(std::chrono::milliseconds(cycle_ms)); + continue; + } + + bool tracking_ok = false; + if (!target_locked) { + if (projectPreferredTagCenter(lock_u, lock_v)) { + tracking_ok = tracker.startTrackingFromPixel(lock_u, lock_v); + if (tracking_ok) { + target_locked = true; + ibvs_target_initialized = false; + if ((step % log_every) == 0) { + std::cout << "[IBVS_REAL_TRACK] step=" << step + << " lock point success" + << " uv=(" << lock_u << ", " << lock_v << ")" + << " active_tag=" << tracker.activeTagId() + << std::endl; + } + } + } + } else { + tracking_ok = tracker.track(); + } + + if (!tracking_ok) { + ++tracking_fail_steps; + ++fail_steps; + sendZeroVelocity(); + if ((step % log_every) == 0) { + std::cout << "[IBVS_REAL_TRACK] step=" << step + << " tracker failed: " + << cmvr::perception::TagRelativeTarget3D::statusToString(tracker.lastStatus()); + if (perception->hasTags()) { + std::cout << " visible_tags="; + for (const auto& tag : perception->tags()) { + std::cout << tag.id << " "; + } + } + std::cout << std::endl; + } + std::this_thread::sleep_for(std::chrono::milliseconds(cycle_ms)); + continue; + } + + const int active_tag_id = tracker.activeTagId(); + Eigen::Vector3d p_t_target = Eigen::Vector3d::Zero(); + if (active_tag_id < 0 || !tracker.getAnchorInTag(active_tag_id, p_t_target)) { + ++tracking_fail_steps; + ++fail_steps; + sendZeroVelocity(); + if ((step % log_every) == 0) { + std::cout << "[IBVS_REAL_TRACK] step=" << step + << " anchor unavailable, active_tag=" << active_tag_id + << std::endl; + } + std::this_thread::sleep_for(std::chrono::milliseconds(cycle_ms)); + continue; + } + + ibvs_controller.setTrackedTagId(active_tag_id); + if (!ibvs_target_initialized || tracker.lastSwitched()) { + if (!ibvs_controller.setTargetFromPointInTag(p_t_target, hover_target_in_camera)) { + ++tracking_fail_steps; + ++fail_steps; + sendZeroVelocity(); + if ((step % log_every) == 0) { + std::cout << "[IBVS_REAL_TRACK] step=" << step + << " setTargetFromPointInTag failed" + << std::endl; + } + std::this_thread::sleep_for(std::chrono::milliseconds(cycle_ms)); + continue; + } + ibvs_target_initialized = true; + if ((step % log_every) == 0) { + std::cout << "[IBVS_REAL_TRACK] step=" << step + << " setTargetFromPointInTag ok" + << " active_tag=" << active_tag_id + << " p_t_target=[" << p_t_target.x() << ", " + << p_t_target.y() << ", " << p_t_target.z() << "]" + << std::endl; + } + } + + std::vector qdot_next; + const bool ok = ibvs_controller.compute(q_now, qdot_next); + if (!ok) { + ++compute_fail_steps; + ++fail_steps; + sendZeroVelocity(); + if ((step % log_every) == 0) { + std::cout << "[IBVS_REAL_TRACK] step=" << step + << " compute failed: " + << cmvr::IbvsController::statusToString(ibvs_controller.lastComputeStatus()) + << " active_tag=" << active_tag_id + << std::endl; + } + std::this_thread::sleep_for(std::chrono::milliseconds(cycle_ms)); + continue; + } + + std::vector qd_send; + for (size_t i = 0; i < kRightArmJointNames.size(); ++i) { + qd_send.push_back({kRightArmJointNames[i], qdot_next[i]}); + } + + robot->speedJ(qd_send); + ++ok_steps; + + if ((step % log_every) == 0) { + const auto& t_co = ibvs_controller.lastTagPositionVisp(); + const auto& v_c = ibvs_controller.lastCameraTwistVisp(); + const auto& p_c_target = tracker.lastTargetInCamera(); + std::cout << "[IBVS_REAL_TRACK] step=" << step + << " active_tag=" << active_tag_id + << " switched=" << static_cast(tracker.lastSwitched()) + << " target_p_c=[" << p_c_target.x() << ", " << p_c_target.y() << ", " << p_c_target.z() << "]" + << " tag=[" << t_co.x() << ", " << t_co.y() << ", " << t_co.z() << "]" + << " v_c=[" << v_c[0] << ", " << v_c[1] << ", " << v_c[2] + << ", " << v_c[3] << ", " << v_c[4] << ", " << v_c[5] << "]" + << " z_source=" << cmvr::IbvsController::depthUsageToString(ibvs_controller.lastDepthUsage()) + << std::endl; + } + + std::this_thread::sleep_for(std::chrono::milliseconds(cycle_ms)); + } + + std::cout << "[IBVS_REAL_TRACK] finished steps=" << max_steps + << " ok_steps=" << ok_steps + << " fail_steps=" << fail_steps + << " perception_fail_steps=" << perception_fail_steps + << " tracking_fail_steps=" << tracking_fail_steps + << " compute_fail_steps=" << compute_fail_steps + << std::endl; + + robot->speedJ(0); + + EXPECT_GT(ok_steps, 0) << "No successful tracked-point IBVS control steps."; +} diff --git a/cmvr-es/service/CMakeLists.txt b/cmvr-es/service/CMakeLists.txt index 27c95800..4ef50063 100644 --- a/cmvr-es/service/CMakeLists.txt +++ b/cmvr-es/service/CMakeLists.txt @@ -18,6 +18,7 @@ target_link_libraries(service PRIVATE osqp cmvr_es::device_manager cmvr_es::controller + cmvr_es::applications protobuf::libprotobuf ) @@ -81,4 +82,3 @@ target_link_libraries(grpc_hlc_client_test fcl cmvr_es::device_manager ) - diff --git a/cmvr-es/service/grpc/include/grpc_hlc_service.h b/cmvr-es/service/grpc/include/grpc_hlc_service.h index 01193c8c..14675e8f 100644 --- a/cmvr-es/service/grpc/include/grpc_hlc_service.h +++ b/cmvr-es/service/grpc/include/grpc_hlc_service.h @@ -3,7 +3,10 @@ // #pragma once -#include "device_manager/include/device_manager.h" +#include +#include + +#include "applications/include/touch_screen_app.h" #include "cmvr/api/hlc_service.grpc.pb.h" namespace cmvr { @@ -15,7 +18,11 @@ namespace cmvr { grpc::Status touch(grpc::ServerContext *context, const cmvr::api::Touch_Request *request, cmvr::api::Touch_Response *response) override; private: - device::DeviceManager& dmgr_; + bool ensureTouchAppInitialized(std::string& error_message); + + std::mutex touch_mutex_; + app::TouchScreenApp touch_app_; + bool touch_app_initialized_{false}; }; diff --git a/cmvr-es/service/grpc/src/grpc_hlc_client_test.cpp b/cmvr-es/service/grpc/src/grpc_hlc_client_test.cpp index 25df06ec..9fc65ef0 100644 --- a/cmvr-es/service/grpc/src/grpc_hlc_client_test.cpp +++ b/cmvr-es/service/grpc/src/grpc_hlc_client_test.cpp @@ -26,8 +26,8 @@ TEST(GrpcHlcClientTest, MyTest) { *request.mutable_header()->mutable_timestamp() = google::protobuf::util::TimeUtil::GetCurrentTime(); - request.set_u(12); - request.set_v(13); + request.set_u(600); + request.set_v(360); request.set_max_force(1300); diff --git a/cmvr-es/service/grpc/src/grpc_hlc_service.cpp b/cmvr-es/service/grpc/src/grpc_hlc_service.cpp index ebaf5b3e..bafd84d9 100644 --- a/cmvr-es/service/grpc/src/grpc_hlc_service.cpp +++ b/cmvr-es/service/grpc/src/grpc_hlc_service.cpp @@ -4,37 +4,137 @@ #include "../include/grpc_hlc_service.h" + +#include +#include +#include + +#include #include -#include "devices/robot/humanoid_robot/include/humanoid_robot.h" -#include "controller/include/touch_controller.h" +#include "applications/include/touch_screen_app.h" using namespace cmvr::service; -using namespace cmvr::device; using namespace cmvr::api; using google::protobuf::util::TimeUtil; -gRPCHlcServiceImpl::gRPCHlcServiceImpl():dmgr_(DeviceManager::getInstance()){} +namespace { -grpc::Status gRPCHlcServiceImpl::touch(grpc::ServerContext *context, const cmvr::api::Touch_Request *request, cmvr::api::Touch_Response *response) { - - grpc::Status ret = grpc::Status::OK; - - // try { - // std::shared_ptr robot = nullptr; - // auto cam = dmgr_.getDevice("cam4"); - // auto hand = dmgr_.getDevice("hand1"); - // ctrl::TouchController touch_controller(robot,hand,cam);; - // touch_controller.touch(request->u(),request->v(),request->max_force()); - // response->mutable_header()->set_success(true); - // response->mutable_header()->set_error_message(""); - // }catch (const std::exception& e) { - // response->mutable_header()->set_success(false); - // response->mutable_header()->set_error_message(e.what()); - // ret = grpc::Status(grpc::StatusCode::INTERNAL, e.what()); - // } - // *response->mutable_header()->mutable_timestamp() = TimeUtil::GetCurrentTime(); - return ret; +std::string buildTouchFailureMessage(const cmvr::app::TouchScreenApp& app, + const std::string& prefix) { + return prefix + ", phase=" + + cmvr::app::TouchScreenApp::phaseToString(app.phase()) + + ", status=" + + cmvr::app::TouchScreenApp::statusToString(app.lastStatus()); } +void fillTouchResponse(Touch_Response* response, + const bool success, + const std::string& error_message) { + response->mutable_header()->set_success(success); + response->mutable_header()->set_error_message(error_message); + *response->mutable_header()->mutable_timestamp() = TimeUtil::GetCurrentTime(); +} +} // namespace + +gRPCHlcServiceImpl::gRPCHlcServiceImpl() = default; + +bool gRPCHlcServiceImpl::ensureTouchAppInitialized(std::string& error_message) { + if (touch_app_initialized_) { + return true; + } + + LOG(INFO) << "[gRPCHlcServiceImpl] init TouchScreenApp from config"; + if (!touch_app_.init()) { + error_message = buildTouchFailureMessage(touch_app_, "TouchScreenApp init failed"); + touch_app_initialized_ = false; + return false; + } + + touch_app_initialized_ = true; + return true; +} + +grpc::Status gRPCHlcServiceImpl::touch(grpc::ServerContext *context, const cmvr::api::Touch_Request *request, cmvr::api::Touch_Response *response) { + std::lock_guard lock(touch_mutex_); + + try { + LOG(INFO) << "[gRPCHlcServiceImpl] touch: request.device_id=" + << request->header().device_id() + << " (ignored, using TouchScreenApp config)" + << ", u=" << request->u() + << ", v=" << request->v() + << ", max_force ignored"; + + std::string init_error; + if (!ensureTouchAppInitialized(init_error)) { + fillTouchResponse(response, false, init_error); + return grpc::Status(grpc::StatusCode::INTERNAL, init_error); + } + + if (!touch_app_.startFromPixel(request->u(), request->v())) { + throw std::runtime_error( + buildTouchFailureMessage(touch_app_, "TouchScreenApp startFromPixel failed")); + } + + bool align_reached = false; + bool touch_triggered = false; + auto last_logged_status = cmvr::app::TouchScreenApp::Status::IDLE; + while (touch_app_.isBusy()) { + if (context != nullptr && context->IsCancelled()) { + touch_app_.stop(); + const std::string error = "touch request cancelled"; + fillTouchResponse(response, false, error); + return grpc::Status(grpc::StatusCode::CANCELLED, error); + } + + if (!touch_app_.step()) { + throw std::runtime_error( + buildTouchFailureMessage(touch_app_, "touch flow failed")); + } + + if (touch_app_.lastStatus() == cmvr::app::TouchScreenApp::Status::ALIGN_REACHED) { + align_reached = true; + } + if (touch_app_.lastStatus() == cmvr::app::TouchScreenApp::Status::TOUCH_TRIGGERED) { + touch_triggered = true; + } + + if (touch_app_.lastStatus() != last_logged_status) { + LOG(INFO) << "[gRPCHlcServiceImpl] touch status=" + << cmvr::app::TouchScreenApp::statusToString(touch_app_.lastStatus()) + << ", phase=" + << cmvr::app::TouchScreenApp::phaseToString(touch_app_.phase()) + << ", active_tag=" << touch_app_.lastActiveTagId() + << ", pressure_sum=" << touch_app_.lastTouchPressureSum() + << ", pressure_peak=" << touch_app_.lastTouchPressurePeak(); + last_logged_status = touch_app_.lastStatus(); + } + + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + + if (!align_reached) { + throw std::runtime_error( + buildTouchFailureMessage(touch_app_, "touch exited without ALIGN_REACHED")); + } + if (!touch_triggered) { + throw std::runtime_error( + buildTouchFailureMessage(touch_app_, "touch exited without TOUCH_TRIGGERED")); + } + if (!touch_app_.isFinished()) { + throw std::runtime_error( + buildTouchFailureMessage(touch_app_, "touch did not finish successfully")); + } + + LOG(INFO) << "[gRPCHlcServiceImpl] touch done" + << ", final_status=" + << cmvr::app::TouchScreenApp::statusToString(touch_app_.lastStatus()); + fillTouchResponse(response, true, ""); + return grpc::Status::OK; + } catch (const std::exception& e) { + fillTouchResponse(response, false, e.what()); + return grpc::Status(grpc::StatusCode::INTERNAL, e.what()); + } +} diff --git a/data/plot1.png b/data/plot1.png index 29ea5ccdea7353bce9fceea40962e5e9bae8ee08..11a4ca21d30df1ccb210c7ddf856d256578aefc0 100644 GIT binary patch literal 62702 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 literal 57378 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 R-c@8~X@-`+=WV1P|2NO#_Ei7? diff --git a/protos/cmvr/config/touch_screen_app_config/touch_screen_app_config.proto b/protos/cmvr/config/touch_screen_app_config/touch_screen_app_config.proto new file mode 100644 index 00000000..8b8bf178 --- /dev/null +++ b/protos/cmvr/config/touch_screen_app_config/touch_screen_app_config.proto @@ -0,0 +1,122 @@ +syntax = "proto3"; + +package cmvr.config; + +message TouchScreenVec3 { + optional double x = 1; + optional double y = 2; + optional double z = 3; +} + +message TouchScreenTwist6 { + optional double vx = 1; + optional double vy = 2; + optional double vz = 3; + optional double wx = 4; + optional double wy = 5; + optional double wz = 6; +} + +message TouchScreenMatrix3d { + optional double m00 = 1; + optional double m01 = 2; + optional double m02 = 3; + optional double m10 = 4; + optional double m11 = 5; + optional double m12 = 6; + optional double m20 = 7; + optional double m21 = 8; + optional double m22 = 9; +} + +message TouchScreenInitJointPoint { + optional string joint_name = 1; + optional double rad = 2; +} + +enum TouchScreenDepthPolicy { + TOUCH_SCREEN_DEPTH_POLICY_NONE = 0; + TOUCH_SCREEN_DEPTH_POLICY_PREFER = 1; + TOUCH_SCREEN_DEPTH_POLICY_REQUIRE = 2; +} + +enum TouchScreenTargetPointMethod { + TOUCH_SCREEN_TARGET_POINT_METHOD_TAG_PLANE = 0; + TOUCH_SCREEN_TARGET_POINT_METHOD_DEPTH_IMAGE = 1; +} + +enum TouchScreenFingerType { + TOUCH_SCREEN_FINGER_TYPE_PINKY = 0; + TOUCH_SCREEN_FINGER_TYPE_RING = 1; + TOUCH_SCREEN_FINGER_TYPE_MIDDLE = 2; + TOUCH_SCREEN_FINGER_TYPE_INDEX = 3; + TOUCH_SCREEN_FINGER_TYPE_THUMB = 4; +} + +enum TouchScreenTactileRegion { + TOUCH_SCREEN_TACTILE_REGION_TIP = 0; + TOUCH_SCREEN_TACTILE_REGION_FINGER = 1; + TOUCH_SCREEN_TACTILE_REGION_PAD = 2; + TOUCH_SCREEN_TACTILE_REGION_TIP_AND_FINGER = 3; + TOUCH_SCREEN_TACTILE_REGION_THUMB_MIDDLE = 4; +} + +message TouchScreenAppConfig { + optional string robot_id = 40; + optional string dexhand_id = 41; + optional string camera_id = 42; + optional bool move_to_init_position = 43; + repeated TouchScreenInitJointPoint init_joint_positions = 44; + optional double init_movej_vel = 45; + optional double init_movej_acc = 46; + optional bool move_to_init_position_before_start = 47; + + optional string urdf_path = 1; + optional string base_link = 2; + optional string flange_link = 3; + optional string camera_link = 4; + + optional double tag_size_m = 5; + optional TouchScreenDepthPolicy depth_policy = 6; + optional TouchScreenTargetPointMethod target_point_method = 7; + + TouchScreenVec3 hover_target_in_camera = 8; + optional double target_rx = 9; + optional double target_ry = 10; + optional double target_rz = 11; + + optional double ibvs_lambda = 12; + optional double ibvs_mu = 13; + optional double ibvs_qdot_max = 14; + TouchScreenTwist6 ibvs_vmax6 = 15; + optional bool enable_joint_limit_avoidance = 16; + optional double joint_limit_avoidance_gain = 17; + optional double joint_limit_avoidance_margin_ratio = 18; + optional double joint_limit_avoidance_max_push = 19; + + TouchScreenMatrix3d r_camera_to_visp = 20; + TouchScreenMatrix3d r_camera_to_urdf = 21; + + repeated string control_joint_names = 22; + + optional double align_xy_threshold_m = 23; + optional double align_z_threshold_m = 24; + optional double align_rot_threshold_rad = 25; + optional int32 align_stable_frames = 26; + optional double align_timeout_s = 27; + optional bool pause_after_align_reached = 28; + + TouchScreenTwist6 touch_twist_base = 29; + optional double touch_acceleration = 30; + optional double touch_forward_duration_s = 31; + + optional double dwell_time_s = 32; + TouchScreenTwist6 retract_twist_base = 33; + optional double retract_acceleration = 34; + optional double retract_duration_s = 35; + + optional TouchScreenFingerType tactile_finger = 36; + optional TouchScreenTactileRegion tactile_region = 37; + optional double tactile_pressure_sum_threshold = 38; + optional double tactile_pressure_peak_threshold = 39; +}